diff options
Diffstat (limited to 'app_api/java/com/android/simpleperf/ProfileSession.java')
-rw-r--r-- | app_api/java/com/android/simpleperf/ProfileSession.java | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/app_api/java/com/android/simpleperf/ProfileSession.java b/app_api/java/com/android/simpleperf/ProfileSession.java new file mode 100644 index 0000000..cb0eac3 --- /dev/null +++ b/app_api/java/com/android/simpleperf/ProfileSession.java @@ -0,0 +1,350 @@ +/* + * 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; + } +} |