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
diff --git a/tests/SurfaceComposition/Android.mk b/tests/SurfaceComposition/Android.mk
new file mode 100644
index 0000000..95f69f1
--- /dev/null
+++ b/tests/SurfaceComposition/Android.mk
@@ -0,0 +1,34 @@
+# 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)
diff --git a/tests/SurfaceComposition/AndroidManifest.xml b/tests/SurfaceComposition/AndroidManifest.xml
new file mode 100644
index 0000000..4c0a9b6
--- /dev/null
+++ b/tests/SurfaceComposition/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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>
diff --git a/tests/SurfaceComposition/res/values/themes.xml b/tests/SurfaceComposition/res/values/themes.xml
new file mode 100644
index 0000000..254d707
--- /dev/null
+++ b/tests/SurfaceComposition/res/values/themes.xml
@@ -0,0 +1,25 @@
+<?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>
diff --git a/tests/SurfaceComposition/src/android/surfacecomposition/CustomLayout.java b/tests/SurfaceComposition/src/android/surfacecomposition/CustomLayout.java
new file mode 100644
index 0000000..d626f10
--- /dev/null
+++ b/tests/SurfaceComposition/src/android/surfacecomposition/CustomLayout.java
@@ -0,0 +1,48 @@
+/*
+ * 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);
+ }
+ }
+}
diff --git a/tests/SurfaceComposition/src/android/surfacecomposition/CustomSurfaceView.java b/tests/SurfaceComposition/src/android/surfacecomposition/CustomSurfaceView.java
new file mode 100644
index 0000000..0430662
--- /dev/null
+++ b/tests/SurfaceComposition/src/android/surfacecomposition/CustomSurfaceView.java
@@ -0,0 +1,243 @@
+/*
+ * 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();
+ }
+ }
+}
diff --git a/tests/SurfaceComposition/src/android/surfacecomposition/MemoryAccessTask.java b/tests/SurfaceComposition/src/android/surfacecomposition/MemoryAccessTask.java
new file mode 100644
index 0000000..c716dae
--- /dev/null
+++ b/tests/SurfaceComposition/src/android/surfacecomposition/MemoryAccessTask.java
@@ -0,0 +1,71 @@
+/*
+ * 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.util.Log;
+
+/**
+ * This task will simulate CPU activity by consuming memory bandwidth from the system.
+ * Note: On most system the CPU and GPU will share the same memory.
+ */
+public class MemoryAccessTask {
+ private final static String TAG = "MemoryAccessTask";
+ private final static int BUFFER_SIZE = 32 * 1024 * 1024;
+ private final static int BUFFER_STEP = 256;
+ private boolean mStopRequested;
+ private WorkThread mThread;
+ private final Object mLock = new Object();
+
+ public class WorkThread extends Thread {
+ public void run() {
+ byte[] memory = new byte[BUFFER_SIZE];
+ while (true) {
+ synchronized (mLock) {
+ if (mStopRequested) {
+ break;
+ }
+ }
+ long result = 0;
+ for (int index = 0; index < BUFFER_SIZE; index += BUFFER_STEP) {
+ result += ++memory[index];
+ }
+ Log.v(TAG, "Processing...:" + result);
+ }
+ }
+ }
+
+ public void start() {
+ if (mThread != null) {
+ throw new RuntimeException("Work thread is already started");
+ }
+ mStopRequested = false;
+ mThread = new WorkThread();
+ mThread.start();
+ }
+
+ public void stop() {
+ if (mThread != null) {
+ synchronized (mLock) {
+ mStopRequested = true;
+ }
+ try {
+ mThread.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionMeasuringActivity.java b/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionMeasuringActivity.java
new file mode 100644
index 0000000..e3e1d34
--- /dev/null
+++ b/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionMeasuringActivity.java
@@ -0,0 +1,602 @@
+/*
+ * 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.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.view.Display;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+/**
+ * This activity is designed to measure peformance scores of Android surfaces.
+ * It can work in two modes. In first mode functionality of this activity is
+ * invoked from Cts test (SurfaceCompositionTest). This activity can also be
+ * used in manual mode as a normal app. Different pixel formats are supported.
+ *
+ * measureCompositionScore(pixelFormat)
+ * This test measures surface compositor performance which shows how many
+ * surfaces of specific format surface compositor can combine without dropping
+ * frames. We allow one dropped frame per half second.
+ *
+ * measureAllocationScore(pixelFormat)
+ * This test measures surface allocation/deallocation performance. It shows
+ * how many surface lifecycles (creation, destruction) can be done per second.
+ *
+ * In manual mode, which activated by pressing button 'Compositor speed' or
+ * 'Allocator speed', all possible pixel format are tested and combined result
+ * is displayed in text view. Additional system information such as memory
+ * status, display size and surface format is also displayed and regulary
+ * updated.
+ */
+public class SurfaceCompositionMeasuringActivity extends Activity implements OnClickListener {
+ private final static int MIN_NUMBER_OF_SURFACES = 15;
+ private final static int MAX_NUMBER_OF_SURFACES = 40;
+ private final static int WARM_UP_ALLOCATION_CYCLES = 2;
+ private final static int MEASURE_ALLOCATION_CYCLES = 5;
+ private final static int TEST_COMPOSITOR = 1;
+ private final static int TEST_ALLOCATION = 2;
+ private final static float MIN_REFRESH_RATE_SUPPORTED = 50.0f;
+
+ private final static DecimalFormat DOUBLE_FORMAT = new DecimalFormat("#.00");
+ // Possible selection in pixel format selector.
+ private final static int[] PIXEL_FORMATS = new int[] {
+ PixelFormat.TRANSLUCENT,
+ PixelFormat.TRANSPARENT,
+ PixelFormat.OPAQUE,
+ PixelFormat.RGBA_8888,
+ PixelFormat.RGBX_8888,
+ PixelFormat.RGB_888,
+ PixelFormat.RGB_565,
+ };
+
+
+ private List<CustomSurfaceView> mViews = new ArrayList<CustomSurfaceView>();
+ private Button mMeasureCompositionButton;
+ private Button mMeasureAllocationButton;
+ private Spinner mPixelFormatSelector;
+ private TextView mResultView;
+ private TextView mSystemInfoView;
+ private final Object mLockResumed = new Object();
+ private boolean mResumed;
+
+ // Drop one frame per half second.
+ // TODO(khmel)
+ // Add a feature flag and set the target FPS dependent on the target system as e.g.:
+ // 59FPS for MULTI_WINDOW and 54 otherwise (to satisfy the default lax Android requirements).
+ private double mRefreshRate;
+ private double mTargetFPS;
+
+ private int mWidth;
+ private int mHeight;
+
+ class CompositorScore {
+ double mSurfaces;
+ double mBitrate;
+
+ @Override
+ public String toString() {
+ return DOUBLE_FORMAT.format(mSurfaces) + " surfaces. " +
+ "Bitrate: " + getReadableMemory((long)mBitrate) + "/s";
+ }
+ }
+
+ /**
+ * Measure performance score.
+ *
+ * @return biggest possible number of visible surfaces which surface
+ * compositor can handle.
+ */
+ public CompositorScore measureCompositionScore(int pixelFormat) {
+ waitForActivityResumed();
+ //MemoryAccessTask memAccessTask = new MemoryAccessTask();
+ //memAccessTask.start();
+ // Destroy any active surface.
+ configureSurfacesAndWait(0, pixelFormat, false);
+ CompositorScore score = new CompositorScore();
+ score.mSurfaces = measureCompositionScore(new Measurement(0, 60.0),
+ new Measurement(mViews.size() + 1, 0.0f), pixelFormat);
+ // Assume 32 bits per pixel.
+ score.mBitrate = score.mSurfaces * mTargetFPS * mWidth * mHeight * 4.0;
+ //memAccessTask.stop();
+ return score;
+ }
+
+ static class AllocationScore {
+ double mMedian;
+ double mMin;
+ double mMax;
+
+ @Override
+ public String toString() {
+ return DOUBLE_FORMAT.format(mMedian) + " (min:" + DOUBLE_FORMAT.format(mMin) +
+ ", max:" + DOUBLE_FORMAT.format(mMax) + ") surface allocations per second";
+ }
+ }
+
+ public AllocationScore measureAllocationScore(int pixelFormat) {
+ waitForActivityResumed();
+ AllocationScore score = new AllocationScore();
+ for (int i = 0; i < MEASURE_ALLOCATION_CYCLES + WARM_UP_ALLOCATION_CYCLES; ++i) {
+ long time1 = System.currentTimeMillis();
+ configureSurfacesAndWait(MIN_NUMBER_OF_SURFACES, pixelFormat, false);
+ acquireSurfacesCanvas();
+ long time2 = System.currentTimeMillis();
+ releaseSurfacesCanvas();
+ configureSurfacesAndWait(0, pixelFormat, false);
+ // Give SurfaceFlinger some time to rebuild the layer stack and release the buffers.
+ try {
+ Thread.sleep(500);
+ } catch(InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (i < WARM_UP_ALLOCATION_CYCLES) {
+ // This is warm-up cycles, ignore result so far.
+ continue;
+ }
+ double speed = MIN_NUMBER_OF_SURFACES * 1000.0 / (time2 - time1);
+ score.mMedian += speed / MEASURE_ALLOCATION_CYCLES;
+ if (i == WARM_UP_ALLOCATION_CYCLES) {
+ score.mMin = speed;
+ score.mMax = speed;
+ } else {
+ score.mMin = Math.min(score.mMin, speed);
+ score.mMax = Math.max(score.mMax, speed);
+ }
+ }
+
+ return score;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == mMeasureCompositionButton) {
+ doTest(TEST_COMPOSITOR);
+ } else if (view == mMeasureAllocationButton) {
+ doTest(TEST_ALLOCATION);
+ }
+ }
+
+ private void doTest(final int test) {
+ enableControls(false);
+ final int pixelFormat = PIXEL_FORMATS[mPixelFormatSelector.getSelectedItemPosition()];
+ new Thread() {
+ public void run() {
+ final StringBuffer sb = new StringBuffer();
+ switch (test) {
+ case TEST_COMPOSITOR: {
+ sb.append("Compositor score:");
+ CompositorScore score = measureCompositionScore(pixelFormat);
+ sb.append("\n " + getPixelFormatInfo(pixelFormat) + ":" +
+ score + ".");
+ }
+ break;
+ case TEST_ALLOCATION: {
+ sb.append("Allocation score:");
+ AllocationScore score = measureAllocationScore(pixelFormat);
+ sb.append("\n " + getPixelFormatInfo(pixelFormat) + ":" +
+ score + ".");
+ }
+ break;
+ }
+ runOnUiThreadAndWait(new Runnable() {
+ public void run() {
+ mResultView.setText(sb.toString());
+ enableControls(true);
+ updateSystemInfo(pixelFormat);
+ }
+ });
+ }
+ }.start();
+ }
+
+ /**
+ * Wait until activity is resumed.
+ */
+ public void waitForActivityResumed() {
+ synchronized (mLockResumed) {
+ if (!mResumed) {
+ try {
+ mLockResumed.wait(10000);
+ } catch (InterruptedException e) {
+ }
+ }
+ if (!mResumed) {
+ throw new RuntimeException("Activity was not resumed");
+ }
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ detectRefreshRate();
+
+ // To layouts in parent. First contains list of Surfaces and second
+ // controls. Controls stay on top.
+ RelativeLayout rootLayout = new RelativeLayout(this);
+ rootLayout.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ CustomLayout layout = new CustomLayout(this);
+ layout.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ Rect rect = new Rect();
+ getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
+ mWidth = rect.right;
+ mHeight = rect.bottom;
+ long maxMemoryPerSurface = roundToNextPowerOf2(mWidth) * roundToNextPowerOf2(mHeight) * 4;
+ // Use 75% of available memory.
+ int surfaceCnt = (int)((getMemoryInfo().availMem * 3) / (4 * maxMemoryPerSurface));
+ if (surfaceCnt < MIN_NUMBER_OF_SURFACES) {
+ throw new RuntimeException("Not enough memory to allocate " +
+ MIN_NUMBER_OF_SURFACES + " surfaces.");
+ }
+ if (surfaceCnt > MAX_NUMBER_OF_SURFACES) {
+ surfaceCnt = MAX_NUMBER_OF_SURFACES;
+ }
+
+ LinearLayout controlLayout = new LinearLayout(this);
+ controlLayout.setOrientation(LinearLayout.VERTICAL);
+ controlLayout.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ mMeasureCompositionButton = createButton("Compositor speed.", controlLayout);
+ mMeasureAllocationButton = createButton("Allocation speed", controlLayout);
+
+ String[] pixelFomats = new String[PIXEL_FORMATS.length];
+ for (int i = 0; i < pixelFomats.length; ++i) {
+ pixelFomats[i] = getPixelFormatInfo(PIXEL_FORMATS[i]);
+ }
+ mPixelFormatSelector = new Spinner(this);
+ ArrayAdapter<String> pixelFormatSelectorAdapter =
+ new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, pixelFomats);
+ pixelFormatSelectorAdapter.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mPixelFormatSelector.setAdapter(pixelFormatSelectorAdapter);
+ mPixelFormatSelector.setLayoutParams(new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ controlLayout.addView(mPixelFormatSelector);
+
+ mResultView = new TextView(this);
+ mResultView.setBackgroundColor(0);
+ mResultView.setText("Press button to start test.");
+ mResultView.setLayoutParams(new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ controlLayout.addView(mResultView);
+
+ mSystemInfoView = new TextView(this);
+ mSystemInfoView.setBackgroundColor(0);
+ mSystemInfoView.setLayoutParams(new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ controlLayout.addView(mSystemInfoView);
+
+ for (int i = 0; i < surfaceCnt; ++i) {
+ CustomSurfaceView view = new CustomSurfaceView(this, "Surface:" + i);
+ // Create all surfaces overlapped in order to prevent SurfaceFlinger
+ // to filter out surfaces by optimization in case surface is opaque.
+ // In case surface is transparent it will be drawn anyway. Note that first
+ // surface covers whole screen and must stand below other surfaces. Z order of
+ // layers is not predictable and there is only one way to force first
+ // layer to be below others is to mark it as media and all other layers
+ // to mark as media overlay.
+ if (i == 0) {
+ view.setLayoutParams(new CustomLayout.LayoutParams(0, 0, mWidth, mHeight));
+ view.setZOrderMediaOverlay(false);
+ } else {
+ // Z order of other layers is not predefined so make offset on x and reverse
+ // offset on y to make sure that surface is visible in any layout.
+ int x = i;
+ int y = (surfaceCnt - i);
+ view.setLayoutParams(new CustomLayout.LayoutParams(x, y, x + mWidth, y + mHeight));
+ view.setZOrderMediaOverlay(true);
+ }
+ view.setVisibility(View.INVISIBLE);
+ layout.addView(view);
+ mViews.add(view);
+ }
+
+ rootLayout.addView(layout);
+ rootLayout.addView(controlLayout);
+
+ setContentView(rootLayout);
+ }
+
+ private Button createButton(String caption, LinearLayout layout) {
+ Button button = new Button(this);
+ button.setText(caption);
+ button.setLayoutParams(new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ button.setOnClickListener(this);
+ layout.addView(button);
+ return button;
+ }
+
+ private void enableControls(boolean enabled) {
+ mMeasureCompositionButton.setEnabled(enabled);
+ mMeasureAllocationButton.setEnabled(enabled);
+ mPixelFormatSelector.setEnabled(enabled);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ updateSystemInfo(PixelFormat.UNKNOWN);
+
+ synchronized (mLockResumed) {
+ mResumed = true;
+ mLockResumed.notifyAll();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ synchronized (mLockResumed) {
+ mResumed = false;
+ }
+ }
+
+ class Measurement {
+ Measurement(int surfaceCnt, double fps) {
+ mSurfaceCnt = surfaceCnt;
+ mFPS = fps;
+ }
+
+ public final int mSurfaceCnt;
+ public final double mFPS;
+ }
+
+ private double measureCompositionScore(Measurement ok, Measurement fail, int pixelFormat) {
+ if (ok.mSurfaceCnt + 1 == fail.mSurfaceCnt) {
+ // Interpolate result.
+ double fraction = (mTargetFPS - fail.mFPS) / (ok.mFPS - fail.mFPS);
+ return ok.mSurfaceCnt + fraction;
+ }
+
+ int medianSurfaceCnt = (ok.mSurfaceCnt + fail.mSurfaceCnt) / 2;
+ Measurement median = new Measurement(medianSurfaceCnt,
+ measureFPS(medianSurfaceCnt, pixelFormat));
+
+ if (median.mFPS >= mTargetFPS) {
+ return measureCompositionScore(median, fail, pixelFormat);
+ } else {
+ return measureCompositionScore(ok, median, pixelFormat);
+ }
+ }
+
+ private double measureFPS(int surfaceCnt, int pixelFormat) {
+ configureSurfacesAndWait(surfaceCnt, pixelFormat, true);
+ // At least one view is visible and it is enough to update only
+ // one overlapped surface in order to force SurfaceFlinger to send
+ // all surfaces to compositor.
+ double fps = mViews.get(0).measureFPS(mRefreshRate * 0.8, mRefreshRate * 0.999);
+
+ // Make sure that surface configuration was not changed.
+ validateSurfacesNotChanged();
+
+ return fps;
+ }
+
+ private void waitForSurfacesConfigured(final int pixelFormat) {
+ for (int i = 0; i < mViews.size(); ++i) {
+ CustomSurfaceView view = mViews.get(i);
+ if (view.getVisibility() == View.VISIBLE) {
+ view.waitForSurfaceReady();
+ } else {
+ view.waitForSurfaceDestroyed();
+ }
+ }
+ runOnUiThreadAndWait(new Runnable() {
+ @Override
+ public void run() {
+ updateSystemInfo(pixelFormat);
+ }
+ });
+ }
+
+ private void validateSurfacesNotChanged() {
+ for (int i = 0; i < mViews.size(); ++i) {
+ CustomSurfaceView view = mViews.get(i);
+ view.validateSurfaceNotChanged();
+ }
+ }
+
+ private void configureSurfaces(int surfaceCnt, int pixelFormat, boolean invalidate) {
+ for (int i = 0; i < mViews.size(); ++i) {
+ CustomSurfaceView view = mViews.get(i);
+ if (i < surfaceCnt) {
+ view.setMode(pixelFormat, invalidate);
+ view.setVisibility(View.VISIBLE);
+ } else {
+ view.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ private void configureSurfacesAndWait(final int surfaceCnt, final int pixelFormat,
+ final boolean invalidate) {
+ runOnUiThreadAndWait(new Runnable() {
+ @Override
+ public void run() {
+ configureSurfaces(surfaceCnt, pixelFormat, invalidate);
+ }
+ });
+ waitForSurfacesConfigured(pixelFormat);
+ }
+
+ private void acquireSurfacesCanvas() {
+ for (int i = 0; i < mViews.size(); ++i) {
+ CustomSurfaceView view = mViews.get(i);
+ view.acquireCanvas();
+ }
+ }
+
+ private void releaseSurfacesCanvas() {
+ for (int i = 0; i < mViews.size(); ++i) {
+ CustomSurfaceView view = mViews.get(i);
+ view.releaseCanvas();
+ }
+ }
+
+ private static String getReadableMemory(long bytes) {
+ long unit = 1024;
+ if (bytes < unit) {
+ return bytes + " B";
+ }
+ int exp = (int) (Math.log(bytes) / Math.log(unit));
+ return String.format("%.1f %sB", bytes / Math.pow(unit, exp),
+ "KMGTPE".charAt(exp-1));
+ }
+
+ private MemoryInfo getMemoryInfo() {
+ ActivityManager activityManager = (ActivityManager)
+ getSystemService(ACTIVITY_SERVICE);
+ MemoryInfo memInfo = new MemoryInfo();
+ activityManager.getMemoryInfo(memInfo);
+ return memInfo;
+ }
+
+ private void updateSystemInfo(int pixelFormat) {
+ int visibleCnt = 0;
+ for (int i = 0; i < mViews.size(); ++i) {
+ if (mViews.get(i).getVisibility() == View.VISIBLE) {
+ ++visibleCnt;
+ }
+ }
+
+ MemoryInfo memInfo = getMemoryInfo();
+ String info = "Available " +
+ getReadableMemory(memInfo.availMem) + " from " +
+ getReadableMemory(memInfo.totalMem) + ".\nVisible " +
+ visibleCnt + " from " + mViews.size() + " " +
+ getPixelFormatInfo(pixelFormat) + " surfaces.\n" +
+ "View size: " + mWidth + "x" + mHeight +
+ ". Refresh rate: " + DOUBLE_FORMAT.format(mRefreshRate) + ".";
+ mSystemInfoView.setText(info);
+ }
+
+ private void detectRefreshRate() {
+ WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
+ mRefreshRate = wm.getDefaultDisplay().getRefreshRate();
+ if (mRefreshRate < MIN_REFRESH_RATE_SUPPORTED)
+ throw new RuntimeException("Unsupported display refresh rate: " + mRefreshRate);
+ mTargetFPS = mRefreshRate - 2.0f;
+ }
+
+ private int roundToNextPowerOf2(int value) {
+ --value;
+ value |= value >> 1;
+ value |= value >> 2;
+ value |= value >> 4;
+ value |= value >> 8;
+ value |= value >> 16;
+ return value + 1;
+ }
+
+ public static String getPixelFormatInfo(int pixelFormat) {
+ switch (pixelFormat) {
+ case PixelFormat.TRANSLUCENT:
+ return "TRANSLUCENT";
+ case PixelFormat.TRANSPARENT:
+ return "TRANSPARENT";
+ case PixelFormat.OPAQUE:
+ return "OPAQUE";
+ case PixelFormat.RGBA_8888:
+ return "RGBA_8888";
+ case PixelFormat.RGBX_8888:
+ return "RGBX_8888";
+ case PixelFormat.RGB_888:
+ return "RGB_888";
+ case PixelFormat.RGB_565:
+ return "RGB_565";
+ default:
+ return "PIX.FORMAT:" + pixelFormat;
+ }
+ }
+
+ /**
+ * A helper that executes a task in the UI thread and waits for its completion.
+ *
+ * @param task - task to execute.
+ */
+ private void runOnUiThreadAndWait(Runnable task) {
+ new UIExecutor(task);
+ }
+
+ class UIExecutor implements Runnable {
+ private final Object mLock = new Object();
+ private Runnable mTask;
+ private boolean mDone = false;
+
+ UIExecutor(Runnable task) {
+ mTask = task;
+ mDone = false;
+ runOnUiThread(this);
+ synchronized (mLock) {
+ while (!mDone) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public void run() {
+ mTask.run();
+ synchronized (mLock) {
+ mDone = true;
+ mLock.notify();
+ }
+ }
+ }
+}
diff --git a/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionTest.java b/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionTest.java
new file mode 100644
index 0000000..6e9e739
--- /dev/null
+++ b/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.graphics.PixelFormat;
+import android.surfacecomposition.SurfaceCompositionMeasuringActivity.AllocationScore;
+import android.surfacecomposition.SurfaceCompositionMeasuringActivity.CompositorScore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+public class SurfaceCompositionTest extends
+ ActivityInstrumentationTestCase2<SurfaceCompositionMeasuringActivity> {
+ private final static String TAG = "SurfaceCompositionTest";
+
+ // Pass threshold for major pixel formats.
+ private final static int[] TEST_PIXEL_FORMATS = new int[] {
+ PixelFormat.TRANSLUCENT,
+ PixelFormat.OPAQUE,
+ };
+
+ // Based on Nexus 9 performance which is usually < 9.0.
+ private final static double[] MIN_ACCEPTED_COMPOSITION_SCORE = new double[] {
+ 8.0,
+ 8.0,
+ };
+
+ // Based on Nexus 6 performance which is usually < 28.0.
+ private final static double[] MIN_ACCEPTED_ALLOCATION_SCORE = new double[] {
+ 20.0,
+ 20.0,
+ };
+
+ public SurfaceCompositionTest() {
+ super(SurfaceCompositionMeasuringActivity.class);
+ }
+
+ private void testRestoreContexts() {
+ }
+
+ @SmallTest
+ public void testSurfaceCompositionPerformance() {
+ for (int i = 0; i < TEST_PIXEL_FORMATS.length; ++i) {
+ int pixelFormat = TEST_PIXEL_FORMATS[i];
+ String formatName = SurfaceCompositionMeasuringActivity.getPixelFormatInfo(pixelFormat);
+ CompositorScore score = getActivity().measureCompositionScore(pixelFormat);
+ Log.i(TAG, "testSurfaceCompositionPerformance(" + formatName + ") = " + score);
+ assertTrue("Device does not support surface(" + formatName + ") composition " +
+ "performance score. " + score.mSurfaces + " < " +
+ MIN_ACCEPTED_COMPOSITION_SCORE[i] + ".",
+ score.mSurfaces >= MIN_ACCEPTED_COMPOSITION_SCORE[i]);
+ }
+ }
+
+ @SmallTest
+ public void testSurfaceAllocationPerformance() {
+ for (int i = 0; i < TEST_PIXEL_FORMATS.length; ++i) {
+ int pixelFormat = TEST_PIXEL_FORMATS[i];
+ String formatName = SurfaceCompositionMeasuringActivity.getPixelFormatInfo(pixelFormat);
+ AllocationScore score = getActivity().measureAllocationScore(pixelFormat);
+ Log.i(TAG, "testSurfaceAllocationPerformance(" + formatName + ") = " + score);
+ assertTrue("Device does not support surface(" + formatName + ") allocation " +
+ "performance score. " + score.mMedian + " < " +
+ MIN_ACCEPTED_ALLOCATION_SCORE[i] + ".",
+ score.mMedian >= MIN_ACCEPTED_ALLOCATION_SCORE[i]);
+ }
+ }
+}