|  | // Copyright 2020 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "content/browser/tracing/startup_tracing_controller.h" | 
|  | #include "base/command_line.h" | 
|  | #include "base/files/file.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/logging.h" | 
|  | #include "base/no_destructor.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/synchronization/waitable_event.h" | 
|  | #include "base/task/bind_post_task.h" | 
|  | #include "base/task/sequenced_task_runner.h" | 
|  | #include "base/task/task_traits.h" | 
|  | #include "base/task/thread_pool.h" | 
|  | #include "base/thread_annotations.h" | 
|  | #include "base/threading/thread_restrictions.h" | 
|  | #include "base/threading/thread_task_runner_handle.h" | 
|  | #include "base/trace_event/typed_macros.h" | 
|  | #include "build/build_config.h" | 
|  | #include "components/tracing/common/trace_startup_config.h" | 
|  | #include "components/tracing/common/tracing_switches.h" | 
|  | #include "content/public/browser/browser_task_traits.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "services/tracing/public/cpp/perfetto/perfetto_config.h" | 
|  | #include "services/tracing/public/cpp/perfetto/trace_packet_tokenizer.h" | 
|  | #include "third_party/perfetto/include/perfetto/ext/tracing/core/trace_packet.h" | 
|  | #include "third_party/perfetto/include/perfetto/tracing/core/trace_config.h" | 
|  | #include "third_party/perfetto/include/perfetto/tracing/tracing.h" | 
|  |  | 
|  | #if BUILDFLAG(IS_ANDROID) | 
|  | #include "content/browser/android/tracing_controller_android.h" | 
|  | #endif  // BUILDFLAG(IS_ANDROID) | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | // A helper class responsible for coordinating emergency trace finalisation | 
|  | // (e.g. when the process is about to be killed), which can be initiated from | 
|  | // any thread. | 
|  | class EmergencyTraceFinalisationCoordinator { | 
|  | public: | 
|  | static EmergencyTraceFinalisationCoordinator& GetInstance() { | 
|  | static base::NoDestructor<EmergencyTraceFinalisationCoordinator> g_instance; | 
|  | return *g_instance; | 
|  | } | 
|  |  | 
|  | void OnTracingStarted(scoped_refptr<base::SequencedTaskRunner> task_runner, | 
|  | base::OnceClosure stop_tracing) { | 
|  | tracing_started_.Set(); | 
|  | base::AutoLock lock(lock_); | 
|  | startup_tracing_controller_task_runner_ = std::move(task_runner); | 
|  | stop_tracing_ = std::move(stop_tracing); | 
|  | } | 
|  |  | 
|  | void OnTracingStopped() { finalisation_.Signal(); } | 
|  |  | 
|  | // May be called multiple times per session, e.g. if a second thread | 
|  | // encounters a crash after the first. | 
|  | void StopAndBlockUntilStopped() { | 
|  | // If DCHECK fires before tracing has started, there isn't much for us to | 
|  | // do. | 
|  | if (!tracing_started_.IsSet()) | 
|  | return; | 
|  |  | 
|  | base::trace_event::TraceLog::GetInstance() | 
|  | ->SetCurrentThreadBlocksMessageLoop(); | 
|  |  | 
|  | base::OnceClosure stop_tracing; | 
|  | scoped_refptr<base::SequencedTaskRunner> task_runner; | 
|  | { | 
|  | base::AutoLock lock(lock_); | 
|  | task_runner = startup_tracing_controller_task_runner_; | 
|  | stop_tracing = std::move(stop_tracing_); | 
|  | } | 
|  |  | 
|  | if (task_runner->RunsTasksInCurrentSequence()) { | 
|  | VLOG(0) << "Ignored an emergency tracing stop request from the " | 
|  | "StartupTracingController sequence"; | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (stop_tracing) | 
|  | task_runner->PostTask(FROM_HERE, std::move(stop_tracing)); | 
|  |  | 
|  | base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_wait; | 
|  |  | 
|  | // Wait for the tracing to be finished before processing. | 
|  | // Note that we should wait even if |stop_tracing| is null — if a second | 
|  | // thread hits DCHECK while the first has posted a task and waits for the | 
|  | // trace to be written, the second one should wait as well to avoid crashing | 
|  | // the process. | 
|  | finalisation_.Wait(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | base::WaitableEvent finalisation_; | 
|  | base::AtomicFlag tracing_started_; | 
|  |  | 
|  | base::Lock lock_; | 
|  | scoped_refptr<base::SequencedTaskRunner> | 
|  | startup_tracing_controller_task_runner_ GUARDED_BY(lock_); | 
|  | base::OnceClosure stop_tracing_ GUARDED_BY(lock_); | 
|  | }; | 
|  |  | 
|  | class StartupTracingController::BackgroundTracer { | 
|  | public: | 
|  | enum class WriteMode { kAfterStopping, kStreaming }; | 
|  |  | 
|  | BackgroundTracer(WriteMode write_mode, | 
|  | TempFilePolicy temp_file_policy, | 
|  | base::FilePath output_file, | 
|  | tracing::TraceStartupConfig::OutputFormat output_format, | 
|  | perfetto::TraceConfig trace_config, | 
|  | base::OnceClosure on_tracing_finished) | 
|  | : state_(State::kTracing), | 
|  | write_mode_(write_mode), | 
|  | temp_file_policy_(temp_file_policy), | 
|  | task_runner_(base::SequencedTaskRunner::GetCurrentDefault()), | 
|  | output_file_(output_file), | 
|  | output_format_(output_format), | 
|  | on_tracing_finished_(std::move(on_tracing_finished)) { | 
|  | tracing_session_ = | 
|  | perfetto::Tracing::NewTrace(perfetto::BackendType::kCustomBackend); | 
|  |  | 
|  | if (write_mode_ == WriteMode::kStreaming) { | 
|  | #if !BUILDFLAG(IS_WIN) | 
|  | OpenFile(output_file_); | 
|  | tracing_session_->Setup(trace_config, file_.TakePlatformFile()); | 
|  | #else | 
|  | NOTREACHED() << "Streaming to file is not supported on Windows yet"; | 
|  | #endif | 
|  | } else { | 
|  | tracing_session_->Setup(trace_config); | 
|  | } | 
|  |  | 
|  | // |StartBlocking| can take a non-trivial amount of time, so | 
|  | // EmergencyTraceFinalisationController should be set up before it to catch | 
|  | // DCHECKs early. | 
|  | EmergencyTraceFinalisationCoordinator::GetInstance().OnTracingStarted( | 
|  | task_runner_, | 
|  | base::BindOnce(&BackgroundTracer::Stop, weak_ptr_factory_.GetWeakPtr(), | 
|  | absl::nullopt)); | 
|  |  | 
|  | tracing_session_->SetOnStopCallback([&]() { OnTracingStopped(); }); | 
|  | tracing_session_->StartBlocking(); | 
|  |  | 
|  | TRACE_EVENT("startup", "StartupTracingController::Start"); | 
|  | } | 
|  |  | 
|  | void Stop(absl::optional<base::FilePath> output_file) { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  |  | 
|  | // Tracing might have already been finished due to a timeout. | 
|  | if (state_ != State::kTracing) { | 
|  | // Note: updating output files is not supported together with | 
|  | // timeout-based tracing. | 
|  | return; | 
|  | } | 
|  | state_ = State::kStopping; | 
|  |  | 
|  | if (output_file) | 
|  | output_file_ = output_file.value(); | 
|  | tracing_session_->StopBlocking(); | 
|  | } | 
|  |  | 
|  | void OnTracingStopped() { | 
|  | if (!task_runner_->RunsTasksInCurrentSequence()) { | 
|  | // The owner of BackgroundTracer is responsible for ensuring that | 
|  | // BackgroundTracer stays alive until |on_tracing_finished_| is called. | 
|  | task_runner_->PostTask(FROM_HERE, | 
|  | base::BindOnce(&BackgroundTracer::OnTracingStopped, | 
|  | base::Unretained(this))); | 
|  | return; | 
|  | } | 
|  |  | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | // State will be kStopping if Stop() is called and kTracing if tracing | 
|  | // finishes due to a timeout. | 
|  | DCHECK(state_ == State::kStopping || state_ == State::kTracing); | 
|  | if (write_mode_ == WriteMode::kStreaming) { | 
|  | // No need to explicitly call ReadTrace as Perfetto has already written | 
|  | // the file. | 
|  | Finalise(); | 
|  | return; | 
|  | } | 
|  | state_ = State::kWritingToFile; | 
|  |  | 
|  | OpenFile(output_file_); | 
|  |  | 
|  | // The owner of BackgroundTracer is responsible for ensuring that | 
|  | // BackgroundTracer stays alive until |on_tracing_finished_| is called. | 
|  | tracing_session_->ReadTrace( | 
|  | [this](perfetto::TracingSession::ReadTraceCallbackArgs args) { | 
|  | WriteData(args.data, args.size); | 
|  |  | 
|  | if (args.has_more) | 
|  | return; | 
|  |  | 
|  | Finalise(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | private: | 
|  | void WriteData(const char* data, size_t size) { | 
|  | // Last chunk can be empty. | 
|  | if (size == 0) | 
|  | return; | 
|  |  | 
|  | // Proto files should be written directly to the file. | 
|  | if (output_format_ == tracing::TraceStartupConfig::OutputFormat::kProto) { | 
|  | file_.WriteAtCurrentPos(data, size); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // For JSON, we need to extract raw data from the packet. | 
|  | if (!trace_packet_tokenizer_) { | 
|  | trace_packet_tokenizer_ = | 
|  | std::make_unique<tracing::TracePacketTokenizer>(); | 
|  | } | 
|  |  | 
|  | std::vector<perfetto::TracePacket> packets = trace_packet_tokenizer_->Parse( | 
|  | reinterpret_cast<const uint8_t*>(data), size); | 
|  | for (const auto& packet : packets) { | 
|  | for (const auto& slice : packet.slices()) { | 
|  | file_.WriteAtCurrentPos(reinterpret_cast<const char*>(slice.start), | 
|  | slice.size); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Open |file_| for writing and set |written_to_file_| accordingly. | 
|  | // In order to atomically commit the trace file, create a temporary file first | 
|  | // which then will be subsequently renamed. | 
|  | void OpenFile(const base::FilePath& path) { | 
|  | if (temp_file_policy_ == TempFilePolicy::kUseTemporaryFile) { | 
|  | file_ = base::CreateAndOpenTemporaryFileInDir(path.DirName(), | 
|  | &written_to_file_); | 
|  | if (file_.IsValid()) | 
|  | return; | 
|  |  | 
|  | VLOG(1) << "Failed to create temporary file, using file '" << path | 
|  | << "' directly instead"; | 
|  | } | 
|  |  | 
|  | // On Android, it might not be possible to create a temporary file. | 
|  | // In that case, we should use the file directly. | 
|  | file_.Initialize(output_file_, | 
|  | base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); | 
|  | written_to_file_ = output_file_; | 
|  |  | 
|  | if (!file_.IsValid()) | 
|  | LOG(ERROR) << "Startup tracing failed: couldn't open file: " << path; | 
|  | } | 
|  |  | 
|  | // Close the file and rename if needed. | 
|  | void Finalise() { | 
|  | DCHECK_NE(state_, State::kFinished); | 
|  | file_.Close(); | 
|  |  | 
|  | if (written_to_file_ != output_file_) { | 
|  | base::File::Error error; | 
|  | if (!base::ReplaceFile(written_to_file_, output_file_, &error)) { | 
|  | LOG(ERROR) << "Cannot move file '" << written_to_file_ << "' to '" | 
|  | << output_file_ | 
|  | << "' : " << base::File::ErrorToString(error); | 
|  | } else { | 
|  | written_to_file_ = output_file_; | 
|  | } | 
|  | } | 
|  |  | 
|  | VLOG(0) << "Completed startup tracing to " << written_to_file_; | 
|  | EmergencyTraceFinalisationCoordinator::GetInstance().OnTracingStopped(); | 
|  |  | 
|  | state_ = State::kFinished; | 
|  | std::move(on_tracing_finished_).Run(); | 
|  | } | 
|  |  | 
|  | enum class State { | 
|  | kTracing, | 
|  | kStopping, | 
|  | kWritingToFile, | 
|  | kFinished, | 
|  | }; | 
|  | State state_; | 
|  |  | 
|  | const WriteMode write_mode_; | 
|  | const TempFilePolicy temp_file_policy_; | 
|  |  | 
|  | scoped_refptr<base::SequencedTaskRunner> task_runner_; | 
|  |  | 
|  | // Output file might be customised during the execution (e.g. test result | 
|  | // becomes available), which means that if Perfetto has already started | 
|  | // streaming the trace, the trace file should be renamed after trace | 
|  | // completes. | 
|  | base::FilePath output_file_; | 
|  | base::FilePath written_to_file_; | 
|  |  | 
|  | base::File file_; | 
|  |  | 
|  | const tracing::TraceStartupConfig::OutputFormat output_format_; | 
|  |  | 
|  | // Tokenizer to extract the json data from the data received from the tracing | 
|  | // service. | 
|  | std::unique_ptr<tracing::TracePacketTokenizer> trace_packet_tokenizer_; | 
|  |  | 
|  | base::OnceClosure on_tracing_finished_; | 
|  |  | 
|  | std::unique_ptr<perfetto::TracingSession> tracing_session_; | 
|  |  | 
|  | SEQUENCE_CHECKER(sequence_checker_); | 
|  |  | 
|  | base::WeakPtrFactory<BackgroundTracer> weak_ptr_factory_{this}; | 
|  | }; | 
|  |  | 
|  | // static | 
|  | StartupTracingController& StartupTracingController::GetInstance() { | 
|  | // Note: no DCHECK_CURRENTLY_ON, as it can be called prior to initialisation | 
|  | // of BrowserThreads. | 
|  |  | 
|  | static base::NoDestructor<StartupTracingController> g_instance; | 
|  | return *g_instance; | 
|  | } | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | base::FilePath BasenameToPath(std::string basename) { | 
|  | #if BUILDFLAG(IS_ANDROID) | 
|  | return TracingControllerAndroid::GenerateTracingFilePath(basename); | 
|  | #else | 
|  | // Default to saving the startup trace into the current dir. | 
|  | return base::FilePath().AppendASCII(basename); | 
|  | #endif | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | StartupTracingController::StartupTracingController() = default; | 
|  | StartupTracingController::~StartupTracingController() = default; | 
|  |  | 
|  | base::FilePath StartupTracingController::GetOutputPath() { | 
|  | auto* command_line = base::CommandLine::ForCurrentProcess(); | 
|  |  | 
|  | base::FilePath path_from_config = | 
|  | tracing::TraceStartupConfig::GetInstance()->GetResultFile(); | 
|  | if (!path_from_config.empty()) | 
|  | return path_from_config; | 
|  |  | 
|  | // If --trace-startup-file is specified, use it. | 
|  | if (command_line->HasSwitch(switches::kTraceStartupFile)) { | 
|  | base::FilePath result = | 
|  | command_line->GetSwitchValuePath(switches::kTraceStartupFile); | 
|  | if (result.empty()) | 
|  | return BasenameToPath("chrometrace.log"); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | base::FilePath result = | 
|  | command_line->GetSwitchValuePath(switches::kEnableTracingOutput); | 
|  | if (result.empty() && command_line->HasSwitch(switches::kTraceStartup)) { | 
|  | // If --trace-startup is present, return chrometrace.log for backwards | 
|  | // compatibility. | 
|  | return BasenameToPath("chrometrace.log"); | 
|  | } | 
|  |  | 
|  | // If a non-directory path is specified, use it. | 
|  | if (!result.empty() && !result.EndsWithSeparator()) | 
|  | return result; | 
|  |  | 
|  | std::string basename = default_basename_; | 
|  | if (basename.empty()) | 
|  | basename = "chrometrace.log"; | 
|  |  | 
|  | // If a non-empty directory is specified, use it. | 
|  | if (!result.empty()) | 
|  | return result.AppendASCII(basename); | 
|  |  | 
|  | // If the directory is empty, go through BasenameToPath to generate a valid | 
|  | // path on Android. | 
|  | return BasenameToPath(basename); | 
|  | } | 
|  |  | 
|  | void StartupTracingController::StartIfNeeded() { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | DCHECK_NE(state_, State::kRunning); | 
|  |  | 
|  | auto* trace_startup_config = tracing::TraceStartupConfig::GetInstance(); | 
|  | if (!trace_startup_config->AttemptAdoptBySessionOwner( | 
|  | tracing::TraceStartupConfig::SessionOwner::kTracingController)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | state_ = State::kRunning; | 
|  |  | 
|  | // Use USER_VISIBLE priority for the drainer because BEST_EFFORT tasks are not | 
|  | // run at startup and we want the trace file to be written soon. | 
|  | auto background_task_runner = base::ThreadPool::CreateSequencedTaskRunner( | 
|  | {base::MayBlock(), base::TaskPriority::USER_VISIBLE, | 
|  | base::TaskShutdownBehavior::BLOCK_SHUTDOWN}); | 
|  |  | 
|  | auto output_format = | 
|  | tracing::TraceStartupConfig::GetInstance()->GetOutputFormat(); | 
|  |  | 
|  | BackgroundTracer::WriteMode write_mode; | 
|  | #if BUILDFLAG(IS_WIN) | 
|  | // TODO(crbug.com/1158482/): Perfetto does not (yet) support writing directly | 
|  | // to a file on Windows. | 
|  | write_mode = BackgroundTracer::WriteMode::kAfterStopping; | 
|  | #else | 
|  | // Only protos can be incrementally written to a file - legacy json needs to | 
|  | // go through an additional conversion step after, which requires the entire | 
|  | // trace to be available. | 
|  | write_mode = | 
|  | output_format == tracing::TraceStartupConfig::OutputFormat::kProto | 
|  | ? BackgroundTracer::WriteMode::kStreaming | 
|  | : BackgroundTracer::WriteMode::kAfterStopping; | 
|  | #endif | 
|  |  | 
|  | const auto& chrome_config = | 
|  | tracing::TraceStartupConfig::GetInstance()->GetTraceConfig(); | 
|  | perfetto::TraceConfig perfetto_config = tracing::GetDefaultPerfettoConfig( | 
|  | chrome_config, /*privacy_filtering_enabled=*/false, | 
|  | /*convert_to_legacy_json=*/output_format == | 
|  | tracing::TraceStartupConfig::OutputFormat::kLegacyJSON); | 
|  |  | 
|  | int duration_in_seconds = | 
|  | tracing::TraceStartupConfig::GetInstance()->GetStartupDuration(); | 
|  | perfetto_config.set_duration_ms(duration_in_seconds * 1000); | 
|  |  | 
|  | background_tracer_ = base::SequenceBound<BackgroundTracer>( | 
|  | std::move(background_task_runner), write_mode, temp_file_policy_, | 
|  | GetOutputPath(), output_format, perfetto_config, | 
|  | base::BindOnce( | 
|  | [](StartupTracingController* controller) { | 
|  | GetUIThreadTaskRunner({})->PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&StartupTracingController::OnStoppedOnUIThread, | 
|  | base::Unretained(controller))); | 
|  | }, | 
|  | this)); | 
|  | } | 
|  |  | 
|  | void StartupTracingController::Stop(base::OnceClosure on_tracing_finished) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  |  | 
|  | if (state_ != State::kRunning) { | 
|  | // Both kStopped and kNotRunning are valid states. | 
|  | std::move(on_tracing_finished).Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | DCHECK(!on_tracing_finished_) << "Stop() should be called only once."; | 
|  | on_tracing_finished_ = std::move(on_tracing_finished); | 
|  |  | 
|  | background_tracer_.AsyncCall(&BackgroundTracer::Stop) | 
|  | .WithArgs(GetOutputPath()); | 
|  | } | 
|  |  | 
|  | void StartupTracingController::OnStoppedOnUIThread() { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | DCHECK_EQ(state_, State::kRunning); | 
|  | state_ = State::kStopped; | 
|  | background_tracer_.Reset(); | 
|  |  | 
|  | if (on_tracing_finished_) | 
|  | std::move(on_tracing_finished_).Run(); | 
|  |  | 
|  | tracing::TraceStartupConfig::GetInstance()->SetDisabled(); | 
|  | } | 
|  |  | 
|  | void StartupTracingController::SetUsingTemporaryFile( | 
|  | StartupTracingController::TempFilePolicy temp_file_policy) { | 
|  | DCHECK_EQ(state_, State::kNotEnabled) << "Should be called before Start()"; | 
|  | temp_file_policy_ = temp_file_policy; | 
|  | } | 
|  |  | 
|  | void StartupTracingController::SetDefaultBasename( | 
|  | std::string basename, | 
|  | ExtensionType extension_type) { | 
|  | if (!tracing::TraceStartupConfig::GetInstance()->IsEnabled()) | 
|  | return; | 
|  |  | 
|  | if (basename_for_test_set_) | 
|  | return; | 
|  |  | 
|  | if (extension_type == ExtensionType::kAppendAppropriate) { | 
|  | switch (tracing::TraceStartupConfig::GetInstance()->GetOutputFormat()) { | 
|  | case tracing::TraceStartupConfig::OutputFormat::kLegacyJSON: | 
|  | basename += ".json"; | 
|  | break; | 
|  | case tracing::TraceStartupConfig::OutputFormat::kProto: | 
|  | basename += ".pftrace"; | 
|  | break; | 
|  | } | 
|  | } | 
|  | default_basename_ = basename; | 
|  | } | 
|  |  | 
|  | void StartupTracingController::SetDefaultBasenameForTest( | 
|  | std::string basename, | 
|  | ExtensionType extension_type) { | 
|  | basename_for_test_set_ = false; | 
|  | SetDefaultBasename(basename, extension_type); | 
|  | basename_for_test_set_ = true; | 
|  | } | 
|  |  | 
|  | void StartupTracingController::WaitUntilStopped() { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  |  | 
|  | base::RunLoop run_loop; | 
|  | Stop(run_loop.QuitClosure()); | 
|  | run_loop.Run(); | 
|  | } | 
|  |  | 
|  | void StartupTracingController::ShutdownAndWaitForStopIfNeeded() { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  |  | 
|  | if (should_continue_on_shutdown_) | 
|  | return; | 
|  |  | 
|  | WaitUntilStopped(); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void StartupTracingController::EmergencyStop() { | 
|  | if (GetIOThreadTaskRunner({})->RunsTasksInCurrentSequence()) { | 
|  | VLOG(0) << "Emergency tracing stop request from IO thread is ignored - not " | 
|  | "possible to finalise trace without running tasks on IO thread"; | 
|  | return; | 
|  | } | 
|  |  | 
|  | EmergencyTraceFinalisationCoordinator::GetInstance() | 
|  | .StopAndBlockUntilStopped(); | 
|  | } | 
|  |  | 
|  | }  // namespace content |