1use super::{
2 audience_guard::{AudienceGuard, Authorized, IdKind},
3 lakehouse_context::LakehouseContext,
4 partition_cache::QueryPartitionProvider,
5 process_streams::get_process_thread_list,
6 read_scope::CallerContext,
7 session_configurator::NoOpSessionConfigurator,
8 view_factory::ViewFactory,
9};
10use crate::dfext::{
11 string_column_accessor::string_column_by_name, typed_column::typed_column_by_name,
12};
13use crate::time::TimeRange;
14use async_stream::stream;
15use datafusion::{
16 arrow::{
17 array::{RecordBatch, TimestampNanosecondArray, UInt32Array},
18 datatypes::SchemaRef,
19 },
20 catalog::{Session, TableProvider},
21 common::Result as DFResult,
22 error::DataFusionError,
23 execution::{SendableRecordBatchStream, TaskContext},
24 logical_expr::{Expr, TableType},
25 physical_expr::EquivalenceProperties,
26 physical_plan::{
27 DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties,
28 execution_plan::{Boundedness, EmissionType},
29 limit::GlobalLimitExec,
30 stream::RecordBatchStreamAdapter,
31 },
32};
33use futures::{StreamExt, TryStreamExt, stream};
34use micromegas_perfetto::{chunk_sender::ChunkSender, streaming_writer::PerfettoWriter};
35use micromegas_tracing::prelude::*;
36use std::{
37 fmt::{self, Debug, Formatter},
38 sync::Arc,
39};
40use uuid::Uuid;
41
42pub use super::process_spans_table_function::SpanTypes;
43
44#[derive(Debug, thiserror::Error)]
50#[error("Process {0} not found")]
51struct ProcessNotFoundError(String);
52
53pub struct PerfettoTraceExecutionPlan {
55 schema: SchemaRef,
56 process_id: String,
57 process_uuid: Uuid,
58 span_types: SpanTypes,
59 time_range: TimeRange,
60 lakehouse: Arc<LakehouseContext>,
61 view_factory: Arc<ViewFactory>,
62 part_provider: Arc<dyn QueryPartitionProvider>,
63 guard: Arc<AudienceGuard>,
64 authorized: Option<Authorized>,
68 properties: Arc<PlanProperties>,
69}
70
71impl PerfettoTraceExecutionPlan {
72 #[allow(clippy::too_many_arguments)]
73 pub(crate) fn new(
74 schema: SchemaRef,
75 process_id: String,
76 process_uuid: Uuid,
77 span_types: SpanTypes,
78 time_range: TimeRange,
79 lakehouse: Arc<LakehouseContext>,
80 view_factory: Arc<ViewFactory>,
81 part_provider: Arc<dyn QueryPartitionProvider>,
82 guard: Arc<AudienceGuard>,
83 ) -> Self {
84 let properties = PlanProperties::new(
85 EquivalenceProperties::new(schema.clone()),
86 Partitioning::UnknownPartitioning(1),
87 EmissionType::Final,
88 Boundedness::Bounded,
89 );
90
91 Self {
92 schema,
93 process_id,
94 process_uuid,
95 span_types,
96 time_range,
97 lakehouse,
98 view_factory,
99 part_provider,
100 guard,
101 authorized: None,
102 properties: Arc::new(properties),
103 }
104 }
105
106 fn with_authorized(&self, authorized: Authorized) -> Self {
108 Self {
109 schema: self.schema.clone(),
110 process_id: self.process_id.clone(),
111 process_uuid: self.process_uuid,
112 span_types: self.span_types,
113 time_range: self.time_range,
114 lakehouse: self.lakehouse.clone(),
115 view_factory: self.view_factory.clone(),
116 part_provider: self.part_provider.clone(),
117 guard: self.guard.clone(),
118 authorized: Some(authorized),
119 properties: self.properties.clone(),
120 }
121 }
122}
123
124impl Debug for PerfettoTraceExecutionPlan {
125 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
126 f.debug_struct("PerfettoTraceExecutionPlan")
127 .field("process_id", &self.process_id)
128 .field("span_types", &self.span_types)
129 .field("time_range", &self.time_range)
130 .finish()
131 }
132}
133
134impl DisplayAs for PerfettoTraceExecutionPlan {
135 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
136 write!(
137 f,
138 "PerfettoTraceExecutionPlan: process_id={}, span_types={:?}, time_range={}..{}",
139 self.process_id, self.span_types, self.time_range.begin, self.time_range.end
140 )
141 }
142}
143
144impl ExecutionPlan for PerfettoTraceExecutionPlan {
145 fn name(&self) -> &str {
146 "PerfettoTraceExecutionPlan"
147 }
148
149 fn schema(&self) -> SchemaRef {
150 self.schema.clone()
151 }
152
153 fn properties(&self) -> &Arc<PlanProperties> {
154 &self.properties
155 }
156
157 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
158 vec![]
159 }
160
161 fn with_new_children(
162 self: Arc<Self>,
163 _children: Vec<Arc<dyn ExecutionPlan>>,
164 ) -> DFResult<Arc<dyn ExecutionPlan>> {
165 Ok(self)
166 }
167
168 #[span_fn]
169 fn execute(
170 &self,
171 _partition: usize,
172 _context: Arc<TaskContext>,
173 ) -> DFResult<SendableRecordBatchStream> {
174 let schema = self.schema.clone();
175 let process_id = self.process_id.clone();
176 let span_types = self.span_types;
177 let time_range = self.time_range;
178 let lakehouse = self.lakehouse.clone();
179 let view_factory = self.view_factory.clone();
180 let part_provider = self.part_provider.clone();
181 let Some(authorized) = &self.authorized else {
183 return Err(DataFusionError::Internal(
184 "perfetto_trace_chunks: unauthorized plan (no witness from scan)".into(),
185 ));
186 };
187 let caller = authorized.internal_caller();
188
189 let stream = generate_perfetto_trace_stream(
191 process_id,
192 span_types,
193 time_range,
194 lakehouse,
195 view_factory,
196 part_provider,
197 caller,
198 );
199
200 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
201 }
202
203 fn apply_expressions(
204 &self,
205 _f: &mut dyn FnMut(
206 &Arc<dyn datafusion::physical_plan::PhysicalExpr>,
207 ) -> DFResult<datafusion::common::tree_node::TreeNodeRecursion>,
208 ) -> DFResult<datafusion::common::tree_node::TreeNodeRecursion> {
209 Ok(datafusion::common::tree_node::TreeNodeRecursion::Continue)
210 }
211}
212
213#[span_fn]
215fn generate_perfetto_trace_stream(
216 process_id: String,
217 span_types: SpanTypes,
218 time_range: TimeRange,
219 lakehouse: Arc<LakehouseContext>,
220 view_factory: Arc<ViewFactory>,
221 part_provider: Arc<dyn QueryPartitionProvider>,
222 caller: CallerContext,
223) -> impl futures::Stream<Item = DFResult<RecordBatch>> {
224 stream! {
225 const CHUNK_SIZE: usize = 8 * 1024; let (chunk_sender, mut chunk_receiver) = tokio::sync::mpsc::channel(16);
228
229 let chunk_sender_writer = ChunkSender::new(chunk_sender, CHUNK_SIZE);
231
232 let generation_task = spawn_with_context(async move {
234 generate_streaming_perfetto_trace(
235 chunk_sender_writer,
236 process_id,
237 span_types,
238 time_range,
239 lakehouse,
240 view_factory,
241 part_provider,
242 caller,
243 ).await
244 });
245
246 while let Some(chunk_result) = chunk_receiver.recv().await {
248 match chunk_result {
249 Ok(batch) => yield Ok(batch),
250 Err(e) => {
251 error!("Error in chunk generation: {:?}", e);
255 yield Err(datafusion::error::DataFusionError::Internal(
256 format!("Chunk generation failed: {}", e)
257 ));
258 return;
259 }
260 }
261 }
262
263 match generation_task.await {
265 Ok(Ok(())) => {}, Ok(Err(e)) => {
267 if e.downcast_ref::<ProcessNotFoundError>().is_some() {
268 warn!("Trace generation failed: {:?}", e);
270 yield Err(datafusion::error::DataFusionError::Execution(
271 format!("Trace generation failed: {}", e)
272 ));
273 } else {
274 error!("Trace generation failed: {:?}", e);
277 yield Err(datafusion::error::DataFusionError::Internal(
278 format!("Trace generation failed: {}", e)
279 ));
280 }
281 }
282 Err(e) => {
283 error!("Task panicked: {:?}", e);
284 yield Err(datafusion::error::DataFusionError::Internal(
285 format!("Task panicked: {}", e)
286 ));
287 }
288 }
289 }
290}
291
292#[allow(clippy::too_many_arguments)]
294async fn generate_streaming_perfetto_trace(
295 chunk_sender: ChunkSender,
296 process_id: String,
297 span_types: SpanTypes,
298 time_range: TimeRange,
299 lakehouse: Arc<LakehouseContext>,
300 view_factory: Arc<ViewFactory>,
301 part_provider: Arc<dyn QueryPartitionProvider>,
302 caller: CallerContext,
303) -> anyhow::Result<()> {
304 info!(
305 "Generating streaming Perfetto trace for process {} with span types {:?} from {} to {}",
306 process_id, span_types, time_range.begin, time_range.end
307 );
308
309 let ctx = super::query::make_session_context(
315 lakehouse,
316 part_provider,
317 Some(TimeRange {
318 begin: time_range.begin,
319 end: time_range.end,
320 }),
321 view_factory,
322 Arc::new(NoOpSessionConfigurator),
323 caller,
324 )
325 .await?;
326
327 let mut writer = PerfettoWriter::new(Box::new(chunk_sender), &process_id);
329
330 let process_exe = get_process_exe(&process_id, &ctx).await?;
331 writer.emit_process_descriptor(&process_exe).await?;
332 writer.flush().await?; let threads = get_process_thread_list(&process_id, &ctx).await?;
335 for (stream_id, thread_id, thread_name) in &threads {
336 writer
337 .emit_thread_descriptor(stream_id, *thread_id, thread_name)
338 .await?;
339 }
340 if !threads.is_empty() {
341 writer.flush().await?; }
343
344 if matches!(span_types, SpanTypes::Async | SpanTypes::Both) {
345 writer.emit_async_track_descriptor().await?;
346 writer.flush().await?; }
348
349 if matches!(span_types, SpanTypes::Thread | SpanTypes::Both) {
350 generate_thread_spans_with_writer(&mut writer, &ctx, &time_range, &threads).await?;
351 }
352
353 if matches!(span_types, SpanTypes::Async | SpanTypes::Both) {
354 generate_async_spans_with_writer(&mut writer, &process_id, &ctx, &time_range).await?;
355 }
356
357 writer.flush().await?; Ok(())
359}
360
361async fn get_process_exe(
363 process_id: &str,
364 ctx: &datafusion::execution::context::SessionContext,
365) -> anyhow::Result<String> {
366 let sql = format!(
367 r#"
368 SELECT exe
369 FROM processes
370 WHERE process_id = '{}'
371 LIMIT 1
372 "#,
373 process_id
374 );
375
376 let df = ctx.sql(&sql).await?;
377 let batches = df.collect().await?;
378
379 if batches.is_empty() || batches[0].num_rows() == 0 {
380 return Err(ProcessNotFoundError(process_id.to_owned()).into());
381 }
382
383 let exes = string_column_by_name(&batches[0], "exe")?;
384 Ok(exes.value(0)?.to_owned())
385}
386
387fn format_thread_spans_query(stream_id: &str, time_range: &TimeRange) -> String {
389 format!(
390 r#"
391 SELECT "begin", "end", name, filename, target, line
392 FROM view_instance('thread_spans', '{}')
393 WHERE begin <= TIMESTAMP '{}'
394 AND end >= TIMESTAMP '{}'
395 ORDER BY begin
396 "#,
397 stream_id,
398 time_range.end.to_rfc3339(),
399 time_range.begin.to_rfc3339()
400 )
401}
402
403async fn generate_thread_spans_with_writer(
415 writer: &mut PerfettoWriter,
416 ctx: &datafusion::execution::context::SessionContext,
417 time_range: &TimeRange,
418 threads: &[(String, i32, String)],
419) -> anyhow::Result<()> {
420 let max_concurrent = std::thread::available_parallelism()
421 .map(|n| n.get())
422 .unwrap_or(4);
423
424 let queries: Vec<(String, String)> = threads
426 .iter()
427 .map(|(stream_id, _, _)| {
428 (
429 stream_id.clone(),
430 format_thread_spans_query(stream_id, time_range),
431 )
432 })
433 .collect();
434
435 let streams: Vec<(String, SendableRecordBatchStream)> = stream::iter(queries)
437 .map(|(stream_id, sql)| {
438 let ctx = ctx.clone();
439 async move {
440 spawn_with_context(async move {
441 let df = ctx.sql(&sql).await?;
442 let stream = df.execute_stream().await?;
443 Ok::<_, anyhow::Error>((stream_id, stream))
444 })
445 .await?
446 }
447 })
448 .buffered(max_concurrent)
449 .try_collect()
450 .await?;
451
452 for (stream_id, data_stream) in streams {
454 write_thread_spans(writer, &stream_id, data_stream).await?;
455 }
456 Ok(())
457}
458
459pub async fn write_thread_spans(
468 writer: &mut PerfettoWriter,
469 stream_id: &str,
470 mut data_stream: SendableRecordBatchStream,
471) -> anyhow::Result<()> {
472 writer.set_current_thread(stream_id);
473
474 let mut previous_begin_ns: Option<i64> = None;
475 let mut span_count = 0;
476 while let Some(batch_result) = data_stream.next().await {
477 let batch = batch_result?;
478 let begin_times: &TimestampNanosecondArray = typed_column_by_name(&batch, "begin")?;
479 let end_times: &TimestampNanosecondArray = typed_column_by_name(&batch, "end")?;
480 let names = string_column_by_name(&batch, "name")?;
481 let filenames = string_column_by_name(&batch, "filename")?;
482 let targets = string_column_by_name(&batch, "target")?;
483 let lines: &UInt32Array = typed_column_by_name(&batch, "line")?;
484
485 for i in 0..batch.num_rows() {
486 let begin_time = begin_times.value(i);
487 if let Some(previous) = previous_begin_ns
488 && begin_time < previous
489 {
490 anyhow::bail!(
491 "thread spans out of order for stream {stream_id}: begin {begin_time} follows {previous}"
492 );
493 }
494 previous_begin_ns = Some(begin_time);
495
496 let begin_ns = begin_time as u64;
497 let end_ns = end_times.value(i) as u64;
498 let name = names.value(i)?;
499 let filename = filenames.value(i)?;
500 let target = targets.value(i)?;
501 let line = lines.value(i);
502
503 writer
504 .emit_span(begin_ns, end_ns, name, target, filename, line)
505 .await?;
506
507 span_count += 1;
508 if span_count % 10 == 0 {
509 writer.flush().await?;
510 }
511 }
512 }
513 Ok(())
514}
515
516async fn generate_async_spans_with_writer(
518 writer: &mut PerfettoWriter,
519 process_id: &str,
520 ctx: &datafusion::execution::context::SessionContext,
521 time_range: &TimeRange,
522) -> anyhow::Result<()> {
523 let sql = format!(
524 r#"
525 WITH begin_events AS (
526 SELECT span_id, time as begin_time, name, filename, target, line
527 FROM view_instance('async_events', '{}')
528 WHERE time >= TIMESTAMP '{}'
529 AND time <= TIMESTAMP '{}'
530 AND event_type = 'begin'
531 ),
532 end_events AS (
533 SELECT span_id, time as end_time
534 FROM view_instance('async_events', '{}')
535 WHERE time >= TIMESTAMP '{}'
536 AND time <= TIMESTAMP '{}'
537 AND event_type = 'end'
538 )
539 SELECT
540 b.span_id,
541 b.begin_time,
542 e.end_time,
543 b.name,
544 b.filename,
545 b.target,
546 b.line
547 FROM begin_events b
548 INNER JOIN end_events e ON b.span_id = e.span_id
549 ORDER BY b.begin_time
550 "#,
551 process_id,
552 time_range.begin.to_rfc3339(),
553 time_range.end.to_rfc3339(),
554 process_id,
555 time_range.begin.to_rfc3339(),
556 time_range.end.to_rfc3339(),
557 );
558
559 let df = ctx.sql(&sql).await?;
560 let mut stream = df.execute_stream().await?;
561
562 let mut span_count = 0;
563 while let Some(batch_result) = stream.next().await {
564 let batch = batch_result?;
565 let span_ids: &datafusion::arrow::array::Int64Array =
566 typed_column_by_name(&batch, "span_id")?;
567 let begin_times: &TimestampNanosecondArray = typed_column_by_name(&batch, "begin_time")?;
568 let end_times: &TimestampNanosecondArray = typed_column_by_name(&batch, "end_time")?;
569 let names = string_column_by_name(&batch, "name")?;
570 let filenames = string_column_by_name(&batch, "filename")?;
571 let targets = string_column_by_name(&batch, "target")?;
572 let lines: &UInt32Array = typed_column_by_name(&batch, "line")?;
573 for i in 0..batch.num_rows() {
574 let _span_id = span_ids.value(i);
575 let begin_ns = begin_times.value(i) as u64;
576 let end_ns = end_times.value(i) as u64;
577 let name = names.value(i)?;
578 let filename = filenames.value(i)?;
579 let target = targets.value(i)?;
580 let line = lines.value(i);
581
582 if begin_ns < end_ns {
583 writer
585 .emit_async_span_begin(begin_ns, name, target, filename, line)
586 .await?;
587 writer
588 .emit_async_span_end(end_ns, name, target, filename, line)
589 .await?;
590
591 span_count += 1;
592 if span_count % 10 == 0 {
594 writer.flush().await?;
595 }
596 } else {
597 warn!("Skipping async span with invalid duration");
598 }
599 }
600 }
601
602 Ok(())
603}
604
605#[derive(Debug)]
607pub struct PerfettoTraceTableProvider {
608 execution_plan: Arc<PerfettoTraceExecutionPlan>,
609}
610
611impl PerfettoTraceTableProvider {
612 pub(crate) fn new(execution_plan: Arc<PerfettoTraceExecutionPlan>) -> Self {
616 Self { execution_plan }
617 }
618}
619
620#[async_trait::async_trait]
621impl TableProvider for PerfettoTraceTableProvider {
622 fn schema(&self) -> SchemaRef {
623 self.execution_plan.schema()
624 }
625
626 fn table_type(&self) -> TableType {
627 TableType::Base
628 }
629
630 async fn scan(
631 &self,
632 _state: &dyn Session,
633 _projection: Option<&Vec<usize>>,
634 _filters: &[Expr],
635 limit: Option<usize>,
636 ) -> DFResult<Arc<dyn ExecutionPlan>> {
637 let authorized = self
638 .execution_plan
639 .guard
640 .authorize(
641 self.execution_plan.process_uuid,
642 IdKind::Process,
643 "perfetto_trace_chunks",
644 )
645 .await?;
646 let plan: Arc<dyn ExecutionPlan> =
650 Arc::new(self.execution_plan.with_authorized(authorized));
651 if let Some(fetch) = limit {
652 Ok(Arc::new(GlobalLimitExec::new(plan, 0, Some(fetch))))
653 } else {
654 Ok(plan)
655 }
656 }
657}