blob: c56660b75acab4a68ae60b2ed38b347e6fbbd6f5 [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 com.android.helpers;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* SimpleperfHelper is used to start and stop simpleperf sample collection and move the output
* sample file to the destination folder.
*/
public class SimpleperfHelper {
private static final String LOG_TAG = SimpleperfHelper.class.getSimpleName();
private static final String SIMPLEPERF_TMP_FILE_PATH = "/data/local/tmp/perf.data";
private static final String SIMPLEPERF_REPORT_TMP_FILE_PATH = "/data/local/tmp/perf_report.txt";
private static final String SIMPLEPERF_START_CMD = "simpleperf %s -o %s %s";
private static final String SIMPLEPERF_STOP_CMD = "pkill -INT simpleperf";
private static final String SIMPLEPERF_PROC_ID_CMD = "pidof simpleperf";
private static final String REMOVE_CMD = "rm %s";
private static final String MOVE_CMD = "mv %s %s";
private static final int SIMPLEPERF_START_WAIT_COUNT = 3;
private static final int SIMPLEPERF_START_WAIT_TIME = 1000;
private static final int SIMPLEPERF_STOP_WAIT_COUNT = 60;
private static final long SIMPLEPERF_STOP_WAIT_TIME = 15000;
private final UiDevice mUiDevice;
/** Constructor to receive visible UiDevice. Should not be used except for testing. */
@VisibleForTesting
public SimpleperfHelper(UiDevice uidevice) {
mUiDevice = uidevice;
}
public SimpleperfHelper() {
mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}
public boolean startCollecting(String subcommand, String arguments) {
try {
// Cleanup any running simpleperf sessions.
Log.i(LOG_TAG, "Cleanup simpleperf before starting.");
if (isSimpleperfRunning()) {
Log.i(LOG_TAG, "Simpleperf is already running. Stopping simpleperf.");
if (!stopSimpleperf()) {
return false;
}
}
Log.i(LOG_TAG, String.format("Starting simpleperf"));
new Thread() {
@Override
public void run() {
String startCommand =
String.format(
SIMPLEPERF_START_CMD,
subcommand,
SIMPLEPERF_TMP_FILE_PATH,
arguments);
Log.i(LOG_TAG, String.format("Start command: %s", startCommand));
try {
String startOutput = mUiDevice.executeShellCommand(startCommand);
Log.i(
LOG_TAG,
String.format("Simpleperf start command output - %s", startOutput));
} catch (IOException e) {
Log.e(LOG_TAG, "Failed to start simpleperf.");
}
}
}.start();
int waitCount = 0;
while (!isSimpleperfRunning()) {
if (waitCount < SIMPLEPERF_START_WAIT_COUNT) {
SystemClock.sleep(SIMPLEPERF_START_WAIT_TIME);
waitCount++;
continue;
}
Log.e(LOG_TAG, "Simpleperf sampling failed to start.");
return false;
}
} catch (IOException e) {
Log.e(LOG_TAG, "Unable to start simpleperf sampling due to :" + e.getMessage());
return false;
}
Log.i(LOG_TAG, "Simpleperf sampling started successfully.");
return true;
}
/**
* Stop the simpleperf sample collection under /data/local/tmp/perf.data and copy the output to
* the destination file.
*
* @param destinationFile file to copy the simpleperf sample file to.
* @return true if the trace collection is successful otherwise false.
*/
public boolean stopCollecting(String destinationFile) {
Log.i(LOG_TAG, "Stopping simpleperf.");
try {
if (stopSimpleperf()) {
if (!copyFileOutput(destinationFile)) {
return false;
}
} else {
Log.e(LOG_TAG, "Simpleperf failed to stop");
return false;
}
} catch (IOException e) {
Log.e(LOG_TAG, "Unable to stop the simpleperf samping due to " + e.getMessage());
return false;
}
return true;
}
/**
* Utility method for sending the signal to stop simpleperf.
*
* @return true if simpleperf is successfully stopped.
*/
public boolean stopSimpleperf() throws IOException {
if (!isSimpleperfRunning()) {
Log.e(LOG_TAG, "Simpleperf stop called, but simpleperf is not running.");
return false;
}
String stopOutput = mUiDevice.executeShellCommand(SIMPLEPERF_STOP_CMD);
Log.i(LOG_TAG, String.format("Simpleperf stop command ran: %s", SIMPLEPERF_STOP_CMD));
int waitCount = 0;
while (isSimpleperfRunning()) {
if (waitCount < SIMPLEPERF_STOP_WAIT_COUNT) {
SystemClock.sleep(SIMPLEPERF_STOP_WAIT_TIME);
waitCount++;
continue;
}
Log.e(LOG_TAG, "Simpleperf failed to stop");
return false;
}
Log.i(LOG_TAG, "Simpleperf stopped successfully.");
return true;
}
/**
* Method for generating simpleperf report and getting report metrics.
*
* @param path Path to read binary record from.
* @param processToPid Map with process names and PIDs to look for in record file.
* @param symbols Symbols to report events from the processes recorded
* @return Map containing recorded processes and nested map of symbols and event count for each
* symbol.
*/
public Map<String /*event-process-symbol*/, String /*eventCount*/> getSimpleperfReport(
String path,
Map.Entry<String, String> processToPid,
Map<String, String> symbols,
int testIterations) {
try {
String reportCommand =
String.format(
"simpleperf report -i %s --pids %s --sort pid,symbol -o %s"
+ " --print-event-count --children",
path, processToPid.getValue(), SIMPLEPERF_REPORT_TMP_FILE_PATH);
Log.i(LOG_TAG, String.format("Report command: %s", reportCommand));
mUiDevice.executeShellCommand(reportCommand);
return getMetrics(processToPid.getKey(), symbols, testIterations);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not generate report: " + e.getMessage());
}
return new HashMap<>();
}
/**
* Utility method for extracting metrics from given simpleperf report.
*
* @param process Individually extracted processes recorded in binary record file.
* @param symbols Symbols to report events from the processes recorded.
* @return Map containing recorded event counts from symbols within process
*/
private Map<String, String> getMetrics(
String process, Map<String, String> symbols, int testIterations) {
Map<String, String> results = new HashMap<>();
try {
String eventName = "";
BufferedReader reader =
new BufferedReader(
new FileReader(SimpleperfHelper.SIMPLEPERF_REPORT_TMP_FILE_PATH));
for (String line; (line = reader.readLine()) != null; ) {
// Checking for top of the report to find event name and event count.
// Event count: 3498520605
if (line.contains(": ")) {
String[] splitLine = line.split(": ");
if (splitLine[0].equals("Event")) {
eventName = splitLine[1].split(" ")[0];
} else if (splitLine[0].equals("Event count")) {
String key = String.join("-", process, eventName);
long count = Long.parseLong(splitLine[1]) / testIterations;
results.put(key, String.valueOf(count));
}
}
// Parsing lines for specific symbols in report to store with event count to results
// Children Self AccEventCount SelfEventCount Pid Symbol
// 54.20% 0.00% 122803507 0 2510 __start_thread
else if (line.contains("%")) {
final String[] splitLine = line.split("\\s+", 6);
final String parsedSymbol = splitLine[5].trim();
final String matchedSymbol = getMatchingSymbol(symbols, parsedSymbol);
if (matchedSymbol == null) {
continue;
}
String key = String.join("-", process, matchedSymbol, eventName);
if (results.containsKey(key + "-percentage")) {
// We are searching for symbols with partial matches so only include the
// first hit if we get multiple matches.
continue;
}
// Remove trailing %
String percentage = splitLine[0].substring(0, splitLine[0].length() - 1);
results.put(key + "-percentage", percentage);
String eventCount = splitLine[2].trim();
long count = Long.parseLong(eventCount) / testIterations;
results.put(key + "-count", String.valueOf(count));
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "Could not open report file: " + e.getMessage());
}
return results;
}
private static String getMatchingSymbol(Map<String, String> symbols, String parsedSymbol) {
for (String candidate : symbols.keySet()) {
if (parsedSymbol.contains(candidate)) {
return symbols.get(candidate);
}
}
return null;
}
/**
* Convert process name into process ID usable for simpleperf commands
*
* @param process the name of a running process
* @return String containing the process ID
*/
public String getPID(String process) {
try {
return mUiDevice.executeShellCommand("pidof " + process).trim();
} catch (Exception e) {
Log.e(LOG_TAG, "Could not resolve PID for " + process, e);
return "";
}
}
/**
* Check if there is a simpleperf instance running.
*
* @return true if there is a running simpleperf instance, otherwise false.
*/
private boolean isSimpleperfRunning() {
try {
String simpleperfProcId = mUiDevice.executeShellCommand(SIMPLEPERF_PROC_ID_CMD);
Log.i(LOG_TAG, String.format("Simpleperf process id - %s", simpleperfProcId));
if (simpleperfProcId.isEmpty()) {
return false;
}
} catch (IOException e) {
Log.e(LOG_TAG, "Unable to check simpleperf status: " + e.getMessage());
return false;
}
return true;
}
/**
* Copy the temporary simpleperf output file to the given destinationFile.
*
* @param destinationFile file to copy simpleperf output into.
* @return true if the simpleperf file copied successfully, otherwise false.
*/
public boolean copyFileOutput(String destinationFile) {
Path path = Paths.get(destinationFile);
String destDirectory = path.getParent().toString();
// Check if directory already exists
File directory = new File(destDirectory);
if (!directory.exists()) {
boolean success = directory.mkdirs();
if (!success) {
Log.e(
LOG_TAG,
String.format(
"Result output directory %s not created successfully.",
destDirectory));
return false;
}
}
// Copy the collected trace from /data/local/tmp to the destinationFile.
try {
String moveResult =
mUiDevice.executeShellCommand(
String.format(MOVE_CMD, SIMPLEPERF_TMP_FILE_PATH, destinationFile));
if (!moveResult.isEmpty()) {
Log.e(
LOG_TAG,
String.format(
"Unable to move simpleperf output file from %s to %s due to %s",
SIMPLEPERF_TMP_FILE_PATH, destinationFile, moveResult));
return false;
}
} catch (IOException e) {
Log.e(
LOG_TAG,
"Unable to move the simpleperf sample file to destination file."
+ e.getMessage());
return false;
}
return true;
}
}