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