summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeoff Mendal <mendal@google.com>2015-03-18 18:24:35 -0500
committerGeoff Mendal <mendal@google.com>2015-03-18 18:24:35 -0500
commit39c6037837309ff2814e9b7ff29cd73f3786a218 (patch)
tree548aac06dc0dfd70b52da73702e6c3171001fbe9
parente9713da1b6f0b9664a25cd561911cd241c432a69 (diff)
parentdd0862415ca6928664b0d1b61d93db9866c4d563 (diff)
downloadjanktesthelper-39c6037837309ff2814e9b7ff29cd73f3786a218.tar.gz
Merge jank/ from platform/frameworks/uiautomator to /
-rw-r--r--Android.mk27
-rw-r--r--src/android/support/test/jank/GfxMonitor.java49
-rw-r--r--src/android/support/test/jank/JankTest.java65
-rw-r--r--src/android/support/test/jank/JankTestBase.java193
-rw-r--r--src/android/support/test/jank/WindowAnimationFrameStatsMonitor.java32
-rw-r--r--src/android/support/test/jank/WindowContentFrameStatsMonitor.java32
-rw-r--r--src/android/support/test/jank/internal/FrameStatsMonitorBase.java79
-rw-r--r--src/android/support/test/jank/internal/GfxMonitorImpl.java250
-rw-r--r--src/android/support/test/jank/internal/JankMonitor.java27
-rw-r--r--src/android/support/test/jank/internal/JankMonitorFactory.java50
-rw-r--r--src/android/support/test/jank/internal/MetricsHelper.java38
-rw-r--r--src/android/support/test/jank/internal/WindowAnimationFrameMonitorImpl.java71
-rw-r--r--src/android/support/test/jank/internal/WindowContentFrameMonitorImpl.java105
13 files changed, 1018 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..fc8806f
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2014 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)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_JAVA_LIBRARIES := android-support-test
+LOCAL_MODULE := janktesthelper
+LOCAL_SDK_VERSION := current
+LOCAL_JAVACFLAGS := -source 6 -target 6
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/src/android/support/test/jank/GfxMonitor.java b/src/android/support/test/jank/GfxMonitor.java
new file mode 100644
index 0000000..71ede73
--- /dev/null
+++ b/src/android/support/test/jank/GfxMonitor.java
@@ -0,0 +1,49 @@
+/*
+ * 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.support.test.jank;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Annotation used to configure a gfx monitor. */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface GfxMonitor {
+ /** The name of the process to monitor */
+ String processName();
+
+ public static final String KEY_AVG_NUM_JANKY = "gfx-avg-jank";
+ public static final String KEY_MAX_NUM_JANKY = "gfx-max-jank";
+ public static final String KEY_AVG_MISSED_VSYNC = "gfx-avg-missed-vsync";
+ public static final String KEY_MAX_MISSED_VSYNC = "gfx-max-missed-vsync";
+ public static final String KEY_AVG_HIGH_INPUT_LATENCY = "gfx-avg-high-input-latency";
+ public static final String KEY_MAX_HIGH_INPUT_LATENCY = "gfx-max-high-input-latency";
+ public static final String KEY_AVG_SLOW_UI_THREAD = "gfx-avg-slow-ui-thread";
+ public static final String KEY_MAX_SLOW_UI_THREAD = "gfx-max-slow-ui-thread";
+ public static final String KEY_AVG_SLOW_BITMAP_UPLOADS = "gfx-avg-slow-bitmap-uploads";
+ public static final String KEY_MAX_SLOW_BITMAP_UPLOADS = "gfx-max-slow-bitmap-uploads";
+ public static final String KEY_AVG_SLOW_DRAW = "gfx-avg-slow-draw";
+ public static final String KEY_MAX_SLOW_DRAW = "gfx-max-slow-draw";
+ public static final String KEY_AVG_FRAME_TIME_90TH_PERCENTILE = "gfx-avg-frame-time-90";
+ public static final String KEY_MAX_FRAME_TIME_90TH_PERCENTILE = "gfx-max-frame-time-90";
+ public static final String KEY_AVG_FRAME_TIME_95TH_PERCENTILE = "gfx-avg-frame-time-95";
+ public static final String KEY_MAX_FRAME_TIME_95TH_PERCENTILE = "gfx-max-frame-time-95";
+ public static final String KEY_AVG_FRAME_TIME_99TH_PERCENTILE = "gfx-avg-frame-time-99";
+ public static final String KEY_MAX_FRAME_TIME_99TH_PERCENTILE = "gfx-max-frame-time-99";
+}
diff --git a/src/android/support/test/jank/JankTest.java b/src/android/support/test/jank/JankTest.java
new file mode 100644
index 0000000..c79c6fa
--- /dev/null
+++ b/src/android/support/test/jank/JankTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.support.test.jank;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Annotation used to configure a jank test method. */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface JankTest {
+
+ /** The minimum number of frames expected */
+ int expectedFrames();
+
+ /** Default iteration count to run if not specified by command line **/
+ int defaultIterationCount() default 20;
+
+ /**
+ * Alternate method to execute before the test method
+ * <p>
+ * Note: the annotated method must have same signature as {@link JankTestBase#beforeTest()}
+ */
+ String beforeTest() default "beforeTest";
+
+ /**
+ * Alternate method to execute before each iteration
+ * <p>
+ * Note: the annotated method must have same signature as {@link JankTestBase#beforeLoop()}
+ */
+ String beforeLoop() default "beforeLoop";
+
+ /**
+ * Alternate method to execute after each iteration
+ * <p>
+ * Note: the annotated method must have same signature as {@link JankTestBase#afterLoop()}
+ */
+ String afterLoop() default "afterLoop";
+
+ /**
+ * Alternate method to execute after all iterations have completed.
+ * <p>
+ * <b>Important:</b> the annotated method must take a parameter of type
+ * {@link android.os.Bundle}.</p>
+ *
+ * @see JankTestBase#afterTest(android.os.Bundle)
+ */
+ String afterTest() default "afterTest";
+}
diff --git a/src/android/support/test/jank/JankTestBase.java b/src/android/support/test/jank/JankTestBase.java
new file mode 100644
index 0000000..bb2605d
--- /dev/null
+++ b/src/android/support/test/jank/JankTestBase.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2014 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.support.test.jank;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.jank.internal.JankMonitorFactory;
+import android.support.test.jank.internal.JankMonitor;
+import android.support.test.runner.AndroidJUnitRunner;
+import android.test.InstrumentationTestCase;
+import android.test.InstrumentationTestRunner;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.List;
+
+/**
+ * Base test class for measuring Jank.
+ *
+ * This test class automatically monitors jank while executing each test method. Each test method is
+ * executed several times in a loop, according to the 'iterations' command line parameter.
+ *
+ * To perform additional setup / tear down steps for each iteration, subclasses can optionally
+ * override {@link JankTestBase#beforeLoop()} and {@link JankTestBase#afterLoop()} methods.
+ *
+ * Test methods must be configured with the {@link JankTest} annotation. At minimum, the type of
+ * jank to measure and the number of expected frames must be specified.
+ */
+public class JankTestBase extends InstrumentationTestCase {
+
+ private Bundle arguments = null;
+ private int mCurrentIteration = 0;
+
+
+ /** Called once before executing a test method. */
+ public void beforeTest() throws Exception {
+ // Default implementation. Do nothing.
+ }
+
+ /** Called before each iteration of the test method. */
+ public void beforeLoop() throws Exception {
+ // Default implementation. Do nothing.
+ }
+
+ /** Called after each iteration of the test method. */
+ public void afterLoop() throws Exception {
+ // Default implementation. Do nothing.
+ }
+
+ /**
+ * Called once after all iterations have completed.
+ * <p>Note: default implementation reports the aggregated jank metrics via
+ * {@link Instrumentation#sendStatus(int, Bundle)}
+ * @param metrics the aggregated jank metrics after looped execution
+ */
+ public void afterTest(Bundle metrics) {
+ getInstrumentation().sendStatus(Activity.RESULT_OK, metrics);
+ }
+
+ /** Return the index of the currently executing iteration. */
+ public final int getCurrentIteration() {
+ return mCurrentIteration;
+ }
+
+ @Override
+ protected final void runTest() throws Throwable {
+
+ // Resolve test methods
+ Method testMethod = resolveMethod(getName());
+ JankTest annotation = testMethod.getAnnotation(JankTest.class);
+ Method beforeTest = resolveMethod(annotation.beforeTest());
+ Method beforeLoop = resolveMethod(annotation.beforeLoop());
+ Method afterLoop = resolveMethod(annotation.afterLoop());
+ Method afterTest = resolveAfterTest(annotation.afterTest());
+
+ // Get the appropriate JankMonitors for the test type
+ JankMonitorFactory factory = new JankMonitorFactory(getInstrumentation().getUiAutomation());
+ List<JankMonitor> monitors = factory.getJankMonitors(testMethod);
+ assertTrue("No monitors configured for this test", monitors.size() > 0);
+
+ // Test setup
+ beforeTest.invoke(this, (Object[])null);
+
+ // Execute the test several times according to the "iteration" parameter
+ int iterations = Integer.valueOf(getArguments().getString("iterations",
+ Integer.toString(annotation.defaultIterationCount())));
+ for (; mCurrentIteration < iterations; mCurrentIteration++) {
+ // Loop setup
+ beforeLoop.invoke(this, (Object[])null);
+
+ // Start monitoring jank
+ for (JankMonitor monitor : monitors) {
+ monitor.startIteration();
+ }
+
+ // Run the test method
+ testMethod.invoke(this, (Object[])null);
+
+ // Stop monitoring
+ for (JankMonitor monitor : monitors) {
+ int numFrames = monitor.stopIteration();
+
+ // Fail the test if we didn't get enough frames
+ assertTrue(String.format("Too few frames received. Expected: %d, Received: %d.",
+ annotation.expectedFrames(), numFrames),
+ numFrames >= annotation.expectedFrames());
+ }
+
+ // Loop tear down
+ afterLoop.invoke(this, (Object[])null);
+ }
+
+ // Report aggregated results
+ Bundle metrics = new Bundle();
+ for (JankMonitor monitor : monitors) {
+ metrics.putAll(monitor.getMetrics());
+ }
+ afterTest.invoke(this, metrics);
+ }
+
+
+ /** Returns a {@link Method}} object representing the method with the given {@code name}. */
+ private Method resolveMethod(String name) {
+ assertNotNull(name);
+
+ Method method = null;
+ try {
+ method = getClass().getMethod(name, (Class[]) null);
+ } catch (NoSuchMethodException e) {
+ fail(String.format("Method \"%s\" not found", name));
+ }
+
+ if (!Modifier.isPublic(method.getModifiers())) {
+ fail(String.format("Method \"%s\" should be public", name));
+ }
+
+ return method;
+ }
+
+ /**
+ * Returns a {@link Method}} object representing the method annotated with
+ * {@link JankTest#afterTest()}.
+ */
+ private Method resolveAfterTest(String name) {
+ assertNotNull(name);
+
+ Method method = null;
+ try {
+ method = getClass().getMethod(name, Bundle.class);
+ } catch (NoSuchMethodException e) {
+ fail("method annotated with JankTest#afterTest has wrong signature");
+ }
+
+ if (!Modifier.isPublic(method.getModifiers())) {
+ fail(String.format("Method \"%s\" should be public", name));
+ }
+
+ return method;
+ }
+
+ /** Returns a {@link Bundle} containing the command line parameters. */
+ protected final Bundle getArguments() {
+ if (arguments == null) {
+ Instrumentation instrumentation = getInstrumentation();
+ // Attempt to obtain the command line arguments bundle, but this is only supported by
+ // InstrumentationTestRunner or AndroidJUnitRunner
+ if (instrumentation instanceof InstrumentationTestRunner) {
+ arguments = ((InstrumentationTestRunner) instrumentation).getArguments();
+ } else if (instrumentation instanceof AndroidJUnitRunner) {
+ arguments = InstrumentationRegistry.getArguments();
+ } else {
+ throw new RuntimeException("Unsupported test runner");
+ }
+ }
+ return arguments;
+ }
+}
diff --git a/src/android/support/test/jank/WindowAnimationFrameStatsMonitor.java b/src/android/support/test/jank/WindowAnimationFrameStatsMonitor.java
new file mode 100644
index 0000000..79b466b
--- /dev/null
+++ b/src/android/support/test/jank/WindowAnimationFrameStatsMonitor.java
@@ -0,0 +1,32 @@
+/*
+ * 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.support.test.jank;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Annotation used to configure a window animation frame monitor. */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface WindowAnimationFrameStatsMonitor {
+ public static final String KEY_AVG_NUM_JANKY = "frame-avg-jank";
+ public static final String KEY_MAX_NUM_JANKY = "frame-max-jank";
+ public static final String KEY_AVG_FPS = "frame-fps";
+ public static final String KEY_AVG_LONGEST_FRAME = "frame-max-frame-duration";
+}
diff --git a/src/android/support/test/jank/WindowContentFrameStatsMonitor.java b/src/android/support/test/jank/WindowContentFrameStatsMonitor.java
new file mode 100644
index 0000000..848aeca
--- /dev/null
+++ b/src/android/support/test/jank/WindowContentFrameStatsMonitor.java
@@ -0,0 +1,32 @@
+/*
+ * 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.support.test.jank;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Annotation used to configure a window content frame monitor. */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface WindowContentFrameStatsMonitor {
+ public static final String KEY_AVG_NUM_JANKY = "frame-avg-jank";
+ public static final String KEY_MAX_NUM_JANKY = "frame-max-jank";
+ public static final String KEY_AVG_FPS = "frame-fps";
+ public static final String KEY_AVG_LONGEST_FRAME = "frame-max-frame-duration";
+}
diff --git a/src/android/support/test/jank/internal/FrameStatsMonitorBase.java b/src/android/support/test/jank/internal/FrameStatsMonitorBase.java
new file mode 100644
index 0000000..8821b14
--- /dev/null
+++ b/src/android/support/test/jank/internal/FrameStatsMonitorBase.java
@@ -0,0 +1,79 @@
+/*
+ * 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.support.test.jank.internal;
+
+import android.util.Log;
+import android.view.FrameStats;
+
+import java.util.ArrayList;
+
+/**
+ * Abstract base class for {@link android.view.FrameStats} based {@link JankMonitor}s.
+ *
+ * Reports average and max jank, as well as average frames per second and the longest normalized
+ * frame time.
+ */
+abstract class FrameStatsMonitorBase implements JankMonitor {
+
+ private static final String TAG = "JankTestHelper";
+
+ // Maximum normalized error in frame duration before the frame is considered janky
+ private static final double MAX_ERROR = 0.5f;
+
+ // Maximum normalized frame duration before the frame is considered a pause
+ private static final double PAUSE_THRESHOLD = 15.0f;
+
+ // Accumulated stats
+ ArrayList<Integer> mJankyFrames = new ArrayList<Integer>();
+ ArrayList<Double> mFps = new ArrayList<Double>();
+ ArrayList<Double> mLongestNormalizedFrames = new ArrayList<Double>();
+
+ protected void analyze(FrameStats stats) {
+ int frameCount = stats.getFrameCount();
+ long refreshPeriod = stats.getRefreshPeriodNano();
+
+ int numJanky = 0;
+ double longestFrameNormalized = 0.0f;
+ double totalDuration = 0.0f;
+ // Skip first frame
+ for (int i = 2; i < frameCount; i++) {
+ // Handle frames that have not been presented.
+ if (stats.getFramePresentedTimeNano(i) == -1) {
+ // The animation must not have completed. Warn and break out of the loop.
+ Log.w(TAG, "Skipping fenced frame.");
+ frameCount = i;
+ break;
+ }
+ long frameDuration = stats.getFramePresentedTimeNano(i) -
+ stats.getFramePresentedTimeNano(i - 1);
+ double normalized = (double)frameDuration / refreshPeriod;
+ if (normalized < PAUSE_THRESHOLD) {
+ if (normalized > 1.0f + MAX_ERROR) {
+ numJanky++;
+ }
+ longestFrameNormalized = Math.max(longestFrameNormalized, normalized);
+ }
+ totalDuration += frameDuration;
+ }
+ double fps = (double)(frameCount - 2) / totalDuration * 1000000000;
+
+ // Store metrics from this run
+ mJankyFrames.add(numJanky);
+ mFps.add(fps);
+ mLongestNormalizedFrames.add(longestFrameNormalized);
+ }
+}
diff --git a/src/android/support/test/jank/internal/GfxMonitorImpl.java b/src/android/support/test/jank/internal/GfxMonitorImpl.java
new file mode 100644
index 0000000..bc1abd5
--- /dev/null
+++ b/src/android/support/test/jank/internal/GfxMonitorImpl.java
@@ -0,0 +1,250 @@
+/*
+ * 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.support.test.jank.internal;
+
+import android.app.UiAutomation;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.support.test.jank.GfxMonitor;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import junit.framework.Assert;
+
+/**
+ * Monitors dumpsys gfxinfo to detect janky frames.
+ *
+ * Reports average and max jank. Additionally reports summary statistics for common problems that
+ * can lead to dropped frames.
+ */
+class GfxMonitorImpl implements JankMonitor {
+
+ // Patterns used for parsing dumpsys gfxinfo output
+ private static final Pattern TOTAL_FRAMES_PATTERN =
+ Pattern.compile("\\s*Total frames rendered: (\\d+)");
+ private static final Pattern JANKY_FRAMES_PATTERN =
+ Pattern.compile("\\s*Janky frames: (\\d+) \\(.*\\)");
+ private static final Pattern MISSED_VSYNC_PATTERN =
+ Pattern.compile("\\s*Number Missed Vsync: (\\d+)");
+ private static final Pattern INPUT_LATENCY_PATTERN =
+ Pattern.compile("\\s*Number High input latency: (\\d+)");
+ private static final Pattern SLOW_UI_PATTERN =
+ Pattern.compile("\\s*Number Slow UI thread: (\\d+)");
+ private static final Pattern SLOW_BITMAP_PATTERN =
+ Pattern.compile("\\s*Number Slow bitmap uploads: (\\d+)");
+ private static final Pattern SLOW_DRAW_PATTERN =
+ Pattern.compile("\\s*Number Slow draw: (\\d+)");
+ private static final Pattern FRAME_TIME_90TH_PERCENTILE_PATTERN =
+ Pattern.compile("\\s*90th percentile: (\\d+)ms");
+ private static final Pattern FRAME_TIME_95TH_PERCENTILE_PATTERN =
+ Pattern.compile("\\s*95th percentile: (\\d+)ms");
+ private static final Pattern FRAME_TIME_99TH_PERCENTILE_PATTERN =
+ Pattern.compile("\\s*99th percentile: (\\d+)ms");
+
+ // Used to invoke dumpsys gfxinfo
+ private UiAutomation mUiAutomation;
+ private String mProcess;
+
+ // Metrics accumulated for each iteration
+ private List<Integer> jankyFrames = new ArrayList<Integer>();
+ private List<Integer> missedVsync = new ArrayList<Integer>();
+ private List<Integer> highInputLatency = new ArrayList<Integer>();
+ private List<Integer> slowUiThread = new ArrayList<Integer>();
+ private List<Integer> slowBitmapUploads = new ArrayList<Integer>();
+ private List<Integer> slowDraw = new ArrayList<Integer>();
+ private List<Integer> frameTime90thPercentile = new ArrayList<Integer>();
+ private List<Integer> frameTime95thPercentile = new ArrayList<Integer>();
+ private List<Integer> frameTime99thPercentile = new ArrayList<Integer>();
+
+
+ public GfxMonitorImpl(UiAutomation automation, String process) {
+ mUiAutomation = automation;
+ mProcess = process;
+ }
+
+ @Override
+ public void startIteration() throws IOException {
+ // Clear out any previous data
+ ParcelFileDescriptor stdout = mUiAutomation.executeShellCommand(
+ String.format("dumpsys gfxinfo %s reset", mProcess));
+
+ // Read the output, but don't do anything with it
+ BufferedReader stream = new BufferedReader(new InputStreamReader(
+ new ParcelFileDescriptor.AutoCloseInputStream(stdout)));
+ while (stream.readLine() != null) {
+ }
+ }
+
+ @Override
+ public int stopIteration() throws IOException {
+ ParcelFileDescriptor stdout = mUiAutomation.executeShellCommand(
+ String.format("dumpsys gfxinfo %s", mProcess));
+ BufferedReader stream = new BufferedReader(new InputStreamReader(
+ new ParcelFileDescriptor.AutoCloseInputStream(stdout)));
+
+ // Wait until we enter the frame stats section
+ String line;
+ while ((line = stream.readLine()) != null) {
+ if (line.startsWith("Frame stats:")) {
+ break;
+ }
+ }
+ Assert.assertTrue("Failed to locate frame stats in gfxinfo output",
+ line != null && line.startsWith("Frame stats:"));
+
+ // The frame stats section has the following output:
+ // Frame stats:
+ // Total frames rendered: ###
+ // Janky frames: ### (##.##%)
+ // 90th percentile: ##ms
+ // 95th percentile: ##ms
+ // 99th percentile: ##ms
+ // Number Missed Vsync: #
+ // Number High input latency: #
+ // Number Slow UI thread: #
+ // Number Slow bitmap uploads: #
+ // Number Slow draw: #
+
+ // Get Total Frames
+ String part;
+ if ((part = getMatchGroup(stream.readLine(), TOTAL_FRAMES_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse total frames");
+ }
+ int totalFrames = Integer.parseInt(part);
+
+ // Get Num Janky
+ if ((part = getMatchGroup(stream.readLine(), JANKY_FRAMES_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse janky frames");
+ }
+ jankyFrames.add(Integer.parseInt(part));
+
+ // Get 90th percentile
+ if ((part = getMatchGroup(stream.readLine(), FRAME_TIME_90TH_PERCENTILE_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse 90th percentile");
+ }
+ frameTime90thPercentile.add(Integer.parseInt(part));
+
+ // Get 95th percentile
+ if ((part = getMatchGroup(stream.readLine(), FRAME_TIME_95TH_PERCENTILE_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse 95th percentile");
+ }
+ frameTime95thPercentile.add(Integer.parseInt(part));
+
+ // Get 99th percentile
+ if ((part = getMatchGroup(stream.readLine(), FRAME_TIME_99TH_PERCENTILE_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse 99th percentile");
+ }
+ frameTime99thPercentile.add(Integer.parseInt(part));
+
+ // Get Missed Vsync
+ if ((part = getMatchGroup(stream.readLine(), MISSED_VSYNC_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse number missed vsync");
+ }
+ missedVsync.add(Integer.parseInt(part));
+
+ // Get High input latency
+ if ((part = getMatchGroup(stream.readLine(), INPUT_LATENCY_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse number high input latency");
+ }
+ highInputLatency.add(Integer.parseInt(part));
+
+ // Get Slow UI thread
+ if ((part = getMatchGroup(stream.readLine(), SLOW_UI_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse number slow ui thread");
+ }
+ slowUiThread.add(Integer.parseInt(part));
+
+ // Get Slow bitmap uploads
+ if ((part = getMatchGroup(stream.readLine(), SLOW_BITMAP_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse number slow bitmap uploads");
+ }
+ slowBitmapUploads.add(Integer.parseInt(part));
+
+ // Get Slow draw
+ if ((part = getMatchGroup(stream.readLine(), SLOW_DRAW_PATTERN, 1)) == null) {
+ Assert.fail("Failed to parse number slow draw");
+ }
+ slowDraw.add(Integer.parseInt(part));
+
+ return totalFrames;
+ }
+
+ public Bundle getMetrics() {
+ Bundle metrics = new Bundle();
+
+ // Store average and max jank
+ metrics.putDouble(GfxMonitor.KEY_AVG_NUM_JANKY,
+ MetricsHelper.computeAverageInt(jankyFrames));
+ metrics.putInt(GfxMonitor.KEY_MAX_NUM_JANKY, Collections.max(jankyFrames));
+
+ // Store average and max percentile frame times
+ metrics.putDouble(GfxMonitor.KEY_AVG_FRAME_TIME_90TH_PERCENTILE,
+ MetricsHelper.computeAverageInt(frameTime90thPercentile));
+ metrics.putInt(GfxMonitor.KEY_MAX_FRAME_TIME_90TH_PERCENTILE,
+ Collections.max(frameTime90thPercentile));
+ metrics.putDouble(GfxMonitor.KEY_AVG_FRAME_TIME_95TH_PERCENTILE,
+ MetricsHelper.computeAverageInt(frameTime95thPercentile));
+ metrics.putInt(GfxMonitor.KEY_MAX_FRAME_TIME_95TH_PERCENTILE,
+ Collections.max(frameTime95thPercentile));
+ metrics.putDouble(GfxMonitor.KEY_AVG_FRAME_TIME_99TH_PERCENTILE,
+ MetricsHelper.computeAverageInt(frameTime99thPercentile));
+ metrics.putInt(GfxMonitor.KEY_MAX_FRAME_TIME_99TH_PERCENTILE,
+ Collections.max(frameTime99thPercentile));
+
+ // Store average and max missed vsync
+ metrics.putDouble(GfxMonitor.KEY_AVG_MISSED_VSYNC,
+ MetricsHelper.computeAverageInt(missedVsync));
+ metrics.putInt(GfxMonitor.KEY_MAX_MISSED_VSYNC, Collections.max(missedVsync));
+
+ // Store average and max high input latency
+ metrics.putDouble(GfxMonitor.KEY_AVG_HIGH_INPUT_LATENCY,
+ MetricsHelper.computeAverageInt(highInputLatency));
+ metrics.putInt(GfxMonitor.KEY_MAX_HIGH_INPUT_LATENCY, Collections.max(highInputLatency));
+
+ // Store average and max slow ui thread
+ metrics.putDouble(GfxMonitor.KEY_AVG_SLOW_UI_THREAD,
+ MetricsHelper.computeAverageInt(slowUiThread));
+ metrics.putInt(GfxMonitor.KEY_MAX_SLOW_UI_THREAD, Collections.max(slowUiThread));
+
+ // Store average and max slow bitmap uploads
+ metrics.putDouble(GfxMonitor.KEY_AVG_SLOW_BITMAP_UPLOADS,
+ MetricsHelper.computeAverageInt(slowUiThread));
+ metrics.putInt(GfxMonitor.KEY_MAX_SLOW_BITMAP_UPLOADS, Collections.max(slowUiThread));
+
+ // Store average and max slow draw
+ metrics.putDouble(GfxMonitor.KEY_AVG_SLOW_DRAW, MetricsHelper.computeAverageInt(slowDraw));
+ metrics.putInt(GfxMonitor.KEY_MAX_SLOW_DRAW, Collections.max(slowDraw));
+
+ return metrics;
+ }
+
+ private String getMatchGroup(String input, Pattern pattern, int groupIndex) {
+ String ret = null;
+ Matcher matcher = pattern.matcher(input);
+ if (matcher.matches()) {
+ ret = matcher.group(groupIndex);
+ }
+ return ret;
+ }
+}
diff --git a/src/android/support/test/jank/internal/JankMonitor.java b/src/android/support/test/jank/internal/JankMonitor.java
new file mode 100644
index 0000000..6816f6b
--- /dev/null
+++ b/src/android/support/test/jank/internal/JankMonitor.java
@@ -0,0 +1,27 @@
+/*
+ * 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.support.test.jank.internal;
+
+import android.os.Bundle;
+
+public interface JankMonitor {
+ public abstract void startIteration() throws Throwable;
+
+ public abstract int stopIteration() throws Throwable;
+
+ public abstract Bundle getMetrics() throws Throwable;
+}
diff --git a/src/android/support/test/jank/internal/JankMonitorFactory.java b/src/android/support/test/jank/internal/JankMonitorFactory.java
new file mode 100644
index 0000000..9c25262
--- /dev/null
+++ b/src/android/support/test/jank/internal/JankMonitorFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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.support.test.jank.internal;
+
+import android.app.UiAutomation;
+import android.support.test.jank.WindowAnimationFrameStatsMonitor;
+import android.support.test.jank.WindowContentFrameStatsMonitor;
+import android.support.test.jank.GfxMonitor;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class JankMonitorFactory {
+
+ private UiAutomation mUiAutomation;
+
+ public JankMonitorFactory(UiAutomation automation) {
+ mUiAutomation = automation;
+ }
+
+ public List<JankMonitor> getJankMonitors(Method testMethod) {
+ List<JankMonitor> monitors = new ArrayList<JankMonitor>();
+ if (testMethod.getAnnotation(GfxMonitor.class) != null) {
+ String process = testMethod.getAnnotation(GfxMonitor.class).processName();
+ monitors.add(new GfxMonitorImpl(mUiAutomation, process));
+ }
+ if (testMethod.getAnnotation(WindowContentFrameStatsMonitor.class) != null) {
+ monitors.add(new WindowContentFrameStatsMonitorImpl(mUiAutomation));
+ }
+ if (testMethod.getAnnotation(WindowAnimationFrameStatsMonitor.class) != null) {
+ monitors.add(new WindowAnimationFrameStatsMonitorImpl(mUiAutomation));
+ }
+ return monitors;
+ }
+}
diff --git a/src/android/support/test/jank/internal/MetricsHelper.java b/src/android/support/test/jank/internal/MetricsHelper.java
new file mode 100644
index 0000000..20ba8d2
--- /dev/null
+++ b/src/android/support/test/jank/internal/MetricsHelper.java
@@ -0,0 +1,38 @@
+/*
+ * 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.support.test.jank.internal;
+
+import java.util.List;
+
+class MetricsHelper {
+
+ public static double computeAverageFloat(List<Double> values) {
+ double sum = 0.0f;
+ for (Double value : values) {
+ sum += value;
+ }
+ return sum / values.size();
+ }
+
+ public static double computeAverageInt(List<Integer> values) {
+ double sum = 0.0f;
+ for (Integer value : values) {
+ sum += value;
+ }
+ return sum / values.size();
+ }
+}
diff --git a/src/android/support/test/jank/internal/WindowAnimationFrameMonitorImpl.java b/src/android/support/test/jank/internal/WindowAnimationFrameMonitorImpl.java
new file mode 100644
index 0000000..2a0ad11
--- /dev/null
+++ b/src/android/support/test/jank/internal/WindowAnimationFrameMonitorImpl.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.support.test.jank.internal;
+
+import android.app.UiAutomation;
+import android.os.Bundle;
+import android.support.test.jank.WindowAnimationFrameStatsMonitor;
+import android.view.FrameStats;
+
+import java.util.Collections;
+
+/**
+ * Monitors {@link android.view.WindowAnimationFrameStats} to detect janky frames.
+ *
+ * Reports average and max jank, as well as average frames per second and max frame times.
+ */
+class WindowAnimationFrameStatsMonitorImpl extends FrameStatsMonitorBase {
+
+ private UiAutomation mUiAutomation;
+
+ public WindowAnimationFrameStatsMonitorImpl(UiAutomation automation) {
+ mUiAutomation = automation;
+ }
+
+ public Bundle getMetrics() {
+ Bundle metrics = new Bundle();
+
+ // Store average and max jank
+ metrics.putDouble(WindowAnimationFrameStatsMonitor.KEY_AVG_NUM_JANKY,
+ MetricsHelper.computeAverageInt(mJankyFrames));
+ metrics.putInt(WindowAnimationFrameStatsMonitor.KEY_MAX_NUM_JANKY,
+ Collections.max(mJankyFrames));
+
+ // Store average fps
+ metrics.putDouble(WindowAnimationFrameStatsMonitor.KEY_AVG_FPS,
+ MetricsHelper.computeAverageFloat(mFps));
+
+ // Store average max frame duration
+ metrics.putDouble(WindowAnimationFrameStatsMonitor.KEY_AVG_LONGEST_FRAME,
+ MetricsHelper.computeAverageFloat(mLongestNormalizedFrames));
+
+ return metrics;
+ }
+
+ @Override
+ public void startIteration() {
+ // Clear out any previous data
+ mUiAutomation.clearWindowAnimationFrameStats();
+ }
+
+ @Override
+ public int stopIteration() {
+ FrameStats stats = mUiAutomation.getWindowAnimationFrameStats();
+ analyze(stats);
+ return stats.getFrameCount();
+ }
+}
diff --git a/src/android/support/test/jank/internal/WindowContentFrameMonitorImpl.java b/src/android/support/test/jank/internal/WindowContentFrameMonitorImpl.java
new file mode 100644
index 0000000..30db841
--- /dev/null
+++ b/src/android/support/test/jank/internal/WindowContentFrameMonitorImpl.java
@@ -0,0 +1,105 @@
+/*
+ * 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.support.test.jank.internal;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.UiAutomation;
+import android.os.Bundle;
+import android.support.test.jank.WindowContentFrameStatsMonitor;
+import android.util.Log;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.FrameStats;
+
+import java.util.Collections;
+
+/**
+ * Monitors {@link android.view.WindowContentFrameStats} to detect janky frames.
+ *
+ * Reports average and max jank, as well as average frames per second and max frame times.
+ */
+class WindowContentFrameStatsMonitorImpl extends FrameStatsMonitorBase {
+
+ private static final String TAG = "JankTestHelper";
+
+ private UiAutomation mUiAutomation;
+ private int mWindowId = -1;
+
+ public WindowContentFrameStatsMonitorImpl(UiAutomation automation) {
+ mUiAutomation = automation;
+ }
+
+ public Bundle getMetrics() {
+ Bundle metrics = new Bundle();
+
+ // Store average and max jank
+ metrics.putDouble(WindowContentFrameStatsMonitor.KEY_AVG_NUM_JANKY,
+ MetricsHelper.computeAverageInt(mJankyFrames));
+ metrics.putInt(WindowContentFrameStatsMonitor.KEY_MAX_NUM_JANKY,
+ Collections.max(mJankyFrames));
+
+ // Store average fps
+ metrics.putDouble(WindowContentFrameStatsMonitor.KEY_AVG_FPS,
+ MetricsHelper.computeAverageFloat(mFps));
+
+ // Store average max frame duration
+ metrics.putDouble(WindowContentFrameStatsMonitor.KEY_AVG_LONGEST_FRAME,
+ MetricsHelper.computeAverageFloat(mLongestNormalizedFrames));
+
+ return metrics;
+ }
+
+ @Override
+ public void startIteration() {
+ // Save the window id
+ mWindowId = getCurrentWindow();
+
+ // Clear out any previous data
+ mUiAutomation.clearWindowContentFrameStats(mWindowId);
+ }
+
+ @Override
+ public int stopIteration() {
+ int currentWindow = getCurrentWindow();
+ if (currentWindow != mWindowId) {
+ Log.w(TAG, "Current window changed during the test. Did you mean to use "
+ + "WindowAnimationFrameStatsMonitor?");
+ }
+ FrameStats stats = mUiAutomation.getWindowContentFrameStats(currentWindow);
+ analyze(stats);
+
+ mWindowId = -1;
+ return stats.getFrameCount();
+ }
+
+ /** Returns the id of the current window. */
+ private int getCurrentWindow() {
+ // Subscribe to window information
+ AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
+ info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
+ mUiAutomation.setServiceInfo(info);
+
+ AccessibilityNodeInfo activeWindowRoot = mUiAutomation.getRootInActiveWindow();
+
+ for (AccessibilityWindowInfo window : mUiAutomation.getWindows()) {
+ if (window.getRoot().equals(activeWindowRoot)) {
+ return window.getId();
+ }
+ }
+ throw new RuntimeException("Could not find active window");
+ }
+}