/* * 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(); } } }