| // 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 <fstream> |
| #include <iostream> |
| #include <unordered_set> |
| |
| #include "lib/network/fidl/network_service.fidl.h" |
| #include "garnet/bin/trace/commands/record.h" |
| #include "garnet/bin/trace/results_output.h" |
| #include "lib/fxl/files/file.h" |
| #include "lib/fxl/files/path.h" |
| #include "lib/fxl/logging.h" |
| #include "lib/fxl/strings/split_string.h" |
| #include "lib/fxl/strings/string_number_conversions.h" |
| #include "lib/fsl/tasks/message_loop.h" |
| |
| namespace tracing { |
| |
| namespace { |
| |
| // Command line options. |
| const char kSpecFile[] = "spec-file"; |
| const char kCategories[] = "categories"; |
| const char kAppendArgs[] = "append-args"; |
| const char kOutputFile[] = "output-file"; |
| const char kDuration[] = "duration"; |
| const char kDetach[] = "detach"; |
| const char kDecouple[] = "decouple"; |
| const char kBufferSize[] = "buffer-size"; |
| const char kUploadServerUrl[] = "upload-server-url"; |
| const char kUploadMaster[] = "upload-master"; |
| const char kUploadBot[] = "upload-bot"; |
| const char kUploadPointId[] = "upload-point-id"; |
| |
| bool EnsureNonEmpty(std::ostream& err, |
| const fxl::CommandLine& command_line, |
| size_t index) { |
| if (command_line.options()[index].value.empty()) { |
| err << "--" << command_line.options()[index].name << " can't be empty"; |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| bool Record::Options::Setup(const fxl::CommandLine& command_line) { |
| const std::unordered_set<std::string> known_options = { |
| kSpecFile, kCategories, kAppendArgs, kOutputFile, |
| kDuration, kDetach, kDecouple, kBufferSize, |
| kUploadServerUrl, kUploadMaster, kUploadBot, kUploadPointId}; |
| |
| for (auto& option : command_line.options()) { |
| if (known_options.count(option.name) == 0) { |
| err() << "Unknown option: " << option.name << std::endl; |
| return false; |
| } |
| } |
| |
| size_t index = 0; |
| // Read the spec file first. Arguments passed on the command line override the |
| // spec. |
| // --spec-file=<file> |
| if (command_line.HasOption(kSpecFile, &index)) { |
| std::string spec_file_path = command_line.options()[index].value; |
| if (!files::IsFile(spec_file_path)) { |
| err() << spec_file_path << " is not a file" << std::endl; |
| return false; |
| } |
| |
| std::string content; |
| if (!files::ReadFileToString(spec_file_path, &content)) { |
| err() << "Can't read " << spec_file_path << std::endl; |
| return false; |
| } |
| |
| Spec spec; |
| if (!DecodeSpec(content, &spec)) { |
| err() << "Can't decode " << spec_file_path << std::endl; |
| return false; |
| } |
| app = std::move(spec.app); |
| args = std::move(spec.args); |
| categories = std::move(spec.categories); |
| duration = std::move(spec.duration); |
| measurements = std::move(spec.measurements); |
| upload_metadata.test_suite_name = std::move(spec.test_suite_name); |
| } |
| |
| // --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>,... |
| if (command_line.HasOption(kAppendArgs, &index)) { |
| auto append_args = |
| fxl::SplitStringCopy(command_line.options()[index].value, ",", |
| fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty); |
| std::move(std::begin(append_args), std::end(append_args), |
| std::back_inserter(args)); |
| } |
| |
| // --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)) { |
| uint64_t seconds; |
| if (!fxl::StringToNumberWithError(command_line.options()[index].value, |
| &seconds)) { |
| err() << "Failed to parse command-line option duration: " |
| << command_line.options()[index].value; |
| return false; |
| } |
| duration = fxl::TimeDelta::FromSeconds(seconds); |
| } |
| |
| // --detach |
| detach = command_line.HasOption(kDetach); |
| |
| // --decouple |
| decouple = command_line.HasOption(kDecouple); |
| |
| // --buffer-size=<megabytes> |
| if (command_line.HasOption(kBufferSize, &index)) { |
| uint32_t megabytes; |
| if (!fxl::StringToNumberWithError(command_line.options()[index].value, |
| &megabytes)) { |
| err() << "Failed to parse command-line option buffer-size: " |
| << command_line.options()[index].value; |
| return false; |
| } |
| buffer_size_megabytes_hint = megabytes; |
| } |
| |
| int upload_param_count = 0; |
| upload_param_count += command_line.HasOption(kUploadServerUrl); |
| upload_param_count += command_line.HasOption(kUploadMaster); |
| upload_param_count += command_line.HasOption(kUploadBot); |
| upload_param_count += command_line.HasOption(kUploadPointId); |
| if (upload_param_count != 0 && upload_param_count != 4) { |
| err() << "All of " << kUploadServerUrl << ", " << kUploadMaster << ", " |
| << kUploadBot << ", " << kUploadPointId |
| << " are required for results upload" << std::endl; |
| return false; |
| } |
| if (upload_param_count > 0) { |
| upload_results = true; |
| if (upload_metadata.test_suite_name.empty()) { |
| err() << "To upload results, the spec file must specify its " |
| << "`test_suite_name`" << std::endl; |
| return false; |
| } |
| } |
| |
| // --upload-server-url |
| if (command_line.HasOption(kUploadServerUrl, &index)) { |
| if (!EnsureNonEmpty(err(), command_line, index)) { |
| return false; |
| } |
| upload_metadata.server_url = command_line.options()[index].value; |
| } |
| |
| // --upload-master |
| if (command_line.HasOption(kUploadMaster, &index)) { |
| if (!EnsureNonEmpty(err(), command_line, index)) { |
| return false; |
| } |
| upload_metadata.master = command_line.options()[index].value; |
| } |
| |
| // --upload-bot |
| if (command_line.HasOption(kUploadBot, &index)) { |
| if (!EnsureNonEmpty(err(), command_line, index)) { |
| return false; |
| } |
| upload_metadata.bot = command_line.options()[index].value; |
| } |
| |
| // --upload-point-id=<integer> |
| if (command_line.HasOption(kUploadPointId, &index)) { |
| if (!EnsureNonEmpty(err(), command_line, index)) { |
| return false; |
| } |
| uint64_t point_id; |
| if (!fxl::StringToNumberWithError(command_line.options()[index].value, |
| &point_id)) { |
| err() << "Failed to parse command-line option upload-point-id: " |
| << command_line.options()[index].value; |
| return false; |
| } |
| upload_results = true; |
| upload_metadata.point_id = point_id; |
| } |
| |
| // <command> <args...> |
| const auto& positional_args = command_line.positional_args(); |
| if (!positional_args.empty()) { |
| if (!app.empty() || !args.empty()) { |
| FXL_LOG(WARNING) << "The app and args passed on the command line" |
| << "override those from the tspec file."; |
| } |
| app = positional_args[0]; |
| args = std::vector<std::string>(positional_args.begin() + 1, |
| positional_args.end()); |
| } |
| |
| return true; |
| } |
| |
| Command::Info Record::Describe() { |
| return Command::Info{ |
| [](app::ApplicationContext* context) { |
| return std::make_unique<Record>(context); |
| }, |
| "record", |
| "starts tracing and records data", |
| {{"spec-file=[none]", "Tracing specification file"}, |
| {"output-file=[/data/trace.json]", "Trace data is stored in this file"}, |
| {"duration=[10s]", |
| "Trace will be active for this long after the session has been " |
| "started"}, |
| {"categories=[\"\"]", "Categories that should be enabled for tracing"}, |
| {"append-args=[\"\"]", |
| "Additional args for the app being traced, appended to those from the " |
| "spec file, if any"}, |
| {"detach=[false]", |
| "Don't stop the traced program when tracing finished"}, |
| {"decouple=[false]", "Don't stop tracing when the traced program exits"}, |
| {"buffer-size=[4]", |
| "Maximum size of trace buffer for each provider in megabytes"}, |
| {"upload-server-url=[none]", "Url of the Catapult dashboard server"}, |
| {"upload-master=[none]", "Name of the buildbot master"}, |
| {"upload-bot=[none]", "Buildbot builder name"}, |
| {"upload-point-id=[none]", "Integer identifier of the sample"}, |
| {"[command args]", |
| "Run program before starting trace. The program is terminated when " |
| "tracing ends unless --detach is specified"}}}; |
| } |
| |
| Record::Record(app::ApplicationContext* context) |
| : CommandWithTraceController(context), weak_ptr_factory_(this) {} |
| |
| void Record::Run(const fxl::CommandLine& command_line) { |
| if (!options_.Setup(command_line)) { |
| err() << "Error parsing options from command line - aborting" << std::endl; |
| exit(1); |
| } |
| |
| std::ofstream out_file(options_.output_file_name, |
| std::ios_base::out | std::ios_base::trunc); |
| if (!out_file.is_open()) { |
| err() << "Failed to open " << options_.output_file_name << " for writing" |
| << std::endl; |
| exit(1); |
| } |
| |
| exporter_.reset(new ChromiumExporter(std::move(out_file))); |
| tracer_.reset(new Tracer(trace_controller().get())); |
| if (!options_.measurements.duration.empty()) { |
| aggregate_events_ = true; |
| measure_duration_.reset( |
| new measure::MeasureDuration(options_.measurements.duration)); |
| } |
| if (!options_.measurements.time_between.empty()) { |
| aggregate_events_ = true; |
| measure_time_between_.reset( |
| new measure::MeasureTimeBetween(options_.measurements.time_between)); |
| } |
| |
| tracing_ = true; |
| |
| auto trace_options = TraceOptions::New(); |
| trace_options->categories = |
| fidl::Array<fidl::String>::From(options_.categories); |
| trace_options->buffer_size_megabytes_hint = |
| options_.buffer_size_megabytes_hint; |
| |
| tracer_->Start( |
| std::move(trace_options), |
| [this](trace::Record record) { |
| exporter_->ExportRecord(record); |
| |
| if (aggregate_events_ && record.type() == trace::RecordType::kEvent) { |
| events_.push_back(fbl::move(record)); |
| } |
| }, |
| [](fbl::String error) { err() << error.c_str() << std::endl; }, |
| [this] { |
| if (!options_.app.empty()) |
| LaunchApp(); |
| StartTimer(); |
| }, |
| [this] { DoneTrace(); }); |
| } |
| |
| void Record::StopTrace() { |
| if (tracing_) { |
| out() << "Stopping trace..." << std::endl; |
| tracing_ = false; |
| tracer_->Stop(); |
| } |
| } |
| |
| void Record::ProcessMeasurements(fxl::Closure on_done) { |
| if (!events_.empty()) { |
| std::sort( |
| std::begin(events_), std::end(events_), |
| [](const trace::Record& e1, const trace::Record& e2) { |
| return e1.GetEvent().timestamp < e2.GetEvent().timestamp; |
| }); |
| } |
| |
| for (const auto& event : events_) { |
| if (measure_duration_) { |
| measure_duration_->Process(event.GetEvent()); |
| } |
| if (measure_time_between_) { |
| measure_time_between_->Process(event.GetEvent()); |
| } |
| } |
| |
| std::unordered_map<uint64_t, std::vector<trace_ticks_t>> ticks; |
| if (measure_duration_) { |
| ticks.insert(measure_duration_->results().begin(), |
| measure_duration_->results().end()); |
| } |
| if (measure_time_between_) { |
| ticks.insert(measure_time_between_->results().begin(), |
| measure_time_between_->results().end()); |
| } |
| |
| uint64_t ticks_per_second = zx_ticks_per_second(); |
| FXL_DCHECK(ticks_per_second); |
| std::vector<measure::Result> results = |
| measure::ComputeResults(options_.measurements, ticks, ticks_per_second); |
| |
| // Fail and quit if any of the measurements has empty results. This is so that |
| // we can notice when benchmarks break (e.g. in CQ or on perfbots). |
| bool errored = false; |
| for (auto& result : results) { |
| if (result.samples.empty()) { |
| err() << "No results for measurement \"" << result.label << "\"." |
| << std::endl; |
| errored = true; |
| } |
| } |
| OutputResults(out(), results); |
| if (errored) { |
| err() << "One or more measurements had empty results. Quitting." |
| << std::endl; |
| exit(1); |
| } |
| |
| if (options_.upload_results) { |
| network::NetworkServicePtr network_service = |
| context()->ConnectToEnvironmentService<network::NetworkService>(); |
| UploadResults(out(), err(), std::move(network_service), |
| options_.upload_metadata, |
| results, [on_done = std::move(on_done)](bool succeeded) { |
| if (!succeeded) { |
| err() << "dashboard upload failed" << std::endl; |
| exit(1); |
| } |
| on_done(); |
| }); |
| } else { |
| on_done(); |
| } |
| } |
| |
| void Record::DoneTrace() { |
| tracer_.reset(); |
| exporter_.reset(); |
| |
| out() << "Trace file written to " << options_.output_file_name << std::endl; |
| |
| if (measure_duration_ || measure_time_between_) { |
| ProcessMeasurements([] { fsl::MessageLoop::GetCurrent()->QuitNow(); }); |
| } else { |
| fsl::MessageLoop::GetCurrent()->QuitNow(); |
| } |
| } |
| |
| void Record::LaunchApp() { |
| auto launch_info = app::ApplicationLaunchInfo::New(); |
| launch_info->url = fidl::String::From(options_.app); |
| launch_info->arguments = fidl::Array<fidl::String>::From(options_.args); |
| |
| out() << "Launching " << launch_info->url << std::endl; |
| context()->launcher()->CreateApplication(std::move(launch_info), |
| GetProxy(&application_controller_)); |
| application_controller_.set_connection_error_handler([this] { |
| out() << "Application terminated" << std::endl; |
| if (!options_.decouple) |
| StopTrace(); |
| }); |
| if (options_.detach) |
| application_controller_->Detach(); |
| } |
| |
| void Record::StartTimer() { |
| fsl::MessageLoop::GetCurrent()->task_runner()->PostDelayedTask( |
| [weak = weak_ptr_factory_.GetWeakPtr()] { |
| if (weak) |
| weak->StopTrace(); |
| }, |
| options_.duration); |
| out() << "Starting trace; will stop in " << options_.duration.ToSecondsF() |
| << " seconds..." << std::endl; |
| } |
| |
| } // namespace tracing |