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