Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9dbde7b0 authored by Yury Khmel's avatar Yury Khmel
Browse files

SufaceComposition performance test.

Implement set of low-level tests to measure graphics performance.

Design and test result:
https://docs.google.com/a/google.com/document/d/1LYlUxjjmC2JBulAIIO8UVfvjeHWEALzgyUzqMMzwiGE/edit?usp=sharing

Change-Id: I48efbce5dcdac1b8caa2cd332777ce0b06d40ed2
parent ddc453a3
Loading
Loading
Loading
Loading
+34 −0
Original line number Diff line number Diff line
# 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.

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

# Don't include this package in any target
LOCAL_MODULE_TAGS := tests
# When built, explicitly put it in the data partition.
LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)

LOCAL_DEX_PREOPT := false

LOCAL_PROGUARD_ENABLED := disabled

LOCAL_SRC_FILES := $(call all-java-files-under, src)

LOCAL_PACKAGE_NAME := SurfaceComposition

LOCAL_SDK_VERSION := current

include $(BUILD_PACKAGE)
+36 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
 * 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.
 -->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="android.surfacecomposition">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <application android:theme="@style/noeffects">
        <uses-library android:name="android.test.runner" />
        <activity android:name="android.surfacecomposition.SurfaceCompositionMeasuringActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <uses-library android:name="android.test.runner" />
    </application>

    <!--  self-instrumenting test package. -->
    <instrumentation android:name="android.test.InstrumentationTestRunner"
                     android:targetPackage="android.surfacecomposition">
    </instrumentation>
</manifest>
+25 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
 * 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.
 -->
<resources>
    <style name="noeffects" parent="@android:style/Theme.Holo.NoActionBar.Fullscreen">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:fadingEdge">none</item>
        <item name="android:windowContentTransitions">false</item>
        <item name="android:windowAnimationStyle">@null</item>
    </style>
</resources>
+48 −0
Original line number Diff line number Diff line
/*
 * 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 android.content.Context;
import android.view.View;
import android.view.ViewGroup;

public class CustomLayout extends ViewGroup {
    public CustomLayout(Context context) {
        super(context);
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {
        private int mLeft, mTop, mRight, mBottom;

        public LayoutParams(int left, int top, int right, int bottom) {
            super(0, 0);
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            CustomLayout.LayoutParams lp = (CustomLayout.LayoutParams) child.getLayoutParams();
            child.layout(lp.mLeft, lp.mTop, lp.mRight, lp.mBottom);
        }
    }
}
+243 −0
Original line number Diff line number Diff line
/*
 * 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();
        }
    }
}
Loading