blob: fff45e50afa000ef17dd934b1c544090fbf91764 [file] [log] [blame]
/*
* Copyright (C) 2022 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.tradefed.util;
import com.android.loganalysis.util.config.Option;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.tracing.CloseableTraceScope;
import com.android.tradefed.log.LogUtil.CLog;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/** A utility class for recording perfetto trace on a {@link ITestDevice}. */
public class PerfettoTraceRecorder {
private static final String TRACE_NAME_FORMAT = "device-trace_%s_";
@Option(
name = "perfetto-executable",
description = "Perfetto script file which will be used to record trace.")
private File perfettoExecutable = null;
@Option(name = "output-path", description = "Path where the files will be saved.")
private String outputPath = System.getProperty("java.io.tmpdir");
// A device-metadata map to store trace related metadata.
private Map<ITestDevice, DeviceTraceMetadata> deviceMetadataMap = new LinkedHashMap<>();
// A list of device serials where trace has started.
private Set<String> deviceSerialsWithTrace = new HashSet<>();
// Time in ms to wait for the perfettoScript to start
private static final long SCRIPT_START_TIMEOUT = 10 * 1000;
/**
* Starts recording perfetto trace on device. Must call {@link
* PerfettoTraceRecorder#stopTrace(ITestDevice)} afterwards to stop the trace recording.
*
* @param device A {@link ITestDevice} where trace will be recorded.
* @param extraConfigs A map of extra configs that needs to be added in the trace config file.
*/
public void startTrace(ITestDevice device, Map<String, String> extraConfigs)
throws IOException {
if (deviceMetadataMap.containsKey(device)) {
CLog.d(
"Already recording trace on %s in pid %s.",
device.getSerialNumber(), deviceMetadataMap.get(device).getProcess().pid());
return;
}
// Stores metadata related to this trace
DeviceTraceMetadata deviceTraceMetadata = new DeviceTraceMetadata();
// Get the perfetto executable
if (perfettoExecutable == null) {
perfettoExecutable = FileUtil.createTempFile("record_android_trace", ".txt");
InputStream script =
PerfettoTraceRecorder.class.getResourceAsStream(
"/perfetto/record_android_trace");
FileUtil.writeToFile(script, perfettoExecutable);
}
deviceTraceMetadata.setPerfettoScript(perfettoExecutable, false);
// Make the script executable
RunUtil.getDefault()
.runTimedCmd(10000, "chmod", "u+x", perfettoExecutable.getAbsolutePath());
// Get the trace config file from resource
File traceConfigFile = FileUtil.createTempFile("trace_config", ".textproto");
InputStream configStream =
PerfettoTraceRecorder.class.getResourceAsStream("/perfetto/trace_config.textproto");
String configStr = StreamUtil.getStringFromStream(configStream);
// insert extra configs in the trace config file
if (extraConfigs != null) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> configKeyValue : extraConfigs.entrySet()) {
sb.append(
String.format(
"%s: %s\n", configKeyValue.getKey(), configKeyValue.getValue()));
}
String injectedStr = sb.toString();
configStr = configStr.replace("# {injected_config}", injectedStr);
}
FileUtil.writeToFile(configStr, traceConfigFile);
deviceTraceMetadata.setTraceConfig(traceConfigFile, true);
File traceOutput =
FileUtil.createTempFile(
String.format(TRACE_NAME_FORMAT, device.getSerialNumber()),
".perfetto-trace");
deviceTraceMetadata.setTraceOutput(traceOutput, false);
// start trace
List<String> cmd =
Arrays.asList(
perfettoExecutable.getAbsolutePath(),
"-c",
traceConfigFile.getAbsolutePath(),
"-s",
device.getSerialNumber(),
"-o",
traceOutput.getAbsolutePath(),
"-n");
Process process =
RunUtil.getDefault()
.runCmdInBackground(
cmd,
new OutputStream() {
public String output = "";
@Override
public void write(int b) throws IOException {
output += b;
checkOutput();
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len)
throws IOException {
output += new String(b, off, len);
checkOutput();
}
private void checkOutput() {
if (output.contains("beginning of main")) {
deviceSerialsWithTrace.add(device.getSerialNumber());
}
}
});
try (CloseableTraceScope ignore = new CloseableTraceScope("perfetto-script-start-time")) {
long startTime = System.currentTimeMillis();
while (!deviceSerialsWithTrace.contains(device.getSerialNumber())
&& System.currentTimeMillis() - startTime < SCRIPT_START_TIMEOUT) {
// wait until perfetto trace has started
RunUtil.getDefault().sleep(1 * 1000);
}
}
if (!deviceSerialsWithTrace.contains(device.getSerialNumber())) {
CLog.w(
"Perfetto script did not start on device %s within %sms. Trace file may miss"
+ " some events.",
device.getSerialNumber(), SCRIPT_START_TIMEOUT);
}
deviceTraceMetadata.setProcess(process);
deviceMetadataMap.put(device, deviceTraceMetadata);
}
/**
* Stops recording perfetto trace on the device.
*
* <p>Must have called {@link PerfettoTraceRecorder#startTrace(ITestDevice, Map)} before.
*
* @param device device for which to stop the recording. @Return Returns the perfetto trace
* file.
*/
public File stopTrace(ITestDevice device) {
if (deviceMetadataMap.containsKey(device)) {
// remove the metadata from the map so that a new trace can be started.
DeviceTraceMetadata metadata = deviceMetadataMap.remove(device);
deviceSerialsWithTrace.remove(device.getSerialNumber());
CommandResult result =
RunUtil.getDefault()
.runTimedCmd(
10000,
"kill",
"-2",
String.valueOf(metadata.getProcess().pid()));
if (result.getStatus() != CommandStatus.SUCCESS) {
CLog.d(result.getStderr());
return null;
}
// wait for the recorder to finish and pull the trace file
try {
// 10 second wait is arbitrary. Should be enough time to pull big trace files.
metadata.getProcess().waitFor(10 * 1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
CLog.w(e);
}
metadata.cleanUp();
return metadata.getTraceOutput();
}
return null;
}
/** Stores metadata related to a trace running on an {@link ITestDevice}. */
private class DeviceTraceMetadata {
// the process running the script
public Process process;
// config file which was used to collect trace
private File traceConfig;
// Output file where traces will be pulled after trace is stopped.
private File traceOutput;
// Script used to run the trace
private File perfettoScript;
private List<File> tempFiles = new ArrayList<>();
public Process getProcess() {
return process;
}
public void setProcess(Process process) {
this.process = process;
}
public File getTraceConfig() {
return traceConfig;
}
public void setTraceConfig(File traceConfig, boolean needToDelete) {
this.traceConfig = traceConfig;
if (needToDelete) {
tempFiles.add(traceConfig);
}
}
public File getTraceOutput() {
return traceOutput;
}
public void setTraceOutput(File traceOutput, boolean needToDelete) {
this.traceOutput = traceOutput;
if (needToDelete) {
tempFiles.add(traceOutput);
}
}
public File getPerfettoScript() {
return perfettoScript;
}
public void setPerfettoScript(File perfettoScript, boolean needToDelete) {
this.perfettoScript = perfettoScript;
if (needToDelete) {
tempFiles.add(perfettoScript);
}
}
public void cleanUp() {
for (File file : tempFiles) {
FileUtil.deleteFile(file);
}
}
}
}