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