blob: 2422c9405b1016b23cc4fdf0dd20252a4e806ca2 [file] [log] [blame]
/*
* Copyright (C) 2017 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.device.metric;
import com.android.ddmlib.NullOutputReceiver;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.CollectingOutputReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceRuntimeException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A {@link IMetricCollector} that runs atrace during a test and collects the result and log
* them to the invocation.
*/
@OptionClass(alias = "atrace")
public class AtraceCollector extends BaseDeviceMetricCollector {
@Option(name = "categories",
description = "the tracing categories atrace will capture")
private List<String> mCategories = new ArrayList<>();
@Option(
name = "log-path",
description = "the temporary location the trace log will be saved to on device"
)
private String mLogPath = "/data/local/tmp/";
@Option(
name = "log-filename",
description = "the temporary location the trace log will be saved to on device"
)
private String mLogFilename = "atrace";
@Option(name = "preserve-ondevice-log",
description = "delete the trace log on the target device after the host collects it")
private boolean mPreserveOndeviceLog = false;
@Option(name = "compress-dump",
description = "produce a compressed trace dump")
private boolean mCompressDump = true;
@Option(name = "atrace-on-boot",
description = "enable atrace collection for bootup")
private boolean mTraceOnBoot = false;
@Option(name = "skip-atrace-start",
description = "Skip atrace start if the option is enabled. Needed when atrace is"
+ "enabled through fastboot option.")
private boolean mSkipAtraceStart = false;
/* These options will arrange a post processing executable binary to be ran on the collected
* trace.
* E.G.
* <option name="post-process-binary" value="/path/to/analyzer.par"/>
* <option name="post-process-input-file-key" value="TRACE_FILE"/>
* <option name="post-process-args" value="--input_file TRACE_FILE --arg1 --arg2"/>
* Will be executed as
* /path/to/analyzer.par --input_file TRACE_FILE --arg1 --arg2
*/
@Option(
name = "post-process-input-file-key",
description =
"The string that will be replaced with the absolute path to the trace file "
+ "in post-process-args"
)
private String mLogProcessingTraceInput = "TRACE_FILE";
@Option(
name = "post-process-binary",
description = "a self-contained binary that will be executed on the trace file"
)
private File mLogProcessingBinary = null;
@Option(name = "post-process-args", description = "args for the binary")
private List<String> mLogProcessingArgs = new ArrayList<>();
@Option(
name = "post-process-timeout",
isTimeVal = true,
description =
"The amount of time (eg, 1m2s) that Tradefed will wait for the "
+ "postprocessing subprocess to finish"
)
private long mLogProcessingTimeoutMilliseconds = 0;
/* If the tool is producing files to upload, this can be used to key in to which files are
* produced and upload them.
* The first matching group in the regex is treated as a file and uploaded.
* Eg, if this output is produced by the postprocessing binary:
*
* my-metric-name /tmp/a.txt
*
* then setting this option to: "my-metric-name (.*txt)" will upload /tmp/a.txt
*
* If not set, or the file cannot be found, the tool will still upload its stdout and stderr.
* Can be specified multiple times.
*/
@Option(
name = "post-process-output-file-regex",
description =
"A regex that will be applied to the stdout of the post processing program."
+ "the first matching group will be treated as a file and uploaded as "
+ "a test log.")
private List<String> mLogProcessingOutputRegex = new ArrayList<>();
private IRunUtil mRunUtil = RunUtil.getDefault();
private Thread mThread;
private static final long DEVICE_OFFLINE_TIMEOUT_MS = 60 * 1000;
private static final long DEVICE_ONLINE_TIMEOUT_MS = 60 * 1000;
protected String fullLogPath() {
return Paths.get(mLogPath, mLogFilename + "." + getLogType().getFileExt()).toString();
}
protected LogDataType getLogType() {
if (mCompressDump) {
return LogDataType.ATRACE;
} else {
return LogDataType.TEXT;
}
}
protected void startTracing(ITestDevice device) throws DeviceNotAvailableException {
//atrace --async_start will set a variety of sysfs entries, and then exit.
String cmd = "atrace --async_start ";
if (mCompressDump) {
cmd += "-z ";
}
cmd += String.join(" ", mCategories);
CollectingOutputReceiver c = new CollectingOutputReceiver();
CLog.i("issuing command : %s to device: %s", cmd, device.getSerialNumber());
device.executeShellCommand(cmd, c, 1, TimeUnit.SECONDS, 1);
CLog.i("command output: %s", c.getOutput());
}
@Override
public void onTestStart(DeviceMetricData testData) throws DeviceNotAvailableException {
if(mSkipAtraceStart) {
CLog.d("Skip atrace start because tracing is enabled through fastboot option");
return;
}
if (mCategories.isEmpty()) {
CLog.d("no categories specified to trace, not running AtraceMetricCollector");
return;
}
if (mTraceOnBoot) {
mThread = new Thread(() -> {
try {
for (ITestDevice device : getDevices()) {
// wait for device reboot
device.waitForDeviceNotAvailable(DEVICE_OFFLINE_TIMEOUT_MS);
device.waitForDeviceOnline(DEVICE_ONLINE_TIMEOUT_MS);
// wait for device to be in root
device.waitForDeviceNotAvailable(DEVICE_OFFLINE_TIMEOUT_MS);
device.waitForDeviceOnline();
startTracing(device);
}
} catch (DeviceNotAvailableException e) {
CLog.e("Error starting atrace");
CLog.e(e);
}
});
mThread.setDaemon(true);
mThread.setName("AtraceCollector-on-boot");
mThread.start();
} else {
for (ITestDevice device : getDevices()) {
startTracing(device);
}
}
}
protected void stopTracing(ITestDevice device) throws DeviceNotAvailableException {
CLog.i("collecting atrace log from device: %s", device.getSerialNumber());
device.executeShellCommand(
"atrace --async_stop -z -c -o " + fullLogPath(),
new NullOutputReceiver(),
300,
TimeUnit.SECONDS,
1);
CLog.d("Trace collected successfully.");
}
private void postProcess(File trace) {
if (mLogProcessingBinary == null
|| !mLogProcessingBinary.exists()
|| !mLogProcessingBinary.canExecute()) {
CLog.w("No trace postprocessor specified. Skipping trace postprocessing.");
return;
}
List<String> commandLine = new ArrayList<String>();
commandLine.add(mLogProcessingBinary.getAbsolutePath());
for (String entry : mLogProcessingArgs) {
commandLine.add(entry.replaceAll(mLogProcessingTraceInput, trace.getAbsolutePath()));
}
String[] commandLineArr = new String[commandLine.size()];
commandLine.toArray(commandLineArr);
CommandResult result =
mRunUtil.runTimedCmd(mLogProcessingTimeoutMilliseconds, commandLineArr);
CLog.v(
"Trace postprocessing status: %s\nstdout: %s\nstderr: ",
result.getStatus(), result.getStdout(), result.getStderr());
if (result.getStdout() == null) {
return;
}
for (String regex : mLogProcessingOutputRegex) {
Pattern pattern = Pattern.compile(regex);
for (String line : result.getStdout().split("\n")) {
Matcher m = pattern.matcher(line);
if (m.find() && m.groupCount() == 1) {
File f = new File(m.group(1));
if (f.exists() && !f.isDirectory()) {
LogDataType type;
switch (FileUtil.getExtension(f.getName())) {
case ".png":
type = LogDataType.PNG;
break;
case ".txt":
type = LogDataType.TEXT;
break;
default:
type = LogDataType.UNKNOWN;
break;
}
try (FileInputStreamSource stream = new FileInputStreamSource(f)) {
testLog(FileUtil.getBaseName(f.getName()), type, stream);
}
}
}
}
}
}
@Override
public void onTestEnd(
DeviceMetricData testData,
final Map<String, Metric> currentTestCaseMetrics,
TestDescription test)
throws DeviceNotAvailableException {
if (!mSkipAtraceStart && mCategories.isEmpty()) {
return;
}
// Stop and collect the atrace only if the atrace start is skipped which
// then uses the default categories or if the categories are explicitly
// passed.
for (ITestDevice device : getDevices()) {
try {
stopTracing(device);
File trace = device.pullFile(fullLogPath());
if (trace != null) {
CLog.i("Log size: %s bytes", String.valueOf(trace.length()));
try (FileInputStreamSource streamSource = new FileInputStreamSource(trace)) {
testLog(
mLogFilename + "_" + test + device.getSerialNumber() + "_",
getLogType(),
streamSource);
}
postProcess(trace);
trace.delete();
} else {
throw new DeviceRuntimeException(
String.format("failed to pull log: %s", fullLogPath()),
DeviceErrorIdentifier.FAIL_PULL_FILE);
}
if (!mPreserveOndeviceLog) {
device.deleteFile(fullLogPath());
}
else {
CLog.w("preserving ondevice atrace log: %s", fullLogPath());
}
} catch (DeviceRuntimeException e) {
CLog.e("Error retrieving atrace log! device not available:");
CLog.e(e);
}
}
}
@VisibleForTesting
void setRunUtil(IRunUtil util) {
mRunUtil = util;
}
}