Skip to main content

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, &current_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, &current_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}