Merge "Add ability to receive command line args in JUnit3 tests."
diff --git a/uiautomator/api/current.txt b/uiautomator/api/current.txt
index 609c1b3..664fbbc 100644
--- a/uiautomator/api/current.txt
+++ b/uiautomator/api/current.txt
@@ -11,6 +11,7 @@
public class UiDevice {
method public void clearLastTraversedText();
method public boolean click(int, int);
+ method public boolean drag(int, int, int, int, int);
method public void dumpWindowHierarchy(java.lang.String);
method public void freezeRotation() throws android.os.RemoteException;
method public deprecated java.lang.String getCurrentActivityName();
@@ -67,6 +68,8 @@
method public boolean clickAndWaitForNewWindow(long) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean clickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean clickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException;
+ method public boolean dragTo(com.android.uiautomator.core.UiObject, int) throws com.android.uiautomator.core.UiObjectNotFoundException;
+ method public boolean dragTo(int, int, int) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean exists();
method protected android.view.accessibility.AccessibilityNodeInfo findAccessibilityNodeInfo(long);
method public android.graphics.Rect getBounds() throws com.android.uiautomator.core.UiObjectNotFoundException;
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java b/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
index 7deb3a6..a00f2ea 100644
--- a/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
@@ -424,6 +424,20 @@
* @return true if the swipe executed successfully
*/
public boolean swipe(int downX, int downY, int upX, int upY, int steps) {
+ return swipe(downX, downY, upX, upY, steps, false /*drag*/);
+ }
+
+ /**
+ * Handle swipes/drags in any direction.
+ * @param downX
+ * @param downY
+ * @param upX
+ * @param upY
+ * @param steps
+ * @param drag when true, the swipe becomes a drag swipe
+ * @return true if the swipe executed successfully
+ */
+ public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) {
boolean ret = false;
int swipeSteps = steps;
double xStep = 0;
@@ -438,6 +452,8 @@
// first touch starts exactly at the point requested
ret = touchDown(downX, downY);
+ if (drag)
+ SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
for(int i = 1; i < swipeSteps; i++) {
ret &= touchMove(downX + (int)(xStep * i), downY + (int)(yStep * i));
if(ret == false)
@@ -448,6 +464,8 @@
// a preset delay.
SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
}
+ if (drag)
+ SystemClock.sleep(REGULAR_CLICK_LENGTH);
ret &= touchUp(upX, upY);
return(ret);
}
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
index 2305e3a..4d8016e 100644
--- a/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
@@ -398,7 +398,26 @@
public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
Tracer.trace(startX, startY, endX, endY, steps);
return mUiAutomationBridge.getInteractionController()
- .scrollSwipe(startX, startY, endX, endY, steps);
+ .swipe(startX, startY, endX, endY, steps);
+ }
+
+ /**
+ * Performs a swipe from one coordinate to another using the number of steps
+ * to determine smoothness and speed. Each step execution is throttled to 5ms
+ * per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
+ *
+ * @param startX
+ * @param startY
+ * @param endX
+ * @param endY
+ * @param steps is the number of move steps sent to the system
+ * @return false if the operation fails or the coordinates are invalid
+ * @since API Level 18
+ */
+ public boolean drag(int startX, int startY, int endX, int endY, int steps) {
+ Tracer.trace(startX, startY, endX, endY, steps);
+ return mUiAutomationBridge.getInteractionController()
+ .swipe(startX, startY, endX, endY, steps, true);
}
/**
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
index 2594888..8e82a98 100644
--- a/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
@@ -188,6 +188,42 @@
}
/**
+ * Performs a drag of this object to a destination UiObject. Note that the number of steps
+ * used can influence the drag speed and varying speeds may impact the results. Consider
+ * evaluating different speeds when testing this method.
+ *
+ * @param destObj
+ * @param steps usually 40 steps. More or less to change the speed.
+ * @return true of successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public boolean dragTo(UiObject destObj, int steps) throws UiObjectNotFoundException {
+ Rect srcRect = getVisibleBounds();
+ Rect dstRect = destObj.getVisibleBounds();
+ return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(),
+ dstRect.centerX(), dstRect.centerY(), steps, true);
+ }
+
+ /**
+ * Performs a drag of this object to arbitrary coordinates. Note that the number of steps
+ * used will influence the drag speed and varying speeds may impact the results. Consider
+ * evaluating different speeds when testing this method.
+ *
+ * @param destX
+ * @param destY
+ * @param steps
+ * @return true of successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public boolean dragTo(int destX, int destY, int steps) throws UiObjectNotFoundException {
+ Rect srcRect = getVisibleBounds();
+ return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), destX, destY,
+ steps, true);
+ }
+
+ /**
* Perform the action on the UI element that is represented by this UiObject. Also see
* {@link UiScrollable#scrollToBeginning(int)}, {@link UiScrollable#scrollToEnd(int)},
* {@link UiScrollable#scrollBackward()}, {@link UiScrollable#scrollForward()}.
diff --git a/uiautomator_test_libraries/Android.mk b/uiautomator_test_libraries/Android.mk
new file mode 100644
index 0000000..f975016
--- /dev/null
+++ b/uiautomator_test_libraries/Android.mk
@@ -0,0 +1,33 @@
+#
+# Copyright (C) 2012 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_target_dir := $(TARGET_OUT_DATA)/local/tmp
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE := com.android.uiautomator.platform.common
+
+LOCAL_SDK_VERSION := 16
+
+LOCAL_JAVA_LIBRARIES := uiautomator_sdk_v$(LOCAL_SDK_VERSION)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(local_target_dir)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/AppHelperBase.java b/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/AppHelperBase.java
new file mode 100644
index 0000000..e3d9b61
--- /dev/null
+++ b/uiautomator_test_libraries/src/com/android/uiautomator/common/helpers/AppHelperBase.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2013 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 com.android.uiautomator.common.helpers;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+
+/**
+ * Base app helper class intended for all app helper to extend.
+ * This class provides common APIs that are expected to be present across
+ * all app helpers.
+ */
+public abstract class AppHelperBase {
+
+ /*
+ * App helpers should provide methods for accessing various UI widgets.
+ * Assume the app has an Action Bar, the helper should provide something similar to
+ * SomeAppHelper.ActionBar.getRefreshButton(). Methods like this help the tests check the
+ * state of the targeted widget as well as clicking it if need be. These types of methods are
+ * referred to as object getters. If there are different components, consider creating internal
+ * name spaces as in the .ActionBar example for better context.
+ *
+ * Adding basic units of functionality APIs is also very helpful to test.
+ * Consider the Alarm clock application as an example. It would be helpful if its helper
+ * provided basic functionality such as, setAlarm(Date) and deleteAlarm(Date). Such basic
+ * and key functionality helper methods, will abstract the tests from the UI implementation and
+ * make tests more reliable.
+ */
+
+ /**
+ * Launches the application.
+ *
+ * This is typically performed by executing a shell command to launch the application
+ * via Intent. It is possible to launch the application by automating the Launcher
+ * views and finding the target app icon to launch, however, this is prone to failure if
+ * the Launcher UI implementation differ from one platform to another.
+ */
+ abstract public void open();
+
+ /**
+ * Checks if the application is in foreground.
+ *
+ * This is typically performed by verifying the current package name of the foreground
+ * application. See UiDevice.getCurrentPackageName()
+ * @return true if open, else false.
+ */
+ abstract public boolean isOpen();
+
+
+ /**
+ * Helper to execute a shell command.
+ * @param command
+ */
+ protected void runShellCommand(String command) {
+ Process p = null;
+ BufferedReader resultReader = null;
+ try {
+ p = Runtime.getRuntime().exec(command);
+ int status = p.waitFor();
+ if (status != 0) {
+ System.err.println(String.format("Run shell command: %s, status: %s", command,
+ status));
+ }
+ } catch (IOException e) {
+ System.err.println("// Exception from command " + command + ":");
+ System.err.println(e.toString());
+ } catch (InterruptedException e) {
+ System.err.println("// Interrupted while waiting for the command to finish. ");
+ System.err.println(e.toString());
+ } finally {
+ try {
+ if (resultReader != null) {
+ resultReader.close();
+ }
+ if (p != null) {
+ p.destroy();
+ }
+ } catch (IOException e) {
+ System.err.println(e.toString());
+ }
+ }
+ }
+}
diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/platform/JankTestBase.java b/uiautomator_test_libraries/src/com/android/uiautomator/platform/JankTestBase.java
new file mode 100644
index 0000000..42e4db1
--- /dev/null
+++ b/uiautomator_test_libraries/src/com/android/uiautomator/platform/JankTestBase.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2013 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 com.android.uiautomator.platform;
+
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.uiautomator.core.UiDevice;
+import com.android.uiautomator.testrunner.UiAutomatorTestCase;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Helper class for jankiness test.
+ * All the jank test must extend the JankBaseHelper
+ */
+public class JankTestBase extends UiAutomatorTestCase {
+ protected UiDevice mDevice;
+ protected BufferedWriter mWriter = null;
+ protected BufferedWriter mStatusWriter = null;
+ protected int mIteration = 1; // default iteration is set 1
+ protected Bundle mParams;
+ protected String mTestCaseName;
+ protected int mSuccessTestRuns = 0;
+
+ private final static String TAG = "BasePerformance";
+ // holds all params for the derived tests
+ private final static String PROPERTY_FILE_NAME = "UiJankinessTests.conf";
+ private final static String PARAM_CONFIG = "conf";
+ private final static String LOCAL_TMP_DIR = "/data/local/tmp/";
+ // File that hold the test results
+ private static String OUTPUT_FILE_NAME = LOCAL_TMP_DIR + "UiJankinessTestsOutput.txt";
+ // File that hold test status, e.g successful test iterations
+ private static String STATUS_FILE_NAME = LOCAL_TMP_DIR + "UiJankinessTestsStatus.txt";
+ private static int SUCCESS_THRESHOLD = 80;
+ private static boolean DEBUG = false;
+
+ /* Array to record jankiness data in each test iteration */
+ private int[] jankinessArray;
+ /* Array to record frame rate in each test iteration */
+ private double[] frameRateArray;
+ /* Array to save max accumulated frame number in each test iteration */
+ private int[] maxDeltaVsyncArray;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance();
+
+ mWriter = new BufferedWriter(new FileWriter(new File(OUTPUT_FILE_NAME), true));
+ mStatusWriter = new BufferedWriter(new FileWriter(new File(STATUS_FILE_NAME), true));
+
+ mParams = getParams();
+ if (mParams != null && !mParams.isEmpty()) {
+ log("mParams is not empty, get properties.");
+ String mIterationStr;
+ if ((mIterationStr = getPropertyString(mParams, "iteration")) != null) {
+ mIteration = Integer.valueOf(mIterationStr);
+ }
+ }
+ jankinessArray = new int[mIteration];
+ frameRateArray = new double[mIteration];
+ maxDeltaVsyncArray = new int[mIteration];
+ mTestCaseName = this.getName();
+
+ mSuccessTestRuns = 0;
+ mDevice.pressHome();
+ }
+
+ /**
+ * Expects a file from the command line via conf param or default following format each on its
+ * own line. <code>
+ * key=Value
+ * Browser_URL1=cnn.com
+ * Browser_URL2=google.com
+ * Camera_ShutterDelay=1000
+ * etc...
+ * </code>
+ * @param Bundle params
+ * @param key
+ * @return the value of the property else defaultValue
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ protected String getPropertyString(Bundle params, String key)
+ throws FileNotFoundException, IOException {
+ Properties prop = new Properties();
+ prop.load(new FileInputStream(new File(LOCAL_TMP_DIR,
+ params.getString(PARAM_CONFIG, PROPERTY_FILE_NAME))));
+ String value = prop.getProperty(key);
+ if (value != null && !value.isEmpty())
+ return value;
+ return null;
+ }
+
+ /**
+ * Expects a file from the command line via conf param or default following format each on its
+ * own line. <code>
+ * key=Value
+ * Browser_URL1=cnn.com
+ * Browser_URL2=google.com
+ * Camera_ShutterDelay=1000
+ * etc...
+ * </code>
+ * @param Bundle params
+ * @param key
+ * @return the value of the property else defaultValue
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ protected long getPropertyLong(Bundle params, String key)
+ throws FileNotFoundException, IOException {
+ Properties prop = new Properties();
+ prop.load(new FileInputStream(new File(LOCAL_TMP_DIR,
+ params.getString(PARAM_CONFIG, PROPERTY_FILE_NAME))));
+ String value = prop.getProperty(key);
+ if (value != null && !value.trim().isEmpty())
+ return Long.valueOf(value.trim());
+ return 0;
+ }
+
+ /**
+ * Process the raw data, calculate jankiness, frame rate and max accumulated frames number
+ * @param testCaseName
+ * @param iteration
+ */
+ protected void recordResults(String testCaseName, int iteration) {
+ long refreshPeriod = SurfaceFlingerHelper.getRefreshPeriod();
+ // get jankiness count
+ int jankinessCount = SurfaceFlingerHelper.getVsyncJankiness();
+ // get frame rate
+ double frameRate = SurfaceFlingerHelper.getFrameRate();
+ // get max accumulated frames
+ int maxDeltaVsync = SurfaceFlingerHelper.getMaxDeltaVsync();
+
+ // only record data when they are valid
+ if (jankinessCount >=0 && frameRate > 0) {
+ jankinessArray[iteration] = jankinessCount;
+ frameRateArray[iteration] = frameRate;
+ maxDeltaVsyncArray[iteration] = maxDeltaVsync;
+ mSuccessTestRuns++;
+ }
+ String msg = String.format("%s, iteration %d\n" +
+ "refresh period: %d\n" +
+ "jankiness count: %d\n" +
+ "frame rate: %f\n" +
+ "max accumulated frames: %d\n",
+ testCaseName, iteration, refreshPeriod,
+ jankinessCount, frameRate, maxDeltaVsync);
+ log(msg);
+ if (DEBUG) {
+ SurfaceFlingerHelper.printData(testCaseName, iteration);
+ }
+ }
+
+ /**
+ * Process data from all test iterations, and save to disk
+ * @param testCaseName
+ */
+ protected void saveResults(String testCaseName) {
+ // write test status into status file
+ try {
+ mStatusWriter.write(String.format("%s: %d success runs out of %d iterations\n",
+ testCaseName, mSuccessTestRuns, mIteration));
+ } catch (IOException e) {
+ log("failed to write output for test case " + testCaseName);
+ }
+
+ // if successful test runs is less than the threshold, no results will be saved.
+ if (mSuccessTestRuns * 100 / mIteration < SUCCESS_THRESHOLD) {
+ log(String.format("In %s, # of successful test runs out of %s iterations: %d ",
+ testCaseName, mIteration, mSuccessTestRuns));
+ log(String.format("threshold is %d%%", SUCCESS_THRESHOLD));
+ return;
+ }
+
+ if (DEBUG) {
+ print(jankinessArray, "jankiness array");
+ print(frameRateArray, "frame rate array");
+ print(maxDeltaVsyncArray, "max delta vsync array");
+ }
+ double avgJankinessCount = getAverage(jankinessArray);
+ int maxJankinessCount = getMaxValue(jankinessArray);
+ double avgFrameRate = getAverage(frameRateArray);
+ double avgMaxDeltaVsync = getAverage(maxDeltaVsyncArray);
+
+ String avgMsg = String.format("%s\n" +
+ "average number of jankiness: %f\n" +
+ "max number of jankiness: %d\n" +
+ "average frame rate: %f\n" +
+ "average of max accumulated frames: %f\n",
+ testCaseName, avgJankinessCount, maxJankinessCount, avgFrameRate, avgMaxDeltaVsync);
+ log(avgMsg);
+
+ try {
+ mWriter.write(avgMsg);
+ } catch (IOException e) {
+ log("failed to write output for test case " + testCaseName);
+ }
+ }
+
+ // return the max value in an integer array
+ private int getMaxValue(int[] intArray) {
+ int index = 0;
+ int max = intArray[index];
+ for (int i = 1; i < intArray.length; i++) {
+ if (max < intArray[i]) {
+ max = intArray[i];
+ }
+ }
+ return max;
+ }
+
+ private double getAverage(int[] intArray) {
+ int mean = 0;
+ int numberTests = 0;
+ for (int i = 0; i < intArray.length; i++) {
+ // in case in some iteration, test fails, no data points is collected
+ if (intArray[i] >= 0) {
+ mean += intArray[i];
+ ++numberTests;
+ }
+ }
+ return (double)mean/numberTests;
+ }
+
+ private double getAverage(double[] doubleArray) {
+ double mean = 0;
+ int numberTests = 0;
+ for (int i = 0; i < doubleArray.length; i++) {
+ // in case in some iteration, test fails, no data points is collected
+ if (doubleArray[i] >= 0) {
+ mean += doubleArray[i];
+ ++numberTests;
+ }
+ }
+ return mean/numberTests;
+ }
+
+ private void print(int[] intArray, String arrayName) {
+ log("start to print array for " + arrayName);
+ for (int i = 0; i < intArray.length; i++) {
+ log(String.format("%d: %d", i, intArray[i]));
+ }
+ }
+
+ private void print(double[] doubleArray, String arrayName) {
+ log("start to print array for " + arrayName);
+ for (int i = 0; i < doubleArray.length; i++) {
+ log(String.format("%d: %f", i, doubleArray[i]));
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ if (mWriter != null) {
+ mWriter.close();
+ }
+ if (mStatusWriter != null) {
+ mStatusWriter.close();
+ }
+ }
+
+ private void log(String message) {
+ Log.v(TAG, message);
+ }
+
+ /**
+ * Set the total number of test iteration
+ * @param iteration
+ */
+ protected void setIteration(int iteration){
+ mIteration = iteration;
+ }
+
+ /**
+ * Get the total number of test iteration
+ * @return iteration
+ */
+ protected int getIteration(){
+ return mIteration;
+ }
+
+}
diff --git a/uiautomator_test_libraries/src/com/android/uiautomator/platform/SurfaceFlingerHelper.java b/uiautomator_test_libraries/src/com/android/uiautomator/platform/SurfaceFlingerHelper.java
new file mode 100644
index 0000000..1fcdafd
--- /dev/null
+++ b/uiautomator_test_libraries/src/com/android/uiautomator/platform/SurfaceFlingerHelper.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2013 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 com.android.uiautomator.platform;
+
+import android.os.Environment;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.ArrayList;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.BufferedWriter;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.lang.Math;
+
+/*
+ * Tools to measure jankiness through SurfaceFlinger
+ */
+public class SurfaceFlingerHelper {
+ private static String TAG = "SurfaceFlingerHelper";
+ private static int BUFFER_SIZE = 128;
+ private static int BUFFER_NUMBER = 3;
+ private static String CLEAR_BUFFER_CMD = "dumpsys SurfaceFlinger --latency-clear %s";
+ private static String FRAME_LATENCY_CMD = "dumpsys SurfaceFlinger --latency %s";
+ private final static String RAW_DATA_DIR = "UiJankinessRawData";
+ private final static String LOCAL_TMP_DIR = "/data/local/tmp/";
+ /* If the latency between two frames is greater than this number, it it treated as a pause
+ * not a jankiness */
+ private final static int PAUSE_LATENCY = 20;
+
+ /* An array list which includes the raw buffer information from frame latency tool */
+ private static List<List<String>> mFrameBufferData = new ArrayList<List<String>>(BUFFER_SIZE);
+
+ /* Record the refresh period returned from driver */
+ private static long mRefreshPeriod = -1;
+
+ /* Record the size of frame latency */
+ private static int mFrameLatencySampleSize = 0;
+
+ /* An integer array which includes delta vsync */
+ private static long[] mDeltaVsync = new long[BUFFER_SIZE];
+
+ /* Integer array for delta of delta vsync */
+ private static long[] mDelta2Vsync = new long[BUFFER_SIZE];
+
+ /* the maximum delta vsync number */
+ private static long mMaxDeltaVsync;
+
+ /* Normalized data */
+ private static double[] mNormalizedDelta2Vsync = new double[BUFFER_SIZE];
+ private static int[] mRoundNormalizedDelta2Vsync = new int[BUFFER_SIZE];
+
+ /**
+ * Run clear buffer command and clear the saved frame buffer results
+ *
+ * @param windowName the window name that the buffer will be cleared
+ */
+ public static void clearBuffer(String windowName) {
+ // clear results
+ if (mFrameBufferData != null) {
+ mFrameBufferData.clear();
+ }
+ Arrays.fill(mDeltaVsync, -1);
+ Arrays.fill(mDelta2Vsync, -1);
+ Arrays.fill(mNormalizedDelta2Vsync, -1.0);
+ Arrays.fill(mRoundNormalizedDelta2Vsync, -1);
+ mRefreshPeriod = -1;
+ mFrameLatencySampleSize = 0;
+ mMaxDeltaVsync = 0;
+
+ Process p = null;
+ BufferedReader resultReader = null;
+ String command = String.format(CLEAR_BUFFER_CMD, windowName);
+ try {
+ p = Runtime.getRuntime().exec(command);
+ int status = p.waitFor();
+ if (status != 0) {
+ System.err.println(String.format("Run shell command: %s, status: %s",
+ command, status));
+ }
+ } catch (IOException e) {
+ System.err.println("// Exception from command " + command + ":");
+ System.err.println(e.toString());
+ } catch (InterruptedException e) {
+ System.err.println("// Interrupted while waiting for the command to finish. ");
+ System.err.println(e.toString());
+ } finally {
+ try {
+ if (resultReader != null) {
+ resultReader.close();
+ }
+ if (p != null) {
+ p.destroy();
+ }
+ } catch (IOException e) {
+ System.err.println(e.toString());
+ }
+ }
+ }
+
+ /**
+ * Run frame latency command to get the raw data, save raw data on the disk
+ *
+ * @param windowName
+ * @return
+ */
+ public static boolean dumpFrameLatency(String windowName, String fileName, int index) {
+ BufferedWriter fw = null;
+ Process p = null;
+ BufferedReader resultReader = null;
+ String command = String.format(FRAME_LATENCY_CMD, windowName);
+ // if the raw directory doesn't exit, create the directory
+ File rawDataDir = new File(LOCAL_TMP_DIR, RAW_DATA_DIR);
+ try {
+ if (!rawDataDir.exists()) {
+ if (!rawDataDir.mkdir()) {
+ log(String.format("create directory %s failed, you can manually create " +
+ "it and start the test again", rawDataDir));
+ return false;
+ }
+ }
+ } catch (SecurityException e) {
+ System.err.println(e.toString());
+ }
+ String rawFileName = String.format("%s/%s_%d.txt", RAW_DATA_DIR, fileName, index);
+
+ try {
+ p = Runtime.getRuntime().exec(command);
+ int status = p.waitFor();
+ if (status != 0) {
+ System.err.println(String.format("Run shell command: %s, status: %s",
+ command, status));
+ }
+ resultReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
+ fw = new BufferedWriter(new FileWriter(new File(
+ LOCAL_TMP_DIR, rawFileName), false));
+ // The first line will always show the refresh period
+ String line = resultReader.readLine();
+ Log.v("testing", "output line = " + line);
+ fw.write(line);
+ fw.write("\n");
+ mRefreshPeriod = Long.parseLong(line.trim());
+ //log("reading refresh period: " + mRefreshPeriod);
+ if (mRefreshPeriod < 0) {
+ return false;
+ }
+
+ while((line = resultReader.readLine()) != null) {
+ fw.write(line);
+ fw.write("\n");
+
+ // remove the last line which is empty
+ if (line.trim().isEmpty()) {
+ break;
+ }
+ String[] bufferValues = line.split("\\s+");
+ if (bufferValues[0].trim().compareTo("0") == 0) {
+ continue;
+ }
+ List<String> delayArray = Arrays.asList(bufferValues);
+ mFrameBufferData.add(delayArray);
+ ++mFrameLatencySampleSize;
+ }
+ log("frame latency sample size: " + mFrameLatencySampleSize);
+ } catch (InterruptedException e) {
+ System.err.println("// Exception from command " + command + ":");
+ System.err.println(e.toString());
+ } catch (IOException e) {
+ log("Open file error: " + e.toString());
+ return false;
+ }
+ finally {
+ try {
+ if (resultReader != null) {
+ resultReader.close();
+ }
+ if (fw != null) {
+ fw.close();
+ }
+ if (p != null) {
+ p.destroy();
+ }
+ } catch (IOException e) {
+ System.err.println(e.toString());
+ }
+ }
+ return true;
+ }
+
+ public static long getRefreshPeriod() {
+ if (mRefreshPeriod < 0) {
+ // Haven't dump the frame latency yet
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving the refresh period");
+ }
+ return mRefreshPeriod;
+ }
+
+ public static String getFrameBufferData() {
+ if (mFrameBufferData.get(0) == null) {
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame buffer data");
+ return null;
+ }
+ String rawData = String.format("%d\n", mRefreshPeriod);
+ List<String> tempList = new ArrayList<String>(BUFFER_NUMBER);
+ for (int i = 0; i < mFrameLatencySampleSize; i++) {
+ tempList = mFrameBufferData.get(i);
+ for (int j = 0; j < BUFFER_NUMBER; j++) {
+ rawData += String.format("%s", tempList.get(j));
+ if (j < BUFFER_NUMBER -1) {
+ rawData += " ";
+ } else {
+ rawData += "\n";
+ }
+ }
+ }
+ return rawData;
+ }
+
+ /**
+ * Calculate delta(vsync)
+ * @return
+ */
+ public static long[] getDeltaVsync() {
+ if (mRefreshPeriod < 0) {
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame latency");
+ return null;
+ }
+ if (mDeltaVsync[0] < 0 ) {
+ mDeltaVsync = new long[BUFFER_SIZE];
+ // keep a record of the max DeltaVsync
+ mMaxDeltaVsync = 0;
+ // get the first frame vsync time
+ long preVsyncTime = Long.parseLong(mFrameBufferData.get(0).get(1));
+ for (int i = 1; i < mFrameLatencySampleSize; i++) {
+ long curVsyncTime = Long.parseLong(mFrameBufferData.get(i).get(1));
+ mDeltaVsync[i] = curVsyncTime - preVsyncTime;
+ preVsyncTime = curVsyncTime;
+ if (mMaxDeltaVsync < mDeltaVsync[i]) {
+ mMaxDeltaVsync = mDeltaVsync[i];
+ }
+ }
+ }
+ return mDeltaVsync;
+ }
+
+ /**
+ * Calculate difference between delta vsync
+ * @return
+ */
+ public static long[] getDelta2Vsync() {
+ if (mRefreshPeriod < 0) {
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame latency");
+ return null;
+ }
+ if (mDeltaVsync[0] < 0) {
+ getDeltaVsync();
+ }
+ if (mDelta2Vsync[0] < 0) {
+ mDelta2Vsync = new long[BUFFER_SIZE];
+ for (int i = 1; i < mFrameLatencySampleSize; i++) {
+ mDelta2Vsync[i] = mDeltaVsync[i] - mDeltaVsync[i - 1];
+ }
+ }
+ return mDelta2Vsync;
+ }
+
+ /**
+ * normalized delta(delta(vsync)) by refresh period
+ * @return
+ */
+ public static double[] getNormalizedDelta2Vsync() {
+ if (mRefreshPeriod < 0) {
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame latency");
+ return null;
+ }
+ if (mDelta2Vsync[0] < 0) {
+ getDelta2Vsync();
+ }
+ if (mNormalizedDelta2Vsync[0] < 0) {
+ mNormalizedDelta2Vsync = new double[BUFFER_SIZE];
+ for (int i = 0; i < mFrameLatencySampleSize; i++) {
+ mNormalizedDelta2Vsync[i] = (double)mDelta2Vsync[i] /mRefreshPeriod;
+ }
+ }
+ return mNormalizedDelta2Vsync;
+ }
+
+ public static int[] getRoundNormalizedDelta2Vsync() {
+ if (mRefreshPeriod < 0) {
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" for number of jankiness.");
+ return null;
+ }
+ if (mNormalizedDelta2Vsync[0] < 0) {
+ getNormalizedDelta2Vsync();
+ }
+
+ for (int i = 0; i < mFrameLatencySampleSize; i++) {
+ int value = (int)Math.round(Math.max(mNormalizedDelta2Vsync[i], 0.0));
+ mRoundNormalizedDelta2Vsync[i] = value;
+ }
+ return mRoundNormalizedDelta2Vsync;
+ }
+
+ /*
+ * Get number of jankiness using Vsync time difference
+ */
+ public static int getVsyncJankiness() {
+ if (mRefreshPeriod < 0) {
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" for number of jankiness.");
+ return -1;
+ }
+ if (mRoundNormalizedDelta2Vsync[0] < 0) {
+ getRoundNormalizedDelta2Vsync();
+ }
+ int numberJankiness = 0;
+ for (int i = 0; i < mFrameLatencySampleSize; i++) {
+ int value = mRoundNormalizedDelta2Vsync[i];
+ // ignore the latency which is too long
+ if (value > 0 && value < PAUSE_LATENCY) {
+ numberJankiness++;
+ }
+ }
+ return numberJankiness;
+ }
+
+ /* Track the maximum delta which shows the accumulating time
+ * before animation starts */
+ public static int getMaxDeltaVsync() {
+ return Math.round((float)mMaxDeltaVsync /mRefreshPeriod);
+ }
+
+ /**
+ * Calculate frame rate
+ * @return
+ */
+ public static double getFrameRate() {
+ if (mRefreshPeriod < 0) {
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" before calcuating average frame rate");
+ return -1.0;
+ }
+ if (mFrameBufferData.get(0) == null) {
+ log("Run command \"" + FRAME_LATENCY_CMD + " \" before retrieving frame buffer data");
+ return -1.0;
+ }
+ long startTime = Long.parseLong(mFrameBufferData.get(0).get(1));
+ long endTime = Long.parseLong(mFrameBufferData.get(mFrameLatencySampleSize - 1).get(1));
+ long totalDuration = endTime - startTime;
+ return (double)((mFrameLatencySampleSize - 1) * Math.pow(10, 9))/totalDuration;
+ }
+
+ /**
+ * Print raw data and processed results into file <testcasename_[iteration]_processed.txt>
+ * @param fileName
+ * @param index
+ */
+ public static void printData(String fileName, int index) {
+ String rawAndProcDataFileName = String.format("%s/%s_%d_processed.txt", RAW_DATA_DIR,
+ fileName, index);
+ log("write raw data and process data into file: " + rawAndProcDataFileName);
+ BufferedWriter fw = null;
+ try {
+ fw = new BufferedWriter(new FileWriter(new File(
+ LOCAL_TMP_DIR, rawAndProcDataFileName), false));
+ // Show the number of jankiness first:
+ fw.write(String.format("Jankiness count: %d\n", getVsyncJankiness()));
+ fw.write(String.format("Max accumulated frames: %d\n", getMaxDeltaVsync()));
+ fw.write(String.format("Frame rate is: %f\n", getFrameRate()));
+
+ // refresh period
+ fw.write(String.valueOf(mRefreshPeriod));
+ fw.write("\n");
+ fw.write("app\tvsync\tset\tdelta(vsync)\tdelta^2(vsync)\t" +
+ "delta^2(vsync)/refreshPeriod\t normalized delta^2(vsync)\n");
+
+ for (int i = 0; i < mFrameLatencySampleSize; i++) {
+ // write raw data
+ List<String> rawData = mFrameBufferData.get(i);
+ String line = String.format("%s\t%s\t%s\t%d\t%d\t%f\t%d\n",
+ rawData.get(0), rawData.get(1), rawData.get(2),
+ mDeltaVsync[i], mDelta2Vsync[i],
+ mNormalizedDelta2Vsync[i], mRoundNormalizedDelta2Vsync[i]);
+ fw.write(line);
+ }
+ } catch (IOException e) {
+ log("Open file error: " + e.toString());
+ } finally {
+ try {
+ if (fw != null) {
+ fw.flush();
+ fw.close();
+ }
+ }
+ catch (IOException e) {
+ System.err.println(e.toString());
+ }
+ }
+ }
+
+ private static void log(String msg) {
+ Log.v(TAG, msg);
+ }
+}