Add Instrumentation-based jank utility.

Change-Id: I7a80dc2d5282997b383993df4d203fb01141bd57
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..8ffa1de
--- /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_MODULE := janktesthelper
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+include $(LOCAL_PATH)/tests/Android.mk
diff --git a/src/android/support/test/jank/JankResult.java b/src/android/support/test/jank/JankResult.java
new file mode 100644
index 0000000..a5a1eb4
--- /dev/null
+++ b/src/android/support/test/jank/JankResult.java
@@ -0,0 +1,70 @@
+/*
+ * 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.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 {
+
+    // 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++) {
+            // TODO: Handle fenced frames that have not been presented. Either skip or throw.
+            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
new file mode 100644
index 0000000..1256ab7
--- /dev/null
+++ b/src/android/support/test/jank/JankTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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 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 type of jank to measure */
+    JankType type();
+
+    /** The minimum number of frames expected */
+    int expectedFrames();
+
+    /** Alternate method to execute before the test method */
+    String beforeTest() default "beforeTest";
+
+    /** Alternate method to execute before each iteration */
+    String beforeLoop() default "beforeLoop";
+
+    /** Alternate method to execute after each iteration */
+    String afterLoop() default "afterLoop";
+
+    /** Alternate method to execute after all iterations have completed */
+    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..2ace5c6
--- /dev/null
+++ b/src/android/support/test/jank/JankTestBase.java
@@ -0,0 +1,182 @@
+/*
+ * 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.app.UiAutomation;
+import android.os.Bundle;
+import android.test.InstrumentationTestCase;
+import android.test.InstrumentationTestRunner;
+import android.util.Log;
+import android.view.FrameStats;
+import android.view.WindowAnimationFrameStats;
+import android.view.WindowContentFrameStats;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import java.lang.Math;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * 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 {
+
+    public static final String KEY_AVG_JANK = "avg-jank";
+    public static final String KEY_MAX_JANK = "max-jank";
+    public static final String KEY_AVG_FPS = "avg-fps";
+    public static final String KEY_AVG_MAX_FRAME_DURATION = "avg-max-frame-duration";
+
+    public static final String DEFAULT_ITERATIONS = "20";
+
+    private Bundle arguments = null;
+    private int mCurrentIteration = 0;
+
+
+    /** Called once before executing a test method. */
+    public void beforeTest() {
+        // Default implementation. Do nothing.
+    }
+
+    /** Called before each iteration of the test method. */
+    public void beforeLoop() {
+        // Default implementation. Do nothing.
+    }
+
+    /** Called after each iteration of the test method. */
+    public void afterLoop() {
+        // Default implementation. Do nothing.
+    }
+
+    /** Called once after all iterations have completed. */
+    public void afterTest() {
+        // Default implementation. Do nothing.
+    }
+
+    /** 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  = resolveMethod(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;
+
+        // Test setup
+        beforeTest.invoke(this, (Object[])null);
+
+        // Execute the test several times according to the "iteration" parameter
+        int iterations = Integer.valueOf(getArguments().getString("iterations",
+                DEFAULT_ITERATIONS));
+        for (; mCurrentIteration < iterations; mCurrentIteration++) {
+            // Loop setup
+            beforeLoop.invoke(this, (Object[])null);
+
+            // Start monitoring jank
+            jank.startMonitor(annotation.type());
+
+            // Run the test method
+            testMethod.invoke(this, (Object[])null);
+
+            // Stop monitoring
+            JankResult result = jank.stopMonitor();
+
+            // Fail the test if we didn't get enough frames
+            assertTrue(result.numFrames > annotation.expectedFrames());
+
+            // Update stats
+            sumJankyFrames += result.numJanky;
+            maxJankyFrames = Math.max(maxJankyFrames, result.numJanky);
+            sumFps += result.fps;
+            sumLongestFrame += result.longestFrameNormalized;
+
+            // Loop tear down
+            afterLoop.invoke(this, (Object[])null);
+        }
+
+        // Test tear down
+        afterTest.invoke(this, (Object[])null);
+
+        // Report results
+        Bundle status = new Bundle();
+        status.putDouble(KEY_AVG_JANK, (double)sumJankyFrames / iterations);
+        status.putInt(KEY_MAX_JANK, maxJankyFrames);
+        status.putDouble(KEY_AVG_FPS, sumFps / iterations);
+        status.putDouble(KEY_AVG_MAX_FRAME_DURATION, sumLongestFrame / iterations);
+        getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+    }
+
+
+    /** 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 Bundle} containing the command line parameters. */
+    private Bundle getArguments() {
+        if (arguments == null) {
+            Instrumentation instrumentation = getInstrumentation();
+            // This hack only works for InstrumentationTestRunner subclasses
+            if (instrumentation instanceof InstrumentationTestRunner) {
+                arguments = ((InstrumentationTestRunner)instrumentation).getArguments();
+            } else {
+                throw new RuntimeException("Unsupported test runner");
+            }
+        }
+        return arguments;
+    }
+}
diff --git a/src/android/support/test/jank/JankType.java b/src/android/support/test/jank/JankType.java
new file mode 100644
index 0000000..b1a0fde
--- /dev/null
+++ b/src/android/support/test/jank/JankType.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/** Enumeration used to specify the type of jank to measure. */
+public enum JankType {
+    CONTENT_FRAMES, ANIMATION_FRAMES
+}
diff --git a/src/android/support/test/jank/JankUtil.java b/src/android/support/test/jank/JankUtil.java
new file mode 100644
index 0000000..9e4497b
--- /dev/null
+++ b/src/android/support/test/jank/JankUtil.java
@@ -0,0 +1,125 @@
+/*
+ * 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.view.accessibility.AccessibilityWindowInfo;
+import android.view.FrameStats;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** The {@link JankUtil} class provides functionality for monitoring jank. */
+public class JankUtil {
+
+    // 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(getCurrentWindow());
+        } 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;
+
+        public WindowContentJankMonitor(int windowId) {
+            mWindowId = windowId;
+        }
+
+        @Override
+        public void clear() {
+            mUiAutomation.clearWindowContentFrameStats(mWindowId);
+        }
+
+        @Override
+        public FrameStats getStats() {
+            return mUiAutomation.getWindowContentFrameStats(mWindowId);
+        }
+    }
+
+    /** 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();
+        }
+    }
+}