| /* | |
| * 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 <time.h> | |
| #include <unistd.h> | |
| #include <mutex> | |
| #include <sstream> | |
| #include <android/log.h> | |
| namespace simpleperf { | |
| constexpr int AID_USER_OFFSET = 100000; | |
| enum RecordCmd { | |
| CMD_PAUSE_RECORDING = 1, | |
| CMD_RESUME_RECORDING, | |
| }; | |
| class RecordOptionsImpl { | |
| public: | |
| std::string output_filename; | |
| 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; | |
| } | |
| static std::string GetDefaultOutputFilename() { | |
| time_t t = time(nullptr); | |
| struct tm tm; | |
| if (localtime_r(&t, &tm) != &tm) { | |
| return "perf.data"; | |
| } | |
| char* buf = nullptr; | |
| asprintf(&buf, "perf-%02d-%02d-%02d-%02d-%02d.data", tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, | |
| tm.tm_min, tm.tm_sec); | |
| std::string result = buf; | |
| free(buf); | |
| return result; | |
| } | |
| std::vector<std::string> RecordOptions::ToRecordArgs() const { | |
| std::vector<std::string> args; | |
| std::string output_filename = impl_->output_filename; | |
| if (output_filename.empty()) { | |
| output_filename = GetDefaultOutputFilename(); | |
| } | |
| args.insert(args.end(), {"-o", 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(); | |
| std::string FindSimpleperfInTempDir(); | |
| 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; | |
| bool trace_offcpu_ = false; | |
| }; | |
| 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_); | |
| } | |
| for (const auto& arg : args) { | |
| if (arg == "--trace-offcpu") { | |
| trace_offcpu_ = true; | |
| } | |
| } | |
| 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_); | |
| } | |
| if (trace_offcpu_) { | |
| Abort("--trace-offcpu doesn't work well with pause/resume recording"); | |
| } | |
| 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; | |
| } | |
| static std::string ReadFile(FILE* fp) { | |
| std::string s; | |
| if (fp == nullptr) { | |
| return 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); | |
| } | |
| fclose(fp); | |
| return s; | |
| } | |
| static bool RunCmd(std::vector<const char*> args, std::string* stdout) { | |
| int stdout_fd[2]; | |
| if (pipe(stdout_fd) != 0) { | |
| return false; | |
| } | |
| args.push_back(nullptr); | |
| // Fork handlers (like gsl_library_close) may hang in a multi-thread environment. | |
| // So we use vfork instead of fork to avoid calling them. | |
| int pid = vfork(); | |
| if (pid == -1) { | |
| return false; | |
| } | |
| if (pid == 0) { | |
| // child process | |
| close(stdout_fd[0]); | |
| dup2(stdout_fd[1], 1); | |
| close(stdout_fd[1]); | |
| execvp(const_cast<char*>(args[0]), const_cast<char**>(args.data())); | |
| _exit(1); | |
| } | |
| // parent process | |
| close(stdout_fd[1]); | |
| int status; | |
| pid_t result = TEMP_FAILURE_RETRY(waitpid(pid, &status, 0)); | |
| if (result == -1) { | |
| Abort("failed to call waitpid: %s", strerror(errno)); | |
| } | |
| if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { | |
| return false; | |
| } | |
| if (stdout == nullptr) { | |
| close(stdout_fd[0]); | |
| } else { | |
| *stdout = ReadFile(fdopen(stdout_fd[0], "r")); | |
| } | |
| return true; | |
| } | |
| std::string ProfileSessionImpl::FindSimpleperf() { | |
| // 1. Try /data/local/tmp/simpleperf first. Probably it's newer than /system/bin/simpleperf. | |
| std::string simpleperf_path = FindSimpleperfInTempDir(); | |
| if (!simpleperf_path.empty()) { | |
| return simpleperf_path; | |
| } | |
| // 2. Try /system/bin/simpleperf, which is available on Android >= Q. | |
| simpleperf_path = "/system/bin/simpleperf"; | |
| if (IsExecutableFile(simpleperf_path)) { | |
| return simpleperf_path; | |
| } | |
| Abort("can't find simpleperf on device. Please run api_profiler.py."); | |
| return ""; | |
| } | |
| std::string ProfileSessionImpl::FindSimpleperfInTempDir() { | |
| const std::string path = "/data/local/tmp/simpleperf"; | |
| if (!IsExecutableFile(path)) { | |
| return ""; | |
| } | |
| // Copy it to app_dir to execute it. | |
| const std::string to_path = app_data_dir_ + "/simpleperf"; | |
| if (!RunCmd({"/system/bin/cp", path.c_str(), to_path.c_str()}, nullptr)) { | |
| return ""; | |
| } | |
| // For apps with target sdk >= 29, executing app data file isn't allowed. | |
| // For android R, app context isn't allowed to use perf_event_open. | |
| // So test executing downloaded simpleperf. | |
| std::string s; | |
| if (!RunCmd({to_path.c_str(), "list", "sw"}, &s)) { | |
| return ""; | |
| } | |
| if (s.find("cpu-clock") == std::string::npos) { | |
| return ""; | |
| } | |
| return to_path; | |
| } | |
| void ProfileSessionImpl::CheckIfPerfEnabled() { | |
| std::string s; | |
| if (!RunCmd({"/system/bin/getprop", "security.perf_harden"}, &s)) { | |
| return; // Omit check if getprop doesn't exist. | |
| } | |
| if (!s.empty() && s[0] == '1') { | |
| Abort("linux perf events aren't enabled on the device. Please run api_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. | |
| // Fork handlers (like gsl_library_close) may hang in a multi-thread environment. | |
| // So we use vfork instead of fork to avoid calling them. | |
| int pid = vfork(); | |
| 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); | |
| 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; | |
| int uid = getuid(); | |
| if (uid >= AID_USER_OFFSET) { | |
| int user_id = uid / AID_USER_OFFSET; | |
| app_data_dir = "/data/user/" + std::to_string(user_id) + "/" + 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 |