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.util.Random;
19
20import android.content.Context;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.view.Surface;
24import android.view.SurfaceHolder;
25import android.view.SurfaceView;
26
27/**
28 * This provides functionality to measure Surface update frame rate. The idea is to
29 * constantly invalidates Surface in a separate thread. Lowest possible way is to
30 * use SurfaceView which works with Surface. This gives a very small overhead
31 * and very close to Android internals. Note, that lockCanvas is blocking
32 * methods and it returns once SurfaceFlinger consumes previous buffer. This
33 * gives the change to measure real performance of Surface compositor.
34 */
35public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
36    private final static long DURATION_TO_WARMUP_MS = 50;
37    private final static long DURATION_TO_MEASURE_ROUGH_MS = 500;
38    private final static long DURATION_TO_MEASURE_PRECISE_MS = 3000;
39    private final static Random mRandom = new Random();
40
41    private final Object mSurfaceLock = new Object();
42    private Surface mSurface;
43    private boolean mDrawNameOnReady = true;
44    private boolean mSurfaceWasChanged = false;
45    private String mName;
46    private Canvas mCanvas;
47
48    class ValidateThread extends Thread {
49        private double mFPS = 0.0f;
50        // Used to support early exit and prevent long computation.
51        private double mBadFPS;
52        private double mPerfectFPS;
53
54        ValidateThread(double badFPS, double perfectFPS) {
55            mBadFPS = badFPS;
56            mPerfectFPS = perfectFPS;
57        }
58
59        public void run() {
60            long startTime = System.currentTimeMillis();
61            while (System.currentTimeMillis() - startTime < DURATION_TO_WARMUP_MS) {
62                invalidateSurface(false);
63            }
64
65            startTime = System.currentTimeMillis();
66            long endTime;
67            int frameCnt = 0;
68            while (true) {
69                invalidateSurface(false);
70                endTime = System.currentTimeMillis();
71                ++frameCnt;
72                mFPS = (double)frameCnt * 1000.0 / (endTime - startTime);
73                if ((endTime - startTime) >= DURATION_TO_MEASURE_ROUGH_MS) {
74                    // Test if result looks too bad or perfect and stop early.
75                    if (mFPS <= mBadFPS || mFPS >= mPerfectFPS) {
76                        break;
77                    }
78                }
79                if ((endTime - startTime) >= DURATION_TO_MEASURE_PRECISE_MS) {
80                    break;
81                }
82            }
83        }
84
85        public double getFPS() {
86            return mFPS;
87        }
88    }
89
90    public CustomSurfaceView(Context context, String name) {
91        super(context);
92        mName = name;
93        getHolder().addCallback(this);
94    }
95
96    public void setMode(int pixelFormat, boolean drawNameOnReady) {
97        mDrawNameOnReady = drawNameOnReady;
98        getHolder().setFormat(pixelFormat);
99    }
100
101    public void acquireCanvas() {
102        synchronized (mSurfaceLock) {
103            if (mCanvas != null) {
104                throw new RuntimeException("Surface canvas was already acquired.");
105            }
106            if (mSurface != null) {
107                mCanvas = mSurface.lockCanvas(null);
108            }
109        }
110    }
111
112    public void releaseCanvas() {
113        synchronized (mSurfaceLock) {
114            if (mCanvas != null) {
115                if (mSurface == null) {
116                    throw new RuntimeException(
117                            "Surface was destroyed but canvas was not released.");
118                }
119                mSurface.unlockCanvasAndPost(mCanvas);
120                mCanvas = null;
121            }
122        }
123    }
124
125    /**
126     * Invalidate surface.
127     */
128    private void invalidateSurface(boolean drawSurfaceId) {
129        synchronized (mSurfaceLock) {
130            if (mSurface != null) {
131                Canvas canvas = mSurface.lockCanvas(null);
132                // Draw surface name for debug purpose only. This does not affect the test
133                // because it is drawn only during allocation.
134                if (drawSurfaceId) {
135                    int textSize = canvas.getHeight() / 24;
136                    Paint paint = new Paint();
137                    paint.setTextSize(textSize);
138                    int textWidth = (int)(paint.measureText(mName) + 0.5f);
139                    int x = mRandom.nextInt(canvas.getWidth() - textWidth);
140                    int y = textSize + mRandom.nextInt(canvas.getHeight() - textSize);
141                    // Create effect of fog to visually control correctness of composition.
142                    paint.setColor(0xFFFF8040);
143                    canvas.drawARGB(32, 255, 255, 255);
144                    canvas.drawText(mName, x, y, paint);
145                }
146                mSurface.unlockCanvasAndPost(canvas);
147            }
148        }
149    }
150
151    /**
152     * Wait until surface is created and ready to use or return immediately if surface
153     * already exists.
154     */
155    public void waitForSurfaceReady() {
156        synchronized (mSurfaceLock) {
157            if (mSurface == null) {
158                try {
159                    mSurfaceLock.wait(5000);
160                } catch(InterruptedException e) {
161                    e.printStackTrace();
162                }
163            }
164            if (mSurface == null)
165                throw new RuntimeException("Surface is not ready.");
166            mSurfaceWasChanged = false;
167        }
168    }
169
170    /**
171     * Wait until surface is destroyed or return immediately if surface does not exist.
172     */
173    public void waitForSurfaceDestroyed() {
174        synchronized (mSurfaceLock) {
175            if (mSurface != null) {
176                try {
177                    mSurfaceLock.wait(5000);
178                } catch(InterruptedException e) {
179                    e.printStackTrace();
180                }
181            }
182            if (mSurface != null)
183                throw new RuntimeException("Surface still exists.");
184            mSurfaceWasChanged = false;
185        }
186    }
187
188    /**
189     * Validate that surface has not been changed since waitForSurfaceReady or
190     * waitForSurfaceDestroyed.
191     */
192    public void validateSurfaceNotChanged() {
193        synchronized (mSurfaceLock) {
194            if (mSurfaceWasChanged) {
195                throw new RuntimeException("Surface was changed during the test execution.");
196            }
197        }
198    }
199
200    public double measureFPS(double badFPS, double perfectFPS) {
201        try {
202            ValidateThread validateThread = new ValidateThread(badFPS, perfectFPS);
203            validateThread.start();
204            validateThread.join();
205            return validateThread.getFPS();
206        } catch (InterruptedException e) {
207            throw new RuntimeException(e);
208        }
209    }
210
211    @Override
212    public void surfaceCreated(SurfaceHolder holder) {
213        synchronized (mSurfaceLock) {
214            mSurfaceWasChanged = true;
215        }
216    }
217
218    @Override
219    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
220        // This method is always called at least once, after surfaceCreated.
221        synchronized (mSurfaceLock) {
222            mSurface = holder.getSurface();
223            // We only need to invalidate the surface for the compositor performance test so that
224            // it gets included in the composition process. For allocation performance we
225            // don't need to invalidate surface and this allows us to remove non-necessary
226            // surface invalidation from the test.
227            if (mDrawNameOnReady) {
228                invalidateSurface(true);
229            }
230            mSurfaceWasChanged = true;
231            mSurfaceLock.notify();
232        }
233    }
234
235    @Override
236    public void surfaceDestroyed(SurfaceHolder holder) {
237        synchronized (mSurfaceLock) {
238            mSurface = null;
239            mSurfaceWasChanged = true;
240            mSurfaceLock.notify();
241        }
242    }
243}
244