SurfaceCompositionMeasuringActivity.java revision 9dbde7b09f2366d2a239b1a4c234d5cf2de51739
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.surfacecomposition; 17 18import java.text.DecimalFormat; 19import java.util.ArrayList; 20import java.util.List; 21 22import android.app.ActionBar; 23import android.app.Activity; 24import android.app.ActivityManager; 25import android.app.ActivityManager.MemoryInfo; 26import android.content.Context; 27import android.graphics.Color; 28import android.graphics.PixelFormat; 29import android.graphics.Rect; 30import android.graphics.drawable.ColorDrawable; 31import android.os.Bundle; 32import android.view.Display; 33import android.view.View; 34import android.view.View.OnClickListener; 35import android.view.ViewGroup; 36import android.view.Window; 37import android.view.WindowManager; 38import android.widget.ArrayAdapter; 39import android.widget.Button; 40import android.widget.LinearLayout; 41import android.widget.RelativeLayout; 42import android.widget.Spinner; 43import android.widget.TextView; 44 45/** 46 * This activity is designed to measure peformance scores of Android surfaces. 47 * It can work in two modes. In first mode functionality of this activity is 48 * invoked from Cts test (SurfaceCompositionTest). This activity can also be 49 * used in manual mode as a normal app. Different pixel formats are supported. 50 * 51 * measureCompositionScore(pixelFormat) 52 * This test measures surface compositor performance which shows how many 53 * surfaces of specific format surface compositor can combine without dropping 54 * frames. We allow one dropped frame per half second. 55 * 56 * measureAllocationScore(pixelFormat) 57 * This test measures surface allocation/deallocation performance. It shows 58 * how many surface lifecycles (creation, destruction) can be done per second. 59 * 60 * In manual mode, which activated by pressing button 'Compositor speed' or 61 * 'Allocator speed', all possible pixel format are tested and combined result 62 * is displayed in text view. Additional system information such as memory 63 * status, display size and surface format is also displayed and regulary 64 * updated. 65 */ 66public class SurfaceCompositionMeasuringActivity extends Activity implements OnClickListener { 67 private final static int MIN_NUMBER_OF_SURFACES = 15; 68 private final static int MAX_NUMBER_OF_SURFACES = 40; 69 private final static int WARM_UP_ALLOCATION_CYCLES = 2; 70 private final static int MEASURE_ALLOCATION_CYCLES = 5; 71 private final static int TEST_COMPOSITOR = 1; 72 private final static int TEST_ALLOCATION = 2; 73 private final static float MIN_REFRESH_RATE_SUPPORTED = 50.0f; 74 75 private final static DecimalFormat DOUBLE_FORMAT = new DecimalFormat("#.00"); 76 // Possible selection in pixel format selector. 77 private final static int[] PIXEL_FORMATS = new int[] { 78 PixelFormat.TRANSLUCENT, 79 PixelFormat.TRANSPARENT, 80 PixelFormat.OPAQUE, 81 PixelFormat.RGBA_8888, 82 PixelFormat.RGBX_8888, 83 PixelFormat.RGB_888, 84 PixelFormat.RGB_565, 85 }; 86 87 88 private List<CustomSurfaceView> mViews = new ArrayList<CustomSurfaceView>(); 89 private Button mMeasureCompositionButton; 90 private Button mMeasureAllocationButton; 91 private Spinner mPixelFormatSelector; 92 private TextView mResultView; 93 private TextView mSystemInfoView; 94 private final Object mLockResumed = new Object(); 95 private boolean mResumed; 96 97 // Drop one frame per half second. 98 // TODO(khmel) 99 // Add a feature flag and set the target FPS dependent on the target system as e.g.: 100 // 59FPS for MULTI_WINDOW and 54 otherwise (to satisfy the default lax Android requirements). 101 private double mRefreshRate; 102 private double mTargetFPS; 103 104 private int mWidth; 105 private int mHeight; 106 107 class CompositorScore { 108 double mSurfaces; 109 double mBitrate; 110 111 @Override 112 public String toString() { 113 return DOUBLE_FORMAT.format(mSurfaces) + " surfaces. " + 114 "Bitrate: " + getReadableMemory((long)mBitrate) + "/s"; 115 } 116 } 117 118 /** 119 * Measure performance score. 120 * 121 * @return biggest possible number of visible surfaces which surface 122 * compositor can handle. 123 */ 124 public CompositorScore measureCompositionScore(int pixelFormat) { 125 waitForActivityResumed(); 126 //MemoryAccessTask memAccessTask = new MemoryAccessTask(); 127 //memAccessTask.start(); 128 // Destroy any active surface. 129 configureSurfacesAndWait(0, pixelFormat, false); 130 CompositorScore score = new CompositorScore(); 131 score.mSurfaces = measureCompositionScore(new Measurement(0, 60.0), 132 new Measurement(mViews.size() + 1, 0.0f), pixelFormat); 133 // Assume 32 bits per pixel. 134 score.mBitrate = score.mSurfaces * mTargetFPS * mWidth * mHeight * 4.0; 135 //memAccessTask.stop(); 136 return score; 137 } 138 139 static class AllocationScore { 140 double mMedian; 141 double mMin; 142 double mMax; 143 144 @Override 145 public String toString() { 146 return DOUBLE_FORMAT.format(mMedian) + " (min:" + DOUBLE_FORMAT.format(mMin) + 147 ", max:" + DOUBLE_FORMAT.format(mMax) + ") surface allocations per second"; 148 } 149 } 150 151 public AllocationScore measureAllocationScore(int pixelFormat) { 152 waitForActivityResumed(); 153 AllocationScore score = new AllocationScore(); 154 for (int i = 0; i < MEASURE_ALLOCATION_CYCLES + WARM_UP_ALLOCATION_CYCLES; ++i) { 155 long time1 = System.currentTimeMillis(); 156 configureSurfacesAndWait(MIN_NUMBER_OF_SURFACES, pixelFormat, false); 157 acquireSurfacesCanvas(); 158 long time2 = System.currentTimeMillis(); 159 releaseSurfacesCanvas(); 160 configureSurfacesAndWait(0, pixelFormat, false); 161 // Give SurfaceFlinger some time to rebuild the layer stack and release the buffers. 162 try { 163 Thread.sleep(500); 164 } catch(InterruptedException e) { 165 e.printStackTrace(); 166 } 167 if (i < WARM_UP_ALLOCATION_CYCLES) { 168 // This is warm-up cycles, ignore result so far. 169 continue; 170 } 171 double speed = MIN_NUMBER_OF_SURFACES * 1000.0 / (time2 - time1); 172 score.mMedian += speed / MEASURE_ALLOCATION_CYCLES; 173 if (i == WARM_UP_ALLOCATION_CYCLES) { 174 score.mMin = speed; 175 score.mMax = speed; 176 } else { 177 score.mMin = Math.min(score.mMin, speed); 178 score.mMax = Math.max(score.mMax, speed); 179 } 180 } 181 182 return score; 183 } 184 185 @Override 186 public void onClick(View view) { 187 if (view == mMeasureCompositionButton) { 188 doTest(TEST_COMPOSITOR); 189 } else if (view == mMeasureAllocationButton) { 190 doTest(TEST_ALLOCATION); 191 } 192 } 193 194 private void doTest(final int test) { 195 enableControls(false); 196 final int pixelFormat = PIXEL_FORMATS[mPixelFormatSelector.getSelectedItemPosition()]; 197 new Thread() { 198 public void run() { 199 final StringBuffer sb = new StringBuffer(); 200 switch (test) { 201 case TEST_COMPOSITOR: { 202 sb.append("Compositor score:"); 203 CompositorScore score = measureCompositionScore(pixelFormat); 204 sb.append("\n " + getPixelFormatInfo(pixelFormat) + ":" + 205 score + "."); 206 } 207 break; 208 case TEST_ALLOCATION: { 209 sb.append("Allocation score:"); 210 AllocationScore score = measureAllocationScore(pixelFormat); 211 sb.append("\n " + getPixelFormatInfo(pixelFormat) + ":" + 212 score + "."); 213 } 214 break; 215 } 216 runOnUiThreadAndWait(new Runnable() { 217 public void run() { 218 mResultView.setText(sb.toString()); 219 enableControls(true); 220 updateSystemInfo(pixelFormat); 221 } 222 }); 223 } 224 }.start(); 225 } 226 227 /** 228 * Wait until activity is resumed. 229 */ 230 public void waitForActivityResumed() { 231 synchronized (mLockResumed) { 232 if (!mResumed) { 233 try { 234 mLockResumed.wait(10000); 235 } catch (InterruptedException e) { 236 } 237 } 238 if (!mResumed) { 239 throw new RuntimeException("Activity was not resumed"); 240 } 241 } 242 } 243 244 @Override 245 protected void onCreate(Bundle savedInstanceState) { 246 super.onCreate(savedInstanceState); 247 248 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 249 250 detectRefreshRate(); 251 252 // To layouts in parent. First contains list of Surfaces and second 253 // controls. Controls stay on top. 254 RelativeLayout rootLayout = new RelativeLayout(this); 255 rootLayout.setLayoutParams(new ViewGroup.LayoutParams( 256 ViewGroup.LayoutParams.MATCH_PARENT, 257 ViewGroup.LayoutParams.MATCH_PARENT)); 258 259 CustomLayout layout = new CustomLayout(this); 260 layout.setLayoutParams(new ViewGroup.LayoutParams( 261 ViewGroup.LayoutParams.MATCH_PARENT, 262 ViewGroup.LayoutParams.MATCH_PARENT)); 263 264 Rect rect = new Rect(); 265 getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); 266 mWidth = rect.right; 267 mHeight = rect.bottom; 268 long maxMemoryPerSurface = roundToNextPowerOf2(mWidth) * roundToNextPowerOf2(mHeight) * 4; 269 // Use 75% of available memory. 270 int surfaceCnt = (int)((getMemoryInfo().availMem * 3) / (4 * maxMemoryPerSurface)); 271 if (surfaceCnt < MIN_NUMBER_OF_SURFACES) { 272 throw new RuntimeException("Not enough memory to allocate " + 273 MIN_NUMBER_OF_SURFACES + " surfaces."); 274 } 275 if (surfaceCnt > MAX_NUMBER_OF_SURFACES) { 276 surfaceCnt = MAX_NUMBER_OF_SURFACES; 277 } 278 279 LinearLayout controlLayout = new LinearLayout(this); 280 controlLayout.setOrientation(LinearLayout.VERTICAL); 281 controlLayout.setLayoutParams(new ViewGroup.LayoutParams( 282 ViewGroup.LayoutParams.MATCH_PARENT, 283 ViewGroup.LayoutParams.MATCH_PARENT)); 284 285 mMeasureCompositionButton = createButton("Compositor speed.", controlLayout); 286 mMeasureAllocationButton = createButton("Allocation speed", controlLayout); 287 288 String[] pixelFomats = new String[PIXEL_FORMATS.length]; 289 for (int i = 0; i < pixelFomats.length; ++i) { 290 pixelFomats[i] = getPixelFormatInfo(PIXEL_FORMATS[i]); 291 } 292 mPixelFormatSelector = new Spinner(this); 293 ArrayAdapter<String> pixelFormatSelectorAdapter = 294 new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, pixelFomats); 295 pixelFormatSelectorAdapter.setDropDownViewResource( 296 android.R.layout.simple_spinner_dropdown_item); 297 mPixelFormatSelector.setAdapter(pixelFormatSelectorAdapter); 298 mPixelFormatSelector.setLayoutParams(new LinearLayout.LayoutParams( 299 ViewGroup.LayoutParams.WRAP_CONTENT, 300 ViewGroup.LayoutParams.WRAP_CONTENT)); 301 controlLayout.addView(mPixelFormatSelector); 302 303 mResultView = new TextView(this); 304 mResultView.setBackgroundColor(0); 305 mResultView.setText("Press button to start test."); 306 mResultView.setLayoutParams(new LinearLayout.LayoutParams( 307 ViewGroup.LayoutParams.WRAP_CONTENT, 308 ViewGroup.LayoutParams.WRAP_CONTENT)); 309 controlLayout.addView(mResultView); 310 311 mSystemInfoView = new TextView(this); 312 mSystemInfoView.setBackgroundColor(0); 313 mSystemInfoView.setLayoutParams(new LinearLayout.LayoutParams( 314 ViewGroup.LayoutParams.WRAP_CONTENT, 315 ViewGroup.LayoutParams.WRAP_CONTENT)); 316 controlLayout.addView(mSystemInfoView); 317 318 for (int i = 0; i < surfaceCnt; ++i) { 319 CustomSurfaceView view = new CustomSurfaceView(this, "Surface:" + i); 320 // Create all surfaces overlapped in order to prevent SurfaceFlinger 321 // to filter out surfaces by optimization in case surface is opaque. 322 // In case surface is transparent it will be drawn anyway. Note that first 323 // surface covers whole screen and must stand below other surfaces. Z order of 324 // layers is not predictable and there is only one way to force first 325 // layer to be below others is to mark it as media and all other layers 326 // to mark as media overlay. 327 if (i == 0) { 328 view.setLayoutParams(new CustomLayout.LayoutParams(0, 0, mWidth, mHeight)); 329 view.setZOrderMediaOverlay(false); 330 } else { 331 // Z order of other layers is not predefined so make offset on x and reverse 332 // offset on y to make sure that surface is visible in any layout. 333 int x = i; 334 int y = (surfaceCnt - i); 335 view.setLayoutParams(new CustomLayout.LayoutParams(x, y, x + mWidth, y + mHeight)); 336 view.setZOrderMediaOverlay(true); 337 } 338 view.setVisibility(View.INVISIBLE); 339 layout.addView(view); 340 mViews.add(view); 341 } 342 343 rootLayout.addView(layout); 344 rootLayout.addView(controlLayout); 345 346 setContentView(rootLayout); 347 } 348 349 private Button createButton(String caption, LinearLayout layout) { 350 Button button = new Button(this); 351 button.setText(caption); 352 button.setLayoutParams(new LinearLayout.LayoutParams( 353 ViewGroup.LayoutParams.WRAP_CONTENT, 354 ViewGroup.LayoutParams.WRAP_CONTENT)); 355 button.setOnClickListener(this); 356 layout.addView(button); 357 return button; 358 } 359 360 private void enableControls(boolean enabled) { 361 mMeasureCompositionButton.setEnabled(enabled); 362 mMeasureAllocationButton.setEnabled(enabled); 363 mPixelFormatSelector.setEnabled(enabled); 364 } 365 366 @Override 367 protected void onResume() { 368 super.onResume(); 369 370 updateSystemInfo(PixelFormat.UNKNOWN); 371 372 synchronized (mLockResumed) { 373 mResumed = true; 374 mLockResumed.notifyAll(); 375 } 376 } 377 378 @Override 379 protected void onPause() { 380 super.onPause(); 381 382 synchronized (mLockResumed) { 383 mResumed = false; 384 } 385 } 386 387 class Measurement { 388 Measurement(int surfaceCnt, double fps) { 389 mSurfaceCnt = surfaceCnt; 390 mFPS = fps; 391 } 392 393 public final int mSurfaceCnt; 394 public final double mFPS; 395 } 396 397 private double measureCompositionScore(Measurement ok, Measurement fail, int pixelFormat) { 398 if (ok.mSurfaceCnt + 1 == fail.mSurfaceCnt) { 399 // Interpolate result. 400 double fraction = (mTargetFPS - fail.mFPS) / (ok.mFPS - fail.mFPS); 401 return ok.mSurfaceCnt + fraction; 402 } 403 404 int medianSurfaceCnt = (ok.mSurfaceCnt + fail.mSurfaceCnt) / 2; 405 Measurement median = new Measurement(medianSurfaceCnt, 406 measureFPS(medianSurfaceCnt, pixelFormat)); 407 408 if (median.mFPS >= mTargetFPS) { 409 return measureCompositionScore(median, fail, pixelFormat); 410 } else { 411 return measureCompositionScore(ok, median, pixelFormat); 412 } 413 } 414 415 private double measureFPS(int surfaceCnt, int pixelFormat) { 416 configureSurfacesAndWait(surfaceCnt, pixelFormat, true); 417 // At least one view is visible and it is enough to update only 418 // one overlapped surface in order to force SurfaceFlinger to send 419 // all surfaces to compositor. 420 double fps = mViews.get(0).measureFPS(mRefreshRate * 0.8, mRefreshRate * 0.999); 421 422 // Make sure that surface configuration was not changed. 423 validateSurfacesNotChanged(); 424 425 return fps; 426 } 427 428 private void waitForSurfacesConfigured(final int pixelFormat) { 429 for (int i = 0; i < mViews.size(); ++i) { 430 CustomSurfaceView view = mViews.get(i); 431 if (view.getVisibility() == View.VISIBLE) { 432 view.waitForSurfaceReady(); 433 } else { 434 view.waitForSurfaceDestroyed(); 435 } 436 } 437 runOnUiThreadAndWait(new Runnable() { 438 @Override 439 public void run() { 440 updateSystemInfo(pixelFormat); 441 } 442 }); 443 } 444 445 private void validateSurfacesNotChanged() { 446 for (int i = 0; i < mViews.size(); ++i) { 447 CustomSurfaceView view = mViews.get(i); 448 view.validateSurfaceNotChanged(); 449 } 450 } 451 452 private void configureSurfaces(int surfaceCnt, int pixelFormat, boolean invalidate) { 453 for (int i = 0; i < mViews.size(); ++i) { 454 CustomSurfaceView view = mViews.get(i); 455 if (i < surfaceCnt) { 456 view.setMode(pixelFormat, invalidate); 457 view.setVisibility(View.VISIBLE); 458 } else { 459 view.setVisibility(View.INVISIBLE); 460 } 461 } 462 } 463 464 private void configureSurfacesAndWait(final int surfaceCnt, final int pixelFormat, 465 final boolean invalidate) { 466 runOnUiThreadAndWait(new Runnable() { 467 @Override 468 public void run() { 469 configureSurfaces(surfaceCnt, pixelFormat, invalidate); 470 } 471 }); 472 waitForSurfacesConfigured(pixelFormat); 473 } 474 475 private void acquireSurfacesCanvas() { 476 for (int i = 0; i < mViews.size(); ++i) { 477 CustomSurfaceView view = mViews.get(i); 478 view.acquireCanvas(); 479 } 480 } 481 482 private void releaseSurfacesCanvas() { 483 for (int i = 0; i < mViews.size(); ++i) { 484 CustomSurfaceView view = mViews.get(i); 485 view.releaseCanvas(); 486 } 487 } 488 489 private static String getReadableMemory(long bytes) { 490 long unit = 1024; 491 if (bytes < unit) { 492 return bytes + " B"; 493 } 494 int exp = (int) (Math.log(bytes) / Math.log(unit)); 495 return String.format("%.1f %sB", bytes / Math.pow(unit, exp), 496 "KMGTPE".charAt(exp-1)); 497 } 498 499 private MemoryInfo getMemoryInfo() { 500 ActivityManager activityManager = (ActivityManager) 501 getSystemService(ACTIVITY_SERVICE); 502 MemoryInfo memInfo = new MemoryInfo(); 503 activityManager.getMemoryInfo(memInfo); 504 return memInfo; 505 } 506 507 private void updateSystemInfo(int pixelFormat) { 508 int visibleCnt = 0; 509 for (int i = 0; i < mViews.size(); ++i) { 510 if (mViews.get(i).getVisibility() == View.VISIBLE) { 511 ++visibleCnt; 512 } 513 } 514 515 MemoryInfo memInfo = getMemoryInfo(); 516 String info = "Available " + 517 getReadableMemory(memInfo.availMem) + " from " + 518 getReadableMemory(memInfo.totalMem) + ".\nVisible " + 519 visibleCnt + " from " + mViews.size() + " " + 520 getPixelFormatInfo(pixelFormat) + " surfaces.\n" + 521 "View size: " + mWidth + "x" + mHeight + 522 ". Refresh rate: " + DOUBLE_FORMAT.format(mRefreshRate) + "."; 523 mSystemInfoView.setText(info); 524 } 525 526 private void detectRefreshRate() { 527 WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE); 528 mRefreshRate = wm.getDefaultDisplay().getRefreshRate(); 529 if (mRefreshRate < MIN_REFRESH_RATE_SUPPORTED) 530 throw new RuntimeException("Unsupported display refresh rate: " + mRefreshRate); 531 mTargetFPS = mRefreshRate - 2.0f; 532 } 533 534 private int roundToNextPowerOf2(int value) { 535 --value; 536 value |= value >> 1; 537 value |= value >> 2; 538 value |= value >> 4; 539 value |= value >> 8; 540 value |= value >> 16; 541 return value + 1; 542 } 543 544 public static String getPixelFormatInfo(int pixelFormat) { 545 switch (pixelFormat) { 546 case PixelFormat.TRANSLUCENT: 547 return "TRANSLUCENT"; 548 case PixelFormat.TRANSPARENT: 549 return "TRANSPARENT"; 550 case PixelFormat.OPAQUE: 551 return "OPAQUE"; 552 case PixelFormat.RGBA_8888: 553 return "RGBA_8888"; 554 case PixelFormat.RGBX_8888: 555 return "RGBX_8888"; 556 case PixelFormat.RGB_888: 557 return "RGB_888"; 558 case PixelFormat.RGB_565: 559 return "RGB_565"; 560 default: 561 return "PIX.FORMAT:" + pixelFormat; 562 } 563 } 564 565 /** 566 * A helper that executes a task in the UI thread and waits for its completion. 567 * 568 * @param task - task to execute. 569 */ 570 private void runOnUiThreadAndWait(Runnable task) { 571 new UIExecutor(task); 572 } 573 574 class UIExecutor implements Runnable { 575 private final Object mLock = new Object(); 576 private Runnable mTask; 577 private boolean mDone = false; 578 579 UIExecutor(Runnable task) { 580 mTask = task; 581 mDone = false; 582 runOnUiThread(this); 583 synchronized (mLock) { 584 while (!mDone) { 585 try { 586 mLock.wait(); 587 } catch (InterruptedException e) { 588 e.printStackTrace(); 589 } 590 } 591 } 592 } 593 594 public void run() { 595 mTask.run(); 596 synchronized (mLock) { 597 mDone = true; 598 mLock.notify(); 599 } 600 } 601 } 602} 603