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.util.Random;
19 
20 import android.content.Context;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.view.Surface;
24 import android.view.SurfaceHolder;
25 import 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  */
35 public 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 
ValidateThread(double badFPS, double perfectFPS)54         ValidateThread(double badFPS, double perfectFPS) {
55             mBadFPS = badFPS;
56             mPerfectFPS = perfectFPS;
57         }
58 
run()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 
getFPS()85         public double getFPS() {
86             return mFPS;
87         }
88     }
89 
CustomSurfaceView(Context context, String name)90     public CustomSurfaceView(Context context, String name) {
91         super(context);
92         mName = name;
93         getHolder().addCallback(this);
94     }
95 
setMode(int pixelFormat, boolean drawNameOnReady)96     public void setMode(int pixelFormat, boolean drawNameOnReady) {
97         mDrawNameOnReady = drawNameOnReady;
98         getHolder().setFormat(pixelFormat);
99     }
100 
acquireCanvas()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 
releaseCanvas()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      */
invalidateSurface(boolean drawSurfaceId)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      */
waitForSurfaceReady()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      */
waitForSurfaceDestroyed()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      */
validateSurfaceNotChanged()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 
measureFPS(double badFPS, double perfectFPS)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
surfaceCreated(SurfaceHolder holder)212     public void surfaceCreated(SurfaceHolder holder) {
213         synchronized (mSurfaceLock) {
214             mSurfaceWasChanged = true;
215         }
216     }
217 
218     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)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
surfaceDestroyed(SurfaceHolder holder)236     public void surfaceDestroyed(SurfaceHolder holder) {
237         synchronized (mSurfaceLock) {
238             mSurface = null;
239             mSurfaceWasChanged = true;
240             mSurfaceLock.notify();
241         }
242     }
243 }
244