| /* | |
| * 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.pm.PackageManager; | |
| 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. | |
| private double mRefreshRate; | |
| private double mTargetFPS; | |
| private boolean mAndromeda; | |
| private int mWidth; | |
| private int mHeight; | |
| class CompositorScore { | |
| double mSurfaces; | |
| double mBandwidth; | |
| @Override | |
| public String toString() { | |
| return DOUBLE_FORMAT.format(mSurfaces) + " surfaces. " + | |
| "Bandwidth: " + getReadableMemory((long)mBandwidth) + "/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.mBandwidth = 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; | |
| } | |
| public boolean isAndromeda() { | |
| return mAndromeda; | |
| } | |
| @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); | |
| // Detect Andromeda devices by having free-form window management feature. | |
| mAndromeda = getPackageManager().hasSystemFeature( | |
| PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT); | |
| 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 platformName = mAndromeda ? "Andromeda" : "Android"; | |
| String info = platformName + ": 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() { | |
| mRefreshRate = getDisplay().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(); | |
| } | |
| } | |
| } | |
| } |