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