blob: 0430662873cff6b7a9dfebaf3804145ba047d49c [file] [log] [blame]
/*
* 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();
}
}
}