Skip to main content

micromegas_analytics/lakehouse/
perfetto_trace_execution_plan.rs

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
38/// Execution plan that generates Perfetto trace chunks
39pub 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        // Create the stream directly without channels
139        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/// Creates a stream of Perfetto trace chunks using streaming architecture
153#[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        // Create channel for streaming chunks
164        const CHUNK_SIZE: usize = 8 * 1024; // 8KB chunks
165        let (chunk_sender, mut chunk_receiver) = tokio::sync::mpsc::channel(16);
166
167        // Create ChunkSender that will stream data through the channel
168        let chunk_sender_writer = ChunkSender::new(chunk_sender, CHUNK_SIZE);
169
170        // Spawn background task to generate trace
171        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        // Stream chunks as they become available
184        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        // Wait for generation task to complete and check for errors
198        match generation_task.await {
199            Ok(Ok(())) => {}, // Success
200            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
216/// Generate Perfetto trace using streaming architecture
217async 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    // Create a context for making queries
232    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    // Use ChunkSender directly as the writer destination
246    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?; // Forces chunk emission
251
252    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?; // Forces chunk emission
260    }
261
262    if matches!(span_types, SpanTypes::Async | SpanTypes::Both) {
263        writer.emit_async_track_descriptor().await?;
264        writer.flush().await?; // Forces chunk emission
265    }
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?; // Final chunk - this handles the chunk_sender.flush() internally
276    Ok(())
277}
278
279/// Get process executable name from the processes table
280async 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
305/// Format the SQL query for thread spans
306fn 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
321/// Generate thread spans with parallel JIT and sequential writing.
322///
323/// JIT partition locking is per-(view_set_name, view_instance_id), and each thread
324/// has a unique stream_id used as view_instance_id. This means different threads
325/// get different lock keys, making parallel JIT safe.
326///
327/// Strategy:
328/// - Spawn tasks with spawn_with_context() for true multi-threaded parallelism
329/// - Use buffered() to limit concurrent spawned tasks
330/// - Collect all streams preserving order
331/// - Consume streams sequentially to write each thread's spans together
332async 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    // Prepare query inputs upfront
343    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    // Build streams in parallel using spawn for true multi-threading
354    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    // Consume streams sequentially - each thread's spans written together
371    for (stream_id, data_stream) in streams {
372        write_thread_spans(writer, &stream_id, data_stream).await?;
373    }
374    Ok(())
375}
376
377/// Writes one thread's spans, in row order, from its query result stream to the Perfetto writer.
378///
379/// `begin` is only guaranteed sorted *within* a single thread's stream (different threads are
380/// independent timelines), so the monotonicity tracker below is local to this call. It is the
381/// runtime backstop for the ordering `ThreadSpansView::get_scan_output_ordering` declares to
382/// DataFusion: once the redundant `Sort` node is elided, DataFusion trusts that declared ordering
383/// and never re-validates it against the actual rows, so a violated invariant would otherwise
384/// silently mis-order the exported trace. Errors instead of writing a row whose `begin` regresses.
385pub 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
434/// Generate async spans using the provided PerfettoWriter
435async 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                // Emit async span begin and end events with single writer
502                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                // Flush every 10 async spans to create multiple chunks
511                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/// TableProvider wrapper for PerfettoTraceExecutionPlan
524#[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        // Wrap the execution plan in a GlobalLimitExec if a limit is provided.
553        // DataFusion trusts us to apply the limit - if we ignore it, too many rows
554        // will be returned to the client.
555        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}