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 anyhow::bail;
8use fuchsia_trace::{
9 BufferingMode, ProlongedContext, TraceState, category_enabled, trace_state, trace_string_ref_t,
10};
11use fuchsia_trace_observer::TraceObserver;
12use futures::{SinkExt, StreamExt};
13use fxt::blob::{BlobHeader, BlobType};
14use perfetto_protos::perfetto::protos::trace_config::buffer_config::FillPolicy;
15use perfetto_protos::perfetto::protos::trace_config::{BufferConfig, DataSource};
16use perfetto_protos::perfetto::protos::{
17 DataSourceConfig, DisableTracingRequest, EnableTracingRequest, FreeBuffersRequest,
18 FtraceConfig, ReadBuffersRequest, TraceConfig, ipc_frame,
19};
20use perfetto_trace_protos::perfetto::protos::frame_timeline_event::{
21 ActualDisplayFrameStart, ActualSurfaceFrameStart, Event, ExpectedDisplayFrameStart,
22 ExpectedSurfaceFrameStart,
23};
24use perfetto_trace_protos::perfetto::protos::ftrace_event::Event::Print;
25use perfetto_trace_protos::perfetto::protos::trace_packet;
26use starnix_core::security;
27use starnix_core::task::dynamic_thread_spawner::SpawnRequestBuilder;
28use starnix_core::task::tracing::TracePerformanceEventManager;
29use starnix_core::task::{CurrentTask, Kernel};
30use starnix_core::vfs::FsString;
31use starnix_logging::{
32 CATEGORY_ATRACE, NAME_PERFETTO_BLOB, log_debug, log_error, log_info, log_warn,
33};
34use starnix_perfetto_trace_decoder::{decode_read_buffers_response, decode_trace, encode_trace};
35
36use starnix_uapi::errors::Errno;
37
38mod atrace;
39
40const PERFETTO_BUFFER_SIZE_KB: u32 = 63488;
41
42/// State needed to act upon trace state changes.
43struct CallbackState {
44 /// The previously observed trace state.
45 prev_state: TraceState,
46 /// Path to the Perfetto consumer socket.
47 socket_path: FsString,
48 /// Connection to the consumer socket, if it has been initialized. This gets initialized the
49 /// first time it is needed.
50 connection: Option<perfetto::Consumer>,
51 /// Prolonged trace context to prevent the Fuchsia trace session from terminating while reading
52 /// data from Perfetto.
53 prolonged_context: Option<ProlongedContext>,
54 /// Partial trace packet returned from Perfetto but not yet written to Fuchsia.
55 packet_data: Vec<u8>,
56
57 event_manager: TracePerformanceEventManager,
58}
59
60impl CallbackState {
61 fn connection(
62 &mut self,
63 current_task: &CurrentTask,
64 ) -> Result<&mut perfetto::Consumer, anyhow::Error> {
65 match self.connection {
66 None => {
67 self.connection =
68 Some(perfetto::Consumer::new(current_task, self.socket_path.as_ref())?);
69 Ok(self.connection.as_mut().unwrap())
70 }
71 Some(ref mut conn) => Ok(conn),
72 }
73 }
74
75 fn handle_stopped(&mut self) {
76 self.prolonged_context = None;
77 self.packet_data.clear();
78 self.event_manager.stop();
79 self.event_manager.clear();
80 }
81
82 fn on_state_change(
83 &mut self,
84 new_state: TraceState,
85 current_task: &CurrentTask,
86 ) -> Result<(), anyhow::Error> {
87 let prev_state = self.prev_state;
88 self.prev_state = new_state;
89 log_debug!(
90 "Perfetto consumer state change. new_state: {new_state:?}, prev_state: {prev_state:?}"
91 );
92 match new_state {
93 TraceState::Started => {
94 if prev_state != TraceState::Stopped {
95 // This means something unexpected has caused the trace_engine to change
96 // states faster than we're processing the trace observer events.
97 log_error!(
98 "Started received in {prev_state:?} state! Cleaning up then starting."
99 );
100 self.handle_stopped();
101 }
102 self.prolonged_context = ProlongedContext::acquire();
103 let connection = self.connection(current_task)?;
104 // A fixed set of data sources that may be of interest. As demand for other sources
105 // is found, add them here, and it may become worthwhile to allow this set to be
106 // configurable per trace session.
107 let mut data_sources = vec![
108 DataSource {
109 config: Some(DataSourceConfig {
110 name: Some("track_event".to_string()),
111 ..Default::default()
112 }),
113 ..Default::default()
114 },
115 DataSource {
116 config: Some(DataSourceConfig {
117 name: Some("android.surfaceflinger.frame".to_string()),
118 target_buffer: Some(0),
119 ..Default::default()
120 }),
121 ..Default::default()
122 },
123 DataSource {
124 config: Some(DataSourceConfig {
125 name: Some("android.surfaceflinger.frametimeline".to_string()),
126 target_buffer: Some(0),
127 ..Default::default()
128 }),
129 ..Default::default()
130 },
131 ];
132 if category_enabled(CATEGORY_ATRACE) {
133 data_sources.push(DataSource {
134 config: Some(DataSourceConfig {
135 name: Some("linux.ftrace".to_string()),
136 ftrace_config: Some(FtraceConfig {
137 ftrace_events: vec!["ftrace/print".to_string()],
138 // Enable all supported atrace categories. This could be improved
139 // in the future to be a subset that is configurable by each trace
140 // session.
141 atrace_categories: vec![
142 "am".to_string(),
143 "adb".to_string(),
144 "aidl".to_string(),
145 "dalvik".to_string(),
146 "audio".to_string(),
147 "binder_lock".to_string(),
148 "binder_driver".to_string(),
149 "bionic".to_string(),
150 "camera".to_string(),
151 "database".to_string(),
152 "gfx".to_string(),
153 "hal".to_string(),
154 "input".to_string(),
155 "network".to_string(),
156 "nnapi".to_string(),
157 "pm".to_string(),
158 "power".to_string(),
159 "rs".to_string(),
160 "res".to_string(),
161 "rro".to_string(),
162 "sched".to_string(),
163 "sm".to_string(),
164 "ss".to_string(),
165 "vibrator".to_string(),
166 "video".to_string(),
167 "view".to_string(),
168 "webview".to_string(),
169 "wm".to_string(),
170 ],
171 atrace_apps: vec!["*".to_string()],
172 ..Default::default()
173 }),
174 ..Default::default()
175 }),
176 ..Default::default()
177 });
178 }
179 connection.enable_tracing(
180 current_task,
181 EnableTracingRequest {
182 trace_config: Some(TraceConfig {
183 buffers: vec![BufferConfig {
184 size_kb: Some(PERFETTO_BUFFER_SIZE_KB),
185 fill_policy: Some(FillPolicy::Discard.into()),
186 ..Default::default()
187 }],
188 data_sources,
189 ..Default::default()
190 }),
191 attach_notification_only: None,
192 },
193 )?;
194 // Once tracing has started, notify the event manager so it can start tracking processes.
195 self.event_manager.start(current_task.kernel());
196 }
197 TraceState::Stopping => {
198 if prev_state != TraceState::Started {
199 // If we receive a stop request and we don't think we're actually tracing, our
200 // local state likely desynced from the global trace state. Clean up our state
201 // and ensure we're stopped so we re-synchronize.
202 log_error!("Stopping received in {prev_state:?} state! Cleaning up.");
203 self.handle_stopped();
204 return Ok(());
205 }
206
207 // We want to hold the prolonged context to ensure the trace session doesn't
208 // exit out from under us, but we also want to ensure we drop the prolonged
209 // context if we bail for whatever reason below.
210 let _local_prolonged_context = std::mem::replace(&mut self.prolonged_context, None);
211 let start_time = std::time::Instant::now();
212
213 let connection = self.connection(current_task)?;
214 let disable_request =
215 connection.disable_tracing(current_task, DisableTracingRequest {})?;
216 loop {
217 let frame = connection.next_frame_blocking(current_task)?;
218 if frame.request_id == Some(disable_request) {
219 break;
220 } else {
221 log_error!(
222 "Ignoring frame while looking for DisableTracingRequest: {frame:?}"
223 );
224 }
225 }
226
227 let read_buffers_request =
228 connection.read_buffers(current_task, ReadBuffersRequest {})?;
229
230 let blob_name_ref = {
231 let Some(context) = fuchsia_trace::Context::acquire() else {
232 bail!("Tracing stopped despite holding prolonged context");
233 };
234 context.register_string_literal(NAME_PERFETTO_BLOB)
235 };
236
237 // IPC responses may be spread across multiple frames, so loop until we get a
238 // message that indicates it is the last one. Additionally, if there are
239 // unrelated messages on the socket (e.g. leftover from a previous trace
240 // session), the loop will read past and ignore them.
241 loop {
242 let frame = self.connection(current_task)?.next_frame_blocking(current_task)?;
243 if frame.request_id != Some(read_buffers_request) {
244 continue;
245 } else {
246 log_debug!(
247 "perfetto_consumer ignoring frame while looking for ReadBuffersRequest {read_buffers_request}: {frame:?}"
248 );
249 }
250 if let Some(ipc_frame::Msg::MsgInvokeMethodReply(reply)) = &frame.msg {
251 if let Ok(response) = decode_read_buffers_response(
252 reply.reply_proto.as_deref().unwrap_or(&[]),
253 ) {
254 for slice in &response.slices {
255 if let Some(data) = &slice.data {
256 self.packet_data.extend(data);
257 }
258 if slice.last_slice_for_packet.unwrap_or(false) {
259 let mut blob_data = Vec::new();
260 // Packet field number = 1, length delimited type = 2.
261 blob_data.push(1 << 3 | 2);
262 // Push a varint encoded length.
263 // See https://protobuf.dev/programming-guides/encoding/
264 const HIGH_BIT: u8 = 0x80;
265 const LOW_SEVEN_BITS: usize = 0x7F;
266 let mut value = self.packet_data.len();
267 while value >= HIGH_BIT as usize {
268 blob_data.push((value & LOW_SEVEN_BITS) as u8 | HIGH_BIT);
269 value >>= 7;
270 }
271 blob_data.push(value as u8);
272 // `append` moves all data out of the passed Vec, so
273 // s.packet_data will be empty after this call.
274 blob_data.append(&mut self.packet_data);
275
276 // At this point blob_data is a full Perfetto Trace protobuf.
277 // Parse the data and replace the linux pids with their
278 // corresponding koid.
279 let rewritten =
280 self.rewrite_pids(&blob_data).unwrap_or(blob_data);
281
282 // Ignore a failure to write the packet here. We don't
283 // return immediately because we want to allow the
284 // remaining records to be recorded as dropped.
285 //
286 // Once we fill a buffer in oneshot mode, we expect to drop
287 // the remaining packets here.
288 //
289 // Rather than logging here, allow the trace system to
290 // aggregate the number of records dropped and we can query
291 // the trace system later to determine if we dropped
292 // records when it's more efficient to do so.
293 let _ = self.forward_packet(blob_name_ref, rewritten);
294 }
295 }
296 } else {
297 log_error!("perfetto_consumer cannot decode protobuf from {reply:?}");
298 }
299 if reply.has_more != Some(true) {
300 break;
301 }
302 } else {
303 log_error!(
304 "perfetto_consumer ignoring non-MsgInvokeMethodReply message: {frame:?}"
305 );
306 }
307 }
308 // The response to a free buffers request does not have anything meaningful,
309 // so we don't need to worry about tracking the request id to match to the
310 // response.
311 let _free_buffers_request_id = self
312 .connection(current_task)?
313 .free_buffers(current_task, FreeBuffersRequest { buffer_ids: vec![0] })?;
314 let elapsed = start_time.elapsed().as_millis();
315 log_info!(
316 "Perfetto frames copied, dropping prolonged trace context. Processing took {elapsed} ms"
317 );
318 }
319 TraceState::Stopped => {
320 self.handle_stopped();
321 }
322 }
323 Ok(())
324 }
325
326 // Forward `data` to the trace buffer by wrapping it in fxt blob records with the name
327 // `blob_name_ref`..
328 fn forward_packet(&self, blob_name_ref: trace_string_ref_t, data: Vec<u8>) -> Option<usize> {
329 // The blob data may be larger than what we can fit in a single record. If so, split it up
330 // over multiple chunks.
331 let mut bytes_written = 0;
332 let mut data_to_write = &data[..];
333
334 // We want to break the data into chunks:
335 // - Bigger chunks means less per-write overheader
336 // - Bigger chunks means less overhead due to blob meta
337 //
338 // However, too big and the blobs won't fit nicely into the trace buffer.
339 // The trace buffer is minimum 1MiB in size, so writing 4k at a time seems like a
340 // reasonable place to start that is both reasonably large and not going to leave a ton of
341 // space at the end of the trace buffer.
342 let max_chunk_size = 4096;
343 while !data_to_write.is_empty() {
344 let chunk_size = data_to_write.len().min(max_chunk_size);
345 let chunk = &data_to_write[..chunk_size];
346 self.forward_blob(blob_name_ref, &chunk)?;
347 data_to_write = &data_to_write[chunk_size..];
348 bytes_written += chunk_size;
349 }
350 Some(bytes_written)
351 }
352
353 // Given a blob name, wrap the data in an fxt perfetto blob and write it to the trace buffer.
354 fn forward_blob(&self, blob_name_ref: trace_string_ref_t, blob_data: &[u8]) -> Option<usize> {
355 let mut header = BlobHeader::empty();
356 header.set_name_ref(blob_name_ref.encoded_value);
357 header.set_payload_len(blob_data.len() as u16);
358 header.set_blob_format_type(BlobType::Perfetto.into());
359
360 let record_bytes = fxt::fxt_builder::FxtBuilder::new(header).atom(blob_data).build();
361 assert!(record_bytes.len() % std::mem::size_of::<u64>() == 0);
362 let num_words = record_bytes.len() / std::mem::size_of::<u64>();
363 let record_data = record_bytes.as_ptr();
364 #[allow(
365 clippy::undocumented_unsafe_blocks,
366 reason = "Force documented unsafe blocks in Starnix"
367 )]
368 let record_words =
369 unsafe { std::slice::from_raw_parts(record_data.cast::<u64>(), num_words) };
370
371 while let Some(context) = fuchsia_trace::Context::acquire() {
372 if let Some(bytes) = context.copy_record(record_words) {
373 return Some(bytes);
374 }
375 if context.buffering_mode() != BufferingMode::Streaming {
376 // If we're not in streaming mode, there will never be room for this record. Drop
377 // it.
378 return None;
379 }
380 // We're writing records pretty quick here, we're just forwarding data from
381 // perfetto with no breaks. trace_manager might not be able to keep up if it's also
382 // servicing other trace-providers. We want to back off we if find that we run out
383 // of space.
384 //
385 // We drop the context to decrement the refcount on the trace session. This allows
386 // trace-engine to switch the buffers if needed and drain out the buffers so that
387 // when we wake, there will hopefully be room.
388 //
389 // TODO(b/304532640)
390 drop(context);
391 std::thread::sleep(std::time::Duration::from_millis(100));
392 }
393 None
394 }
395
396 fn rewrite_pids(&mut self, protobuf_blob: &Vec<u8>) -> anyhow::Result<Vec<u8>> {
397 let mut proto = decode_trace(protobuf_blob.as_slice())?;
398 for p in &mut proto.packet {
399 if let Some(ref mut data) = p.data {
400 match data {
401 trace_packet::Data::FrameTimelineEvent(frame_timeline_event) => {
402 if let Some(evt) = &mut frame_timeline_event.event {
403 // Update the linux pid to the Fuchsia pid. Each event has its own
404 // match arm since the variant data is of a different type for each event.
405 match evt {
406 Event::ExpectedDisplayFrameStart(ExpectedDisplayFrameStart {
407 pid,
408 ..
409 })
410 | Event::ActualDisplayFrameStart(ActualDisplayFrameStart {
411 pid,
412 ..
413 })
414 | Event::ExpectedSurfaceFrameStart(ExpectedSurfaceFrameStart {
415 pid,
416 ..
417 })
418 | Event::ActualSurfaceFrameStart(ActualSurfaceFrameStart {
419 pid,
420 ..
421 }) => {
422 pid.as_mut().map(|pid| {
423 *pid = self.map_to_koid_val(*pid);
424 });
425 }
426 Event::FrameEnd(_frame_end) => {}
427 }
428 }
429 }
430 trace_packet::Data::FtraceEvents(ftrace_bundle) => {
431 for evt in &mut ftrace_bundle.event {
432 if let Some(ref mut pid) = evt.pid {
433 *pid = self.map_thread_to_koid_val(*pid as i32) as u32;
434 }
435 if let Some(ref mut event_data) = evt.event {
436 match event_data {
437 Print(print) => {
438 if let Some(ref mut data) = print.buf {
439 *data = self.map_print_event(data)
440 }
441 }
442 _ => (),
443 }
444 }
445 }
446 }
447 // No need to process other data; we only fixup data that references the pid.
448 _ => (),
449 }
450 }
451 }
452 Ok(encode_trace(&proto))
453 }
454
455 fn map_print_event(&mut self, data: &String) -> String {
456 if let Some(mut event) = atrace::ATraceEvent::parse(&data) {
457 match event {
458 atrace::ATraceEvent::Begin { ref mut pid, .. }
459 | atrace::ATraceEvent::End { ref mut pid }
460 | atrace::ATraceEvent::Instant { ref mut pid, .. }
461 | atrace::ATraceEvent::AsyncBegin { ref mut pid, .. }
462 | atrace::ATraceEvent::AsyncEnd { ref mut pid, .. }
463 | atrace::ATraceEvent::Counter { ref mut pid, .. }
464 | atrace::ATraceEvent::AsyncTrackBegin { ref mut pid, .. }
465 | atrace::ATraceEvent::AsyncTrackEnd { ref mut pid, .. }
466 | atrace::ATraceEvent::Track { ref mut pid, .. } => {
467 *pid = self.map_to_koid_val(*pid as i32) as u64
468 }
469 }
470 event.data()
471 } else {
472 data.to_string()
473 }
474 }
475
476 fn map_thread_to_koid_val(&mut self, pid: i32) -> i32 {
477 if pid == 0 {
478 return 0;
479 }
480 self.event_manager
481 .map_tid_to_koid(pid)
482 .map(|k| {
483 // Truncate the koid down to 32 bits in order to match the perfetto data schema. This is
484 // usually not an issue except for artificial koids which have the 2^63 bit set, such as
485 // virtual threads. This is consistent with the perfetto data importer code:
486 // https://github.com/google/perfetto/blob/c343c8a77c6e665c679e5c1ec845ac6dde0fc685/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc#L490
487 k.raw_koid() as i32
488 })
489 .unwrap_or(pid)
490 }
491
492 fn map_to_koid_val(&mut self, pid: i32) -> i32 {
493 if pid == 0 {
494 return 0;
495 }
496 self.event_manager
497 .map_pid_to_koid(pid)
498 .map(|k| {
499 // Truncate the koid down to 32 bits in order to match the perfetto data schema. This is
500 // usually not an issue except for artificial koids which have the 2^63 bit set, such as
501 // virtual threads. This is consistent with the perfetto data importer code:
502 // https://github.com/google/perfetto/blob/c343c8a77c6e665c679e5c1ec845ac6dde0fc685/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc#L490
503 k.raw_koid() as i32
504 })
505 .unwrap_or(pid)
506 }
507}
508
509pub fn start_perfetto_consumer_thread(kernel: &Kernel, socket_path: FsString) -> Result<(), Errno> {
510 let (mut tx, mut rx) = futures::channel::mpsc::channel::<TraceState>(32);
511
512 // Listens for trace state changes and sends them to Perfetto consumer thread.
513 // Unlike the perfetto thread, it won't block, so we can spawn it on the main async executor.
514 kernel.kthreads.spawn_future(
515 move || async move {
516 let observer = TraceObserver::new();
517 while let Ok(state) = observer.on_state_changed().await {
518 if let Err(e) = tx.send(state).await {
519 log_error!("perfetto-trace-observer failed to send trace state change: {:?}. Receiver dropped.", e);
520 return;
521 }
522 }
523 },
524 "perfetto-trace-observer",
525 );
526
527 // Perfetto consumer task: reads state changes from the channel and handles them.
528 // This task can block, so we spawn it on a dedicated thread to not block the observer or the
529 // main async executor.
530 let worker_closure = async move |current_task: &CurrentTask| {
531 let mut callback_state = CallbackState {
532 prev_state: TraceState::Stopped,
533 socket_path,
534 connection: None,
535 prolonged_context: None,
536 packet_data: Vec::new(),
537 event_manager: TracePerformanceEventManager::new(),
538 };
539
540 fn handle_state_change(
541 callback_state: &mut CallbackState,
542 current_task: &&CurrentTask,
543 state: TraceState,
544 ) -> Result<(), anyhow::Error> {
545 let current_task = current_task;
546 // TODO: https://fxbug.dev/457381697 - Revise how this kernel-internal work is security-
547 // checked.
548 let creds = security::creds_start_internal_operation(current_task);
549 current_task
550 .override_creds(creds, || callback_state.on_state_change(state, current_task))
551 }
552
553 // Check for tracing already started before we began observing.
554 // This happens when tracing is started on boot.
555 let mut state = trace_state();
556 if trace_state() == TraceState::Started {
557 const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
558 // When we do boot tracing, it is possible (even likely), that starnix has started but
559 // perfetto may not be ready to be connected to yet.
560 // In that case poll until it has started.
561 loop {
562 match handle_state_change(&mut callback_state, ¤t_task, state) {
563 Ok(_) => break, // Success, exit loop.
564 Err(e) => {
565 if let Some(errno) = e.downcast_ref::<Errno>() {
566 if errno == &starnix_uapi::errors::ENOENT
567 || errno == &starnix_uapi::errors::ECONNREFUSED
568 {
569 log_warn!(
570 "perfetto_consumer initial start tracing failed because perfetto socket connection not established: {e:?} retrying in 5 seconds..."
571 );
572 std::thread::sleep(RETRY_DELAY);
573 callback_state.prev_state = TraceState::Stopped;
574 callback_state.connection = None;
575 callback_state.prolonged_context = None;
576 state = trace_state();
577 continue; // Retry
578 }
579 }
580 // For any other error, log and exit loop.
581 log_error!(
582 "perfetto_consumer initial start tracing failed with error: {e:?}"
583 );
584 break;
585 }
586 }
587 }
588 }
589
590 while let Some(state) = rx.next().await {
591 handle_state_change(&mut callback_state, ¤t_task, state).unwrap_or_else(|e| {
592 log_error!("perfetto_consumer state change callback error: {:?}", e);
593 })
594 }
595 };
596 let worker_req = SpawnRequestBuilder::new()
597 .with_debug_name("perfetto-consumer")
598 .with_async_closure(worker_closure)
599 .build();
600 kernel.kthreads.spawner().spawn_from_request(worker_req);
601
602 Ok(())
603}