244 lines
8.5 KiB
Java
244 lines
8.5 KiB
Java
/*
|
|
* 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();
|
|
}
|
|
}
|
|
}
|