1use super::{
2 lakehouse_context::LakehouseContext, partition_cache::QueryPartitionProvider,
3 process_streams::get_process_thread_list, session_configurator::NoOpSessionConfigurator,
4 view_factory::ViewFactory,
5};
6use crate::dfext::{
7 string_column_accessor::string_column_by_name, typed_column::typed_column_by_name,
8};
9use crate::time::TimeRange;
10use async_stream::stream;
11use datafusion::{
12 arrow::{
13 array::{RecordBatch, TimestampNanosecondArray, UInt32Array},
14 datatypes::SchemaRef,
15 },
16 catalog::{Session, TableProvider},
17 common::Result as DFResult,
18 execution::{SendableRecordBatchStream, TaskContext},
19 logical_expr::{Expr, TableType},
20 physical_expr::EquivalenceProperties,
21 physical_plan::{
22 DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties,
23 execution_plan::{Boundedness, EmissionType},
24 limit::GlobalLimitExec,
25 stream::RecordBatchStreamAdapter,
26 },
27};
28use futures::{StreamExt, TryStreamExt, stream};
29use micromegas_perfetto::{chunk_sender::ChunkSender, streaming_writer::PerfettoWriter};
30use micromegas_tracing::prelude::*;
31use std::{
32 fmt::{self, Debug, Formatter},
33 sync::Arc,
34};
35
36pub use super::process_spans_table_function::SpanTypes;
37
38pub struct PerfettoTraceExecutionPlan {
40 schema: SchemaRef,
41 process_id: String,
42 span_types: SpanTypes,
43 time_range: TimeRange,
44 lakehouse: Arc<LakehouseContext>,
45 view_factory: Arc<ViewFactory>,
46 part_provider: Arc<dyn QueryPartitionProvider>,
47 properties: Arc<PlanProperties>,
48}
49
50impl PerfettoTraceExecutionPlan {
51 pub fn new(
52 schema: SchemaRef,
53 process_id: String,
54 span_types: SpanTypes,
55 time_range: TimeRange,
56 lakehouse: Arc<LakehouseContext>,
57 view_factory: Arc<ViewFactory>,
58 part_provider: Arc<dyn QueryPartitionProvider>,
59 ) -> Self {
60 let properties = PlanProperties::new(
61 EquivalenceProperties::new(schema.clone()),
62 Partitioning::UnknownPartitioning(1),
63 EmissionType::Final,
64 Boundedness::Bounded,
65 );
66
67 Self {
68 schema,
69 process_id,
70 span_types,
71 time_range,
72 lakehouse,
73 view_factory,
74 part_provider,
75 properties: Arc::new(properties),
76 }
77 }
78}
79
80impl Debug for PerfettoTraceExecutionPlan {
81 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
82 f.debug_struct("PerfettoTraceExecutionPlan")
83 .field("process_id", &self.process_id)
84 .field("span_types", &self.span_types)
85 .field("time_range", &self.time_range)
86 .finish()
87 }
88}
89
90impl DisplayAs for PerfettoTraceExecutionPlan {
91 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
92 write!(
93 f,
94 "PerfettoTraceExecutionPlan: process_id={}, span_types={:?}, time_range={}..{}",
95 self.process_id, self.span_types, self.time_range.begin, self.time_range.end
96 )
97 }
98}
99
100impl ExecutionPlan for PerfettoTraceExecutionPlan {
101 fn name(&self) -> &str {
102 "PerfettoTraceExecutionPlan"
103 }
104
105 fn schema(&self) -> SchemaRef {
106 self.schema.clone()
107 }
108
109 fn properties(&self) -> &Arc<PlanProperties> {
110 &self.properties
111 }
112
113 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
114 vec![]
115 }
116
117 fn with_new_children(
118 self: Arc<Self>,
119 _children: Vec<Arc<dyn ExecutionPlan>>,
120 ) -> DFResult<Arc<dyn ExecutionPlan>> {
121 Ok(self)
122 }
123
124 #[span_fn]
125 fn execute(
126 &self,
127 _partition: usize,
128 _context: Arc<TaskContext>,
129 ) -> DFResult<SendableRecordBatchStream> {
130 let schema = self.schema.clone();
131 let process_id = self.process_id.clone();
132 let span_types = self.span_types;
133 let time_range = self.time_range;
134 let lakehouse = self.lakehouse.clone();
135 let view_factory = self.view_factory.clone();
136 let part_provider = self.part_provider.clone();
137
138 let stream = generate_perfetto_trace_stream(
140 process_id,
141 span_types,
142 time_range,
143 lakehouse,
144 view_factory,
145 part_provider,
146 );
147
148 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
149 }
150}
151
152#[span_fn]
154fn generate_perfetto_trace_stream(
155 process_id: String,
156 span_types: SpanTypes,
157 time_range: TimeRange,
158 lakehouse: Arc<LakehouseContext>,
159 view_factory: Arc<ViewFactory>,
160 part_provider: Arc<dyn QueryPartitionProvider>,
161) -> impl futures::Stream<Item = DFResult<RecordBatch>> {
162 stream! {
163 const CHUNK_SIZE: usize = 8 * 1024; let (chunk_sender, mut chunk_receiver) = tokio::sync::mpsc::channel(16);
166
167 let chunk_sender_writer = ChunkSender::new(chunk_sender, CHUNK_SIZE);
169
170 let generation_task = spawn_with_context(async move {
172 generate_streaming_perfetto_trace(
173 chunk_sender_writer,
174 process_id,
175 span_types,
176 time_range,
177 lakehouse,
178 view_factory,
179 part_provider,
180 ).await
181 });
182
183 while let Some(chunk_result) = chunk_receiver.recv().await {
185 match chunk_result {
186 Ok(batch) => yield Ok(batch),
187 Err(e) => {
188 error!("Error in chunk generation: {:?}", e);
189 yield Err(datafusion::error::DataFusionError::Execution(
190 format!("Chunk generation failed: {}", e)
191 ));
192 return;
193 }
194 }
195 }
196
197 match generation_task.await {
199 Ok(Ok(())) => {}, Ok(Err(e)) => {
201 error!("Trace generation failed: {:?}", e);
202 yield Err(datafusion::error::DataFusionError::Execution(
203 format!("Trace generation failed: {}", e)
204 ));
205 }
206 Err(e) => {
207 error!("Task panicked: {:?}", e);
208 yield Err(datafusion::error::DataFusionError::Execution(
209 format!("Task panicked: {}", e)
210 ));
211 }
212 }
213 }
214}
215
216async fn generate_streaming_perfetto_trace(
218 chunk_sender: ChunkSender,
219 process_id: String,
220 span_types: SpanTypes,
221 time_range: TimeRange,
222 lakehouse: Arc<LakehouseContext>,
223 view_factory: Arc<ViewFactory>,
224 part_provider: Arc<dyn QueryPartitionProvider>,
225) -> anyhow::Result<()> {
226 info!(
227 "Generating streaming Perfetto trace for process {} with span types {:?} from {} to {}",
228 process_id, span_types, time_range.begin, time_range.end
229 );
230
231 let ctx = super::query::make_session_context(
233 lakehouse,
234 part_provider,
235 Some(TimeRange {
236 begin: time_range.begin,
237 end: time_range.end,
238 }),
239 view_factory,
240 Arc::new(NoOpSessionConfigurator),
241 false,
242 )
243 .await?;
244
245 let mut writer = PerfettoWriter::new(Box::new(chunk_sender), &process_id);
247
248 let process_exe = get_process_exe(&process_id, &ctx).await?;
249 writer.emit_process_descriptor(&process_exe).await?;
250 writer.flush().await?; let threads = get_process_thread_list(&process_id, &ctx).await?;
253 for (stream_id, thread_id, thread_name) in &threads {
254 writer
255 .emit_thread_descriptor(stream_id, *thread_id, thread_name)
256 .await?;
257 }
258 if !threads.is_empty() {
259 writer.flush().await?; }
261
262 if matches!(span_types, SpanTypes::Async | SpanTypes::Both) {
263 writer.emit_async_track_descriptor().await?;
264 writer.flush().await?; }
266
267 if matches!(span_types, SpanTypes::Thread | SpanTypes::Both) {
268 generate_thread_spans_with_writer(&mut writer, &ctx, &time_range, &threads).await?;
269 }
270
271 if matches!(span_types, SpanTypes::Async | SpanTypes::Both) {
272 generate_async_spans_with_writer(&mut writer, &process_id, &ctx, &time_range).await?;
273 }
274
275 writer.flush().await?; Ok(())
277}
278
279async fn get_process_exe(
281 process_id: &str,
282 ctx: &datafusion::execution::context::SessionContext,
283) -> anyhow::Result<String> {
284 let sql = format!(
285 r#"
286 SELECT exe
287 FROM processes
288 WHERE process_id = '{}'
289 LIMIT 1
290 "#,
291 process_id
292 );
293
294 let df = ctx.sql(&sql).await?;
295 let batches = df.collect().await?;
296
297 if batches.is_empty() || batches[0].num_rows() == 0 {
298 anyhow::bail!("Process {} not found", process_id);
299 }
300
301 let exes = string_column_by_name(&batches[0], "exe")?;
302 Ok(exes.value(0)?.to_owned())
303}
304
305fn format_thread_spans_query(stream_id: &str, time_range: &TimeRange) -> String {
307 format!(
308 r#"
309 SELECT "begin", "end", name, filename, target, line
310 FROM view_instance('thread_spans', '{}')
311 WHERE begin <= TIMESTAMP '{}'
312 AND end >= TIMESTAMP '{}'
313 ORDER BY begin
314 "#,
315 stream_id,
316 time_range.end.to_rfc3339(),
317 time_range.begin.to_rfc3339()
318 )
319}
320
321async fn generate_thread_spans_with_writer(
333 writer: &mut PerfettoWriter,
334 ctx: &datafusion::execution::context::SessionContext,
335 time_range: &TimeRange,
336 threads: &[(String, i32, String)],
337) -> anyhow::Result<()> {
338 let max_concurrent = std::thread::available_parallelism()
339 .map(|n| n.get())
340 .unwrap_or(4);
341
342 let queries: Vec<(String, String)> = threads
344 .iter()
345 .map(|(stream_id, _, _)| {
346 (
347 stream_id.clone(),
348 format_thread_spans_query(stream_id, time_range),
349 )
350 })
351 .collect();
352
353 let streams: Vec<(String, SendableRecordBatchStream)> = stream::iter(queries)
355 .map(|(stream_id, sql)| {
356 let ctx = ctx.clone();
357 async move {
358 spawn_with_context(async move {
359 let df = ctx.sql(&sql).await?;
360 let stream = df.execute_stream().await?;
361 Ok::<_, anyhow::Error>((stream_id, stream))
362 })
363 .await?
364 }
365 })
366 .buffered(max_concurrent)
367 .try_collect()
368 .await?;
369
370 for (stream_id, data_stream) in streams {
372 write_thread_spans(writer, &stream_id, data_stream).await?;
373 }
374 Ok(())
375}
376
377pub async fn write_thread_spans(
386 writer: &mut PerfettoWriter,
387 stream_id: &str,
388 mut data_stream: SendableRecordBatchStream,
389) -> anyhow::Result<()> {
390 writer.set_current_thread(stream_id);
391
392 let mut previous_begin_ns: Option<i64> = None;
393 let mut span_count = 0;
394 while let Some(batch_result) = data_stream.next().await {
395 let batch = batch_result?;
396 let begin_times: &TimestampNanosecondArray = typed_column_by_name(&batch, "begin")?;
397 let end_times: &TimestampNanosecondArray = typed_column_by_name(&batch, "end")?;
398 let names = string_column_by_name(&batch, "name")?;
399 let filenames = string_column_by_name(&batch, "filename")?;
400 let targets = string_column_by_name(&batch, "target")?;
401 let lines: &UInt32Array = typed_column_by_name(&batch, "line")?;
402
403 for i in 0..batch.num_rows() {
404 let begin_time = begin_times.value(i);
405 if let Some(previous) = previous_begin_ns
406 && begin_time < previous
407 {
408 anyhow::bail!(
409 "thread spans out of order for stream {stream_id}: begin {begin_time} follows {previous}"
410 );
411 }
412 previous_begin_ns = Some(begin_time);
413
414 let begin_ns = begin_time as u64;
415 let end_ns = end_times.value(i) as u64;
416 let name = names.value(i)?;
417 let filename = filenames.value(i)?;
418 let target = targets.value(i)?;
419 let line = lines.value(i);
420
421 writer
422 .emit_span(begin_ns, end_ns, name, target, filename, line)
423 .await?;
424
425 span_count += 1;
426 if span_count % 10 == 0 {
427 writer.flush().await?;
428 }
429 }
430 }
431 Ok(())
432}
433
434async fn generate_async_spans_with_writer(
436 writer: &mut PerfettoWriter,
437 process_id: &str,
438 ctx: &datafusion::execution::context::SessionContext,
439 time_range: &TimeRange,
440) -> anyhow::Result<()> {
441 let sql = format!(
442 r#"
443 WITH begin_events AS (
444 SELECT span_id, time as begin_time, name, filename, target, line
445 FROM view_instance('async_events', '{}')
446 WHERE time >= TIMESTAMP '{}'
447 AND time <= TIMESTAMP '{}'
448 AND event_type = 'begin'
449 ),
450 end_events AS (
451 SELECT span_id, time as end_time
452 FROM view_instance('async_events', '{}')
453 WHERE time >= TIMESTAMP '{}'
454 AND time <= TIMESTAMP '{}'
455 AND event_type = 'end'
456 )
457 SELECT
458 b.span_id,
459 b.begin_time,
460 e.end_time,
461 b.name,
462 b.filename,
463 b.target,
464 b.line
465 FROM begin_events b
466 INNER JOIN end_events e ON b.span_id = e.span_id
467 ORDER BY b.begin_time
468 "#,
469 process_id,
470 time_range.begin.to_rfc3339(),
471 time_range.end.to_rfc3339(),
472 process_id,
473 time_range.begin.to_rfc3339(),
474 time_range.end.to_rfc3339(),
475 );
476
477 let df = ctx.sql(&sql).await?;
478 let mut stream = df.execute_stream().await?;
479
480 let mut span_count = 0;
481 while let Some(batch_result) = stream.next().await {
482 let batch = batch_result?;
483 let span_ids: &datafusion::arrow::array::Int64Array =
484 typed_column_by_name(&batch, "span_id")?;
485 let begin_times: &TimestampNanosecondArray = typed_column_by_name(&batch, "begin_time")?;
486 let end_times: &TimestampNanosecondArray = typed_column_by_name(&batch, "end_time")?;
487 let names = string_column_by_name(&batch, "name")?;
488 let filenames = string_column_by_name(&batch, "filename")?;
489 let targets = string_column_by_name(&batch, "target")?;
490 let lines: &UInt32Array = typed_column_by_name(&batch, "line")?;
491 for i in 0..batch.num_rows() {
492 let _span_id = span_ids.value(i);
493 let begin_ns = begin_times.value(i) as u64;
494 let end_ns = end_times.value(i) as u64;
495 let name = names.value(i)?;
496 let filename = filenames.value(i)?;
497 let target = targets.value(i)?;
498 let line = lines.value(i);
499
500 if begin_ns < end_ns {
501 writer
503 .emit_async_span_begin(begin_ns, name, target, filename, line)
504 .await?;
505 writer
506 .emit_async_span_end(end_ns, name, target, filename, line)
507 .await?;
508
509 span_count += 1;
510 if span_count % 10 == 0 {
512 writer.flush().await?;
513 }
514 } else {
515 warn!("Skipping async span with invalid duration");
516 }
517 }
518 }
519
520 Ok(())
521}
522
523#[derive(Debug)]
525pub struct PerfettoTraceTableProvider {
526 execution_plan: Arc<PerfettoTraceExecutionPlan>,
527}
528
529impl PerfettoTraceTableProvider {
530 pub fn new(execution_plan: Arc<PerfettoTraceExecutionPlan>) -> Self {
531 Self { execution_plan }
532 }
533}
534
535#[async_trait::async_trait]
536impl TableProvider for PerfettoTraceTableProvider {
537 fn schema(&self) -> SchemaRef {
538 self.execution_plan.schema()
539 }
540
541 fn table_type(&self) -> TableType {
542 TableType::Base
543 }
544
545 async fn scan(
546 &self,
547 _state: &dyn Session,
548 _projection: Option<&Vec<usize>>,
549 _filters: &[Expr],
550 limit: Option<usize>,
551 ) -> DFResult<Arc<dyn ExecutionPlan>> {
552 let plan: Arc<dyn ExecutionPlan> = self.execution_plan.clone();
556 if let Some(fetch) = limit {
557 Ok(Arc::new(GlobalLimitExec::new(plan, 0, Some(fetch))))
558 } else {
559 Ok(plan)
560 }
561 }
562}