blob: 6060a4de85d77b04dbc289307008da0968c44f1f [file] [log] [blame]
/*
* Copyright (C) 2018 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 androidx.benchmark.perfetto;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.RequiresApi;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* PerfettoHelper is used to start and stop the perfetto tracing and move the
* output perfetto trace file to destination folder.
*/
@RequiresApi(28)
class PerfettoHelper {
private static final String LOG_TAG = PerfettoHelper.class.getSimpleName();
// Command to start the perfetto tracing in the background.
// perfetto -b -c /data/misc/perfetto-traces/trace_config.pb -o
// /data/misc/perfetto-traces/trace_output.pb
private static final String PERFETTO_TMP_OUTPUT_FILE =
"/data/misc/perfetto-traces/trace_output.pb";
// Additional arg to indicate that the perfetto config file is text format.
private static final String PERFETTO_TXT_PROTO_ARG = " --txt";
// Command to check the perfetto process id.
private static final String PERFETTO_PROC_ID_CMD = "pidof perfetto";
// Command to move the perfetto output trace file to given folder.
private static final String MOVE_CMD = "mv %s %s";
// Max wait count for checking if perfetto is stopped successfully
private static final int PERFETTO_KILL_WAIT_COUNT = 12;
// Check if perfetto is stopped every 5 secs.
private static final long PERFETTO_KILL_WAIT_TIME = 5000;
private UiDevice mUIDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
/**
* Start the perfetto tracing in background using the given config file.
*
* The output will be written to /data/misc/perfetto-traces/trace_output.pb. Perfetto has
* write access only to /data/misc/perfetto-traces/ folder. The config file may be anywhere
* readable by shell.
*
* @param configFilePath used for collecting the perfetto trace.
* @param isTextProtoConfig true if the config file is textproto format otherwise false.
* @return true if trace collection started successfully otherwise return false.
*/
public boolean startCollecting(String configFilePath, boolean isTextProtoConfig) {
if (configFilePath == null || configFilePath.isEmpty()) {
Log.e(LOG_TAG, "Perfetto config file name is null or empty.");
return false;
}
try {
// Cleanup already existing perfetto process.
Log.i(LOG_TAG, "Cleanup perfetto before starting.");
if (isPerfettoRunning()) {
Log.i(LOG_TAG, "Perfetto tracing is already running. Stopping perfetto.");
if (!stopPerfetto()) {
return false;
}
}
// Remove already existing temporary output trace file if any.
String output = mUIDevice.executeShellCommand("rm " + PERFETTO_TMP_OUTPUT_FILE);
Log.i(LOG_TAG, String.format("Perfetto output file cleanup - %s", output));
// Start perfetto tracing. Note that we need to use executeShellScript to be able to
// pipe the input via cat, as **Perfetto cannot read from the filesystem without root**.
String perfettoCmd = "cat " + configFilePath
+ " | perfetto --background -c - -o " + PERFETTO_TMP_OUTPUT_FILE;
if (isTextProtoConfig) {
perfettoCmd = perfettoCmd + PERFETTO_TXT_PROTO_ARG;
}
Log.i(LOG_TAG, "Starting perfetto tracing with cmd: " + perfettoCmd);
String startOutput = ShellUtilsKt.executeShellScript(mUIDevice, perfettoCmd, null);
Log.i(LOG_TAG, String.format("Perfetto start command output - %s", startOutput));
// TODO : Once the output status is available use that for additional validation.
if (!isPerfettoRunning()) {
Log.e(LOG_TAG, "Perfetto tracing failed to start.");
return false;
}
} catch (IOException ioe) {
Log.e(LOG_TAG, "Unable to start the perfetto tracing due to :" + ioe.getMessage());
return false;
}
Log.i(LOG_TAG, "Perfetto tracing started successfully.");
return true;
}
/**
* Stop the perfetto trace collection under /data/misc/perfetto-traces/trace_output.pb after
* waiting for given time in msecs and copy the output to the destination file.
*
* @param waitTimeInMsecs time to wait in msecs before stopping the trace collection.
* @param destinationFile file to copy the perfetto output trace.
* @return true if the trace collection is successful otherwise false.
*/
public boolean stopCollecting(long waitTimeInMsecs, String destinationFile) {
// Wait for the dump interval before stopping the trace.
Log.i(LOG_TAG, String.format(
"Waiting for %d msecs before stopping perfetto.", waitTimeInMsecs));
SystemClock.sleep(waitTimeInMsecs);
// Stop the perfetto and copy the output file.
Log.i(LOG_TAG, "Stopping perfetto.");
try {
if (stopPerfetto()) {
Log.i(LOG_TAG, String.format("Writing to %s.", destinationFile));
if (!copyFileOutput(destinationFile)) {
return false;
}
} else {
Log.e(LOG_TAG, "Perfetto failed to stop.");
return false;
}
} catch (IOException ioe) {
Log.e(LOG_TAG, "Unable to stop the perfetto tracing due to " + ioe.getMessage());
return false;
}
return true;
}
/**
* Utility method for stopping perfetto.
*
* @return true if perfetto is stopped successfully.
*/
public boolean stopPerfetto() throws IOException {
String stopOutput = mUIDevice.executeShellCommand("kill -INT " + perfettoPid());
Log.i(LOG_TAG, String.format("Perfetto stop command output - %s", stopOutput));
int waitCount = 0;
while (isPerfettoRunning()) {
// 60 secs timeout for perfetto shutdown.
if (waitCount < PERFETTO_KILL_WAIT_COUNT) {
// Check every 5 secs if perfetto stopped successfully.
SystemClock.sleep(PERFETTO_KILL_WAIT_TIME);
waitCount++;
continue;
}
return false;
}
Log.e(LOG_TAG, "Perfetto stopped successfully.");
return true;
}
/**
* Returns perfetto process pid if running, or null otherwise.
*/
private String perfettoPid() {
try {
String perfettoProcId = mUIDevice.executeShellCommand(PERFETTO_PROC_ID_CMD);
Log.i(LOG_TAG, String.format("Perfetto process id - %s", perfettoProcId));
if (perfettoProcId.isEmpty()) {
return null;
}
return perfettoProcId;
} catch (IOException ioe) {
Log.e(LOG_TAG, "Not able to check the perfetto status due to:" + ioe.getMessage());
return null;
}
}
/**
* Check if perfetto process is running or not.
*
* @return true if perfetto is running otherwise false.
*/
public boolean isPerfettoRunning() {
return perfettoPid() != null;
}
/**
* Copy the temporary perfetto trace output file from /data/misc/perfetto-traces/ to given
* destinationFile.
*
* @param destinationFile file to copy the perfetto output trace.
* @return true if the trace file copied successfully otherwise false.
*/
private boolean copyFileOutput(String destinationFile) {
Path path = Paths.get(destinationFile);
String destDirectory = path.getParent().toString();
// Check if the 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/misc/perfetto-traces/trace_output.pb to
// destinationFile
try {
String moveResult = mUIDevice.executeShellCommand(String.format(
MOVE_CMD, PERFETTO_TMP_OUTPUT_FILE, destinationFile));
if (!moveResult.isEmpty()) {
Log.e(LOG_TAG, String.format(
"Unable to move perfetto output file from %s to %s due to %s",
PERFETTO_TMP_OUTPUT_FILE, destinationFile, moveResult));
return false;
}
} catch (IOException ioe) {
Log.e(LOG_TAG,
"Unable to move the perfetto trace file to destination file."
+ ioe.getMessage());
return false;
}
return true;
}
}