blob: 3b29f57ccda52126e1a7aa22f95507bdc049106b [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.util.CommandResult;
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;
/**
* 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;
/* 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;
private IRunUtil mRunUtil = RunUtil.getDefault();
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) {
//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());
try {
device.executeShellCommand(cmd, c, 1, TimeUnit.SECONDS, 1);
} catch (DeviceNotAvailableException e) {
CLog.e("Error starting atrace:");
CLog.e(e);
}
CLog.i("command output: %s", c.getOutput());
}
@Override
public void onTestStart(DeviceMetricData testData) {
if (mCategories.isEmpty()) {
CLog.d("no categories specified to trace, not running AtraceMetricCollector");
return;
}
for (ITestDevice device : getDevices()) {
startTracing(device);
}
}
protected void stopTracing(ITestDevice device) {
CLog.i("collecting atrace log from device: %s", device.getSerialNumber());
try {
device.executeShellCommand(
"atrace --async_stop -o " + fullLogPath(),
new NullOutputReceiver(),
60,
TimeUnit.SECONDS,
1);
} catch (DeviceNotAvailableException e) {
CLog.e("Error stopping atrace");
CLog.e(e);
}
}
private void postProcess(File trace, String id) {
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());
}
@Override
public void onTestEnd(
DeviceMetricData testData, final Map<String, Metric> currentTestCaseMetrics) {
if (mCategories.isEmpty())
return;
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 + device.getSerialNumber(),
getLogType(),
streamSource);
}
postProcess(trace, device.getSerialNumber());
trace.delete();
} else {
throw new DeviceRuntimeException("failed to pull log: " + fullLogPath());
}
if (!mPreserveOndeviceLog) {
device.executeShellCommand("rm -f " + fullLogPath());
}
else {
CLog.w("preserving ondevice atrace log: %s", fullLogPath());
}
} catch (DeviceNotAvailableException | DeviceRuntimeException e) {
CLog.e("Error retrieving atrace log! device not available:");
CLog.e(e);
}
}
}
@VisibleForTesting
void setRunUtil(IRunUtil util) {
mRunUtil = util;
}
}