blob: cb0eac3df401ff2e3ef849658ac4f122fc19cee5 [file] [log] [blame]
/*
* Copyright (C) 2019 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.simpleperf;
import android.os.Build;
import android.system.OsConstants;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* This class uses `simpleperf record` cmd to generate a recording file.
* It allows users to start recording with some options, pause/resume recording
* to only profile interested code, and stop recording.
* </p>
*
* <p>
* Example:
* RecordOptions options = new RecordOptions();
* options.setDwarfCallGraph();
* ProfileSession session = new ProfileSession();
* session.StartRecording(options);
* Thread.sleep(1000);
* session.PauseRecording();
* Thread.sleep(1000);
* session.ResumeRecording();
* Thread.sleep(1000);
* session.StopRecording();
* </p>
*
* <p>
* It throws an Error when error happens. To read error messages of simpleperf record
* process, filter logcat with `simpleperf`.
* </p>
*/
public class ProfileSession {
private static final String SIMPLEPERF_PATH_IN_IMAGE = "/system/bin/simpleperf";
enum State {
NOT_YET_STARTED,
STARTED,
PAUSED,
STOPPED,
}
private State state = State.NOT_YET_STARTED;
private String appDataDir;
private String simpleperfPath;
private String simpleperfDataDir;
private Process simpleperfProcess;
private boolean traceOffcpu = false;
/**
* @param appDataDir the same as android.content.Context.getDataDir().
* ProfileSession stores profiling data in appDataDir/simpleperf_data/.
*/
public ProfileSession(String appDataDir) {
this.appDataDir = appDataDir;
simpleperfDataDir = appDataDir + "/simpleperf_data";
}
/**
* ProfileSession assumes appDataDir as /data/data/app_package_name.
*/
public ProfileSession() {
String packageName = "";
try {
String s = readInputStream(new FileInputStream("/proc/self/cmdline"));
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '\0') {
s = s.substring(0, i);
break;
}
}
packageName = s;
} catch (IOException e) {
throw new Error("failed to find packageName: " + e.getMessage());
}
if (packageName.isEmpty()) {
throw new Error("failed to find packageName");
}
appDataDir = "/data/data/" + packageName;
simpleperfDataDir = appDataDir + "/simpleperf_data";
}
/**
* Start recording.
* @param options RecordOptions
*/
public void startRecording(RecordOptions options) {
startRecording(options.toRecordArgs());
}
/**
* Start recording.
* @param args arguments for `simpleperf record` cmd.
*/
public synchronized void startRecording(List<String> args) {
if (state != State.NOT_YET_STARTED) {
throw new AssertionError("startRecording: session in wrong state " + state);
}
for (String arg : args) {
if (arg.equals("--trace-offcpu")) {
traceOffcpu = true;
}
}
simpleperfPath = findSimpleperf();
checkIfPerfEnabled();
createSimpleperfDataDir();
createSimpleperfProcess(simpleperfPath, args);
state = State.STARTED;
}
/**
* Pause recording. No samples are generated in paused state.
*/
public synchronized void pauseRecording() {
if (state != State.STARTED) {
throw new AssertionError("pauseRecording: session in wrong state " + state);
}
if (traceOffcpu) {
throw new AssertionError(
"--trace-offcpu option doesn't work well with pause/resume recording");
}
sendCmd("pause");
state = State.PAUSED;
}
/**
* Resume a paused session.
*/
public synchronized void resumeRecording() {
if (state != State.PAUSED) {
throw new AssertionError("resumeRecording: session in wrong state " + state);
}
sendCmd("resume");
state = State.STARTED;
}
/**
* Stop recording and generate a recording file under appDataDir/simpleperf_data/.
*/
public synchronized void stopRecording() {
if (state != State.STARTED && state != State.PAUSED) {
throw new AssertionError("stopRecording: session in wrong state " + state);
}
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P + 1 &&
simpleperfPath.equals(SIMPLEPERF_PATH_IN_IMAGE)) {
// The simpleperf shipped on Android Q contains a bug, which may make it abort if
// calling simpleperfProcess.destroy().
destroySimpleperfProcessWithoutClosingStdin();
} else {
simpleperfProcess.destroy();
}
try {
int exitCode = simpleperfProcess.waitFor();
if (exitCode != 0) {
throw new AssertionError("simpleperf exited with error: " + exitCode);
}
} catch (InterruptedException e) {
}
simpleperfProcess = null;
state = State.STOPPED;
}
private void destroySimpleperfProcessWithoutClosingStdin() {
// In format "Process[pid=? ..."
String s = simpleperfProcess.toString();
final String prefix = "Process[pid=";
if (s.startsWith(prefix)) {
int startIndex = prefix.length();
int endIndex = s.indexOf(',');
if (endIndex > startIndex) {
int pid = Integer.parseInt(s.substring(startIndex, endIndex).trim());
android.os.Process.sendSignal(pid, OsConstants.SIGTERM);
return;
}
}
simpleperfProcess.destroy();
}
private String readInputStream(InputStream in) {
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String result = reader.lines().collect(Collectors.joining("\n"));
try {
reader.close();
} catch (IOException e) {
}
return result;
}
private String findSimpleperf() {
// 1. Try /data/local/tmp/simpleperf. Probably it's newer than /system/bin/simpleperf.
String simpleperfPath = findSimpleperfInTempDir();
if (simpleperfPath != null) {
return simpleperfPath;
}
// 2. Try /system/bin/simpleperf, which is available on Android >= Q.
simpleperfPath = SIMPLEPERF_PATH_IN_IMAGE;
if (isExecutableFile(simpleperfPath)) {
return simpleperfPath;
}
throw new Error("can't find simpleperf on device. Please run api_profiler.py.");
}
private boolean isExecutableFile(String path) {
File file = new File(path);
return file.canExecute();
}
private String findSimpleperfInTempDir() {
String path = "/data/local/tmp/simpleperf";
File file = new File(path);
if (!file.isFile()){
return null;
}
// Copy it to app dir to execute it.
String toPath = appDataDir + "/simpleperf";
try {
Process process = new ProcessBuilder()
.command("cp", path, toPath).start();
process.waitFor();
} catch (Exception e) {
return null;
}
if (!isExecutableFile(toPath)) {
return null;
}
// For apps with target sdk >= 29, executing app data file isn't allowed. So test executing
// it.
try {
Process process = new ProcessBuilder()
.command(toPath).start();
process.waitFor();
} catch (Exception e) {
return null;
}
return toPath;
}
private void checkIfPerfEnabled() {
String value = "";
Process process;
try {
process = new ProcessBuilder()
.command("/system/bin/getprop", "security.perf_harden").start();
} catch (IOException e) {
// Omit check if getprop doesn't exist.
return;
}
try {
process.waitFor();
} catch (InterruptedException e) {
}
value = readInputStream(process.getInputStream());
if (value.startsWith("1")) {
throw new Error("linux perf events aren't enabled on the device." +
" Please run api_profiler.py.");
}
}
private void createSimpleperfDataDir() {
File file = new File(simpleperfDataDir);
if (!file.isDirectory()) {
file.mkdir();
}
}
private void createSimpleperfProcess(String simpleperfPath, List<String> recordArgs) {
// 1. Prepare simpleperf arguments.
ArrayList<String> args = new ArrayList<>();
args.add(simpleperfPath);
args.add("record");
args.add("--log-to-android-buffer");
args.add("--log");
args.add("debug");
args.add("--stdio-controls-profiling");
args.add("--in-app");
args.add("--tracepoint-events");
args.add("/data/local/tmp/tracepoint_events");
args.addAll(recordArgs);
// 2. Create the simpleperf process.
ProcessBuilder pb = new ProcessBuilder(args).directory(new File(simpleperfDataDir));
try {
simpleperfProcess = pb.start();
} catch (IOException e) {
throw new Error("failed to create simpleperf process: " + e.getMessage());
}
// 3. Wait until simpleperf starts recording.
String startFlag = readReply();
if (!startFlag.equals("started")) {
throw new Error("failed to receive simpleperf start flag");
}
}
private void sendCmd(String cmd) {
cmd += "\n";
try {
simpleperfProcess.getOutputStream().write(cmd.getBytes());
simpleperfProcess.getOutputStream().flush();
} catch (IOException e) {
throw new Error("failed to send cmd to simpleperf: " + e.getMessage());
}
if (!readReply().equals("ok")) {
throw new Error("failed to run cmd in simpleperf: " + cmd);
}
}
private String readReply() {
// Read one byte at a time to stop at line break or EOF. BufferedReader will try to read
// more than available and make us blocking, so don't use it.
String s = "";
while (true) {
int c = -1;
try {
c = simpleperfProcess.getInputStream().read();
} catch (IOException e) {
}
if (c == -1 || c == '\n') {
break;
}
s += (char)c;
}
return s;
}
}