diff options
author | Yabin Cui <yabinc@google.com> | 2019-02-14 16:23:05 -0800 |
---|---|---|
committer | Yabin Cui <yabinc@google.com> | 2019-02-20 13:43:09 -0800 |
commit | 82c4c23a97dacdf140988dcc1dc46c0cd34ce93e (patch) | |
tree | 0ecc3c4287ed2976ef8d497e426116cf54b0852a /simpleperf/app_api | |
parent | d9121cec265ca1ecaea7fcf415ea5a0ddd39530e (diff) | |
download | extras-82c4c23a97dacdf140988dcc1dc46c0cd34ce93e.tar.gz |
simpleperf: Add Java/C++ API to control simpleperf in app code.
Also add JavaApi app to test the Java API, and CppApi App to test
the C++ API.
Also expose help msgs for options used in the API.
Bug: 123717243
Test: run apps manually and verify the generated profiling data.
Change-Id: I64c9de82b3f7eb3d76c6aff0703e12ce33233e49
Diffstat (limited to 'simpleperf/app_api')
-rw-r--r-- | simpleperf/app_api/cpp/simpleperf.cpp | 418 | ||||
-rw-r--r-- | simpleperf/app_api/cpp/simpleperf.h | 159 | ||||
-rw-r--r-- | simpleperf/app_api/java/com/android/simpleperf/ProfileSession.java | 279 | ||||
-rw-r--r-- | simpleperf/app_api/java/com/android/simpleperf/RecordOptions.java | 157 |
4 files changed, 1013 insertions, 0 deletions
diff --git a/simpleperf/app_api/cpp/simpleperf.cpp b/simpleperf/app_api/cpp/simpleperf.cpp new file mode 100644 index 00000000..11e70e6f --- /dev/null +++ b/simpleperf/app_api/cpp/simpleperf.cpp @@ -0,0 +1,418 @@ +/* + * 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. + */ + +#include "simpleperf.h" + +#include <limits.h> +#include <stdarg.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <mutex> +#include <sstream> +#include <android/log.h> + +namespace simpleperf { + +enum RecordCmd { + CMD_PAUSE_RECORDING = 1, + CMD_RESUME_RECORDING, +}; + +class RecordOptionsImpl { + public: + std::string output_filename = "perf.data"; + std::string event = "cpu-cycles"; + size_t freq = 4000; + double duration_in_second = 0.0; + std::vector<pid_t> threads; + bool dwarf_callgraph = false; + bool fp_callgraph = false; + bool trace_offcpu = false; +}; + +RecordOptions::RecordOptions() : impl_(new RecordOptionsImpl) { +} + +RecordOptions::~RecordOptions() { + delete impl_; +} + +RecordOptions& RecordOptions::SetOutputFilename(const std::string &filename) { + impl_->output_filename = filename; + return *this; +} + +RecordOptions& RecordOptions::SetEvent(const std::string &event) { + impl_->event = event; + return *this; +} + +RecordOptions& RecordOptions::SetSampleFrequency(size_t freq) { + impl_->freq = freq; + return *this; +} + +RecordOptions& RecordOptions::SetDuration(double duration_in_second) { + impl_->duration_in_second = duration_in_second; + return *this; +} + +RecordOptions& RecordOptions::SetSampleThreads(const std::vector<pid_t> &threads) { + impl_->threads = threads; + return *this; +} + +RecordOptions& RecordOptions::RecordDwarfCallGraph() { + impl_->dwarf_callgraph = true; + impl_->fp_callgraph = false; + return *this; +} + +RecordOptions& RecordOptions::RecordFramePointerCallGraph() { + impl_->fp_callgraph = true; + impl_->dwarf_callgraph = false; + return *this; +} + +RecordOptions& RecordOptions::TraceOffCpu() { + impl_->trace_offcpu = true; + return *this; +} + +std::vector<std::string> RecordOptions::ToRecordArgs() const { + std::vector<std::string> args; + args.insert(args.end(), {"-o", impl_->output_filename}); + args.insert(args.end(), {"-e", impl_->event}); + args.insert(args.end(), {"-f", std::to_string(impl_->freq)}); + if (impl_->duration_in_second != 0.0) { + args.insert(args.end(), {"--duration", std::to_string(impl_->duration_in_second)}); + } + if (impl_->threads.empty()) { + args.insert(args.end(), {"-p", std::to_string(getpid())}); + } else { + std::ostringstream os; + os << *(impl_->threads.begin()); + for (auto it = std::next(impl_->threads.begin()); it != impl_->threads.end(); ++it) { + os << "," << *it; + } + args.insert(args.end(), {"-t", os.str()}); + } + if (impl_->dwarf_callgraph) { + args.push_back("-g"); + } else if (impl_->fp_callgraph) { + args.insert(args.end(), {"--call-graph", "fp"}); + } + if (impl_->trace_offcpu) { + args.push_back("--trace-offcpu"); + } + return args; +} + +static void Abort(const char* fmt, ...) { + va_list vl; + va_start(vl, fmt); + __android_log_vprint(ANDROID_LOG_FATAL, "simpleperf", fmt, vl); + va_end(vl); + abort(); +} + +class ProfileSessionImpl { + public: + ProfileSessionImpl(const std::string& app_data_dir) + : app_data_dir_(app_data_dir), + simpleperf_data_dir_(app_data_dir + "/simpleperf_data") {} + ~ProfileSessionImpl(); + void StartRecording(const std::vector<std::string>& args); + void PauseRecording(); + void ResumeRecording(); + void StopRecording(); + + private: + std::string FindSimpleperf(); + void CheckIfPerfEnabled(); + void CreateSimpleperfDataDir(); + void CreateSimpleperfProcess(const std::string& simpleperf_path, + const std::vector<std::string>& record_args); + void SendCmd(const std::string& cmd); + std::string ReadReply(); + + enum State { + NOT_YET_STARTED, + STARTED, + PAUSED, + STOPPED, + }; + + const std::string app_data_dir_; + const std::string simpleperf_data_dir_; + std::mutex lock_; // Protect all members below. + State state_ = NOT_YET_STARTED; + pid_t simpleperf_pid_ = -1; + int control_fd_ = -1; + int reply_fd_ = -1; +}; + +ProfileSessionImpl::~ProfileSessionImpl() { + if (control_fd_ != -1) { + close(control_fd_); + } + if (reply_fd_ != -1) { + close(reply_fd_); + } +} + +void ProfileSessionImpl::StartRecording(const std::vector<std::string> &args) { + std::lock_guard<std::mutex> guard(lock_); + if (state_ != NOT_YET_STARTED) { + Abort("startRecording: session in wrong state %d", state_); + } + std::string simpleperf_path = FindSimpleperf(); + CheckIfPerfEnabled(); + CreateSimpleperfDataDir(); + CreateSimpleperfProcess(simpleperf_path, args); + state_ = STARTED; +} + +void ProfileSessionImpl::PauseRecording() { + std::lock_guard<std::mutex> guard(lock_); + if (state_ != STARTED) { + Abort("pauseRecording: session in wrong state %d", state_); + } + SendCmd("pause"); + state_ = PAUSED; +} + +void ProfileSessionImpl::ResumeRecording() { + std::lock_guard<std::mutex> guard(lock_); + if (state_ != PAUSED) { + Abort("resumeRecording: session in wrong state %d", state_); + } + SendCmd("resume"); + state_ = STARTED; +} + +void ProfileSessionImpl::StopRecording() { + std::lock_guard<std::mutex> guard(lock_); + if (state_ != STARTED && state_ != PAUSED) { + Abort("stopRecording: session in wrong state %d", state_); + } + // Send SIGINT to simpleperf to stop recording. + if (kill(simpleperf_pid_, SIGINT) == -1) { + Abort("failed to stop simpleperf: %s", strerror(errno)); + } + int status; + pid_t result = TEMP_FAILURE_RETRY(waitpid(simpleperf_pid_, &status, 0)); + if (result == -1) { + Abort("failed to call waitpid: %s", strerror(errno)); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + Abort("simpleperf exited with error, status = 0x%x", status); + } + state_ = STOPPED; +} + +void ProfileSessionImpl::SendCmd(const std::string& cmd) { + std::string data = cmd + "\n"; + if (TEMP_FAILURE_RETRY(write(control_fd_, &data[0], data.size())) != + static_cast<ssize_t>(data.size())) { + Abort("failed to send cmd to simpleperf: %s", strerror(errno)); + } + if (ReadReply() != "ok") { + Abort("failed to run cmd in simpleperf: %s", cmd.c_str()); + } +} + +static bool IsExecutableFile(const std::string& path) { + struct stat st; + if (stat(path.c_str(), &st) == 0) { + if (S_ISREG(st.st_mode) && (st.st_mode & S_IXUSR)) { + return true; + } + } + return false; +} + +std::string ProfileSessionImpl::FindSimpleperf() { + std::vector<std::string> candidates = { + // For debuggable apps, simpleperf is put to the appDir by api_app_profiler.py. + app_data_dir_ + "/simpleperf", + // For profileable apps on Android >= Q, use simpleperf in system image. + "/system/bin/simpleperf" + }; + for (const auto& path : candidates) { + if (IsExecutableFile(path)) { + return path; + } + } + Abort("can't find simpleperf on device. Please run api_app_profiler.py."); + return ""; +} + +static std::string ReadFile(FILE* fp) { + std::string s; + char buf[200]; + while (true) { + ssize_t n = fread(buf, 1, sizeof(buf), fp); + if (n <= 0) { + break; + } + s.insert(s.end(), buf, buf + n); + } + return s; +} + +void ProfileSessionImpl::CheckIfPerfEnabled() { + FILE* fp = popen("/system/bin/getprop security.perf_harden", "re"); + if (fp == nullptr) { + return; // Omit check if getprop doesn't exist. + } + std::string s = ReadFile(fp); + pclose(fp); + if (!s.empty() && s[0] == '1') { + Abort("linux perf events aren't enabled on the device. Please run api_app_profiler.py."); + } +} + +void ProfileSessionImpl::CreateSimpleperfDataDir() { + struct stat st; + if (stat(simpleperf_data_dir_.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) { + return; + } + if (mkdir(simpleperf_data_dir_.c_str(), 0700) == -1) { + Abort("failed to create simpleperf data dir %s: %s", simpleperf_data_dir_.c_str(), + strerror(errno)); + } +} + +void ProfileSessionImpl::CreateSimpleperfProcess(const std::string &simpleperf_path, + const std::vector<std::string> &record_args) { + // 1. Create control/reply pips. + int control_fd[2]; + int reply_fd[2]; + if (pipe(control_fd) != 0 || pipe(reply_fd) != 0) { + Abort("failed to call pipe: %s", strerror(errno)); + } + + // 2. Prepare simpleperf arguments. + std::vector<std::string> args; + args.emplace_back(simpleperf_path); + args.emplace_back("record"); + args.emplace_back("--log-to-android-buffer"); + args.insert(args.end(), {"--log", "debug"}); + args.emplace_back("--stdio-controls-profiling"); + args.emplace_back("--in-app"); + args.insert(args.end(), {"--tracepoint-events", "/data/local/tmp/tracepoint_events"}); + args.insert(args.end(), record_args.begin(), record_args.end()); + char* argv[args.size() + 1]; + for (size_t i = 0; i < args.size(); ++i) { + argv[i] = &args[i][0]; + } + argv[args.size()] = nullptr; + + // 3. Start simpleperf process. + int pid = fork(); + if (pid == -1) { + Abort("failed to fork: %s", strerror(errno)); + } + if (pid == 0) { + // child process + close(control_fd[1]); + dup2(control_fd[0], 0); // simpleperf read control cmd from fd 0. + close(control_fd[0]); + close(reply_fd[0]); + dup2(reply_fd[1], 1); // simpleperf writes reply to fd 1. + close(reply_fd[0]); + chdir(simpleperf_data_dir_.c_str()); + execvp(argv[0], argv); + Abort("failed to call exec: %s", strerror(errno)); + } + // parent process + close(control_fd[0]); + control_fd_ = control_fd[1]; + close(reply_fd[1]); + reply_fd_ = reply_fd[0]; + simpleperf_pid_ = pid; + + // 4. Wait until simpleperf starts recording. + std::string start_flag = ReadReply(); + if (start_flag != "started") { + Abort("failed to receive simpleperf start flag"); + } +} + +std::string ProfileSessionImpl::ReadReply() { + std::string s; + while (true) { + char c; + ssize_t result = TEMP_FAILURE_RETRY(read(reply_fd_, &c, 1)); + if (result <= 0 || c == '\n') { + break; + } + s.push_back(c); + } + return s; +} + +ProfileSession::ProfileSession() { + FILE* fp = fopen("/proc/self/cmdline", "r"); + if (fp == nullptr) { + Abort("failed to open /proc/self/cmdline: %s", strerror(errno)); + } + std::string s = ReadFile(fp); + fclose(fp); + for (int i = 0; i < s.size(); i++) { + if (s[i] == '\0') { + s = s.substr(0, i); + break; + } + } + std::string app_data_dir = "/data/data/" + s; + impl_ = new ProfileSessionImpl(app_data_dir); +} + +ProfileSession::ProfileSession(const std::string& app_data_dir) + : impl_(new ProfileSessionImpl(app_data_dir)) {} + +ProfileSession::~ProfileSession() { + delete impl_; +} + +void ProfileSession::StartRecording(const RecordOptions &options) { + StartRecording(options.ToRecordArgs()); +} + +void ProfileSession::StartRecording(const std::vector<std::string> &record_args) { + impl_->StartRecording(record_args); +} + +void ProfileSession::PauseRecording() { + impl_->PauseRecording(); +} + +void ProfileSession::ResumeRecording() { + impl_->ResumeRecording(); +} + +void ProfileSession::StopRecording() { + impl_->StopRecording(); +} + +} // namespace simpleperf diff --git a/simpleperf/app_api/cpp/simpleperf.h b/simpleperf/app_api/cpp/simpleperf.h new file mode 100644 index 00000000..d6704fc8 --- /dev/null +++ b/simpleperf/app_api/cpp/simpleperf.h @@ -0,0 +1,159 @@ +/* + * 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. + */ + +#pragma once +#include <sys/types.h> +#include <unistd.h> + +#include <string> +#include <vector> + +// A C++ API used to control simpleperf recording. +namespace simpleperf { + +/** + * RecordOptions sets record options used by ProfileSession. The options are + * converted to a string list in toRecordArgs(), which is then passed to + * `simpleperf record` cmd. Run `simpleperf record -h` or + * `run_simpleperf_on_device.py record -h` for help messages. + * + * Example: + * RecordOptions options; + * options.setDuration(3).recordDwarfCallGraph().setOutputFilename("perf.data"); + * ProfileSession session; + * session.startRecording(options); + */ +class RecordOptionsImpl; +class RecordOptions { + public: + RecordOptions(); + ~RecordOptions(); + /** + * Set output filename. Default is perf.data. The file will be generated under simpleperf_data/. + */ + RecordOptions& SetOutputFilename(const std::string& filename); + + /** + * Set event to record. Default is cpu-cycles. See `simpleperf list` for all available events. + */ + RecordOptions& SetEvent(const std::string& event); + + /** + * Set how many samples to generate each second running. Default is 4000. + */ + RecordOptions& SetSampleFrequency(size_t freq); + + /** + * Set record duration. The record stops after `durationInSecond` seconds. By default, + * record stops only when stopRecording() is called. + */ + RecordOptions& SetDuration(double duration_in_second); + + /** + * Record some threads in the app process. By default, record all threads in the process. + */ + RecordOptions& SetSampleThreads(const std::vector<pid_t>& threads); + + /** + * Record dwarf based call graph. It is needed to get Java callstacks. + */ + RecordOptions& RecordDwarfCallGraph(); + + /** + * Record frame pointer based call graph. It is suitable to get C++ callstacks on 64bit devices. + */ + RecordOptions& RecordFramePointerCallGraph(); + + /** + * Trace context switch info to show where threads spend time off cpu. + */ + RecordOptions& TraceOffCpu(); + + /** + * Translate record options into arguments for `simpleperf record` cmd. + */ + std::vector<std::string> ToRecordArgs() const; + + private: + RecordOptionsImpl* impl_; +}; + +/** + * ProfileSession 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. + * + * Example: + * RecordOptions options; + * options.setDwarfCallGraph(); + * ProfileSession session; + * session.StartRecording(options); + * sleep(1); + * session.PauseRecording(); + * sleep(1); + * session.ResumeRecording(); + * sleep(1); + * session.StopRecording(); + * + * It aborts when error happens. To read error messages of simpleperf record + * process, filter logcat with `simpleperf`. + */ +class ProfileSessionImpl; +class ProfileSession { + public: + /** + * @param appDataDir the same as android.content.Context.getDataDir(). + * ProfileSession stores profiling data in appDataDir/simpleperf_data/. + */ + ProfileSession(const std::string& app_data_dir); + + /** + * ProfileSession assumes appDataDir as /data/data/app_package_name. + */ + ProfileSession(); + ~ProfileSession(); + + /** + * Start recording. + * @param options RecordOptions + */ + void StartRecording(const RecordOptions& options); + + /** + * Start recording. + * @param args arguments for `simpleperf record` cmd. + */ + void StartRecording(const std::vector<std::string>& record_args); + + /** + * Pause recording. No samples are generated in paused state. + */ + void PauseRecording(); + + /** + * Resume a paused session. + */ + void ResumeRecording(); + + /** + * Stop recording and generate a recording file under appDataDir/simpleperf_data/. + */ + void StopRecording(); + private: + ProfileSessionImpl* impl_; +}; + +} // namespace simpleperf diff --git a/simpleperf/app_api/java/com/android/simpleperf/ProfileSession.java b/simpleperf/app_api/java/com/android/simpleperf/ProfileSession.java new file mode 100644 index 00000000..10cbb518 --- /dev/null +++ b/simpleperf/app_api/java/com/android/simpleperf/ProfileSession.java @@ -0,0 +1,279 @@ +/* + * 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 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 { + + enum State { + NOT_YET_STARTED, + STARTED, + PAUSED, + STOPPED, + } + + private State state = State.NOT_YET_STARTED; + private String appDataDir; + private String simpleperfDataDir; + private Process simpleperfProcess; + + /** + * @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); + } + String 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); + } + 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); + } + 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 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() { + String[] candidates = new String[]{ + // For debuggable apps, simpleperf is put to the appDir by api_app_profiler.py. + appDataDir + "/simpleperf", + // For profileable apps on Android >= Q, use simpleperf in system image. + "/system/bin/simpleperf" + }; + for (String path : candidates) { + File file = new File(path); + if (file.isFile()) { + return path; + } + } + throw new Error("can't find simpleperf on device. Please run api_app_profiler.py."); + } + + 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_app_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; + } +} diff --git a/simpleperf/app_api/java/com/android/simpleperf/RecordOptions.java b/simpleperf/app_api/java/com/android/simpleperf/RecordOptions.java new file mode 100644 index 00000000..de5a715c --- /dev/null +++ b/simpleperf/app_api/java/com/android/simpleperf/RecordOptions.java @@ -0,0 +1,157 @@ +/* + * 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.system.Os; +import java.util.ArrayList; +import java.util.List; + +/** + * <p> + * This class sets record options used by ProfileSession. The options are + * converted to a string list in toRecordArgs(), which is then passed to + * `simpleperf record` cmd. Run `simpleperf record -h` or + * `run_simpleperf_on_device.py record -h` for help messages. + * </p> + * + * <p> + * Example: + * RecordOptions options = new RecordOptions(); + * options.setDuration(3).recordDwarfCallGraph().setOutputFilename("perf.data"); + * ProfileSession session = new ProfileSession(); + * session.startRecording(options); + * </p> + */ +public class RecordOptions { + + /** + * Set output filename. Default is perf.data. The file will be generated under simpleperf_data/. + */ + public RecordOptions setOutputFilename(String filename) { + outputFilename = filename; + return this; + } + + /** + * Set event to record. Default is cpu-cycles. See `simpleperf list` for all available events. + */ + public RecordOptions setEvent(String event) { + this.event = event; + return this; + } + + /** + * Set how many samples to generate each second running. Default is 4000. + */ + public RecordOptions setSampleFrequency(int freq) { + this.freq = freq; + return this; + } + + /** + * Set record duration. The record stops after `durationInSecond` seconds. By default, + * record stops only when stopRecording() is called. + */ + public RecordOptions setDuration(double durationInSecond) { + this.durationInSecond = durationInSecond; + return this; + } + + /** + * Record some threads in the app process. By default, record all threads in the process. + */ + public RecordOptions setSampleThreads(List<Integer> threads) { + this.threads.addAll(threads); + return this; + } + + /** + * Record dwarf based call graph. It is needed to get Java callstacks. + */ + public RecordOptions recordDwarfCallGraph() { + this.dwarfCallGraph = true; + this.fpCallGraph = false; + return this; + } + + /** + * Record frame pointer based call graph. It is suitable to get C++ callstacks on 64bit devices. + */ + public RecordOptions recordFramePointerCallGraph() { + this.fpCallGraph = true; + this.dwarfCallGraph = false; + return this; + } + + /** + * Trace context switch info to show where threads spend time off cpu. + */ + public RecordOptions traceOffCpu() { + this.traceOffCpu = true; + return this; + } + + /** + * Translate record options into arguments for `simpleperf record` cmd. + */ + public List<String> toRecordArgs() { + ArrayList<String> args = new ArrayList<>(); + args.add("-o"); + args.add(outputFilename); + args.add("-e"); + args.add(event); + args.add("-f"); + args.add(String.valueOf(freq)); + if (durationInSecond != 0.0) { + args.add("--duration"); + args.add(String.valueOf(durationInSecond)); + } + if (threads.isEmpty()) { + args.add("-p"); + args.add(String.valueOf(Os.getpid())); + } else { + String s = ""; + for (int i = 0; i < threads.size(); i++) { + if (i > 0) { + s += ","; + } + s += threads.get(i).toString(); + } + args.add("-t"); + args.add(s); + } + if (dwarfCallGraph) { + args.add("-g"); + } else if (fpCallGraph) { + args.add("--call-graph"); + args.add("fp"); + } + if (traceOffCpu) { + args.add("--trace-offcpu"); + } + return args; + } + + private String outputFilename = "perf.data"; + private String event = "cpu-cycles"; + private int freq = 4000; + private double durationInSecond = 0.0; + private ArrayList<Integer> threads = new ArrayList<>(); + private boolean dwarfCallGraph = false; + private boolean fpCallGraph = false; + private boolean traceOffCpu = false; +} |