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