blob: 4771b6cfc75055a43771250f7ea2dae5defc04a7 [file] [log] [blame]
Yury Khmel9dbde7b2015-08-31 17:51:42 +09001/*
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;
Yury Khmel7578d102015-10-28 14:04:48 +090027import android.content.pm.PackageManager;
Yury Khmel9dbde7b2015-08-31 17:51:42 +090028import android.graphics.Color;
29import android.graphics.PixelFormat;
30import android.graphics.Rect;
31import android.graphics.drawable.ColorDrawable;
32import android.os.Bundle;
33import android.view.Display;
34import android.view.View;
35import android.view.View.OnClickListener;
36import android.view.ViewGroup;
37import android.view.Window;
38import android.view.WindowManager;
39import android.widget.ArrayAdapter;
40import android.widget.Button;
41import android.widget.LinearLayout;
42import android.widget.RelativeLayout;
43import android.widget.Spinner;
44import 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 */
67public 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.
Yury Khmel9dbde7b2015-08-31 17:51:42 +090099 private double mRefreshRate;
100 private double mTargetFPS;
Yury Khmel7578d102015-10-28 14:04:48 +0900101 private boolean mAndromeda;
Yury Khmel9dbde7b2015-08-31 17:51:42 +0900102
103 private int mWidth;
104 private int mHeight;
105
106 class CompositorScore {
107 double mSurfaces;
Yury Khmel9e433bb2015-10-05 19:15:57 +0900108 double mBandwidth;
Yury Khmel9dbde7b2015-08-31 17:51:42 +0900109
110 @Override
111 public String toString() {
112 return DOUBLE_FORMAT.format(mSurfaces) + " surfaces. " +
Yury Khmel9e433bb2015-10-05 19:15:57 +0900113 "Bandwidth: " + getReadableMemory((long)mBandwidth) + "/s";
Yury Khmel9dbde7b2015-08-31 17:51:42 +0900114 }
115 }
116
117 /**
118 * Measure performance score.
119 *
120 * @return biggest possible number of visible surfaces which surface
121 * compositor can handle.
122 */
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.
Yury Khmel9e433bb2015-10-05 19:15:57 +0900133 score.mBandwidth = score.mSurfaces * mTargetFPS * mWidth * mHeight * 4.0;
Yury Khmel9dbde7b2015-08-31 17:51:42 +0900134 //memAccessTask.stop();
135 return score;
136 }
137
138 static class AllocationScore {
139 double mMedian;
140 double mMin;
141 double mMax;
142
143 @Override
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
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
Yury Khmel7578d102015-10-28 14:04:48 +0900184 public boolean isAndromeda() {
185 return mAndromeda;
186 }
187
Yury Khmel9dbde7b2015-08-31 17:51:42 +0900188 @Override
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
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 */
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
248 protected void onCreate(Bundle savedInstanceState) {
249 super.onCreate(savedInstanceState);
250
251 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
252
Yury Khmel7578d102015-10-28 14:04:48 +0900253 // Detect Andromeda devices by having free-form window management feature.
254 mAndromeda = getPackageManager().hasSystemFeature(
255 PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT);
Yury Khmel9dbde7b2015-08-31 17:51:42 +0900256 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
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
366 private void enableControls(boolean enabled) {
367 mMeasureCompositionButton.setEnabled(enabled);
368 mMeasureAllocationButton.setEnabled(enabled);
369 mPixelFormatSelector.setEnabled(enabled);
370 }
371
372 @Override
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
385 protected void onPause() {
386 super.onPause();
387
388 synchronized (mLockResumed) {
389 mResumed = false;
390 }
391 }
392
393 class Measurement {
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
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
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
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
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
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
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
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
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
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
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
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();
Yury Khmel7578d102015-10-28 14:04:48 +0900522 String platformName = mAndromeda ? "Andromeda" : "Android";
523 String info = platformName + ": available " +
Yury Khmel9dbde7b2015-08-31 17:51:42 +0900524 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
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
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
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 */
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
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
601 public void run() {
602 mTask.run();
603 synchronized (mLock) {
604 mDone = true;
605 mLock.notify();
606 }
607 }
608 }
609}