blob: 1fcdafd998092eec9633a40c772d80c5c3837bff [file] [log] [blame]
/*
* 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);
}
}