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.content.pm.PackageManager;
28import android.graphics.Color;
29import android.graphics.PixelFormat;
30import android.graphics.Rect;
31import android.graphics.drawable.ColorDrawable;
32import android.os.Bundle;
33import android.view.Display;
34import android.view.View;
35import android.view.View.OnClickListener;
36import android.view.ViewGroup;
37import android.view.Window;
38import android.view.WindowManager;
39import android.widget.ArrayAdapter;
40import android.widget.Button;
41import android.widget.LinearLayout;
42import android.widget.RelativeLayout;
43import android.widget.Spinner;
44import 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 */
67public 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
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     */
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
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
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
184    public boolean isAndromeda() {
185        return mAndromeda;
186    }
187
188    @Override
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
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     */
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
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
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
366    private void enableControls(boolean enabled) {
367        mMeasureCompositionButton.setEnabled(enabled);
368        mMeasureAllocationButton.setEnabled(enabled);
369        mPixelFormatSelector.setEnabled(enabled);
370    }
371
372    @Override
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
385    protected void onPause() {
386        super.onPause();
387
388        synchronized (mLockResumed) {
389            mResumed = false;
390        }
391    }
392
393    class Measurement {
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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     */
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
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
601        public void run() {
602            mTask.run();
603            synchronized (mLock) {
604                mDone = true;
605                mLock.notify();
606            }
607        }
608    }
609}
610