diff options
author | Geoff Mendal <mendal@google.com> | 2015-03-18 18:24:35 -0500 |
---|---|---|
committer | Geoff Mendal <mendal@google.com> | 2015-03-18 18:24:35 -0500 |
commit | 39c6037837309ff2814e9b7ff29cd73f3786a218 (patch) | |
tree | 548aac06dc0dfd70b52da73702e6c3171001fbe9 | |
parent | e9713da1b6f0b9664a25cd561911cd241c432a69 (diff) | |
parent | dd0862415ca6928664b0d1b61d93db9866c4d563 (diff) | |
download | janktesthelper-39c6037837309ff2814e9b7ff29cd73f3786a218.tar.gz |
Merge jank/ from platform/frameworks/uiautomator to /
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"); + } +} |