summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/GamePerformance/Android.mk39
-rw-r--r--tests/GamePerformance/AndroidManifest.xml40
-rw-r--r--tests/GamePerformance/res/values/themes.xml25
-rw-r--r--tests/GamePerformance/src/android/gameperformance/ATraceRunner.java94
-rw-r--r--tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java91
-rw-r--r--tests/GamePerformance/src/android/gameperformance/CustomSurfaceView.java190
-rw-r--r--tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java132
-rw-r--r--tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java87
-rw-r--r--tests/GamePerformance/src/android/gameperformance/GraphicBufferMetrics.java530
-rw-r--r--tests/GamePerformance/src/android/gameperformance/Utils.java31
10 files changed, 1259 insertions, 0 deletions
diff --git a/tests/GamePerformance/Android.mk b/tests/GamePerformance/Android.mk
new file mode 100644
index 000000000000..58654de34029
--- /dev/null
+++ b/tests/GamePerformance/Android.mk
@@ -0,0 +1,39 @@
+# Copyright (C) 2018 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
+
+LOCAL_DEX_PREOPT := false
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+
+LOCAL_JAVA_LIBRARIES := android.test.base android.test.runner
+
+LOCAL_PACKAGE_NAME := GamePerformance
+
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_CERTIFICATE := platform
+
+
+include $(BUILD_PACKAGE)
diff --git a/tests/GamePerformance/AndroidManifest.xml b/tests/GamePerformance/AndroidManifest.xml
new file mode 100644
index 000000000000..b331e2c07e14
--- /dev/null
+++ b/tests/GamePerformance/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2018 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.gameperformance">
+ <uses-sdk android:minSdkVersion="25"/>
+ <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <application android:theme="@style/noeffects">
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="android.gameperformance.GamePerformanceActivity" >
+ <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.gameperformance">
+ </instrumentation>
+</manifest>
diff --git a/tests/GamePerformance/res/values/themes.xml b/tests/GamePerformance/res/values/themes.xml
new file mode 100644
index 000000000000..63130717fe72
--- /dev/null
+++ b/tests/GamePerformance/res/values/themes.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2018 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/GamePerformance/src/android/gameperformance/ATraceRunner.java b/tests/GamePerformance/src/android/gameperformance/ATraceRunner.java
new file mode 100644
index 000000000000..25754fd79a72
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/ATraceRunner.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 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.gameperformance;
+
+import java.io.BufferedReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import android.app.Instrumentation;
+import android.os.AsyncTask;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+/**
+ * Helper that runs atrace command for required duration and category. Results are read from
+ * the output of atrace and serialized to the provided file. We cannot use direct atrace to
+ * file because atrace is executed in UI automator context and analysis is done in test context.
+ * In last case output file is not accessible from the both contexts.
+ */
+public class ATraceRunner extends AsyncTask<Void, Integer, Boolean>{
+ private final static String TAG = "ATraceRunner";
+
+ // Report that atrace is done.
+ public interface Delegate {
+ public void onProcessed(boolean success);
+ }
+
+ private final Instrumentation mInstrumentation;
+ private final String mOutput;
+ private final int mTimeInSeconds;
+ private final String mCategory;
+ private final Delegate mDelegate;
+
+ public ATraceRunner(Instrumentation instrumentation,
+ String output,
+ int timeInSeconds,
+ String category,
+ Delegate delegate) {
+ mInstrumentation = instrumentation;
+ mOutput = output;
+ mTimeInSeconds = timeInSeconds;
+ mCategory = category;
+ mDelegate = delegate;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ BufferedReader bufferedReader = null;
+ FileWriter writer = null;
+ try {
+ // Run the command.
+ final String cmd = "atrace -t " + mTimeInSeconds + " " + mCategory;
+ Log.i(TAG, "Running atrace... " + cmd);
+ writer = new FileWriter(mOutput);
+ final ParcelFileDescriptor fd =
+ mInstrumentation.getUiAutomation().executeShellCommand(cmd);
+ bufferedReader = new BufferedReader(
+ new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(fd)));
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ writer.write(line);
+ writer.write("\n");
+ }
+ Log.i(TAG, "Running atrace... DONE");
+ return true;
+ } catch (IOException e) {
+ Log.i(TAG, "atrace failed", e);
+ return false;
+ } finally {
+ Utils.closeQuietly(bufferedReader);
+ Utils.closeQuietly(writer);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ mDelegate.onProcessed(result);
+ }
+
+}
diff --git a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java
new file mode 100644
index 000000000000..2b37280ae9b5
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 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.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.content.Context;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+
+public class CustomOpenGLView extends GLSurfaceView {
+ private Random mRandom;
+ private List<Long> mFrameTimes;
+
+ public CustomOpenGLView(Context context) {
+ super(context);
+
+ mRandom = new Random();
+ mFrameTimes = new ArrayList<Long>();
+
+ setEGLContextClientVersion(2);
+
+ setRenderer(new GLSurfaceView.Renderer() {
+ @Override
+ public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+ GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+ gl.glClearDepthf(1.0f);
+ gl.glEnable(GL10.GL_DEPTH_TEST);
+ gl.glDepthFunc(GL10.GL_LEQUAL);
+
+ gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
+ GL10.GL_NICEST); }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl, int width, int height) {
+ GLES20.glViewport(0, 0, width, height);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ GLES20.glClearColor(
+ mRandom.nextFloat(), mRandom.nextFloat(), mRandom.nextFloat(), 1.0f);
+ gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
+ synchronized (mFrameTimes) {
+ mFrameTimes.add(System.currentTimeMillis());
+ }
+ }
+ });
+ setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ }
+
+ /**
+ * Resets frame times in order to calculate fps for different test pass.
+ */
+ public void resetFrameTimes() {
+ synchronized (mFrameTimes) {
+ mFrameTimes.clear();
+ }
+ }
+
+ /**
+ * Returns current fps based on collected frame times.
+ */
+ public double getFps() {
+ synchronized (mFrameTimes) {
+ if (mFrameTimes.size() < 2) {
+ return 0.0f;
+ }
+ return 1000.0 * mFrameTimes.size() /
+ (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0));
+ }
+ }
+}
diff --git a/tests/GamePerformance/src/android/gameperformance/CustomSurfaceView.java b/tests/GamePerformance/src/android/gameperformance/CustomSurfaceView.java
new file mode 100644
index 000000000000..56161362808c
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/CustomSurfaceView.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2018 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.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Trace;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+/**
+ * Minimal SurfaceView that sends buffer on request.
+ */
+public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
+ // Tag for trace when buffer is requested.
+ public final static String LOCAL_REQUEST_BUFFER = "localRequestBuffer";
+ // Tag for trace when buffer is posted.
+ public final static String LOCAL_POST_BUFFER = "localPostBuffer";
+
+ private final Object mSurfaceLock = new Object();
+ // Keeps frame times. Used to calculate fps.
+ private List<Long> mFrameTimes;
+ // Surface to send.
+ private Surface mSurface;
+ private Handler mHandler;
+
+ private Runnable mInvalidateSurfaceTask = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mSurfaceLock) {
+ if (mSurface == null) {
+ return;
+ }
+ invalidateSurface(true, true);
+ mHandler.post(this);
+ }
+ }
+ };
+
+ public CustomSurfaceView(Context context) {
+ super(context);
+ mFrameTimes = new ArrayList<Long>();
+ getHolder().addCallback(this);
+ getHolder().setFormat(PixelFormat.OPAQUE);
+
+ HandlerThread thread = new HandlerThread("SurfaceInvalidator");
+ thread.start();
+ mHandler = new Handler(thread.getLooper());
+ }
+
+ /**
+ * Resets frame times in order to calculate fps for different test pass.
+ */
+ public void resetFrameTimes() {
+ synchronized (mSurfaceLock) {
+ mFrameTimes.clear();
+ }
+ }
+
+ /**
+ * Returns current fps based on collected frame times.
+ */
+ public double getFps() {
+ synchronized (mSurfaceLock) {
+ if (mFrameTimes.size() < 2) {
+ return 0.0f;
+ }
+ return 1000.0 * mFrameTimes.size() /
+ (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0));
+ }
+ }
+
+ /**
+ * Invalidates surface.
+ * @param traceCalls set to true in case we need register trace calls. Not used for warm-up.
+ * @param drawFps perform drawing current fps on surface to have some payload on surface.
+ */
+ public void invalidateSurface(boolean traceCalls, boolean drawFps) {
+ synchronized (mSurfaceLock) {
+ if (mSurface == null) {
+ throw new IllegalStateException("Surface is not ready");
+ }
+ if (traceCalls) {
+ Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, LOCAL_REQUEST_BUFFER);
+ }
+ Canvas canvas = mSurface.lockCanvas(null);
+ if (traceCalls) {
+ Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
+ }
+
+ if (drawFps) {
+ int textSize = canvas.getHeight() / 24;
+ Paint paint = new Paint();
+ paint.setTextSize(textSize);
+ paint.setColor(0xFFFF8040);
+ canvas.drawARGB(92, 255, 255, 255);
+ canvas.drawText("FPS: " + String.format("%.2f", getFps()), 10, 300, paint);
+ }
+
+ if (traceCalls) {
+ Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, LOCAL_POST_BUFFER);
+ }
+ mSurface.unlockCanvasAndPost(canvas);
+ if (traceCalls) {
+ Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
+ }
+
+ mFrameTimes.add(System.currentTimeMillis());
+ }
+ }
+
+ /**
+ * 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 IllegalStateException("Surface is not ready.");
+ }
+ }
+
+ /**
+ * Waits 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) {
+ }
+ }
+ if (mSurface != null)
+ throw new IllegalStateException("Surface still exists.");
+ }
+ }
+
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ }
+
+ @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();
+ mSurfaceLock.notify();
+ mHandler.post(mInvalidateSurfaceTask);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ synchronized (mSurfaceLock) {
+ mHandler.removeCallbacks(mInvalidateSurfaceTask);
+ mSurface = null;
+ mSurfaceLock.notify();
+ }
+ }
+}
diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java
new file mode 100644
index 000000000000..b0e6196b53d7
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2018 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.gameperformance;
+
+import java.util.concurrent.CountDownLatch;
+
+import android.app.Activity;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.RelativeLayout;
+
+/**
+ * Minimal activity that holds SurfaceView or GLSurfaceView.
+ * call attachSurfaceView or attachOpenGLView to switch views.
+ */
+public class GamePerformanceActivity extends Activity {
+ private CustomSurfaceView mSurfaceView = null;
+ private CustomOpenGLView mOpenGLView = null;
+ private RelativeLayout mRootLayout;
+
+ public void attachSurfaceView() throws InterruptedException {
+ synchronized (mRootLayout) {
+ if (mSurfaceView != null) {
+ return;
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mOpenGLView != null) {
+ mRootLayout.removeView(mOpenGLView);
+ mOpenGLView = null;
+ }
+ mSurfaceView = new CustomSurfaceView(GamePerformanceActivity.this);
+ mRootLayout.addView(mSurfaceView);
+ latch.countDown();
+ }
+ });
+ latch.await();
+ mSurfaceView.waitForSurfaceReady();
+ }
+ }
+
+ public void attachOpenGLView() throws InterruptedException {
+ synchronized (mRootLayout) {
+ if (mOpenGLView != null) {
+ return;
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mSurfaceView != null) {
+ mRootLayout.removeView(mSurfaceView);
+ mSurfaceView = null;
+ }
+ mOpenGLView = new CustomOpenGLView(GamePerformanceActivity.this);
+ mRootLayout.addView(mOpenGLView);
+ latch.countDown();
+ }
+ });
+ latch.await();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ // To layouts in parent. First contains list of Surfaces and second
+ // controls. Controls stay on top.
+ mRootLayout = new RelativeLayout(this);
+ mRootLayout.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ Rect rect = new Rect();
+ getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
+
+ mOpenGLView = new CustomOpenGLView(this);
+ mRootLayout.addView(mOpenGLView);
+
+ setContentView(mRootLayout);
+ }
+
+ public void resetFrameTimes() {
+ if (mSurfaceView != null) {
+ mSurfaceView.resetFrameTimes();
+ } else if (mOpenGLView != null) {
+ mOpenGLView.resetFrameTimes();
+ } else {
+ throw new IllegalStateException("Nothing attached");
+ }
+ }
+
+ public double getFps() {
+ if (mSurfaceView != null) {
+ return mSurfaceView.getFps();
+ } else if (mOpenGLView != null) {
+ return mOpenGLView.getFps();
+ } else {
+ throw new IllegalStateException("Nothing attached");
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+} \ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java
new file mode 100644
index 000000000000..e5de7d75886e
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2018 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.gameperformance;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Trace;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+public class GamePerformanceTest extends
+ ActivityInstrumentationTestCase2<GamePerformanceActivity> {
+ private final static String TAG = "GamePerformanceTest";
+
+ private final static int GRAPHIC_BUFFER_WARMUP_LOOP_CNT = 60;
+
+ public GamePerformanceTest() {
+ super(GamePerformanceActivity.class);
+ }
+
+ @SmallTest
+ public void testGraphicBufferMetrics() throws IOException, InterruptedException {
+ Bundle status = new Bundle();
+
+ for (int i = 0; i < 2; ++i) {
+ if (i == 0) {
+ getActivity().attachSurfaceView();
+ } else {
+ getActivity().attachOpenGLView();
+ }
+
+ // Perform warm-up.
+ Thread.sleep(2000);
+
+ // Once atrace is done, this one is triggered.
+ CountDownLatch latch = new CountDownLatch(1);
+
+ final String passTag = i == 0 ? "surface" : "opengl";
+ final String output = (new File(getInstrumentation().getContext().getFilesDir(),
+ "atrace_" + passTag + ".log")).getAbsolutePath();
+ Log.i(TAG, "Collecting traces to " + output);
+ new ATraceRunner(getInstrumentation(), output, 5, "gfx", new ATraceRunner.Delegate() {
+ @Override
+ public void onProcessed(boolean success) {
+ latch.countDown();
+ }
+ }).execute();
+
+ // Reset frame times and perform invalidation loop while atrace is running.
+ getActivity().resetFrameTimes();
+ latch.await();
+
+ // Copy results.
+ final Map<String, Double> metrics =
+ GraphicBufferMetrics.processGraphicBufferResult(output, passTag);
+ for (Map.Entry<String, Double> metric : metrics.entrySet()) {
+ status.putDouble(metric.getKey(), metric.getValue());
+ }
+ // Also record FPS.
+ status.putDouble(passTag + "_fps", getActivity().getFps());
+ }
+
+ getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+ }
+}
diff --git a/tests/GamePerformance/src/android/gameperformance/GraphicBufferMetrics.java b/tests/GamePerformance/src/android/gameperformance/GraphicBufferMetrics.java
new file mode 100644
index 000000000000..dffce1acdec3
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/GraphicBufferMetrics.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2018 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.gameperformance;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Utility class that performs analysis of atrace logs. This is implemented without Android
+ * dependencies and therefore can be used in stand-alone mode.
+ * Idea of this is to track atrace gfx event from graphics buffer producer/consumer.
+ * We analyze here from surfaceflinger
+ * queueBuffer - event when buffer was queued.
+ * acquireBuffer - event when buffer was requested for composition.
+ * releaseBuffer - even when buffer was released after composition.
+ * This also track events, issued locally
+ * localPostBuffer - event when buffer was posted from the local app.
+ *
+ * queueBuffer, acquireBuffer, releaseBuffer is accompanied with buffer name so we
+ * can track life-cycle of particular buffer.
+ * We don't have such information for localPostBuffer, however we can track next queueBuffer
+ * from surfaceflinger corresponds to previous localPostBuffer.
+ *
+ * Following results are calculated:
+ * post_time_[min/max/avr]_mcs - time for localPostBuffer duration.
+ * ready_time_[min/max/avr]_mcs - time from localPostBuffer to when buffer was acquired by
+ * surfaceflinger.
+ * latency_[min/max/avr]_mcs - time from localPostBuffer to when buffer was released by
+ * surfaceflinger.
+ * missed_frame_percents - percentage of missed frames (frames that do not have right sequence
+ * of events).
+ *
+ * Following is example of atrace logs from different platforms
+ * <...>-5237 (-----) [000] ...1 228.380392: tracing_mark_write: B|11|SurfaceView - android.gameperformance/android.gameperformance.GamePerformanceActivity#0: 2
+ * surfaceflinger-5855 ( 5855) [001] ...1 169.627364: tracing_mark_write: B|24|acquireBuffer
+ * HwBinder:617_2-652 ( 617) [002] d..1 360262.921756: sde_evtlog: 617|sde_encoder_virt_atomic_check:855|19|0|0|0|0|0|0|0|0|0|0|0|0|0|0
+ */
+public class GraphicBufferMetrics {
+ private final static String TAG = "GraphicBufferMetrics";
+
+ private final static String KEY_POST_TIME = "post_time";
+ private final static String KEY_READY_TIME = "ready_time";
+ private final static String KEY_LATENCY = "latency";
+ private final static String SUFFIX_MIN = "min";
+ private final static String SUFFIX_MAX = "max";
+ private final static String SUFFIX_MEDIAN = "median";
+ private final static String KEY_MISSED_FRAME_RATE = "missed_frame_percents";
+
+ private final static int EVENT_POST_BUFFER = 0;
+ private final static int EVENT_QUEUE_BUFFER = 1;
+ private final static int EVENT_ACQUIRE_BUFFER = 2;
+ private final static int EVENT_RELEASE_BUFFER = 3;
+
+ // atrace prints this line. Used as a marker to make sure that we can parse its output.
+ private final static String ATRACE_HEADER =
+ "# TASK-PID TGID CPU# |||| TIMESTAMP FUNCTION";
+
+ private final static String[] KNOWN_PHRASES = new String[] {
+ "capturing trace... done", "TRACE:"};
+ private final static List<String> KNWON_PHRASES_LIST = Arrays.asList(KNOWN_PHRASES);
+
+ // Type of the atrace event we can parse and analyze.
+ private final static String FUNCTION_TRACING_MARK_WRITE = "tracing_mark_write";
+
+ // Trace event we can ignore. It contains current timestamp information for the atrace output.
+ private final static String TRACE_EVENT_CLOCK_SYNC = "trace_event_clock_sync:";
+
+ // Threshold we consider test passes successfully. If we cannot collect enough amount of frames
+ // let fail the test. 50 is calculated 10 frames per second running for five seconds.
+ private final static int MINIMAL_SAMPLE_CNT_TO_PASS = 50;
+
+ /**
+ * Raw event in atrace. Stored hierarchically.
+ */
+ private static class RawEvent {
+ // Parent of this event or null for the root holder.
+ public final RawEvent mParent;
+ // Time of the event in mcs.
+ public final long mTime;
+ // Duration of the event in mcs.
+ public long mDuration;
+ // Name/body of the event.
+ public final String mName;
+ // Children events.
+ public final List<RawEvent> mChildren;
+
+ public RawEvent(RawEvent parent, long time, String name) {
+ mParent = parent;
+ mTime = time;
+ mName = name;
+ mDuration = -1;
+ mChildren = new ArrayList<>();
+ }
+
+ /**
+ * Recursively finds child events.
+ * @param path specify path to events. For example a/b. That means to find child with name
+ * 'a' of the current event and in this child find child with name 'b'. Path
+ * can consist from only one segment and that means we analyze only children of
+ * the current event.
+ * @param collector to collect found events.
+ */
+ public void findEvents(String path, List<RawEvent> collector) {
+ final int separator = path.indexOf('/');
+ final String current = separator > 0 ? path.substring(0, separator) : path;
+ final String nextPath = separator > 0 ? path.substring(separator + 1) : null;
+ for (RawEvent child : mChildren) {
+ if (child.mName.equals(current)) {
+ if (nextPath != null) {
+ child.findEvents(nextPath, collector);
+ } else {
+ collector.add(child);
+ }
+ }
+ }
+ }
+
+ public void dump(String prefix) {
+ System.err.print(prefix);
+ System.err.println(mTime + "[" + mDuration + "] " + mName);
+ for (RawEvent e : mChildren) {
+ e.dump(prefix + " ");
+ }
+ }
+ }
+
+ /**
+ * Describes graphic buffer event. local post, queued, acquired, released.
+ */
+ private static class BufferEvent {
+ public final int mType;
+ public final long mTime;
+ public final long mDuration;
+ public final String mBufferId;
+
+ public BufferEvent(int type, long time, long duration, String bufferId) {
+ mType = type;
+ mTime = time;
+ mDuration = duration;
+ mBufferId = bufferId;
+ }
+
+ @Override
+ public String toString() {
+ return "Type: " + mType + ". Time: " + mTime +
+ "[" + mDuration + "]. Buffer: " + mBufferId + ".";
+ }
+ }
+
+ /**
+ * Returns true if given char is digit.
+ */
+ private static boolean isDigitChar(char c) {
+ return (c >= '0') && (c <= '9');
+ }
+
+ /**
+ * Returns true if given char is digit or '.'.
+ */
+ private static boolean isDoubleDigitChar(char c) {
+ return (c == '.') || isDigitChar(c);
+ }
+
+ /**
+ * Convert timestamp string that represents double value in seconds to long time that represents
+ * timestamp in microseconds.
+ */
+ private static long getTimeStamp(String timeStampStr) {
+ return (long)(1000000.0 * Double.parseDouble(timeStampStr));
+ }
+
+ /**
+ * Reads atrace log and build event model. Result is a map, where key specifies event for the
+ * particular thread. Value is the synthetic root RawEvent that holds all events for the
+ * thread. Events are stored hierarchically.
+ */
+ private static Map<Integer, RawEvent> buildEventModel(String fileName) throws IOException {
+ Map<Integer, RawEvent> result = new HashMap<>();
+
+ BufferedReader bufferedReader = null;
+ String line = "";
+ boolean headerDetected = false;
+ try {
+ bufferedReader = new BufferedReader(new FileReader(fileName));
+ while ((line = bufferedReader.readLine()) != null) {
+ // Make sure we find comment that describes output format we can with.
+ headerDetected |= line.equals(ATRACE_HEADER);
+ // Skip comments.
+ if (line.startsWith("#")) {
+ continue;
+ }
+ // Skip known service output
+ if (KNWON_PHRASES_LIST.contains(line)) {
+ continue;
+ }
+
+ if (!headerDetected) {
+ // We don't know the format of this line.
+ throw new IllegalStateException("Header was not detected");
+ }
+
+ // TASK-PID in header exists at position 12. PID position 17 should contains first
+ // digit of thread id after the '-'.
+ if (!isDigitChar(line.charAt(17)) || line.charAt(16) != '-') {
+ throw new IllegalStateException("Failed to parse thread id: " + line);
+ }
+ int rightIndex = line.indexOf(' ', 17);
+ final String threadIdStr = line.substring(17, rightIndex);
+ final int threadId = Integer.parseInt(threadIdStr);
+
+ // TIMESTAMP in header exists at position 45
+ // This position should point in the middle of timestamp which is ended by ':'.
+ int leftIndex = 45;
+ while (isDoubleDigitChar(line.charAt(leftIndex))) {
+ --leftIndex;
+ }
+ rightIndex = line.indexOf(':', 45);
+
+ final String timeStampString = line.substring(leftIndex + 1, rightIndex);
+ final long timeStampMcs = getTimeStamp(timeStampString);
+
+ // Find function name, pointed by FUNCTION. Long timestamp can shift if position
+ // so use end of timestamp to find the function which is ended by ':'.
+ leftIndex = rightIndex + 1;
+ while (Character.isWhitespace(line.charAt(leftIndex))) {
+ ++leftIndex;
+ }
+ rightIndex = line.indexOf(':', leftIndex);
+ final String function = line.substring(leftIndex, rightIndex);
+
+ if (!function.equals(FUNCTION_TRACING_MARK_WRITE)) {
+ continue;
+ }
+
+ // Rest of the line is event body.
+ leftIndex = rightIndex + 1;
+ while (Character.isWhitespace(line.charAt(leftIndex))) {
+ ++leftIndex;
+ }
+
+ final String event = line.substring(leftIndex);
+ if (event.startsWith(TRACE_EVENT_CLOCK_SYNC)) {
+ continue;
+ }
+
+ // Parse event, for example:
+ // B|615|SurfaceView - android.gameperformance.GamePerformanceActivity#0: 1
+ // E|615
+ // C|11253|operation id|2
+ StringTokenizer eventTokenizer = new StringTokenizer(event, "|");
+ final String eventType = eventTokenizer.nextToken();
+
+ // Attach root on demand.
+ if (!result.containsKey(threadId)) {
+ result.put(threadId, new RawEvent(null /* parent */,
+ timeStampMcs,
+ "#ROOT_" + threadId));
+ }
+
+ switch (eventType) {
+ case "B": {
+ // Log entry starts.
+ eventTokenizer.nextToken(); // PID
+ String eventText = eventTokenizer.nextToken();
+ while (eventTokenizer.hasMoreTokens()) {
+ eventText += " ";
+ eventText += eventTokenizer.nextToken();
+ }
+ RawEvent parent = result.get(threadId);
+ RawEvent current = new RawEvent(parent, timeStampMcs, eventText);
+ parent.mChildren.add(current);
+ result.put(threadId, current);
+ }
+ break;
+ case "E": {
+ // Log entry ends.
+ RawEvent current = result.get(threadId);
+ current.mDuration = timeStampMcs - current.mTime;
+ if (current.mParent == null) {
+ // Detect a tail of the previous call. Remove last child element if it
+ // exists once it does not belong to the root.
+ if (!current.mChildren.isEmpty()) {
+ current.mChildren.remove(current.mChildren.size() -1);
+ }
+ } else {
+ result.put(threadId, current.mParent);
+ }
+ }
+ break;
+ case "C":
+ // Counter, ignore
+ break;
+ default:
+ throw new IllegalStateException(
+ "Unrecognized trace: " + line + " # " + eventType + " # " + event);
+ }
+ }
+
+ // Detect incomplete events and detach from the root.
+ Set<Integer> threadIds = new TreeSet<>();
+ threadIds.addAll(result.keySet());
+ for (int threadId : threadIds) {
+ RawEvent root = result.get(threadId);
+ if (root.mParent == null) {
+ // Last trace was closed.
+ continue;
+ }
+ // Find the root.
+ while (root.mParent != null) {
+ root = root.mParent;
+ }
+ // Discard latest incomplete element.
+ root.mChildren.remove(root.mChildren.size() - 1);
+ result.put(threadId, root);
+ }
+ } catch (Exception e) {
+ throw new IOException("Failed to process " + line, e);
+ } finally {
+ Utils.closeQuietly(bufferedReader);
+ }
+
+ return result;
+ }
+
+ /**
+ * Processes provided atrace log and calculates graphics buffer metrics.
+ * @param fileName name of atrace log file.
+ * @param testTag tag to separate results for the different passes.
+ */
+ public static Map<String, Double> processGraphicBufferResult(
+ String fileName, String testTag) throws IOException {
+ final Map<Integer, RawEvent> model = buildEventModel(fileName);
+
+ List<RawEvent> collectorPostBuffer = new ArrayList<>();
+ List<RawEvent> collectorQueueBuffer = new ArrayList<>();
+ List<RawEvent> collectorReleaseBuffer = new ArrayList<>();
+ List<RawEvent> collectorAcquireBuffer = new ArrayList<>();
+
+ // Collect required events.
+ for (RawEvent root : model.values()) {
+ // Surface view
+ root.findEvents("localPostBuffer", collectorPostBuffer);
+ // OpengGL view
+ root.findEvents("eglSwapBuffersWithDamageKHR", collectorPostBuffer);
+
+ root.findEvents("queueBuffer", collectorQueueBuffer);
+ root.findEvents("onMessageReceived/handleMessageInvalidate/latchBuffer/" +
+ "updateTexImage/acquireBuffer",
+ collectorAcquireBuffer);
+ // PI stack
+ root.findEvents(
+ "onMessageReceived/handleMessageRefresh/postComposition/releaseBuffer",
+ collectorReleaseBuffer);
+ // NYC stack
+ root.findEvents(
+ "onMessageReceived/handleMessageRefresh/releaseBuffer",
+ collectorReleaseBuffer);
+ }
+
+ // Convert raw event to buffer events.
+ List<BufferEvent> bufferEvents = new ArrayList<>();
+ for (RawEvent event : collectorPostBuffer) {
+ bufferEvents.add(
+ new BufferEvent(EVENT_POST_BUFFER, event.mTime, event.mDuration, null));
+ }
+ toBufferEvents(EVENT_QUEUE_BUFFER, collectorQueueBuffer, bufferEvents);
+ toBufferEvents(EVENT_ACQUIRE_BUFFER, collectorAcquireBuffer, bufferEvents);
+ toBufferEvents(EVENT_RELEASE_BUFFER, collectorReleaseBuffer, bufferEvents);
+
+ // Sort events based on time. These events are originally taken from different threads so
+ // order is not always preserved.
+ Collections.sort(bufferEvents, new Comparator<BufferEvent>() {
+ @Override
+ public int compare(BufferEvent o1, BufferEvent o2) {
+ if (o1.mTime < o2.mTime) {
+ return -1;
+ } if (o1.mTime > o2.mTime) {
+ return +1;
+ } else {
+ return 0;
+ }
+ }
+ });
+
+ // Collect samples.
+ List<Long> postTimes = new ArrayList<>();
+ List<Long> readyTimes = new ArrayList<>();
+ List<Long> latencyTimes = new ArrayList<>();
+ long missedCnt = 0;
+
+ for (int i = 0; i < bufferEvents.size(); ++i) {
+ if (bufferEvents.get(i).mType != EVENT_POST_BUFFER) {
+ continue;
+ }
+ final int queueIndex = findNextOfType(bufferEvents, i + 1, EVENT_QUEUE_BUFFER);
+ if (queueIndex < 0) {
+ break;
+ }
+ final int acquireIndex = findNextOfBufferId(bufferEvents, queueIndex + 1,
+ bufferEvents.get(queueIndex).mBufferId);
+ if (acquireIndex < 0) {
+ break;
+ }
+ if (bufferEvents.get(acquireIndex).mType != EVENT_ACQUIRE_BUFFER) {
+ // Was not actually presented.
+ ++missedCnt;
+ continue;
+ }
+ final int releaseIndex = findNextOfBufferId(bufferEvents, acquireIndex + 1,
+ bufferEvents.get(queueIndex).mBufferId);
+ if (releaseIndex < 0) {
+ break;
+ }
+ if (bufferEvents.get(releaseIndex).mType != EVENT_RELEASE_BUFFER) {
+ // Was not actually presented.
+ ++missedCnt;
+ continue;
+ }
+
+ postTimes.add(bufferEvents.get(i).mDuration);
+ readyTimes.add(
+ bufferEvents.get(acquireIndex).mTime - bufferEvents.get(i).mTime);
+ latencyTimes.add(
+ bufferEvents.get(releaseIndex).mTime - bufferEvents.get(i).mTime);
+ }
+
+ if (postTimes.size() < MINIMAL_SAMPLE_CNT_TO_PASS) {
+ throw new IllegalStateException("Too few sample cnt: " + postTimes.size() +". " +
+ MINIMAL_SAMPLE_CNT_TO_PASS + " is required.");
+ }
+
+ Map<String, Double> status = new TreeMap<>();
+ addResults(status, testTag, KEY_POST_TIME, postTimes);
+ addResults(status, testTag, KEY_READY_TIME, readyTimes);
+ addResults(status, testTag, KEY_LATENCY, latencyTimes);
+ status.put(testTag + "_" + KEY_MISSED_FRAME_RATE,
+ 100.0 * missedCnt / (missedCnt + postTimes.size()));
+ return status;
+ }
+
+ private static void addResults(
+ Map<String, Double> status, String tag, String key, List<Long> times) {
+ Collections.sort(times);
+ long min = times.get(0);
+ long max = times.get(0);
+ for (long time : times) {
+ min = Math.min(min, time);
+ max = Math.max(max, time);
+ }
+ status.put(tag + "_" + key + "_" + SUFFIX_MIN, (double)min);
+ status.put(tag + "_" + key + "_" + SUFFIX_MAX, (double)max);
+ status.put(tag + "_" + key + "_" + SUFFIX_MEDIAN, (double)times.get(times.size() / 2));
+ }
+
+ // Helper to convert surface flinger events to buffer events.
+ private static void toBufferEvents(
+ int type, List<RawEvent> rawEvents, List<BufferEvent> bufferEvents) {
+ for (RawEvent event : rawEvents) {
+ if (event.mChildren.isEmpty()) {
+ throw new IllegalStateException("Buffer name is expected");
+ }
+ final String bufferName = event.mChildren.get(0).mName;
+ if (bufferName.startsWith("SurfaceView - android.gameperformance")) {
+ bufferEvents.add(
+ new BufferEvent(type, event.mTime, event.mDuration, bufferName));
+ }
+ }
+ }
+
+ private static int findNextOfType(List<BufferEvent> events, int startIndex, int type) {
+ for (int i = startIndex; i < events.size(); ++i) {
+ if (events.get(i).mType == type) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static int findNextOfBufferId(
+ List<BufferEvent> events, int startIndex, String bufferId) {
+ for (int i = startIndex; i < events.size(); ++i) {
+ if (bufferId.equals(events.get(i).mBufferId)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public static void main(String[] args) {
+ if (args.length != 1) {
+ System.err.println("Usage: " + TAG + " atrace.log");
+ return;
+ }
+
+ try {
+ System.out.println("Results:");
+ for (Map.Entry<?, ?> entry :
+ processGraphicBufferResult(args[0], "default").entrySet()) {
+ System.out.println(" " + entry.getKey() + " = " + entry.getValue());
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/tests/GamePerformance/src/android/gameperformance/Utils.java b/tests/GamePerformance/src/android/gameperformance/Utils.java
new file mode 100644
index 000000000000..64819712bf6d
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/Utils.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 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.gameperformance;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+public class Utils {
+ public static void closeQuietly(Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+}