blob: f4fed3191f8bcee9a2bd5bc9369484dc87cfe2ae [file] [log] [blame]
// Copyright 2016 The Fuchsia 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 "src/performance/trace/commands/record.h"
#include <lib/async/cpp/task.h>
#include <lib/async/default.h>
#include <lib/fdio/spawn.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/time.h>
#include <string.h>
#include <zircon/status.h>
#include <memory>
#include <string>
#include <unordered_set>
#include "src/lib/fxl/strings/split_string.h"
#include "src/lib/fxl/strings/string_number_conversions.h"
#include "src/performance/trace/utils.h"
namespace tracing {
namespace {
// Command line options.
const char kCategories[] = "categories";
const char kAppendArgs[] = "append-args";
const char kOutputFile[] = "output-file";
const char kBinary[] = "binary";
const char kCompress[] = "compress";
const char kDuration[] = "duration";
const char kDetach[] = "detach";
const char kDecouple[] = "decouple";
const char kSpawn[] = "spawn";
const char kEnvironmentName[] = "environment-name";
const char kReturnChildResult[] = "return-child-result";
const char kBufferSize[] = "buffer-size";
const char kProviderBufferSize[] = "provider-buffer-size";
const char kBufferingMode[] = "buffering-mode";
const char kTrigger[] = "trigger";
zx_status_t Spawn(const std::vector<std::string>& args, zx::process* subprocess) {
FX_DCHECK(args.size() > 0);
std::vector<const char*> raw_args;
for (const auto& item : args) {
raw_args.push_back(item.c_str());
}
raw_args.push_back(nullptr);
return fdio_spawn(ZX_HANDLE_INVALID, FDIO_SPAWN_CLONE_ALL, raw_args[0], raw_args.data(),
subprocess->reset_and_get_address());
}
} // namespace
bool RecordCommand::Options::Setup(const fxl::CommandLine& command_line) {
const std::unordered_set<std::string> known_options = {
kCategories, kAppendArgs, kOutputFile, kBinary, kCompress,
kDuration, kDetach, kDecouple, kSpawn, kEnvironmentName,
kReturnChildResult, kBufferSize, kProviderBufferSize, kBufferingMode, kTrigger,
};
for (auto& option : command_line.options()) {
if (known_options.count(option.name) == 0) {
FX_LOGS(ERROR) << "Unknown option: " << option.name;
return false;
}
}
size_t index = 0;
// --categories=<cat1>,<cat2>,...
if (command_line.HasOption(kCategories, &index)) {
categories = fxl::SplitStringCopy(command_line.options()[index].value, ",",
fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
}
// --append-args=<arg1>,<arg2>,...
// This option may be repeated, all args are added in order.
// These arguments are added after the command line positional args.
std::vector<std::string> append_args;
if (command_line.HasOption(kAppendArgs, nullptr)) {
auto all_append_args = command_line.GetOptionValues(kAppendArgs);
for (const auto& arg : all_append_args) {
auto args = fxl::SplitStringCopy(arg, ",", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
std::move(std::begin(args), std::end(args), std::back_inserter(append_args));
}
}
// --binary
if (ParseBooleanOption(command_line, kBinary, &binary) == OptionStatus::ERROR) {
return false;
}
if (binary) {
output_file_name = kDefaultBinaryOutputFileName;
}
// --compress
if (ParseBooleanOption(command_line, kCompress, &compress) == OptionStatus::ERROR) {
return false;
}
if (compress) {
output_file_name += ".gz";
}
// --output-file=<file>
if (command_line.HasOption(kOutputFile, &index)) {
output_file_name = command_line.options()[index].value;
}
// --duration=<seconds>
if (command_line.HasOption(kDuration, &index)) {
int64_t seconds;
if (!fxl::StringToNumberWithError(command_line.options()[index].value, &seconds)) {
FX_LOGS(ERROR) << "Failed to parse command-line option " << kDuration << ": "
<< command_line.options()[index].value;
return false;
}
duration = zx::sec(seconds);
}
// --detach
if (ParseBooleanOption(command_line, kDetach, &detach) == OptionStatus::ERROR) {
return false;
}
// --decouple
if (ParseBooleanOption(command_line, kDecouple, &decouple) == OptionStatus::ERROR) {
return false;
}
// --spawn
{
bool spawn_value = false;
OptionStatus spawn_status = ParseBooleanOption(command_line, kSpawn, &spawn_value);
if (spawn_status == OptionStatus::ERROR) {
return false;
}
bool have_spawn = spawn_status == OptionStatus::PRESENT;
if (have_spawn) {
spawn = spawn_value;
}
}
// --environment-name
if (command_line.HasOption(kEnvironmentName, &index)) {
environment_name = command_line.options()[index].value;
}
// --return-child-result=<flag>
if (ParseBooleanOption(command_line, kReturnChildResult, &return_child_result) ==
OptionStatus::ERROR) {
return false;
}
// --buffer-size=<megabytes>
if (command_line.HasOption(kBufferSize, &index)) {
if (!ParseBufferSize(command_line.options()[index].value, &buffer_size_megabytes)) {
return false;
}
}
// --provider-buffer-size=<name:megabytes>
if (command_line.HasOption(kProviderBufferSize)) {
std::vector<std::string_view> args = command_line.GetOptionValues(kProviderBufferSize);
if (!ParseProviderBufferSize(args, &provider_specs)) {
return false;
}
}
// --buffering-mode=oneshot|circular|streaming
if (command_line.HasOption(kBufferingMode, &index)) {
BufferingMode mode;
if (!ParseBufferingMode(command_line.options()[index].value, &mode)) {
return false;
}
buffering_mode = TranslateBufferingMode(mode);
}
// --trigger=<alert>:<action>
if (command_line.HasOption(kTrigger)) {
std::vector<std::string_view> args = command_line.GetOptionValues(kTrigger);
if (!ParseTriggers(args, &trigger_specs)) {
return false;
}
}
// <command> <args...>
const auto& positional_args = command_line.positional_args();
if (!positional_args.empty()) {
app = positional_args[0];
args = std::vector<std::string>(positional_args.begin() + 1, positional_args.end());
}
// Now that we've processed positional args we can append --append-args args.
std::move(std::begin(append_args), std::end(append_args), std::back_inserter(args));
return true;
}
Command::Info RecordCommand::Describe() {
return Command::Info{
[]() { return std::make_unique<RecordCommand>(); },
"record",
"starts tracing and records data",
{{"output-file=[/tmp/trace.json]",
"Trace data is stored in this file. "
"If the output file is \"tcp:TCP-ADDRESS\" then the output is streamed "
"to that address."},
{"binary=[false]",
"Output the binary trace rather than converting to JSON. "
"If this is set, then the default output location will be "
"/tmp/trace.fxt"},
{"compress=[false]",
"Compress trace output. This option is ignored "
"when streaming over a TCP socket."},
{"duration=[10]",
"Trace will be active for this many seconds after the session has been "
"started. The provided value must be integral."},
{"categories=[\"\"]", "Categories that should be enabled for tracing"},
{"append-args=[\"\"]",
"Additional args for the app being traced. The value is a comma-separated list of "
"arguments to pass. This option may be repeated, arguments are added in order."},
{"detach=[false]", "Don't stop the traced program when tracing finished"},
{"decouple=[false]", "Don't stop tracing when the traced program exits"},
{"spawn=[false]", "Use fdio_spawn to run a legacy app."},
{"environment-name=[none]",
"Create a nested environment with the given name and run the app being traced under it."},
{"return-child-result=[true]",
"Return with the same return code as the child. "
"Only valid when a child program is passed."},
{"buffer-size=[4]", "Maximum size of trace buffer for each provider in megabytes"},
{"provider-buffer-size=[provider-name:buffer-size]",
"Specify the buffer size that \"provider-name\" will use. "
"May be specified multiple times, once per provider."},
{"buffering-mode=oneshot|circular|streaming", "The buffering mode to use"},
{"trigger=<alert>:<action>",
"Specifies an action to take when an alert with "
"the specified name is received. Multiple alert/action rules may be "
"specified using multiple --trigger arguments. The only action currently "
"supported is 'stop'. This action causes the session to be stopped and "
"results to be captured"},
{"[command args]",
"Run program after starting trace. The program is terminated when "
"tracing ends unless --detach is specified"}}};
}
RecordCommand::RecordCommand()
: dispatcher_(async_get_default_dispatcher()),
wait_spawned_app_(this),
weak_ptr_factory_(this) {
wait_spawned_app_.set_trigger(ZX_PROCESS_TERMINATED);
}
void RecordCommand::Start(const fxl::CommandLine& command_line) {
if (!options_.Setup(command_line)) {
FX_LOGS(ERROR) << "Error parsing options from command line - aborting";
Done(EXIT_FAILURE);
return;
}
std::unique_ptr<std::ostream> out_stream =
OpenOutputStream(options_.output_file_name, options_.compress);
if (!out_stream) {
FX_LOGS(ERROR) << "Failed to open " << options_.output_file_name << " for writing";
Done(EXIT_FAILURE);
return;
}
Tracer::BytesConsumer bytes_consumer;
Tracer::RecordConsumer record_consumer;
Tracer::ErrorHandler error_handler;
if (options_.binary) {
binary_out_ = std::move(out_stream);
bytes_consumer = [this](const unsigned char* buffer, size_t n_bytes) {
binary_out_->write(reinterpret_cast<const char*>(buffer), n_bytes);
};
record_consumer = [](trace::Record record) {};
error_handler = [](fbl::String error) {};
} else {
exporter_ = std::make_unique<ChromiumExporter>(std::move(out_stream));
bytes_consumer = [](const unsigned char* buffer, size_t n_bytes) {};
record_consumer = [this](trace::Record record) { exporter_->ExportRecord(record); };
error_handler = [](fbl::String error) { FX_LOGS(ERROR) << error.c_str(); };
}
tracer_ = std::make_unique<Tracer>(take_provisioner());
tracing_ = true;
controller::TraceConfig trace_config{
{.categories = options_.categories,
.buffer_size_megabytes_hint = options_.buffer_size_megabytes,
// TODO(dje): start_timeout_milliseconds
.buffering_mode = options_.buffering_mode,
.provider_specs = TranslateProviderSpecs(options_.provider_specs)}};
tracer_->Initialize(
std::move(trace_config), options_.binary, std::move(bytes_consumer),
std::move(record_consumer), std::move(error_handler),
[this] {
DoneTrace();
}, // TODO(https://fxbug.dev/42113074): For now preserve existing behaviour.
[this] { DoneTrace(); }, fit::bind_member(this, &RecordCommand::OnAlert));
tracer_->Start([this](fidl::Result<controller::Session::StartTracing> result) {
if (result.is_error()) {
FX_LOGS(ERROR) << "Unable to start trace: " << result.error_value();
tracing_ = false;
Done(EXIT_FAILURE);
return;
}
if (!options_.app.empty()) {
if (!options_.spawn) {
FX_LOGS(ERROR) << "`--spawn` must be set";
tracing_ = false;
Done(EXIT_FAILURE);
return;
}
LaunchSpawnedApp();
}
StartTimer();
});
}
void RecordCommand::TerminateTrace(int32_t return_code) {
if (tracing_) {
out() << "Terminating trace...\n";
tracing_ = false;
return_code_ = return_code;
tracer_->Terminate();
if (spawned_app_ && !options_.detach) {
KillSpawnedApp();
}
}
}
void RecordCommand::DoneTrace() {
FX_DCHECK(!tracing_);
tracer_.reset();
exporter_.reset();
out() << "Trace file written to " << options_.output_file_name << '\n';
Done(return_code_);
}
// Quote elements of |args| as necessary to ensure the result can be correctly
// parsed by readers. But also do so minimally to maintain the S/N ratio.
// This is just a log message so the result doesn't need to be executable,
// this fact to avoid handling various complicated cases like one arg
// containing a mix of spaces, single quotes, and double quotes.
namespace {
std::string JoinArgsForLogging(const std::vector<std::string>& args) {
std::string result;
for (const auto& arg : args) {
if (result.size() > 0) {
result += " ";
}
if (arg.size() == 0) {
result += "\"\"";
} else if (arg.find(' ') != std::string::npos) {
result += "{" + arg + "}";
} else {
result += arg;
}
}
return result;
}
} // namespace
void RecordCommand::LaunchSpawnedApp() {
std::vector<std::string> all_args = {options_.app};
all_args.insert(all_args.end(), options_.args.begin(), options_.args.end());
// Include the arguments here: It's useful to see how the passed command+args ended up after shell
// processing.
FX_LOGS(INFO) << "Spawning: " << JoinArgsForLogging(all_args);
zx::process subprocess;
zx_status_t status = Spawn(all_args, &subprocess);
if (status != ZX_OK) {
TerminateTrace(EXIT_FAILURE);
FX_LOGS(ERROR) << "Subprocess launch failed: \"" << status
<< "\" Did you provide the full path to the tool?";
return;
}
spawned_app_ = std::move(subprocess);
wait_spawned_app_.set_object(spawned_app_.get());
status = wait_spawned_app_.Begin(dispatcher_);
FX_CHECK(status == ZX_OK) << "Failed to add handler: status=" << status;
}
void RecordCommand::OnSpawnedAppExit(async_dispatcher_t* dispatcher, async::WaitBase* wait,
zx_status_t status, const zx_packet_signal_t* signal) {
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to wait for spawned app: status=" << status;
TerminateTrace(EXIT_FAILURE);
return;
}
if (signal->observed & ZX_PROCESS_TERMINATED) {
zx_info_process_t proc_info;
[[maybe_unused]] zx_status_t info_status =
spawned_app_.get_info(ZX_INFO_PROCESS, &proc_info, sizeof(proc_info), nullptr, nullptr);
FX_DCHECK(info_status == ZX_OK);
out() << "Application exited with return code " << proc_info.return_code << '\n';
if (!options_.decouple) {
if (options_.return_child_result) {
TerminateTrace(proc_info.return_code);
} else {
TerminateTrace(EXIT_SUCCESS);
}
}
} else {
FX_NOTREACHED();
}
}
void RecordCommand::KillSpawnedApp() {
FX_DCHECK(spawned_app_);
// If already dead this is a no-op.
[[maybe_unused]] zx_status_t status = spawned_app_.kill();
FX_DCHECK(status == ZX_OK);
wait_spawned_app_.Cancel();
wait_spawned_app_.set_object(ZX_HANDLE_INVALID);
}
void RecordCommand::OnAlert(std::string alert_name) {
auto iter = options_.trigger_specs.find(alert_name);
if (iter == options_.trigger_specs.end()) {
// No action specified for alert. This is expected.
return;
}
switch (iter->second) {
case Action::kStop:
TerminateTrace(EXIT_SUCCESS);
break;
}
}
void RecordCommand::StartTimer() {
async::PostDelayedTask(
dispatcher_,
[weak = weak_ptr_factory_.GetWeakPtr()] {
if (weak)
weak->TerminateTrace(EXIT_SUCCESS);
},
options_.duration);
out() << "Starting trace; will stop in " << options_.duration.to_nsecs() / 1000000000.0
<< " seconds...\n";
}
} // namespace tracing