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