blob: 43dd87606f7f3ab6973ced4581a95c6205cf3c8c [file] [log] [blame]
// Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "debugd/src/perf_tool.h"
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <map>
#include <utility>
#include <base/bind.h>
#include <base/check.h>
#include <base/check_op.h>
#include <base/logging.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include "debugd/src/error_utils.h"
#include "debugd/src/helpers/scheduler_configuration_utils.h"
#include "debugd/src/process_with_output.h"
namespace debugd {
namespace {
const char kUnsupportedPerfToolErrorName[] =
"org.chromium.debugd.error.UnsupportedPerfTool";
const char kProcessErrorName[] = "org.chromium.debugd.error.RunProcess";
const char kStopProcessErrorName[] = "org.chromium.debugd.error.StopProcess";
const char kInvalidPerfArgumentErrorName[] =
"org.chromium.debugd.error.InvalidPerfArgument";
const char kArgsError[] =
"perf_args must begin with {\"perf\", \"record\"}, "
" {\"perf\", \"stat\"}, or {\"perf\", \"mem\"}";
// Location of quipper on ChromeOS.
const char kQuipperLocation[] = "/usr/bin/quipper";
// Location of the file which contains the range of online CPU numbers.
const char kCpuTopologyLocation[] = "/sys/devices/system/cpu/online";
// Pattern of the directories that contain information about the idle state of
// a CPU on the system.
const char kCpuIdleStatePathPattern[] =
"/sys/devices/system/cpu/cpu%s/cpuidle/state%d/disable";
// Location of the default ETM strobbing settings in configfs.
const char kStrobbingSettingPathPattern[] =
"/sys/kernel/config/cs-syscfg/features/strobing/params/%s/value";
const int kStrobbingWindow = 512;
const int kStrobbingPeriod = 10000;
enum class OptionType {
Boolean, // Has no value.
Value, // Uses another argument.
};
// All quipper options and whether they are blocked in the debugd perf_tool.
const std::map<std::string, OptionType> kQuipperOptions = {
{"--duration", OptionType::Value},
// Blocked, quipper figures out the full path of perf on its own.
// {"--perf_path", OptionType::Value},
// Blocked, perf_tool always return via stdout.
// {"--output_file", OptionType::Value},
{"--run_inject", OptionType::Boolean},
{"--inject_args", OptionType::Value},
};
// Returns one of the above enums given an vector of perf arguments, starting
// with "perf" itself in |args[0]|.
PerfSubcommand GetPerfSubcommandType(std::string command) {
if (command == "record")
return PERF_COMMAND_RECORD;
if (command == "stat")
return PERF_COMMAND_STAT;
if (command == "mem")
return PERF_COMMAND_MEM;
return PERF_COMMAND_UNSUPPORTED;
}
void AddQuipperArguments(brillo::Process* process,
const uint32_t duration_secs,
const std::vector<std::string>& perf_args) {
process->AddArg(kQuipperLocation);
if (duration_secs > 0) {
process->AddArg(base::StringPrintf("%u", duration_secs));
}
for (const auto& arg : perf_args) {
process->AddArg(arg);
}
}
} // namespace
bool ValidateQuipperArguments(const std::vector<std::string>& qp_args,
PerfSubcommand& subcommand,
brillo::ErrorPtr* error) {
for (auto args_iter = qp_args.begin(); args_iter != qp_args.end();
++args_iter) {
if (*args_iter == "--") {
++args_iter;
if (args_iter == qp_args.end()) {
DEBUGD_ADD_ERROR(error, kUnsupportedPerfToolErrorName, kArgsError);
return false;
}
subcommand = GetPerfSubcommandType(*args_iter);
if (subcommand == PERF_COMMAND_UNSUPPORTED) {
DEBUGD_ADD_ERROR(error, kUnsupportedPerfToolErrorName, kArgsError);
return false;
}
return true;
}
const auto& it = kQuipperOptions.find(*args_iter);
if (it == kQuipperOptions.end()) {
DEBUGD_ADD_ERROR_FMT(error, kInvalidPerfArgumentErrorName,
"option %s is not allowed", args_iter->c_str());
return false;
}
if (it->second == OptionType::Value) {
++args_iter;
if (args_iter == qp_args.end()) {
DEBUGD_ADD_ERROR_FMT(error, kInvalidPerfArgumentErrorName,
"option %s needs a following value",
args_iter->c_str());
return false;
}
}
}
DEBUGD_ADD_ERROR(error, kUnsupportedPerfToolErrorName, kArgsError);
return false;
}
PerfTool::PerfTool() {
signal_handler_.Init();
process_reaper_.Register(&signal_handler_);
EtmStrobbingSettings();
}
bool PerfTool::DisableCpuIdleStates() {
if (!cpuidle_states_.empty()) {
LOG(ERROR) << "The cpuidle states are disabled already.";
return false;
}
std::string cpu_range;
if (!base::ReadFileToString(base::FilePath(kCpuTopologyLocation),
&cpu_range)) {
PLOG(ERROR) << "File listing online CPU range missing.";
return false;
}
std::vector<std::string> cpu_nums;
if (!SchedulerConfigurationUtils::ParseCPUNumbers(cpu_range, &cpu_nums)) {
PLOG(ERROR) << "Failed to parse CPU range: " << cpu_range << ".";
return false;
}
for (const auto& cpu : cpu_nums) {
for (int state = 0;; ++state) {
const auto disable_file = base::FilePath(
base::StringPrintf(kCpuIdleStatePathPattern, cpu.c_str(), state));
if (!base::PathExists(disable_file))
break;
std::string disable_state;
base::ReadFileToString(disable_file, &disable_state);
cpuidle_states_.emplace(disable_file, disable_state);
base::WriteFile(disable_file, "1");
}
}
return true;
}
void PerfTool::RestoreCpuIdleStates() {
for (const auto& [path, disable_state] : cpuidle_states_) {
if (base::PathExists(path))
base::WriteFile(path, disable_state);
}
cpuidle_states_.clear();
}
bool PerfTool::GetPerfOutputV2(const std::vector<std::string>& quipper_args,
bool disable_cpu_idle,
const base::ScopedFD& stdout_fd,
uint64_t* session_id,
brillo::ErrorPtr* error) {
PerfSubcommand subcommand;
if (!ValidateQuipperArguments(quipper_args, subcommand, error))
return false;
if (perf_running()) {
// Do not run multiple sessions at the same time. Attempt to start another
// profiler session using this method yields a DBus error. Note that
// starting another session using GetPerfOutput() will still succeed.
DEBUGD_ADD_ERROR(error, kProcessErrorName, "Existing perf tool running.");
return false;
}
if (disable_cpu_idle) {
if (!DisableCpuIdleStates()) {
DEBUGD_ADD_ERROR(error, kProcessErrorName,
"Failed to disable CPU idle states");
return false;
}
}
DCHECK(!profiler_session_id_);
auto quipper_process = std::make_unique<SandboxedProcess>();
quipper_process->SandboxAs("root", "root");
if (!quipper_process->Init()) {
DEBUGD_ADD_ERROR(error, kProcessErrorName,
"Process initialization failure.");
return false;
}
AddQuipperArguments(quipper_process.get(), 0, quipper_args);
quipper_process->BindFd(stdout_fd.get(), 1);
if (!quipper_process->Start()) {
DEBUGD_ADD_ERROR(error, kProcessErrorName, "Process start failure.");
return false;
}
quipper_process_ = std::move(quipper_process);
DCHECK_GT(quipper_process_->pid(), 0);
process_reaper_.WatchForChild(
FROM_HERE, quipper_process_->pid(),
base::BindOnce(&PerfTool::OnQuipperProcessExited,
base::Unretained(this)));
// When GetPerfOutputV2() is used to run the perf tool, the user will read
// from the read end of |stdout_fd| until the write end is closed. At that
// point, it may make another call to GetPerfOutputFd() and expect that will
// start another perf run. |stdout_fd| will be closed when the last process
// holding it exits, which is minijail0 in this case. However, the kernel
// closes fds before signaling process exit. Therefore, it's possible for
// |stdout_fd| to be closed and the user tries to run another
// GetPerfOutputFd() before we're signaled of the process exit. To mitigate
// this, hold on to a dup() of |stdout_fd| until we're signaled that the
// process has exited. This guarantees that the caller can make a new
// GetPerfOutputFd() call when it finishes reading the output.
quipper_process_output_fd_.reset(dup(stdout_fd.get()));
DCHECK(quipper_process_output_fd_.is_valid());
// Generate an opaque, pseudo-unique, session ID using time and process ID.
profiler_session_id_ = *session_id =
static_cast<uint64_t>(base::Time::Now().ToTimeT()) << 32 |
(quipper_process_->pid() & 0xffffffff);
return true;
}
bool PerfTool::GetPerfOutput(uint32_t duration_secs,
const std::vector<std::string>& perf_args,
std::vector<uint8_t>* perf_data,
std::vector<uint8_t>* perf_stat,
int32_t* status,
brillo::ErrorPtr* error) {
PerfSubcommand subcommand;
if (duration_secs > 0) { // legacy option style
if (perf_args.size() < 2) {
DEBUGD_ADD_ERROR(error, kUnsupportedPerfToolErrorName, kArgsError);
return false;
}
subcommand = GetPerfSubcommandType(perf_args[1]);
if (perf_args[0] != "perf" || subcommand == PERF_COMMAND_UNSUPPORTED) {
DEBUGD_ADD_ERROR(error, kUnsupportedPerfToolErrorName, kArgsError);
return false;
}
} else if (!ValidateQuipperArguments(perf_args, subcommand, error)) {
return false;
}
// This whole method is synchronous, so we create a subprocess, let it run to
// completion, then gather up its output to return it.
ProcessWithOutput process;
process.SandboxAs("root", "root");
if (!process.Init()) {
DEBUGD_ADD_ERROR(error, kProcessErrorName,
"Process initialization failure.");
return false;
}
AddQuipperArguments(&process, duration_secs, perf_args);
std::string output_string;
*status = process.Run();
if (*status != 0) {
output_string =
base::StringPrintf("<process exited with status: %d>", *status);
} else {
process.GetOutput(&output_string);
}
switch (subcommand) {
case PERF_COMMAND_RECORD:
case PERF_COMMAND_MEM:
perf_data->assign(output_string.begin(), output_string.end());
break;
case PERF_COMMAND_STAT:
perf_stat->assign(output_string.begin(), output_string.end());
break;
default:
// Discard the output.
break;
}
return true;
}
void PerfTool::OnQuipperProcessExited(const siginfo_t& siginfo) {
// Called after SIGCHLD has been received from the signalfd file descriptor.
// Wait() for the child process wont' block. It'll just reap the zombie child
// process.
quipper_process_->Wait();
quipper_process_ = nullptr;
quipper_process_output_fd_.reset();
profiler_session_id_.reset();
if (!cpuidle_states_.empty())
RestoreCpuIdleStates();
}
bool PerfTool::GetPerfOutputFd(uint32_t duration_secs,
const std::vector<std::string>& perf_args,
const base::ScopedFD& stdout_fd,
uint64_t* session_id,
brillo::ErrorPtr* error) {
PerfSubcommand subcommand;
if (duration_secs > 0) { // legacy option style
if (perf_args.size() < 2) {
DEBUGD_ADD_ERROR(error, kUnsupportedPerfToolErrorName, kArgsError);
return false;
}
subcommand = GetPerfSubcommandType(perf_args[1]);
if (perf_args[0] != "perf" || subcommand == PERF_COMMAND_UNSUPPORTED) {
DEBUGD_ADD_ERROR(error, kUnsupportedPerfToolErrorName, kArgsError);
return false;
}
} else if (!ValidateQuipperArguments(perf_args, subcommand, error)) {
return false;
}
if (perf_running()) {
// Do not run multiple sessions at the same time. Attempt to start another
// profiler session using this method yields a DBus error. Note that
// starting another session using GetPerfOutput() will still succeed.
DEBUGD_ADD_ERROR(error, kProcessErrorName, "Existing perf tool running.");
return false;
}
DCHECK(!profiler_session_id_);
auto quipper_process = std::make_unique<SandboxedProcess>();
quipper_process->SandboxAs("root", "root");
if (!quipper_process->Init()) {
DEBUGD_ADD_ERROR(error, kProcessErrorName,
"Process initialization failure.");
return false;
}
AddQuipperArguments(quipper_process.get(), duration_secs, perf_args);
quipper_process->BindFd(stdout_fd.get(), 1);
if (!quipper_process->Start()) {
DEBUGD_ADD_ERROR(error, kProcessErrorName, "Process start failure.");
return false;
}
quipper_process_ = std::move(quipper_process);
DCHECK_GT(quipper_process_->pid(), 0);
process_reaper_.WatchForChild(
FROM_HERE, quipper_process_->pid(),
base::BindOnce(&PerfTool::OnQuipperProcessExited,
base::Unretained(this)));
// When GetPerfOutputFd() is used to run the perf tool, the user will read
// from the read end of |stdout_fd| until the write end is closed. At that
// point, it may make another call to GetPerfOutputFd() and expect that will
// start another perf run. |stdout_fd| will be closed when the last process
// holding it exits, which is minijail0 in this case. However, the kernel
// closes fds before signaling process exit. Therefore, it's possible for
// |stdout_fd| to be closed and the user tries to run another
// GetPerfOutputFd() before we're signaled of the process exit. To mitigate
// this, hold on to a dup() of |stdout_fd| until we're signaled that the
// process has exited. This guarantees that the caller can make a new
// GetPerfOutputFd() call when it finishes reading the output.
quipper_process_output_fd_.reset(dup(stdout_fd.get()));
DCHECK(quipper_process_output_fd_.is_valid());
// Generate an opaque, pseudo-unique, session ID using time and process ID.
profiler_session_id_ = *session_id =
static_cast<uint64_t>(base::Time::Now().ToTimeT()) << 32 |
(quipper_process_->pid() & 0xffffffff);
return true;
}
bool PerfTool::StopPerf(uint64_t session_id, brillo::ErrorPtr* error) {
if (!profiler_session_id_) {
DEBUGD_ADD_ERROR(error, kStopProcessErrorName, "Perf tool not started");
return false;
}
if (profiler_session_id_ != session_id) {
// Session ID mismatch: return a failure without affecting the existing
// profiler session.
DEBUGD_ADD_ERROR(error, kStopProcessErrorName,
"Invalid profile session id.");
return false;
}
// Stop by sending SIGINT to the profiler session. The sandboxed quipper
// process will be reaped in OnQuipperProcessExited().
if (quipper_process_) {
DCHECK_GT(quipper_process_->pid(), 0);
if (kill(quipper_process_->pid(), SIGINT) != 0) {
PLOG(WARNING) << "Failed to stop the profiler session.";
}
}
return true;
}
void PerfTool::EtmStrobbingSettings() {
const base::FilePath window_path = base::FilePath(
base::StringPrintf(kStrobbingSettingPathPattern, "window"));
const base::FilePath period_path = base::FilePath(
base::StringPrintf(kStrobbingSettingPathPattern, "period"));
if (!base::PathExists(window_path) || !base::PathExists(period_path))
return;
base::WriteFile(window_path, std::to_string(kStrobbingWindow));
base::WriteFile(period_path, std::to_string(kStrobbingPeriod));
etm_available = true;
}
} // namespace debugd