diff options
author | Yury Khmel <khmel@google.com> | 2015-08-31 17:51:42 +0900 |
---|---|---|
committer | Yury Khmel <khmel@google.com> | 2015-09-02 12:44:04 +0900 |
commit | 9dbde7b09f2366d2a239b1a4c234d5cf2de51739 (patch) | |
tree | 0bb13f6873773366a7719a82ae0a9e4d024855c7 /tests/SurfaceComposition | |
parent | ddc453a342c4eab5b44dc1c3e96b767203725237 (diff) |
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
Diffstat (limited to 'tests/SurfaceComposition')
8 files changed, 1140 insertions, 0 deletions
diff --git a/tests/SurfaceComposition/Android.mk b/tests/SurfaceComposition/Android.mk new file mode 100644 index 000000000000..95f69f179c20 --- /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 000000000000..4c0a9b61fd8f --- /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 000000000000..254d707134aa --- /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 000000000000..d626f10b42cb --- /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 000000000000..0430662873cf --- /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 000000000000..c716daee2fa6 --- /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 000000000000..e3e1d34f193e --- /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 000000000000..6e9e7390c2c1 --- /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]); + } + } +} |