starnix_modules_perfetto_consumer/lib.rs
1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![recursion_limit = "256"]
6
7use crate::blob_sink::TraceBufferSink;
8use crate::packet_forwarder::{MonotonicClock, PacketForwarder, forward_slices};
9use anyhow::{Context as _, anyhow, bail};
10use fuchsia_trace::{BufferingMode, ProlongedContext, TraceState, category_enabled, trace_state};
11use fuchsia_trace_observer::TraceObserver;
12use futures::{SinkExt, StreamExt};
13use perfetto_protos::perfetto::protos::trace_config::buffer_config::FillPolicy;
14use perfetto_protos::perfetto::protos::trace_config::{BufferConfig, DataSource};
15use perfetto_protos::perfetto::protos::{
16 DataSourceConfig, DisableTracingRequest, EnableTracingRequest, FreeBuffersRequest,
17 FtraceConfig, ReadBuffersRequest, TraceConfig, ipc_frame,
18};
19use perfetto_trace_protos::perfetto::protos::frame_timeline_event::{
20 ActualDisplayFrameStart, ActualSurfaceFrameStart, Event, ExpectedDisplayFrameStart,
21 ExpectedSurfaceFrameStart,
22};
23use perfetto_trace_protos::perfetto::protos::ftrace_event::Event::Print;
24use perfetto_trace_protos::perfetto::protos::trace_packet;
25use starnix_core::security;
26use starnix_core::task::dynamic_thread_spawner::SpawnRequestBuilder;
27use starnix_core::task::tracing::PidKoidSession;
28use starnix_core::task::{ArgNameAndValue, CurrentTask, Kernel};
29use starnix_core::vfs::FsString;
30use starnix_logging::{CATEGORY_ATRACE, log_debug, log_error, log_info, log_warn};
31use starnix_perfetto_trace_decoder::{decode_read_buffers_response, decode_trace, encode_trace};
32
33use starnix_uapi::errors::Errno;
34
35mod atrace;
36mod blob_sink;
37mod packet_forwarder;
38#[cfg(test)]
39mod testing;
40
41const PERFETTO_BUFFER_SIZE_KB: u32 = 63488;
42
43/// How long a stop drain may keep writing Perfetto data to the trace buffer, measured from the
44/// start of the drain.
45///
46/// The drain holds a prolonged trace context. trace-engine can't finish stopping while that
47/// context is held, so trace_manager has to wait for the drain. trace_manager gives up on a
48/// stopping session after 60 s (`kStopTimeout` in trace_manager.cc). The drain stops writing
49/// after half of that, which leaves the other half for the rest of the drain and for
50/// trace-engine to finish stopping.
51///
52/// The drain checks the deadline before each slice of the ReadBuffers response, before each
53/// packet, and while it waits for buffer space. It can't interrupt a blocked read from the
54/// Perfetto consumer socket, so a read that blocks past the deadline delays the check. Once
55/// the drain has stopped writing, it no longer copies, frames or rewrites the remaining
56/// packets, but it still holds the prolonged context while it reads the rest of the
57/// ReadBuffers response, up to the size of the Perfetto buffer, and then sends FreeBuffers.
58/// That part isn't bounded, which is a known limitation: the context is released only once
59/// FreeBuffers has been sent.
60// TODO(b/571694521): Release the context once the drain stops writing (fxr/1881111).
61const DRAIN_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(30);
62
63/// Supported Android Atrace categories and their corresponding Fuchsia trace category strings.
64/// Each entry is `(android_atrace_category_name, fuchsia_trace_category)`.
65const ALL_ATRACE_CATEGORIES: &[(&str, &str)] = &[
66 ("adb", "starnix:atrace:adb"),
67 ("aidl", "starnix:atrace:aidl"),
68 ("am", "starnix:atrace:am"),
69 ("audio", "starnix:atrace:audio"),
70 ("binder_driver", "starnix:atrace:binder_driver"),
71 ("binder_lock", "starnix:atrace:binder_lock"),
72 ("bionic", "starnix:atrace:bionic"),
73 ("camera", "starnix:atrace:camera"),
74 ("dalvik", "starnix:atrace:dalvik"),
75 ("database", "starnix:atrace:database"),
76 ("gfx", "starnix:atrace:gfx"),
77 ("hal", "starnix:atrace:hal"),
78 ("input", "starnix:atrace:input"),
79 ("network", "starnix:atrace:network"),
80 ("nnapi", "starnix:atrace:nnapi"),
81 ("pm", "starnix:atrace:pm"),
82 ("power", "starnix:atrace:power"),
83 ("res", "starnix:atrace:res"),
84 ("rro", "starnix:atrace:rro"),
85 ("rs", "starnix:atrace:rs"),
86 ("sched", "starnix:atrace:sched"),
87 ("sm", "starnix:atrace:sm"),
88 ("ss", "starnix:atrace:ss"),
89 ("vibrator", "starnix:atrace:vibrator"),
90 ("video", "starnix:atrace:video"),
91 ("view", "starnix:atrace:view"),
92 ("webview", "starnix:atrace:webview"),
93 ("wm", "starnix:atrace:wm"),
94];
95
96/// Trace category that enables application-level tracing (`atrace_apps: ["*"]` and atrace category `"app"`).
97const CATEGORY_ATRACE_APPS: &str = "starnix:atrace:apps";
98
99/// Builds the `FtraceConfig` protobuf for Perfetto based on enabled Fuchsia trace categories.
100///
101/// Returns `None` if no atrace categories or app tracing are enabled.
102fn build_ftrace_config<F>(mut is_category_enabled: F) -> Option<FtraceConfig>
103where
104 F: FnMut(&'static str) -> bool,
105{
106 let blanket_enabled = is_category_enabled(CATEGORY_ATRACE);
107 let apps_enabled = blanket_enabled || is_category_enabled(CATEGORY_ATRACE_APPS);
108
109 let mut atrace_categories =
110 Vec::with_capacity(if blanket_enabled { ALL_ATRACE_CATEGORIES.len() + 1 } else { 0 });
111 for &(atrace_name, fuchsia_cat) in ALL_ATRACE_CATEGORIES {
112 if blanket_enabled || is_category_enabled(fuchsia_cat) {
113 atrace_categories.push(atrace_name.to_string());
114 }
115 }
116 if apps_enabled {
117 atrace_categories.push("app".to_string());
118 }
119
120 let atrace_apps = if apps_enabled { vec!["*".to_string()] } else { Vec::new() };
121
122 if atrace_categories.is_empty() && atrace_apps.is_empty() {
123 None
124 } else {
125 Some(FtraceConfig {
126 ftrace_events: vec!["ftrace/print".to_string()],
127 buffer_size_kb: Some(2048),
128 atrace_categories,
129 atrace_apps,
130 ..Default::default()
131 })
132 }
133}
134
135/// State needed to act upon trace state changes.
136struct CallbackState {
137 /// The previously observed trace state.
138 prev_state: TraceState,
139 /// Path to the Perfetto consumer socket.
140 socket_path: FsString,
141 /// Connection to the consumer socket, if it has been initialized. This gets initialized the
142 /// first time it is needed.
143 connection: Option<perfetto::Consumer>,
144 /// Prolonged trace context to prevent the Fuchsia trace session from terminating while reading
145 /// data from Perfetto. `TraceState::Started` stores it only once it has sent EnableTracing,
146 /// and only `TraceState::Stopping` takes it, to disable Perfetto tracing and read the data.
147 /// So it is `Some` exactly when Started has requested Perfetto tracing and Stopping hasn't run
148 /// since. Holding it keeps trace-engine from reaching Stopped, so the session it belongs to
149 /// can't end, and no other session can start, until Stopping releases it.
150 prolonged_context: Option<ProlongedContext>,
151 /// Partial trace packet returned from Perfetto but not yet written to Fuchsia.
152 packet_data: Vec<u8>,
153
154 /// Recording session for pid/koid mappings, held while tracing is started. Dropping
155 /// it (on stop) releases this client's interest in the shared manager.
156 session: Option<PidKoidSession>,
157}
158
159impl CallbackState {
160 /// Returns the state for a consumer that hasn't seen any trace state change yet.
161 fn new(socket_path: FsString) -> Self {
162 Self {
163 prev_state: TraceState::Stopped,
164 socket_path,
165 connection: None,
166 prolonged_context: None,
167 packet_data: Vec::new(),
168 session: None,
169 }
170 }
171
172 fn connection(
173 &mut self,
174 current_task: &CurrentTask,
175 ) -> Result<&mut perfetto::Consumer, anyhow::Error> {
176 match self.connection {
177 None => {
178 self.connection =
179 Some(perfetto::Consumer::new(current_task, self.socket_path.as_ref())?);
180 Ok(self.connection.as_mut().unwrap())
181 }
182 Some(ref mut conn) => Ok(conn),
183 }
184 }
185
186 /// Clears the state kept for a trace session that has ended. Leaves `prolonged_context`
187 /// alone: only `TraceState::Stopping` releases it (see `prolonged_context`).
188 fn handle_stopped(&mut self) {
189 self.packet_data.clear();
190 self.session = None;
191 }
192
193 fn on_state_change(
194 &mut self,
195 new_state: TraceState,
196 current_task: &CurrentTask,
197 ) -> Result<(), anyhow::Error> {
198 let prev_state = self.prev_state;
199 self.prev_state = new_state;
200 log_debug!(
201 "Perfetto consumer state change. new_state: {new_state:?}, prev_state: {prev_state:?}"
202 );
203 match new_state {
204 TraceState::Started => {
205 if self.prolonged_context.is_some() {
206 // Perfetto tracing has already been requested for the session whose context
207 // we hold, and that session hasn't ended (see `prolonged_context`). So this
208 // Started is a repeat for that session (see
209 // `start_perfetto_consumer_thread()` for how that happens), or a stale one
210 // for a session that ended before we acquired the context. Either way there
211 // is nothing to do.
212 return Ok(());
213 }
214 if prev_state == TraceState::Stopping {
215 // The observer reads the current state when it is woken, so it misses the
216 // previous session's Stopped if the next session has already started by then.
217 // A stale Stopping that disabled Perfetto tracing early (see Stopping below)
218 // can also be followed by Started. Either way the pid/koid session and packet
219 // data are left over from before.
220 log_warn!(
221 "Started received in {prev_state:?} state! Cleaning up then starting."
222 );
223 self.handle_stopped();
224 }
225 // After Stopped, or after a Started that didn't enable Perfetto tracing (see
226 // below), there is nothing to clean up.
227
228 // `ProlongedContext::acquire()` fails only while trace-engine's context count is
229 // zero. With Started reported, that means one of:
230 // - The session has already ended, for example because its Stopping and Stopped
231 // states were queued behind this one.
232 // - The session is still starting. `trace_engine_start()` stores Started before
233 // it makes the count nonzero, and notifies observers only after that, so a
234 // `trace_state()` read in between returns Started while the count is zero.
235 // Either way another state change follows: the end of the session, or its start
236 // notification (see `start_perfetto_consumer_thread()` for why neither is
237 // missed). Skip Perfetto until then. `prolonged_context` stays `None`, so a
238 // later Stopping skips Perfetto and a later Started tries again.
239 let Some(prolonged_context) = ProlongedContext::acquire() else {
240 log_info!("No trace context for Started; not enabling Perfetto tracing yet");
241 return Ok(());
242 };
243 let connection = self.connection(current_task)?;
244 // A fixed set of data sources that may be of interest. As demand for other sources
245 // is found, add them here, and it may become worthwhile to allow this set to be
246 // configurable per trace session.
247 let mut data_sources = vec![
248 DataSource {
249 config: Some(DataSourceConfig {
250 name: Some("track_event".to_string()),
251 ..Default::default()
252 }),
253 ..Default::default()
254 },
255 DataSource {
256 config: Some(DataSourceConfig {
257 name: Some("android.surfaceflinger.frame".to_string()),
258 target_buffer: Some(0),
259 ..Default::default()
260 }),
261 ..Default::default()
262 },
263 DataSource {
264 config: Some(DataSourceConfig {
265 name: Some("android.surfaceflinger.frametimeline".to_string()),
266 target_buffer: Some(0),
267 ..Default::default()
268 }),
269 ..Default::default()
270 },
271 ];
272 if let Some(ftrace_config) = build_ftrace_config(category_enabled) {
273 data_sources.push(DataSource {
274 config: Some(DataSourceConfig {
275 name: Some("linux.ftrace".to_string()),
276 ftrace_config: Some(ftrace_config),
277 ..Default::default()
278 }),
279 ..Default::default()
280 });
281 }
282 connection.enable_tracing(
283 current_task,
284 EnableTracingRequest {
285 trace_config: Some(TraceConfig {
286 buffers: vec![BufferConfig {
287 size_kb: Some(PERFETTO_BUFFER_SIZE_KB),
288 fill_policy: Some(FillPolicy::Discard.into()),
289 ..Default::default()
290 }],
291 data_sources,
292 ..Default::default()
293 }),
294 attach_notification_only: None,
295 },
296 )?;
297 // EnableTracing has been sent now, so hold the context until Stopping tears
298 // Perfetto tracing down. If an error above returned early, EnableTracing wasn't
299 // sent and the context was dropped without being stored.
300 self.prolonged_context = Some(prolonged_context);
301 // Once tracing has started, open a recording session so pid/koid
302 // mappings are tracked for the duration of the trace.
303 self.session = Some(current_task.kernel().trace_event_manager.open());
304 }
305 TraceState::Stopping => {
306 // Hold the prolonged context so that trace-engine can't finish stopping while
307 // we forward Perfetto data. Taking it out of `self` also drops it if we bail
308 // below.
309 let Some(_prolonged_context) = self.prolonged_context.take() else {
310 // Without a prolonged context Perfetto tracing isn't enabled (see
311 // `prolonged_context`), so there is no Perfetto session to disable or read.
312 if prev_state != TraceState::Started {
313 // If we receive a stop request and we don't think we're actually
314 // tracing, our local state likely desynced from the global trace state.
315 // Clean up our state and ensure we're stopped so we re-synchronize.
316 log_error!("Stopping received in {prev_state:?} state! Cleaning up.");
317 self.handle_stopped();
318 }
319 return Ok(());
320 };
321 // With the context held, this Stopping is for the session it belongs to, or a
322 // stale one for a session that ended before we acquired the context. In the
323 // stale case Perfetto tracing pauses for the current session until its own
324 // Started, which is queued after this Stopping, enables it again, unless that
325 // session is already stopping by then. Either way it is not left enabled.
326 let start_time = zx::MonotonicInstant::get();
327
328 let connection = self.connection(current_task)?;
329 let disable_request =
330 connection.disable_tracing(current_task, DisableTracingRequest {})?;
331 loop {
332 let frame = connection.next_frame_blocking(current_task)?;
333 if frame.request_id == Some(disable_request) {
334 break;
335 } else {
336 log_error!(
337 "Ignoring frame while looking for DisableTracingRequest: {frame:?}"
338 );
339 }
340 }
341
342 let read_buffers_request =
343 connection.read_buffers(current_task, ReadBuffersRequest {})?;
344
345 // The buffering mode is fixed for the whole trace session.
346 let retry_on_full = {
347 // A held prolonged context keeps trace-engine's context count above zero,
348 // so this can't fail.
349 let Some(context) = fuchsia_trace::Context::acquire() else {
350 bail!("Tracing stopped despite holding prolonged context");
351 };
352 context.buffering_mode() == BufferingMode::Streaming
353 };
354 let mut forwarder = PacketForwarder::new(
355 TraceBufferSink,
356 MonotonicClock,
357 retry_on_full,
358 start_time + DRAIN_TIMEOUT,
359 );
360
361 // IPC responses may be spread across multiple frames, so loop until we get a
362 // message that indicates it is the last one. Additionally, if there are
363 // unrelated messages on the socket (e.g. leftover from a previous trace
364 // session), the loop will read past and ignore them.
365 //
366 // The loop reads the whole response even after the forwarder has stopped:
367 // skipped slices cost little, and if the consumer stops reading, traced blocks
368 // on the full socket and then shuts the connection down
369 // (`kDefaultIpcTxTimeoutMs` in Perfetto's ipc/host_impl.h). `self.connection`
370 // would keep that dead connection, and the next session's EnableTracing would
371 // fail on it, because `connection()` only reconnects when it is `None`.
372 loop {
373 let frame = self.connection(current_task)?.next_frame_blocking(current_task)?;
374 if frame.request_id != Some(read_buffers_request) {
375 continue;
376 } else {
377 log_debug!(
378 "perfetto_consumer ignoring frame while looking for ReadBuffersRequest {read_buffers_request}: {frame:?}"
379 );
380 }
381 if let Some(ipc_frame::Msg::MsgInvokeMethodReply(reply)) = &frame.msg {
382 if let Ok(response) = decode_read_buffers_response(
383 reply.reply_proto.as_deref().unwrap_or(&[]),
384 ) {
385 // `rewrite_pids()` borrows all of `self`, so `packet_data` is moved
386 // out while the slices are forwarded.
387 let mut packet_data = std::mem::take(&mut self.packet_data);
388 forward_slices(
389 &response.slices,
390 &mut packet_data,
391 &mut forwarder,
392 |blob_data| self.rewrite_pids(&blob_data).unwrap_or(blob_data),
393 );
394 self.packet_data = packet_data;
395 } else {
396 log_error!("perfetto_consumer cannot decode protobuf from {reply:?}");
397 }
398 if reply.has_more != Some(true) {
399 break;
400 }
401 } else {
402 log_error!(
403 "perfetto_consumer ignoring non-MsgInvokeMethodReply message: {frame:?}"
404 );
405 }
406 }
407 // FreeBuffers comes only after the last ReadBuffers reply: traced reads the
408 // buffers in several tasks, and a FreeBuffers handled in between frees them, so
409 // the response would never get its last reply.
410 // The response to a free buffers request does not have anything meaningful,
411 // so we don't need to worry about tracking the request id to match to the
412 // response.
413 let _free_buffers_request_id = self
414 .connection(current_task)?
415 .free_buffers(current_task, FreeBuffersRequest { buffer_ids: vec![0] })?;
416 let elapsed = (zx::MonotonicInstant::get() - start_time).into_millis();
417 let PacketForwarder {
418 packets_written,
419 packets_dropped,
420 bytes_dropped,
421 stop_reason,
422 ..
423 } = forwarder;
424 log_info!(
425 "Perfetto frames copied, dropping prolonged trace context. Processing took \
426 {elapsed} ms. Packets written: {packets_written}, dropped: {packets_dropped} \
427 ({bytes_dropped} bytes), stop reason: {stop_reason:?}"
428 );
429 }
430 TraceState::Stopped => {
431 if self.prolonged_context.is_some() {
432 // The session whose context we hold can't reach Stopped until Stopping
433 // releases the context, so this Stopped is stale: it is for a session that
434 // ended before we acquired the context. The held session's Stopping is still
435 // to come, so keep its context and pid/koid session until then.
436 return Ok(());
437 }
438 self.handle_stopped();
439 }
440 }
441 Ok(())
442 }
443
444 fn rewrite_pids(&mut self, protobuf_blob: &[u8]) -> anyhow::Result<Vec<u8>> {
445 let mut proto = decode_trace(protobuf_blob)?;
446 for p in &mut proto.packet {
447 if let Some(ref mut data) = p.data {
448 match data {
449 trace_packet::Data::FrameTimelineEvent(frame_timeline_event) => {
450 if let Some(evt) = &mut frame_timeline_event.event {
451 // Update the linux pid to the Fuchsia pid. Each event has its own
452 // match arm since the variant data is of a different type for each event.
453 match evt {
454 Event::ExpectedDisplayFrameStart(ExpectedDisplayFrameStart {
455 pid,
456 ..
457 })
458 | Event::ActualDisplayFrameStart(ActualDisplayFrameStart {
459 pid,
460 ..
461 })
462 | Event::ExpectedSurfaceFrameStart(ExpectedSurfaceFrameStart {
463 pid,
464 ..
465 })
466 | Event::ActualSurfaceFrameStart(ActualSurfaceFrameStart {
467 pid,
468 ..
469 }) => {
470 pid.as_mut().map(|pid| {
471 *pid = self.map_to_koid_val(*pid);
472 });
473 }
474 Event::FrameEnd(_frame_end) => {}
475 }
476 }
477 }
478 trace_packet::Data::FtraceEvents(ftrace_bundle) => {
479 for evt in &mut ftrace_bundle.event {
480 if let Some(ref mut pid) = evt.pid {
481 *pid = self.map_thread_to_koid_val(*pid as i32) as u32;
482 }
483 if let Some(ref mut event_data) = evt.event {
484 match event_data {
485 Print(print) => {
486 if let Some(ref mut data) = print.buf {
487 *data = self.map_print_event(data)
488 }
489 }
490 _ => (),
491 }
492 }
493 }
494 }
495 // No need to process other data; we only fixup data that references the pid.
496 _ => (),
497 }
498 }
499 }
500 Ok(encode_trace(&proto))
501 }
502
503 fn map_print_event(&mut self, data: &String) -> String {
504 if let Some(mut event) = atrace::ATraceEvent::parse(&data) {
505 match event {
506 atrace::ATraceEvent::Begin { ref mut pid, .. }
507 | atrace::ATraceEvent::End { ref mut pid }
508 | atrace::ATraceEvent::Instant { ref mut pid, .. }
509 | atrace::ATraceEvent::AsyncBegin { ref mut pid, .. }
510 | atrace::ATraceEvent::AsyncEnd { ref mut pid, .. }
511 | atrace::ATraceEvent::Counter { ref mut pid, .. }
512 | atrace::ATraceEvent::AsyncTrackBegin { ref mut pid, .. }
513 | atrace::ATraceEvent::AsyncTrackEnd { ref mut pid, .. }
514 | atrace::ATraceEvent::Track { ref mut pid, .. } => {
515 *pid = self.map_to_koid_val(*pid as i32) as u64
516 }
517 }
518 event.data()
519 } else {
520 data.to_string()
521 }
522 }
523
524 fn map_thread_to_koid_val(&mut self, pid: i32) -> i32 {
525 self.session
526 .as_ref()
527 .and_then(|s| s.resolve_tid_to_koid(pid))
528 .map(|k| k.raw_koid() as i32)
529 .unwrap_or(pid)
530 }
531
532 fn map_to_koid_val(&mut self, pid: i32) -> i32 {
533 self.session
534 .as_ref()
535 .and_then(|s| s.resolve_pid_to_koid(pid))
536 .map(|k| k.raw_koid() as i32)
537 .unwrap_or(pid)
538 }
539}
540
541fn start_perfetto_consumer_thread(kernel: &Kernel, socket_path: FsString) -> Result<(), Errno> {
542 let (mut tx, mut rx) = futures::channel::mpsc::channel::<TraceState>(32);
543
544 // Register the observer here, before the worker below reads `trace_state()` to find a session
545 // that started earlier (for example on boot). trace-engine's observer protocol is to register
546 // first and then read the state (see `trace_register_observer()` in instrumentation.h), so
547 // that every state change after that read is reported. This holds because registration takes
548 // trace-engine's engine mutex, which `trace_engine_start()` holds from storing Started until
549 // it has made the context count nonzero and notified observers. So if a start begins after
550 // registration, this observer gets its start notification. If it finished before
551 // registration, the worker sees the nonzero count, and then `ProlongedContext::acquire()`
552 // fails only for a session that has since ended, which this observer reports. The observer
553 // clears its event before it reads the state, so if it reads Started before the count is
554 // nonzero, the start notification signals it again.
555 //
556 // One session's Started can also be reported twice: by the worker's read and by the start
557 // notification, if the start falls between registration and that read, or by two observer
558 // reads, if the observer was woken earlier and reads Started before the notification comes.
559 // `CallbackState::on_state_change()` ignores a Started while it holds the prolonged context.
560 let observer = TraceObserver::new();
561
562 // Listens for trace state changes and sends them to Perfetto consumer thread.
563 // Unlike the perfetto thread, it won't block, so we can spawn it on the main async executor.
564 kernel.kthreads.spawn_future(
565 move || async move {
566 while let Ok(state) = observer.on_state_changed().await {
567 if let Err(e) = tx.send(state).await {
568 log_error!("perfetto-trace-observer failed to send trace state change: {:?}. Receiver dropped.", e);
569 return;
570 }
571 }
572 },
573 "perfetto-trace-observer",
574 );
575
576 // Perfetto consumer task: reads state changes from the channel and handles them.
577 // This task can block, so we spawn it on a dedicated thread to not block the observer or the
578 // main async executor.
579 let worker_closure = async move |current_task: &CurrentTask| {
580 let mut callback_state = CallbackState::new(socket_path);
581
582 fn handle_state_change(
583 callback_state: &mut CallbackState,
584 current_task: &&CurrentTask,
585 state: TraceState,
586 ) -> Result<(), anyhow::Error> {
587 let current_task = current_task;
588 // TODO: https://fxbug.dev/457381697 - Revise how this kernel-internal work is security-
589 // checked.
590 let creds = security::creds_start_internal_operation(current_task);
591 current_task
592 .override_creds(creds, || callback_state.on_state_change(state, current_task))
593 }
594
595 // Check for tracing already started before we began observing.
596 // This happens when tracing is started on boot.
597 let mut state = trace_state();
598 if trace_state() == TraceState::Started {
599 const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
600 // When we do boot tracing, it is possible (even likely), that starnix has started but
601 // perfetto may not be ready to be connected to yet.
602 // In that case poll until it has started.
603 loop {
604 match handle_state_change(&mut callback_state, ¤t_task, state) {
605 Ok(_) => break, // Success, exit loop.
606 Err(e) => {
607 if let Some(errno) = e.downcast_ref::<Errno>() {
608 if errno == &starnix_uapi::errors::ENOENT
609 || errno == &starnix_uapi::errors::ECONNREFUSED
610 {
611 log_warn!(
612 "perfetto_consumer initial start tracing failed because perfetto socket connection not established: {e:?} retrying in 5 seconds..."
613 );
614 std::thread::sleep(RETRY_DELAY);
615 // `prev_state` stays at the failed Started, so if the session
616 // is stopping by now, the retry handles its Stopping quietly.
617 callback_state.connection = None;
618 state = trace_state();
619 continue; // Retry
620 }
621 }
622 // For any other error, log and exit loop.
623 log_error!(
624 "perfetto_consumer initial start tracing failed with error: {e:?}"
625 );
626 break;
627 }
628 }
629 }
630 }
631
632 while let Some(state) = rx.next().await {
633 handle_state_change(&mut callback_state, ¤t_task, state).unwrap_or_else(|e| {
634 log_error!("perfetto_consumer state change callback error: {:?}", e);
635 })
636 }
637 };
638 let worker_req = SpawnRequestBuilder::new()
639 .with_debug_name("perfetto-consumer")
640 .with_async_closure(worker_closure)
641 .build();
642 kernel.kthreads.spawner().spawn_from_request(worker_req);
643
644 Ok(())
645}
646
647fn parse_args(args: &[&str]) -> Result<FsString, anyhow::Error> {
648 let mut socket_path: Option<&str> = None;
649 for &arg in args {
650 match ArgNameAndValue::from(arg) {
651 ArgNameAndValue { name: "socket_path", value: Some(path) } if !path.is_empty() => {
652 socket_path = Some(path);
653 }
654 _ => bail!("invalid perfetto argument: {arg}"),
655 }
656 }
657 let socket_path =
658 socket_path.ok_or_else(|| anyhow!("missing required perfetto socket_path=<path>"))?;
659 Ok(socket_path.into())
660}
661
662/// Validates `perfetto` module configuration `args`.
663pub fn validate_args(args: &[&str]) -> Result<(), anyhow::Error> {
664 parse_args(args).map(|_| ())
665}
666
667/// Initializes the Perfetto consumer thread from module configuration `args`.
668pub fn module_init(kernel: &Kernel, args: &[&str]) -> Result<(), anyhow::Error> {
669 let socket_path = parse_args(args)?;
670 start_perfetto_consumer_thread(kernel, socket_path)
671 .context("Failed to start perfetto consumer thread")
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use crate::testing::{TraceSession, lock_trace_engine};
678 use starnix_core::testing::spawn_kernel_and_run;
679
680 // The tests below run `CallbackState::on_state_change()` with and without a trace context.
681 // They have no Perfetto consumer endpoint that answers EnableTracing, DisableTracing and
682 // ReadBuffers, so every attempt to connect to Perfetto fails. So no test covers a Started that
683 // enables Perfetto tracing and keeps the context, or a Stopping that then disables Perfetto
684 // tracing, reads the buffers and releases the context. If Started stopped storing the context
685 // after EnableTracing, every Stopping would skip Perfetto and no test here would fail.
686
687 #[fuchsia::test]
688 async fn started_without_trace_context_skips_perfetto() {
689 spawn_kernel_and_run(async |current_task| {
690 let _lock = lock_trace_engine();
691 // No trace session runs in this test, so `ProlongedContext::acquire()` fails as it
692 // does for a session that has already ended or hasn't finished starting. The second
693 // Started is a repeat that finds no context either.
694 // `started_without_trace_context_tries_again_on_next_started` covers a later Started
695 // that does find one. This test doesn't cover the observer registration order in
696 // `start_perfetto_consumer_thread()`.
697 assert!(ProlongedContext::acquire().is_none(), "a trace session is running");
698 // Nothing listens on this path, so any attempt to connect to Perfetto fails.
699 let mut callback_state = CallbackState::new("/nonexistent/perfetto-consumer".into());
700 for state in [
701 TraceState::Started,
702 TraceState::Started,
703 TraceState::Stopping,
704 TraceState::Stopped,
705 ] {
706 if let Err(e) = callback_state.on_state_change(state, current_task) {
707 panic!("{state:?}: {e:?}");
708 }
709 assert_eq!(callback_state.prev_state, state);
710 assert!(callback_state.connection.is_none(), "{state:?}: connected to Perfetto");
711 assert!(callback_state.prolonged_context.is_none(), "{state:?}: holds a context");
712 assert!(callback_state.session.is_none(), "{state:?}: opened a pid/koid session");
713 }
714 })
715 .await;
716 }
717
718 #[fuchsia::test]
719 async fn started_without_trace_context_tries_again_on_next_started() {
720 spawn_kernel_and_run(async |current_task| {
721 // Declared before `callback_state` so that it is dropped after it, also when a
722 // failed assertion unwinds: dropping a session waits until no context is held.
723 let _session;
724 // Nothing listens on this path, so any attempt to connect to Perfetto fails.
725 let mut callback_state = CallbackState::new("/nonexistent/perfetto-consumer".into());
726 {
727 let _lock = lock_trace_engine();
728 assert!(ProlongedContext::acquire().is_none(), "a trace session is running");
729 // A Started read while the session was still starting, before trace-engine made
730 // the context count nonzero.
731 if let Err(e) = callback_state.on_state_change(TraceState::Started, current_task) {
732 panic!("first Started: {e:?}");
733 }
734 assert!(
735 callback_state.prolonged_context.is_none(),
736 "first Started: holds a context"
737 );
738 assert!(
739 callback_state.connection.is_none(),
740 "first Started: connected to Perfetto"
741 );
742 }
743 // `TraceSession::start()` takes `TRACE_ENGINE_LOCK` itself, so it is released above.
744 // Other tests may run in between, but they don't share `callback_state`.
745 _session = TraceSession::start();
746 // The session's start notification. Started now acquires a context and tries to
747 // enable Perfetto tracing, which fails to connect. Only that attempt can return an
748 // error, so the error shows that this Started got past `ProlongedContext::acquire()`.
749 assert!(
750 callback_state.on_state_change(TraceState::Started, current_task).is_err(),
751 "second Started didn't try to enable Perfetto tracing"
752 );
753 assert!(callback_state.prolonged_context.is_none(), "second Started kept the context");
754 })
755 .await;
756 }
757
758 #[fuchsia::test]
759 async fn trace_context_is_held_until_stopping() {
760 spawn_kernel_and_run(async |current_task| {
761 let _session = TraceSession::start();
762 // Nothing listens on this path, so any attempt to connect to Perfetto fails.
763 let mut callback_state = CallbackState::new("/nonexistent/perfetto-consumer".into());
764 // Stand in for a Started that enabled Perfetto tracing and opened a pid/koid session.
765 // Enabling it for real needs a Perfetto service, which this test doesn't have.
766 callback_state.prev_state = TraceState::Started;
767 callback_state.prolonged_context = ProlongedContext::acquire();
768 assert!(callback_state.prolonged_context.is_some(), "no trace session is running");
769 callback_state.session = Some(current_task.kernel().trace_event_manager.open());
770 // A repeated Started, and stale Stopped and Started states of an earlier session,
771 // keep the context and the pid/koid session, and don't touch Perfetto. The test feeds
772 // these states directly: the trace-engine timing that produces them (see
773 // `start_perfetto_consumer_thread()`) isn't reproduced here.
774 for state in
775 [TraceState::Started, TraceState::Stopped, TraceState::Started, TraceState::Stopped]
776 {
777 if let Err(e) = callback_state.on_state_change(state, current_task) {
778 panic!("{state:?}: {e:?}");
779 }
780 assert!(callback_state.prolonged_context.is_some(), "{state:?}: dropped context");
781 assert!(callback_state.session.is_some(), "{state:?}: dropped pid/koid session");
782 assert!(callback_state.connection.is_none(), "{state:?}: connected to Perfetto");
783 }
784 // Stopping takes the context to disable Perfetto tracing, even after a Stopped. Here
785 // it fails to connect to Perfetto, and drops the context anyway, so the trace session
786 // can stop when `_session` is dropped.
787 assert!(
788 callback_state.on_state_change(TraceState::Stopping, current_task).is_err(),
789 "Stopping didn't try to disable Perfetto tracing"
790 );
791 assert!(callback_state.prolonged_context.is_none(), "Stopping kept the context");
792 })
793 .await;
794 }
795
796 #[fuchsia::test]
797 async fn started_after_stopping_cleans_up_before_starting() {
798 spawn_kernel_and_run(async |current_task| {
799 let _session = TraceSession::start();
800 // Nothing listens on this path, so any attempt to connect to Perfetto fails.
801 let mut callback_state = CallbackState::new("/nonexistent/perfetto-consumer".into());
802 // Stand in for a previous session whose Stopped the observer missed, which left a
803 // pid/koid session and a partial packet behind, and whose Stopping released the
804 // context.
805 callback_state.prev_state = TraceState::Stopping;
806 callback_state.packet_data = vec![0; 8];
807 callback_state.session = Some(current_task.kernel().trace_event_manager.open());
808 // Started clears what is left over, then acquires a context and tries to enable
809 // Perfetto tracing, which fails to connect. Only that attempt can return an error,
810 // so the error shows that this Started got past `ProlongedContext::acquire()`.
811 assert!(
812 callback_state.on_state_change(TraceState::Started, current_task).is_err(),
813 "Started after Stopping didn't try to enable Perfetto tracing"
814 );
815 assert!(callback_state.packet_data.is_empty(), "Started kept the old packet data");
816 assert!(callback_state.session.is_none(), "Started kept the old pid/koid session");
817 assert!(callback_state.prolonged_context.is_none(), "Started kept the context");
818 })
819 .await;
820 }
821
822 #[fuchsia::test]
823 async fn started_that_fails_to_enable_perfetto_keeps_no_context() {
824 spawn_kernel_and_run(async |current_task| {
825 let _session = TraceSession::start();
826 // Nothing listens on this path, so any attempt to connect to Perfetto fails.
827 let mut callback_state = CallbackState::new("/nonexistent/perfetto-consumer".into());
828 // Started acquires a context, then fails to connect, so Perfetto tracing isn't
829 // enabled and the context must not be kept.
830 assert!(
831 callback_state.on_state_change(TraceState::Started, current_task).is_err(),
832 "Started connected to Perfetto"
833 );
834 assert!(callback_state.prolonged_context.is_none(), "Started kept the context");
835 assert!(callback_state.session.is_none(), "Started opened a pid/koid session");
836 // So Stopping has no Perfetto session to disable and doesn't try to connect.
837 if let Err(e) = callback_state.on_state_change(TraceState::Stopping, current_task) {
838 panic!("Stopping: {e:?}");
839 }
840 assert!(callback_state.connection.is_none(), "Stopping connected to Perfetto");
841 })
842 .await;
843 }
844
845 #[fuchsia::test]
846 fn test_blanket_category_enables_all() {
847 let ftrace_config = build_ftrace_config(|cat| cat == CATEGORY_ATRACE)
848 .expect("expected ftrace_config to be Some");
849 assert_eq!(ftrace_config.ftrace_events, vec!["ftrace/print".to_string()]);
850 assert_eq!(ftrace_config.atrace_categories.len(), ALL_ATRACE_CATEGORIES.len() + 1);
851 for &(atrace_name, _) in ALL_ATRACE_CATEGORIES {
852 assert!(
853 ftrace_config.atrace_categories.iter().any(|c| c == atrace_name),
854 "missing category: {atrace_name}"
855 );
856 }
857 assert!(ftrace_config.atrace_categories.iter().any(|c| c == "app"));
858 assert_eq!(ftrace_config.atrace_apps, vec!["*".to_string()]);
859 }
860
861 #[fuchsia::test]
862 fn test_blanket_and_granular_combined_no_duplicates() {
863 let enabled: std::collections::HashSet<&str> =
864 [CATEGORY_ATRACE, "starnix:atrace:am", "starnix:atrace:gfx", CATEGORY_ATRACE_APPS]
865 .into();
866 let ftrace_config = build_ftrace_config(|cat| enabled.contains(cat))
867 .expect("expected ftrace_config to be Some");
868 assert_eq!(ftrace_config.atrace_categories.len(), ALL_ATRACE_CATEGORIES.len() + 1);
869 for &(atrace_name, _) in ALL_ATRACE_CATEGORIES {
870 assert_eq!(
871 ftrace_config.atrace_categories.iter().filter(|&c| c == atrace_name).count(),
872 1,
873 "category {atrace_name} should appear exactly once"
874 );
875 }
876 assert_eq!(
877 ftrace_config.atrace_categories.iter().filter(|&c| c == "app").count(),
878 1,
879 "category app should appear exactly once"
880 );
881 assert_eq!(ftrace_config.atrace_apps, vec!["*".to_string()]);
882 }
883
884 #[fuchsia::test]
885 fn test_granular_categories_without_apps() {
886 let enabled: std::collections::HashSet<&str> =
887 ["starnix:atrace:am", "starnix:atrace:gfx"].into();
888 let ftrace_config = build_ftrace_config(|cat| enabled.contains(cat))
889 .expect("expected ftrace_config to be Some");
890 assert_eq!(ftrace_config.atrace_categories, vec!["am".to_string(), "gfx".to_string()]);
891 assert!(ftrace_config.atrace_apps.is_empty());
892 }
893
894 #[fuchsia::test]
895 fn test_apps_category_enables_both_tag_and_apps() {
896 let ftrace_config = build_ftrace_config(|cat| cat == CATEGORY_ATRACE_APPS)
897 .expect("expected ftrace_config to be Some");
898 assert_eq!(ftrace_config.atrace_categories, vec!["app".to_string()]);
899 assert_eq!(ftrace_config.atrace_apps, vec!["*".to_string()]);
900 }
901
902 #[fuchsia::test]
903 fn test_granular_categories_with_apps() {
904 let enabled: std::collections::HashSet<&str> =
905 ["starnix:atrace:view", CATEGORY_ATRACE_APPS].into();
906 let ftrace_config = build_ftrace_config(|cat| enabled.contains(cat))
907 .expect("expected ftrace_config to be Some");
908 assert_eq!(ftrace_config.atrace_categories, vec!["view".to_string(), "app".to_string()]);
909 assert_eq!(ftrace_config.atrace_apps, vec!["*".to_string()]);
910 }
911
912 #[fuchsia::test]
913 fn test_no_categories_enabled() {
914 assert!(build_ftrace_config(|_| false).is_none());
915 }
916}