blob: e3e1d34f193e56257e8bea14504d5dd24d6b8aa4 [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;
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}