blob: 1cbc8a0cfd3627a9be6d523cdb530f142a80cc2d [file] [log] [blame]
/*
* Copyright (C) 2020 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 android.device.collectors;
import android.device.collectors.annotations.OptionClass;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.helpers.SimpleperfHelper;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
/**
* A {@link SimpleperfListener} that captures simpleperf samples for a test run or per test method
* run and saves the results under
* <root_folder>/<test_display_name>/SimpleperfListener/<test_display_name>-<invocation_count>.perf
*/
@OptionClass(alias = "simpleperf-collector")
public class SimpleperfListener extends BaseMetricListener {
// Default output folder to store the simpleperf sample files.
private static final String DEFAULT_OUTPUT_ROOT = "/sdcard/test_results";
// Default subcommand passed to simpleperf
private static final String DEFAULT_SUBCOMMAND = "record";
// Default arguments passed to simpleperf command
private static final String DEFAULT_ARGUMENTS = "-g --post-unwind=yes -f 500 -a --exclude-perf";
// Destination directory to save the trace results.
private static final String TEST_OUTPUT_ROOT = "test_output_root";
// Simpleperf file path key.
private static final String SIMPLEPERF_FILE_PATH = "simpleperf_file_path";
// Argument determining whether we collect for the entire run, or per test.
public static final String COLLECT_PER_RUN = "per_run";
public static final String SIMPLEPERF_PREFIX = "simpleperf_";
// Skip failure metrics collection if set to true.
public static final String SKIP_TEST_FAILURE_METRICS = "skip_test_failure_metrics";
// Subcommand to pass to simpleperf on start.
public static final String SUBCOMMAND = "subcommand";
// Arguments to pass to simpleperf on start.
public static final String ARGUMENTS = "arguments";
// Arguments to record specific processes separated by commas and spaces are ignored.
// Ex. "surfaceflinger,system_server"
public static final String PROCESSES = "processes_to_record";
// Arguments to record specific events separated by commas and spaces are ignored.
// Ex. "instructions,cpu-cycles"
public static final String EVENTS = "events_to_record";
// Argument to determine if report is generated after recording.
public static final String REPORT = "report";
// Report events per symbol. Symbols are separated by the key to identify the symbol and the
// substring used to search for the symbol.
// Ex. "writeInt32;android::Parcel::writeInt32(;commit;android::SurfaceFlinger::commit("
// symbols matching the substring "android::Parcel::writeInt32(" will be reported as
// "writeInt32" and
// symbols matching "android::SurfaceFlinger::commit(" will be reported as "commit"
public static final String REPORT_SYMBOLS = "symbols_to_report";
// Test iterations used to divide any reported event counts.
public static final String TEST_ITERATIONS = "test_iterations";
// Simpleperf samples collected during the test will be saved under this root folder.
private String mTestOutputRoot;
// Store the method name and invocation count to create a unique filename for each trace.
private Map<String, Integer> mTestIdInvocationCount = new HashMap<>();
private boolean mSimpleperfStartSuccess = false;
private boolean mIsCollectPerRun;
private boolean mIsTestFailed = false;
private boolean mSkipTestFailureMetrics;
private String mSubcommand;
private String mArguments;
private Map<String, String> mProcessToPid = new HashMap<>();
private boolean mReport;
private Map<String, String> mSymbolToMetricKey = new HashMap<>();
private int mTestIterations;
private SimpleperfHelper mSimpleperfHelper = new SimpleperfHelper();
public SimpleperfListener() {
super();
}
/**
* Constructor to simulate receiving the instrumentation arguments. Should not be used except
* for testing.
*/
@VisibleForTesting
SimpleperfListener(Bundle args, SimpleperfHelper helper, Map invocationMap) {
super(args);
mSimpleperfHelper = helper;
mTestIdInvocationCount = invocationMap;
}
@Override
public void onTestRunStart(DataRecord runData, Description description) {
Bundle args = getArgsBundle();
// Whether to collect for the entire run, or per test.
mIsCollectPerRun = Boolean.parseBoolean(args.getString(COLLECT_PER_RUN));
// Destination folder in the device to save all simpleperf sample files.
// Defaulted to /sdcard/test_results if test_output_root is not passed.
mTestOutputRoot = args.getString(TEST_OUTPUT_ROOT, DEFAULT_OUTPUT_ROOT);
// By default this flag is set to false to collect metrics on test failure.
mSkipTestFailureMetrics = "true".equals(args.getString(SKIP_TEST_FAILURE_METRICS));
// Subcommand passed to simpleperf. Defaults to record.
mSubcommand = args.getString(SUBCOMMAND, DEFAULT_SUBCOMMAND);
// Command arguments passed to simpleperf.
mArguments = args.getString(ARGUMENTS, DEFAULT_ARGUMENTS);
// Processes passed into recording arguments for simpleperf.
String processes = args.getString(PROCESSES, "");
String[] individualProcesses = processes.trim().split("\\s*,\\s*");
// Events passed into recording arguments for simpleperf.
String events = args.getString(EVENTS, "");
String[] individualEvents = events.trim().split("\\s*,\\s*");
// Whether to generate report after recording or not, by default set to false.
mReport = "true".equals(args.getString(REPORT));
// Symbols to look for when reporting events for processes.
String[] symbolAndMetricKey = args.getString(REPORT_SYMBOLS, "").trim().split("\\s*;\\s*");
for (int i = 0; i < symbolAndMetricKey.length - 1; i += 2) {
mSymbolToMetricKey.put(symbolAndMetricKey[i + 1], symbolAndMetricKey[i]);
}
// Appending recording argument for recording specified events if given.
if (!events.isEmpty()) {
mArguments += " -e ";
mArguments += String.join(",", individualEvents);
}
// Appending recording argument for recording specified processes if given.
if (!processes.isEmpty()) {
for (String process : individualProcesses) {
mProcessToPid.put(process, mSimpleperfHelper.getPID(process));
}
mArguments += " -p " + String.join(",", mProcessToPid.values());
}
mTestIterations = Integer.parseInt(args.getString(TEST_ITERATIONS, "1"));
Log.i(getTag(), "onTestRunStart arguments mTestIterations=" + mTestIterations);
if (!mIsCollectPerRun) {
return;
}
Log.i(getTag(), "Starting simpleperf before test run started.");
startSimpleperf(mSubcommand, mArguments);
}
@Override
public void onTestStart(DataRecord testData, Description description) {
mIsTestFailed = false;
if (mIsCollectPerRun) {
return;
}
mTestIdInvocationCount.compute(
getTestFileName(description), (key, value) -> (value == null) ? 1 : value + 1);
Log.i(getTag(), "Starting simpleperf before test started.");
startSimpleperf(mSubcommand, mArguments);
}
@Override
public void onTestFail(DataRecord testData, Description description, Failure failure) {
mIsTestFailed = true;
}
@Override
public void onTestEnd(DataRecord testData, Description description) {
if (mIsCollectPerRun) {
return;
}
if (!mSimpleperfStartSuccess) {
Log.i(
getTag(),
"Skipping simpleperf stop attempt onTestEnd because simpleperf did not start"
+ " successfully");
return;
}
if (mSkipTestFailureMetrics && mIsTestFailed) {
Log.i(getTag(), "Skipping metric collection due to test failure");
// Stop the existing simpleperf session.
try {
if (!mSimpleperfHelper.stopSimpleperf()) {
Log.e(getTag(), "Failed to stop the simpleperf process.");
}
} catch (IOException e) {
Log.e(getTag(), "Failed to stop simpleperf", e);
}
} else {
Log.i(getTag(), "Stopping simpleperf after test ended.");
// Construct test output directory in the below format
// <root_folder>/<test_name>/SimpleperfListener/<test_name>-<count>.data
Path path =
Paths.get(
mTestOutputRoot,
getTestFileName(description),
this.getClass().getSimpleName(),
String.format(
"%s%s-%d.data",
SIMPLEPERF_PREFIX,
getTestFileName(description),
mTestIdInvocationCount.get(getTestFileName(description))));
stopSimpleperf(path, testData);
if (mReport) {
getSimpleperfReport(path, testData);
}
}
}
@Override
public void onTestRunEnd(DataRecord runData, Result result) {
if (!mIsCollectPerRun) {
return;
}
if (!mSimpleperfStartSuccess) {
Log.i(getTag(), "Skipping simpleperf stop attempt as simpleperf failed to start.");
return;
}
Log.i(getTag(), "Stopping simpleperf after test run ended");
String uniqueId = Integer.toString(UUID.randomUUID().hashCode());
Path path =
Paths.get(
mTestOutputRoot,
this.getClass().getSimpleName(),
String.format("%s%s.data", SIMPLEPERF_PREFIX, uniqueId));
stopSimpleperf(path, runData);
if (mReport) {
getSimpleperfReport(path, runData);
}
}
/** Start simpleperf sampling. */
public void startSimpleperf(String subcommand, String arguments) {
mSimpleperfStartSuccess = mSimpleperfHelper.startCollecting(subcommand, arguments);
if (!mSimpleperfStartSuccess) {
Log.e(getTag(), "Simpleperf did not start successfully.");
}
}
/** Stop simpleperf sampling and dump the collected file into the given path. */
private void stopSimpleperf(Path path, DataRecord record) {
if (!mSimpleperfHelper.stopCollecting(path.toString())) {
Log.e(getTag(), "Failed to collect the simpleperf output.");
} else {
record.addStringMetric(SIMPLEPERF_FILE_PATH, path.toString());
}
}
/**
* Generate simpleperf report from an existing record file then add parsed metrics to
* DataRecord.
*
* @param path Path to read binary record from.
* @param data DataRecord to store metrics parsed from report
*/
private void getSimpleperfReport(Path path, DataRecord data) {
for (Map.Entry<String, String> entry : mProcessToPid.entrySet()) {
Map<String, String> metricPerProcess =
mSimpleperfHelper.getSimpleperfReport(
path.toString(), entry, mSymbolToMetricKey, mTestIterations);
Log.i(getTag(), "Simpleperf Metrics report collected. " + metricPerProcess);
for (Map.Entry<String /*event-process-symbol*/, String /*eventCount*/> metric :
metricPerProcess.entrySet()) {
data.addStringMetric(metric.getKey(), metric.getValue());
}
}
}
/**
* Returns the packagename.classname_methodname which has no special characters and is used to
* create file names.
*/
public static String getTestFileName(Description description) {
return String.format("%s_%s", description.getClassName(), description.getMethodName());
}
}