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