blob: a09f6016aae160580068dafc095ab14ecbfbb8c6 [file] [log] [blame]
/*
* Copyright (C) 2018 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.graphics.benchmark.metric;
import com.android.graphics.benchmark.ApkInfo;
import com.android.graphics.benchmark.proto.ResultDataProto;
import com.android.tradefed.device.metric.BaseDeviceMetricCollector;
import com.android.tradefed.device.metric.DeviceMetricData;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.graphics.benchmark.ApkInfo;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.config.Option;
import java.util.Timer;
import java.util.TimerTask;
import java.util.ArrayList;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
/** A {@link ScheduledDeviceMetricCollector} to collect graphics benchmarking stats at regular intervals. */
public class GraphicsBenchmarkMetricCollector extends BaseDeviceMetricCollector {
private long mLatestSeen = 0;
private static ApkInfo mTestApk;
private static ResultDataProto.Result mDeviceResultData;
private long mVSyncPeriod = 0;
private ArrayList<Long> mElapsedTimes;
private ITestDevice mDevice;
private boolean mFirstRun = true;
private boolean mFirstLoop;
// TODO: Investigate interaction with sharding support
public static void setAppLayerName(ApkInfo apk) {
mTestApk = apk;
}
// TODO: same sharding concern
public static void setDeviceResultData(ResultDataProto.Result resultData) {
mDeviceResultData = resultData;
}
@Option(
name = "fixed-schedule-rate",
description = "Schedule the timetask as a fixed schedule rate"
)
private boolean mFixedScheduleRate = false;
@Option(
name = "interval",
description = "the interval between two tasks being scheduled",
isTimeVal = true
)
private long mIntervalMs = 1 * 1000L;
private Timer mTimer;
@Override
public final void onTestRunStart(DeviceMetricData runData) {
mDevice = getDevices().get(0);
CLog.v("Test run started on device %s.", mDevice);
mElapsedTimes = new ArrayList<Long>();
mLatestSeen = 0;
mFirstLoop = true;
onStart(runData);
mTimer = new Timer();
TimerTask timerTask =
new TimerTask() {
@Override
public void run() {
try {
collect(runData);
} catch (InterruptedException e) {
mTimer.cancel();
Thread.currentThread().interrupt();
CLog.e("Interrupted exception thrown from task: %s", e);
}
}
};
if (mFixedScheduleRate) {
mTimer.scheduleAtFixedRate(timerTask, 0, mIntervalMs);
} else {
mTimer.schedule(timerTask, 0, mIntervalMs);
}
}
@Override
public final void onTestRunEnd(DeviceMetricData runData) {
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
}
onEnd(runData);
CLog.d("onTestRunEnd");
}
/**
* Task periodically & asynchronously run during the test running.
*
* @param runData the {@link DeviceMetricData} where to put metrics.
* @throws InterruptedException
*/
private void collect(DeviceMetricData runData) throws InterruptedException {
try {
if (mTestApk == null) {
CLog.e("No test apk info provided.");
return;
}
CLog.d("Collecting benchmark stats for layer: %s", mTestApk.getLayerName());
String cmd = "dumpsys SurfaceFlinger --latency \"" + mTestApk.getLayerName()+ "\"";
String[] raw = mDevice.executeShellCommand(cmd).split("\n");
if (mFirstLoop) {
if (raw.length == 1) {
// We didn't get any frame timestamp info. Mostly likely because the app has
// not started yet. Or the app layer name is wrong.
// TODO: figure out how to report it if the app layer name is wrong.
return;
}
mVSyncPeriod = Long.parseLong(raw[0]);
mFirstLoop = false;
}
boolean overlap = false;
for (int i = 1; i < raw.length; i++) {
String[] parts = raw[i].split("\t");
if (parts.length == 3) {
if (sample(Long.parseLong(parts[2]))) {
overlap = true;
}
}
}
if (!overlap) {
CLog.e("No overlap with previous poll, we missed some frames!"); // FIND SOMETHING BETTER
}
} catch (DeviceNotAvailableException | NullPointerException e) {
CLog.e(e);
}
}
private boolean sample(long timeStamp) {
if (timeStamp == Long.MAX_VALUE) {
return true;
}
else if (timeStamp < mLatestSeen) {
return false;
}
else if (timeStamp == mLatestSeen) {
return true;
}
else {
mElapsedTimes.add(timeStamp);
mLatestSeen = timeStamp;
return false;
}
}
private void onStart(DeviceMetricData runData) {}
private void processTimestampsSlice(int runIndex, long startTimestamp, long endTimestamp, BufferedWriter outputFile, DeviceMetricData runData) throws IOException {
double minFPS = Double.MAX_VALUE;
double maxFPS = 0.0;
long totalTimeNs = 0;
outputFile.write("Started run " + runIndex + " at: " + startTimestamp + " ns \n");
outputFile.write("Frame Time\t\tFrames Per Second\n");
long prevTime = 0L;
int numOfTimestamps = 0;
for(long time : mElapsedTimes)
{
if (time < startTimestamp) {
continue;
}
if (time > endTimestamp) {
break;
}
if (prevTime == 0) {
prevTime = time;
continue;
}
long timeDiff = time - prevTime;
prevTime = time;
double currentFPS = 1.0e9/timeDiff;
minFPS = (currentFPS < minFPS ? currentFPS : minFPS);
maxFPS = (currentFPS > maxFPS ? currentFPS : maxFPS);
totalTimeNs += timeDiff;
numOfTimestamps++;
outputFile.write(timeDiff + " ns\t\t" + currentFPS + " fps\n");
}
// There's a fair amount of slop in the system wrt device timing vs host orchestration,
// so it's possible that we'll receive an extra intent after we've stopped caring.
if (numOfTimestamps == 0) {
outputFile.write("No samples in period, assuming spurious intent.\n\n");
return;
}
outputFile.write("\nSTATS\n");
double avgFPS = numOfTimestamps * 1.0e9 / totalTimeNs;
outputFile.write("min FPS = " + minFPS + "\n");
outputFile.write("max FPS = " + maxFPS + "\n");
outputFile.write("avg FPS = " + avgFPS + "\n");
runData.addStringMetric("run_" + runIndex + ".min_fps", Double.toString(minFPS));
runData.addStringMetric("run_" + runIndex + ".max_fps", Double.toString(maxFPS));
runData.addStringMetric("run_" + runIndex + ".fps", Double.toString(avgFPS));
outputFile.write("\n");
}
private void onEnd(DeviceMetricData runData) {
// TODO: Find a way to send the results to the same directory as the inv. log files
try (BufferedWriter outputFile = new BufferedWriter(new FileWriter("/tmp/0/graphics-benchmark/out.txt", !mFirstRun))) {
outputFile.write("VSync Period: " + mVSyncPeriod + "\n\n");
if (mDeviceResultData.getEventsCount() == 0) {
CLog.w("No start benchmark intent given; assuming single run with no loading period to exclude.");
}
long startTime = 0L;
int runIndex = 0;
for (ResultDataProto.Event e : mDeviceResultData.getEventsList()) {
if (e.getType() != ResultDataProto.Event.Type.START_LOOP) {
continue;
}
long endTime = e.getTimestamp() * 1000000; /* ms to ns */
if (startTime != 0) {
processTimestampsSlice(runIndex++, startTime, endTime, outputFile, runData);
}
startTime = endTime;
}
processTimestampsSlice(runIndex, startTime, mElapsedTimes.get(mElapsedTimes.size() - 1), outputFile, runData);
} catch (IOException e) {
throw new RuntimeException(e);
}
mFirstRun = false;
}
}