SurfaceCompositionMeasuringActivity.java revision 9dbde7b09f2366d2a239b1a4c234d5cf2de51739
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 */
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}
603