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