// Copyright 2019 The Chromium 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 "components/ui_devtools/tracing_agent.h" #include #include #include #include "base/bind.h" #include "base/memory/ref_counted.h" #include "base/process/process.h" #include "base/timer/timer.h" #include "base/trace_event/trace_event.h" #include "base/trace_event/traced_value.h" #include "components/ui_devtools/connector_delegate.h" #include "components/ui_devtools/devtools_base_agent.h" #include "mojo/public/cpp/bindings/receiver.h" #include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/system/data_pipe_drainer.h" #include "services/tracing/public/cpp/perfetto/perfetto_config.h" #include "services/tracing/public/cpp/tracing_features.h" #include "services/tracing/public/mojom/constants.mojom.h" #include "services/tracing/public/mojom/perfetto_service.mojom.h" #include "third_party/inspector_protocol/crdtp/json.h" namespace ui_devtools { using ui_devtools::protocol::Response; namespace { // Minimum reporting interval for the buffer usage status. const double kMinimumReportingInterval = 250.0; // We currently don't support concurrent tracing sessions, but are planning to. // For the time being, we're using this flag as a workaround to prevent devtools // users from accidentally starting two concurrent sessions. // TODO(eseckler): Remove once we add support for concurrent sessions to the // perfetto backend. static bool g_any_agent_tracing = false; base::trace_event::TraceConfig::ProcessFilterConfig CreateProcessFilterConfig( base::ProcessId gpu_pid) { base::ProcessId browser_pid = base::Process::Current().Pid(); std::unordered_set included_process_ids({browser_pid}); if (gpu_pid != base::kNullProcessId) included_process_ids.insert(gpu_pid); return base::trace_event::TraceConfig::ProcessFilterConfig( included_process_ids); } } // namespace // This class is passed to StopTracing() and receives the trace data followed by // a notification that data collection is over. class TracingAgent::DevToolsTraceEndpointProxy : public base::RefCountedThreadSafe { public: explicit DevToolsTraceEndpointProxy(base::WeakPtr tracing_agent) : tracing_agent_(tracing_agent) {} void ReceiveTraceChunk(std::unique_ptr chunk) { if (TracingAgent* h = tracing_agent_.get()) h->OnTraceDataCollected(std::move(chunk)); } void ReceiveTraceFinalContents( std::unique_ptr metadata) { if (TracingAgent* h = tracing_agent_.get()) h->OnTraceComplete(); } protected: friend class base::RefCountedThreadSafe; private: ~DevToolsTraceEndpointProxy() = default; base::WeakPtr tracing_agent_; }; // Class used to communicate with the Perfetto Consumer interface. class TracingAgent::PerfettoTracingSession : public tracing::mojom::TracingSessionClient, public mojo::DataPipeDrainer::Client { public: explicit PerfettoTracingSession(ConnectorDelegate* connector) : connector_(connector) {} ~PerfettoTracingSession() override { DCHECK(!tracing_active_); } // TracingAgent::TracingSession implementation: // Make request and set up perfetto for tracing. void EnableTracing(const base::trace_event::TraceConfig& chrome_config, base::OnceClosure on_recording_enabled_callback) { DCHECK(!tracing_session_host_); DCHECK(!tracing_active_); tracing_active_ = true; connector_->BindTracingConsumerHost( consumer_host_.BindNewPipeAndPassReceiver()); DCHECK(consumer_host_); perfetto::TraceConfig perfetto_config = CreatePerfettoConfiguration(chrome_config); mojo::PendingRemote tracing_session_client; receiver_.Bind(tracing_session_client.InitWithNewPipeAndPassReceiver()); receiver_.set_disconnect_handler( base::BindOnce(&PerfettoTracingSession::OnTracingSessionFailed, base::Unretained(this))); on_recording_enabled_callback_ = std::move(on_recording_enabled_callback); consumer_host_->EnableTracing( tracing_session_host_.BindNewPipeAndPassReceiver(), std::move(tracing_session_client), std::move(perfetto_config), tracing::mojom::TracingClientPriority::kUserInitiated); tracing_session_host_.set_disconnect_handler( base::BindOnce(&PerfettoTracingSession::OnTracingSessionFailed, base::Unretained(this))); } void OnTracingEnabled() override { if (on_recording_enabled_callback_) { std::move(on_recording_enabled_callback_).Run(); } } void OnTracingDisabled() override { // Since we're converting the tracing data to JSON, we will receive the // tracing data via ConsumerHost::DisableTracingAndEmitJson(). return; } void DisableTracing( const std::string& agent_label, const scoped_refptr& endpoint) { agent_label_ = agent_label; endpoint_ = endpoint; tracing_active_ = false; if (!tracing_session_host_) { if (endpoint_) { // Will delete |this|. endpoint_->ReceiveTraceFinalContents(nullptr); } return; } mojo::ScopedDataPipeProducerHandle producer_handle; mojo::ScopedDataPipeConsumerHandle consumer_handle; MojoResult result = mojo::CreateDataPipe(nullptr, &producer_handle, &consumer_handle); if (result != MOJO_RESULT_OK) { OnTracingSessionFailed(); return; } drainer_ = std::make_unique( this, std::move(consumer_handle)); tracing_session_host_->DisableTracingAndEmitJson( agent_label_, std::move(producer_handle), /*privacy_filtering_enabled=*/false, base::BindOnce(&PerfettoTracingSession::OnReadBuffersComplete, base::Unretained(this))); } void GetBufferUsage(base::OnceCallback on_buffer_usage_callback) { if (!tracing_session_host_) { std::move(on_buffer_usage_callback).Run(0.0f, 0); return; } DCHECK(on_buffer_usage_callback); tracing_session_host_->RequestBufferUsage(base::BindOnce( &PerfettoTracingSession::OnBufferUsage, base::Unretained(this), std::move(on_buffer_usage_callback))); } bool HasTracingFailed() { return tracing_active_ && !tracing_session_host_; } bool HasDataLossOccurred() { return data_loss_; } private: perfetto::TraceConfig CreatePerfettoConfiguration( const base::trace_event::TraceConfig& chrome_config) { #if DCHECK_IS_ON() base::trace_event::TraceConfig processfilter_stripped_config(chrome_config); processfilter_stripped_config.SetProcessFilterConfig( base::trace_event::TraceConfig::ProcessFilterConfig()); // Ensure that the process filter is the only thing that gets changed // in a configuration during a tracing session. DCHECK((last_config_for_perfetto_.ToString() == base::trace_event::TraceConfig().ToString()) || (last_config_for_perfetto_.ToString() == processfilter_stripped_config.ToString())); last_config_for_perfetto_ = std::move(processfilter_stripped_config); #endif return tracing::GetDefaultPerfettoConfig(chrome_config); } void OnTracingSessionFailed() { tracing_session_host_.reset(); receiver_.reset(); drainer_.reset(); if (on_recording_enabled_callback_) std::move(on_recording_enabled_callback_).Run(); if (pending_disable_tracing_task_) std::move(pending_disable_tracing_task_).Run(); if (endpoint_) { endpoint_->ReceiveTraceFinalContents(nullptr); } } // Notify client about data loss during tracing. void OnBufferUsage(base::OnceCallback on_buffer_usage_callback, bool success, float percent_full, bool data_loss) { if (!success) { std::move(on_buffer_usage_callback).Run(0.0f, 0); return; } data_loss_ |= data_loss; std::move(on_buffer_usage_callback).Run(percent_full, 0); } // mojo::DataPipeDrainer::Client implementation: void OnDataAvailable(const void* data, size_t num_bytes) override { auto data_string = std::make_unique( reinterpret_cast(data), num_bytes); endpoint_->ReceiveTraceChunk(std::move(data_string)); } void OnDataComplete() override { data_complete_ = true; MaybeTraceComplete(); } void OnReadBuffersComplete() { read_buffers_complete_ = true; MaybeTraceComplete(); } void MaybeTraceComplete() { if (read_buffers_complete_ && data_complete_) { // Request stats to check if data loss occurred. GetBufferUsage(base::BindOnce(&PerfettoTracingSession::OnFinalBufferUsage, base::Unretained(this))); } } void OnFinalBufferUsage(float percent_full, size_t approximate_event_count) { if (!endpoint_) return; // Will delete |this|. endpoint_->ReceiveTraceFinalContents(nullptr); } mojo::Receiver receiver_{this}; mojo::Remote tracing_session_host_; mojo::Remote consumer_host_; ConnectorDelegate* connector_; std::string agent_label_; base::OnceClosure on_recording_enabled_callback_; base::OnceClosure pending_disable_tracing_task_; scoped_refptr endpoint_; std::unique_ptr drainer_; bool data_complete_ = false; bool read_buffers_complete_ = false; bool tracing_active_ = false; bool data_loss_ = false; #if DCHECK_IS_ON() base::trace_event::TraceConfig last_config_for_perfetto_; #endif }; TracingAgent::TracingAgent(std::unique_ptr connector) : connector_(std::move(connector)) {} TracingAgent::~TracingAgent() = default; namespace { class TracingNotification : public crdtp::Serializable { public: explicit TracingNotification(std::string json) : json_(std::move(json)) {} void AppendSerialized(std::vector* out) const override { crdtp::Status status = crdtp::json::ConvertJSONToCBOR(crdtp::SpanFrom(json_), out); DCHECK(status.ok()) << status.ToASCIIString(); } private: std::string json_; }; } // namespace void TracingAgent::OnTraceDataCollected( std::unique_ptr trace_fragment) { const std::string valid_trace_fragment = UpdateTraceDataBuffer(*trace_fragment); if (valid_trace_fragment.empty()) return; // Hand-craft protocol notification message so we can substitute JSON // that we already got as string as a bare object, not a quoted string. std::string message( "{ \"method\": \"Tracing.dataCollected\", \"params\": { \"value\": ["); const size_t messageSuffixSize = 10; message.reserve(message.size() + valid_trace_fragment.size() + messageSuffixSize - trace_data_buffer_state_.offset); message.append(valid_trace_fragment.c_str() + trace_data_buffer_state_.offset); message += "] } }"; frontend()->sendRawNotification( std::make_unique(std::move(message))); } void TracingAgent::OnTraceComplete() { if (!trace_data_buffer_state_.data.empty()) OnTraceDataCollected(std::make_unique("")); DCHECK(trace_data_buffer_state_.data.empty()); DCHECK_EQ(0u, trace_data_buffer_state_.pos); DCHECK_EQ(0, trace_data_buffer_state_.open_braces); DCHECK(!trace_data_buffer_state_.in_string); DCHECK(!trace_data_buffer_state_.slashed); bool data_loss = perfetto_session_->HasDataLossOccurred(); perfetto_session_.reset(); frontend()->tracingComplete(data_loss); } void TracingAgent::start( protocol::Maybe categories, protocol::Maybe options, protocol::Maybe buffer_usage_reporting_interval, std::unique_ptr callback) { if (g_any_agent_tracing) { callback->sendFailure(Response::ServerError("Tracing is already started")); return; } if (!categories.isJust() && !options.isJust()) { callback->sendFailure( Response::InvalidParams("categories+options should be specified.")); return; } did_initiate_recording_ = true; buffer_usage_reporting_interval_ = buffer_usage_reporting_interval.fromMaybe(0); // Since we want minimum changes to the devtools frontend, enable the // tracing categories for ui_devtools here. std::string ui_devtools_categories = "disabled-by-default-devtools.timeline,disabled-by-default-devtools." "timeline.frame,views,latency,toplevel," "benchmark,cc,viz,input,latency,gpu,rail,viz,ui"; trace_config_ = base::trace_event::TraceConfig(ui_devtools_categories, options.fromMaybe("")); StartTracing(std::move(callback)); } Response TracingAgent::end() { if (!perfetto_session_) return Response::ServerError("Tracing is not started"); if (perfetto_session_->HasTracingFailed()) return Response::ServerError("Tracing failed"); scoped_refptr endpoint; // Reset the trace data buffer state. trace_data_buffer_state_ = TracingAgent::TraceDataBufferState(); endpoint = new DevToolsTraceEndpointProxy(weak_factory_.GetWeakPtr()); StopTracing(endpoint, tracing::mojom::kChromeTraceEventLabel); return Response::Success(); } void TracingAgent::StartTracing(std::unique_ptr callback) { trace_config_.SetProcessFilterConfig(CreateProcessFilterConfig(gpu_pid_)); perfetto_session_ = std::make_unique(connector_.get()); perfetto_session_->EnableTracing( trace_config_, base::BindOnce(&TracingAgent::OnRecordingEnabled, weak_factory_.GetWeakPtr(), std::move(callback))); g_any_agent_tracing = true; } void TracingAgent::OnRecordingEnabled(std::unique_ptr callback) { EditTraceDataForFrontend(); callback->sendSuccess(); SetupTimer(buffer_usage_reporting_interval_); } void TracingAgent::EditTraceDataForFrontend() { // TracingStartedInBrowser is used to enter _processInspectorTrace, // which is the logic flow that browser devtools uses for the performance // panel. Without this trace event, devtools will use the generic trace, which // does not handle fps logic or use the color scheme defined by the // category-event map. The processes that are of interest in ui_devtools needs // to be specified in the data[frame] property of TracingStartedInBrowser auto process_data = std::make_unique(); process_data->SetBoolean("persistentIds", true); process_data->BeginArray("frames"); process_data->BeginDictionary(); process_data->SetString("frame", "ui_devtools_browser_frame"); process_data->SetString("name", "Browser"); process_data->SetInteger("processId", base::Process::Current().Pid()); process_data->EndDictionary(); process_data->BeginDictionary(); process_data->SetString("frame", "ui_devtools_gpu_frame"); process_data->SetString("name", "Gpu"); process_data->SetInteger("processId", gpu_pid_); process_data->EndDictionary(); process_data->EndArray(); TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"), "TracingStartedInBrowser", TRACE_EVENT_SCOPE_THREAD, "data", std::move(process_data)); // Browser devtools make sure the SetLayerTreeId trace event has the same // layertreeid as the layertreeid from the frame trace events 'DrawFrame' and // 'BeginFrame'. There is only 1 layer tree in ui_devtools, so the layertreeid // for all frames in ui_devtools is 1. This is used to get the frames, which // is later used for the fps metrics. auto layer_tree_data = std::make_unique(); layer_tree_data->SetString("frame", "ui_devtools_browser_frame"); layer_tree_data->SetInteger("layerTreeId", 1); TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"), "SetLayerTreeId", TRACE_EVENT_SCOPE_THREAD, "data", std::move(layer_tree_data)); } void TracingAgent::SetupTimer(double usage_reporting_interval) { if (usage_reporting_interval == 0) return; if (usage_reporting_interval < kMinimumReportingInterval) usage_reporting_interval = kMinimumReportingInterval; base::TimeDelta interval = base::TimeDelta::FromMilliseconds(std::ceil(usage_reporting_interval)); buffer_usage_poll_timer_.reset(new base::RepeatingTimer()); buffer_usage_poll_timer_->Start( FROM_HERE, interval, base::BindRepeating(&TracingAgent::UpdateBufferUsage, weak_factory_.GetWeakPtr())); } void TracingAgent::OnBufferUsage(float percent_full, size_t approximate_event_count) { if (!did_initiate_recording_) return; frontend()->bufferUsage(percent_full, approximate_event_count, percent_full); } void TracingAgent::UpdateBufferUsage() { perfetto_session_->GetBufferUsage( base::BindOnce(&TracingAgent::OnBufferUsage, weak_factory_.GetWeakPtr())); } void TracingAgent::StopTracing( const scoped_refptr& endpoint, const std::string& agent_label) { DCHECK(perfetto_session_); buffer_usage_poll_timer_.reset(); perfetto_session_->DisableTracing(agent_label, endpoint); did_initiate_recording_ = false; g_any_agent_tracing = false; } std::string TracingAgent::UpdateTraceDataBuffer( const std::string& trace_fragment) { size_t end = 0; size_t last_open = 0; TraceDataBufferState& state = trace_data_buffer_state_; state.offset = 0; bool update_offset = state.open_braces == 0; for (; state.pos < trace_fragment.size(); ++state.pos) { char c = trace_fragment[state.pos]; switch (c) { case '{': if (!state.in_string && !state.slashed) { state.open_braces++; if (state.open_braces == 1) { last_open = state.data.size() + state.pos; if (update_offset) { state.offset = last_open; update_offset = false; } } } break; case '}': if (!state.in_string && !state.slashed) { DCHECK_GT(state.open_braces, 0); state.open_braces--; if (state.open_braces == 0) end = state.data.size() + state.pos + 1; } break; case '"': if (!state.slashed) state.in_string = !state.in_string; break; case 'u': if (state.slashed) state.pos += 4; break; } if (state.in_string && c == '\\') { state.slashed = !state.slashed; } else { state.slashed = false; } } // Next starting position is usually 0 except when we are in the middle of // processing a unicode character, i.e. \uxxxx. state.pos -= trace_fragment.size(); std::string complete_str = state.data + trace_fragment; state.data = complete_str.substr(std::max(end, last_open)); complete_str.resize(end); return complete_str; } } // namespace ui_devtools