Add support for dumpsys gfxinfo based jank detection.

Refactored the framework to support the use of multiple jank detection
techniques on a single test. Instead of using the type parameter in the
JankTest annotation, each method of detecting jank is enabled via it's
own annotation.

'dumpsys gfxinfo' based jank detection is enabled by adding the
@GfxMonitor annotation to a test method.

Change-Id: I8277a367f33859ec75726568b7f164ab144f5cd8
diff --git a/src/android/support/test/jank/GfxMonitor.java b/src/android/support/test/jank/GfxMonitor.java
new file mode 100644
index 0000000..36ca81b
--- /dev/null
+++ b/src/android/support/test/jank/GfxMonitor.java
@@ -0,0 +1,30 @@
+/*
+ * 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();
+}
diff --git a/src/android/support/test/jank/JankMetrics.java b/src/android/support/test/jank/JankMetrics.java
deleted file mode 100644
index b227804..0000000
--- a/src/android/support/test/jank/JankMetrics.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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;
-
-/**
- * {@link JankMetrics} contains aggregated {@link JankResult} after looped execution of a test case.
- *
- */
-public class JankMetrics {
-    /** average number of jank across iterations */
-    public double averageJank;
-    /** max jank number among all iterations */
-    public int maxJank;
-    /** average FPS across iterations */
-    public double averageFps;
-    /** average of longest frame duration reported from all frames rendered for each iteration */
-    public double averageMaxFrameDuration;
-}
diff --git a/src/android/support/test/jank/JankResult.java b/src/android/support/test/jank/JankResult.java
deleted file mode 100644
index 71e595c..0000000
--- a/src/android/support/test/jank/JankResult.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.util.Log;
-import android.view.FrameStats;
-
-/** A {@link JankResult} contains the results of a jank monitoring session. This includes the number
- * of frames analyzed, the number of frames that were janky, the average frames per second, as well
- * as the nomalized longest frame time.*/
-public class JankResult {
-
-    private static final String TAG = JankResult.class.getSimpleName();
-
-    // 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;
-
-    public final int numFrames;
-    public final int numJanky;
-    public final double fps;
-    public final double longestFrameNormalized;
-
-    /** Private constructor. Clients should use {@link JankResult#analyze(FrameStats)} instead. */
-    private JankResult(int numFrames, int numJanky, double fps, double longestFrameNormalized) {
-        this.numFrames = numFrames;
-        this.numJanky  = numJanky;
-        this.fps       = fps;
-        this.longestFrameNormalized = longestFrameNormalized;
-    }
-
-    /** Analyze the given {@link FrameStats} and return the resulting jank info. */
-    public static JankResult analyze(FrameStats stats) {
-        int frameCount = stats.getFrameCount();
-        long refreshPeriod = stats.getRefreshPeriodNano();
-
-        int numJanky = 0;
-        double longestFrameNormalized = 0.0f;
-        long totalDuration = 0;
-        // 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;
-
-        return new JankResult(frameCount, numJanky, fps, longestFrameNormalized);
-    }
-}
diff --git a/src/android/support/test/jank/JankTest.java b/src/android/support/test/jank/JankTest.java
index 45a386b..c79c6fa 100644
--- a/src/android/support/test/jank/JankTest.java
+++ b/src/android/support/test/jank/JankTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * 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.
@@ -26,9 +26,6 @@
 @Target(ElementType.METHOD)
 public @interface JankTest {
 
-    /** The type of jank to measure */
-    JankType type();
-
     /** The minimum number of frames expected */
     int expectedFrames();
 
@@ -59,8 +56,10 @@
     /**
      * Alternate method to execute after all iterations have completed.
      * <p>
-     * <b>Important:</b> the annotated method must take a parameter of type {@link JankMetrics}.
-     * See {@link JankTestBase#afterTest(JankMetrics)}
-     * */
+     * <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
index fd85fab..1439da0 100644
--- a/src/android/support/test/jank/JankTestBase.java
+++ b/src/android/support/test/jank/JankTestBase.java
@@ -20,12 +20,15 @@
 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.
@@ -70,14 +73,9 @@
      * <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(JankMetrics metrics) throws Exception {
-        Bundle status = new Bundle();
-        status.putDouble(KEY_AVG_JANK, metrics.averageJank);
-        status.putInt(KEY_MAX_JANK, metrics.maxJank);
-        status.putDouble(KEY_AVG_FPS, metrics.averageFps);
-        status.putDouble(KEY_AVG_MAX_FRAME_DURATION, metrics.averageMaxFrameDuration);
-        getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+     */
+    public void afterTest(Bundle metrics) {
+        getInstrumentation().sendStatus(Activity.RESULT_OK, metrics);
     }
 
     /** Return the index of the currently executing iteration. */
@@ -96,14 +94,10 @@
         Method afterLoop  = resolveMethod(annotation.afterLoop());
         Method afterTest  = resolveAfterTest(annotation.afterTest());
 
-        // Get a JankUtil instance
-        JankUtil jank = JankUtil.getInstance(getInstrumentation());
-
-        // Stats to track
-        int sumJankyFrames = 0;
-        int maxJankyFrames = 0;
-        double sumFps = 0.0f;
-        double sumLongestFrame = 0.0f;
+        // 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);
@@ -116,36 +110,32 @@
             beforeLoop.invoke(this, (Object[])null);
 
             // Start monitoring jank
-            jank.startMonitor(annotation.type());
+            for (JankMonitor monitor : monitors) {
+                monitor.startIteration();
+            }
 
             // Run the test method
             testMethod.invoke(this, (Object[])null);
 
             // Stop monitoring
-            JankResult result = jank.stopMonitor();
+            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(), result.numFrames),
-                    result.numFrames >= annotation.expectedFrames());
-
-            // Update stats
-            sumJankyFrames += result.numJanky;
-            maxJankyFrames = Math.max(maxJankyFrames, result.numJanky);
-            sumFps += result.fps;
-            sumLongestFrame += result.longestFrameNormalized;
+                // 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
-        JankMetrics metrics = new JankMetrics();
-        metrics.averageJank = (double)sumJankyFrames / iterations;
-        metrics.maxJank = maxJankyFrames;
-        metrics.averageFps = sumFps / iterations;
-        metrics.averageMaxFrameDuration = sumLongestFrame / iterations;
-        // Test tear down and reporting
+        Bundle metrics = new Bundle();
+        for (JankMonitor monitor : monitors) {
+            metrics.putAll(monitor.getMetrics());
+        }
         afterTest.invoke(this, metrics);
     }
 
@@ -177,7 +167,7 @@
 
         Method method = null;
         try {
-            method = getClass().getMethod(name, JankMetrics.class);
+            method = getClass().getMethod(name, Bundle.class);
         } catch (NoSuchMethodException e) {
             fail("method annotated with JankTest#afterTest has wrong signature");
         }
diff --git a/src/android/support/test/jank/JankUtil.java b/src/android/support/test/jank/JankUtil.java
deleted file mode 100644
index d433d98..0000000
--- a/src/android/support/test/jank/JankUtil.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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.accessibilityservice.AccessibilityServiceInfo;
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.util.Log;
-import android.view.FrameStats;
-import android.view.accessibility.AccessibilityWindowInfo;
-
-/** The {@link JankUtil} class provides functionality for monitoring jank. */
-public class JankUtil {
-
-    private static final String TAG = JankUtil.class.getSimpleName();
-
-    // Singleton instance
-    private static JankUtil sInstance;
-
-    private UiAutomation mUiAutomation;
-    private JankMonitor mMonitor;
-
-    /** Private constructor. Clients should use {@link JankUtil#getInstance(Instrumentation)}. */
-    private JankUtil(UiAutomation automation) {
-        mUiAutomation = automation;
-
-        // Subscribe to window information
-        AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
-        info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
-        mUiAutomation.setServiceInfo(info);
-    }
-
-    /** Returns a {@link JankUtil} instance. */
-    public static JankUtil getInstance(Instrumentation instrumentation) {
-        if (sInstance == null) {
-            sInstance = new JankUtil(instrumentation.getUiAutomation());
-        }
-        return sInstance;
-    }
-
-    /** Starts monitoring for janky frames of the given {@code type}. */
-    public void startMonitor(JankType type) {
-        if (mMonitor != null) {
-            throw new IllegalStateException("Monitor already started");
-        }
-
-        if (type == JankType.CONTENT_FRAMES) {
-            mMonitor = new WindowContentJankMonitor();
-        } else if (type == JankType.ANIMATION_FRAMES) {
-            mMonitor = new WindowAnimationJankMonitor();
-        } else {
-            throw new RuntimeException("Invalid type");
-        }
-
-        mMonitor.clear();
-    }
-
-    /** Stops monitoring and returns the {@link JankResult} for this monitoring session. */
-    public JankResult stopMonitor() {
-        FrameStats stats = mMonitor.getStats();
-        mMonitor = null;
-        return JankResult.analyze(stats);
-    }
-
-    /** Returns the id of the current application window. */
-    private int getCurrentWindow() {
-        for (AccessibilityWindowInfo window : mUiAutomation.getWindows()) {
-            if (window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION) {
-                return window.getId();
-            }
-        }
-        throw new RuntimeException("No application window found");
-    }
-
-    /** Generic monitoring interface */
-    private static interface JankMonitor {
-        public void clear();
-        public FrameStats getStats();
-    }
-
-    /** Monitor for detecting window content frame jank. */
-    private class WindowContentJankMonitor implements JankMonitor {
-        private int mWindowId = -1;
-
-        @Override
-        public void clear() {
-            mWindowId = getCurrentWindow();
-            mUiAutomation.clearWindowContentFrameStats(mWindowId);
-        }
-
-        @Override
-        public FrameStats getStats() {
-            int currentWindow = getCurrentWindow();
-            if (currentWindow != mWindowId) {
-                Log.w(TAG, "Current window changed during the test. Did you mean to measure " +
-                        "ANIMATION_FRAMES?");
-            }
-            mWindowId = -1;
-            return mUiAutomation.getWindowContentFrameStats(currentWindow);
-        }
-    }
-
-    /** Monitor for detecting window animation frame jank. */
-    private class WindowAnimationJankMonitor implements JankMonitor {
-        @Override
-        public void clear() {
-            mUiAutomation.clearWindowAnimationFrameStats();
-        }
-
-        @Override
-        public FrameStats getStats() {
-            return mUiAutomation.getWindowAnimationFrameStats();
-        }
-    }
-}
diff --git a/src/android/support/test/jank/WindowAnimationFrameStatsMonitor.java b/src/android/support/test/jank/WindowAnimationFrameStatsMonitor.java
new file mode 100644
index 0000000..1f7ab57
--- /dev/null
+++ b/src/android/support/test/jank/WindowAnimationFrameStatsMonitor.java
@@ -0,0 +1,28 @@
+/*
+ * 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 {
+}
diff --git a/src/android/support/test/jank/JankType.java b/src/android/support/test/jank/WindowContentFrameStatsMonitor.java
similarity index 60%
copy from src/android/support/test/jank/JankType.java
copy to src/android/support/test/jank/WindowContentFrameStatsMonitor.java
index b1a0fde..034eec7 100644
--- a/src/android/support/test/jank/JankType.java
+++ b/src/android/support/test/jank/WindowContentFrameStatsMonitor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * 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.
@@ -16,7 +16,13 @@
 
 package android.support.test.jank;
 
-/** Enumeration used to specify the type of jank to measure. */
-public enum JankType {
-    CONTENT_FRAMES, ANIMATION_FRAMES
+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 {
 }
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..162af3a
--- /dev/null
+++ b/src/android/support/test/jank/internal/FrameStatsMonitorBase.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.os.Bundle;
+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;
+
+    // Jank metrics namespace and helper class
+    private static final String MONITOR_PREFIX = "frame";
+    private static final MetricsHelper mMetricsHelper = new MetricsHelper(MONITOR_PREFIX);
+
+    // Key values for the metrics reported by this monitor
+    private static final String KEY_NUM_JANKY = "jank";
+    private static final String KEY_FPS = "fps";
+    private static final String KEY_LONGEST_FRAME = "max-frame-duration";
+
+    // Accumulated stats
+    ArrayList<Integer> mJankyFrames = new ArrayList<Integer>();
+    ArrayList<Double> mFps = new ArrayList<Double>();
+    ArrayList<Double> mLongestNormalizedFrames = new ArrayList<Double>();
+
+
+    public Bundle getMetrics() {
+        Bundle metrics = new Bundle();
+
+        // Store average and max jank
+        mMetricsHelper.putSummaryMetrics(metrics, KEY_NUM_JANKY, mJankyFrames);
+
+        // Store average fps
+        mMetricsHelper.putAverageMetric(metrics, KEY_FPS, mFps);
+
+        // Store average max frame duration
+        mMetricsHelper.putAverageMetric(metrics, KEY_LONGEST_FRAME, mLongestNormalizedFrames);
+
+        return metrics;
+    }
+
+    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..9aae574
--- /dev/null
+++ b/src/android/support/test/jank/internal/GfxMonitorImpl.java
@@ -0,0 +1,194 @@
+/*
+ * 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 java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.util.ArrayList;
+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 {
+
+    // Jank metrics namespace and helper class
+    private static final String MONITOR_PREFIX = "gfx";
+    private static final MetricsHelper mMetricsHelper = new MetricsHelper(MONITOR_PREFIX);
+
+    // Key values for the metrics reported by this monitor
+    private static final String KEY_NUM_JANKY = "jank";
+    private static final String KEY_MISSED_VSYNC = "missed-vsync";
+    private static final String KEY_HIGH_INPUT_LATENCY = "high-input-latency";
+    private static final String KEY_SLOW_UI_THREAD = "slow-ui-thread";
+    private static final String KEY_SLOW_BITMAP_UPLOADS = "slow-bitmap-uploads";
+    private static final String KEY_SLOW_DRAW = "slow-draw";
+
+    // 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+)");
+
+    // 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>();
+
+
+    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: ### (##.##%)
+        //    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 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();
+        mMetricsHelper.putSummaryMetrics(metrics, KEY_NUM_JANKY, jankyFrames);
+        mMetricsHelper.putSummaryMetrics(metrics, KEY_MISSED_VSYNC, missedVsync);
+        mMetricsHelper.putSummaryMetrics(metrics, KEY_HIGH_INPUT_LATENCY, highInputLatency);
+        mMetricsHelper.putSummaryMetrics(metrics, KEY_SLOW_UI_THREAD, slowUiThread);
+        mMetricsHelper.putSummaryMetrics(metrics, KEY_SLOW_BITMAP_UPLOADS, slowBitmapUploads);
+        mMetricsHelper.putSummaryMetrics(metrics, KEY_SLOW_DRAW, 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/JankType.java b/src/android/support/test/jank/internal/JankMonitor.java
similarity index 62%
rename from src/android/support/test/jank/JankType.java
rename to src/android/support/test/jank/internal/JankMonitor.java
index b1a0fde..6816f6b 100644
--- a/src/android/support/test/jank/JankType.java
+++ b/src/android/support/test/jank/internal/JankMonitor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * 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.
@@ -14,9 +14,14 @@
  * limitations under the License.
  */
 
-package android.support.test.jank;
+package android.support.test.jank.internal;
 
-/** Enumeration used to specify the type of jank to measure. */
-public enum JankType {
-    CONTENT_FRAMES, ANIMATION_FRAMES
+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..8a88c88
--- /dev/null
+++ b/src/android/support/test/jank/internal/MetricsHelper.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+import android.text.TextUtils;
+
+import java.util.List;
+
+class MetricsHelper {
+
+    private static final String KEY_SEPARATOR = "-";
+    private static final String MAX_VALUE_PREFIX = "max";
+    private static final String AVG_VALUE_PREFIX = "avg";
+
+    private final String mMonitorPrefix;
+
+    public MetricsHelper(String monitorPrefix) {
+        mMonitorPrefix = monitorPrefix;
+    }
+
+    /** Stores the average metric for the given set of values. */
+    public void putAverageMetric(Bundle metrics, String key, List<Double> values) {
+        double sum = 0.0f;
+        for (Double value : values) {
+            sum += value;
+        }
+        metrics.putDouble(joinKey(mMonitorPrefix, MAX_VALUE_PREFIX, key), sum / values.size());
+    }
+
+    /** Stores the average and max metrics for the given set of values. */
+    public void putSummaryMetrics(Bundle metrics, String key, List<Integer> values) {
+        int max = -1;
+        int sum = 0;
+        for (Integer value : values) {
+            max = Math.max(max, value);
+            sum += value;
+        }
+        metrics.putInt(joinKey(mMonitorPrefix, MAX_VALUE_PREFIX, key), max);
+        metrics.putDouble(joinKey(mMonitorPrefix, AVG_VALUE_PREFIX, key),
+                (double)sum / values.size());
+    }
+
+    private String joinKey(String... parts) {
+        return TextUtils.join(KEY_SEPARATOR, parts);
+    }
+}
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..1fed858
--- /dev/null
+++ b/src/android/support/test/jank/internal/WindowAnimationFrameMonitorImpl.java
@@ -0,0 +1,47 @@
+/*
+ * 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.view.FrameStats;
+
+/**
+ * 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;
+    }
+
+    @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..9ff3a94
--- /dev/null
+++ b/src/android/support/test/jank/internal/WindowContentFrameMonitorImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.test.jank.internal;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.UiAutomation;
+import android.util.Log;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.FrameStats;
+
+/**
+ * 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;
+    }
+
+    @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");
+    }
+}