Skip to main content

datafusion_distributed/metrics/
task_metrics_rewriter.rs

1use crate::common::TreeNodeExt;
2use crate::coordinator::{DistributedExec, MetricsStore};
3use crate::distributed_planner::NetworkBoundaryExt;
4use crate::execution_plans::MetricsWrapperExec;
5use crate::metrics::DISTRIBUTED_DATAFUSION_TASK_ID_LABEL;
6use crate::metrics::collect_plan_metrics;
7use crate::stage::{LocalStage, Stage};
8use crate::{DistributedTaskContext, TaskKey};
9use datafusion::common::HashMap;
10use datafusion::common::plan_err;
11use datafusion::common::tree_node::Transformed;
12use datafusion::common::tree_node::TreeNode;
13use datafusion::common::tree_node::TreeNodeRecursion;
14use datafusion::error::Result;
15use datafusion::physical_plan::ExecutionPlan;
16use datafusion::physical_plan::internal_err;
17use datafusion::physical_plan::metrics::{Label, Metric, MetricsSet};
18use std::sync::Arc;
19
20/// Format to use when displaying metrics for a distributed plan.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum DistributedMetricsFormat {
23    /// Metrics are aggregated across all tasks. ex. a `output_rows=X` represents the output rows for all tasks.
24    Aggregated,
25
26    /// Metrics are labeled with their task id and displayed per task. ex. `output_rows` is shown as
27    /// `output_rows={0:.., 1:..}`, one entry per task.
28    PerTask,
29}
30
31impl DistributedMetricsFormat {
32    pub(crate) fn to_rewrite_ctx(self, task_id: u64) -> RewriteCtx {
33        match self {
34            DistributedMetricsFormat::Aggregated => RewriteCtx::default(),
35            DistributedMetricsFormat::PerTask => RewriteCtx::from_task_id(task_id),
36        }
37    }
38}
39
40/// Rewrites a distributed plan with metrics. Does nothing if the root node is not a [DistributedExec].
41/// Returns an error if the distributed plan was not executed.
42///
43/// Waits for all worker task metrics to arrive before rewriting, so the result is always complete.
44pub async fn rewrite_distributed_plan_with_metrics(
45    plan: Arc<dyn ExecutionPlan>,
46    format: DistributedMetricsFormat,
47) -> Result<Arc<dyn ExecutionPlan>> {
48    let Some(distributed_exec) = plan.downcast_ref::<DistributedExec>() else {
49        return Ok(plan);
50    };
51
52    distributed_exec.wait_for_metrics().await;
53
54    let Some(metrics_collection) = distributed_exec.metrics_store.clone() else {
55        return Ok(plan);
56    };
57
58    let head_stage = distributed_exec.head_stage()?;
59    let task_metrics = collect_plan_metrics(&head_stage)?;
60
61    // Rewrite the DistributedExec's child plan with metrics.
62    let dist_exec_plan_with_metrics = rewrite_local_plan_with_metrics(
63        format.to_rewrite_ctx(0), // Task id is 0 for the DistributedExec plan
64        distributed_exec.plan_for_viz()?,
65        task_metrics,
66    )?;
67
68    let transformed = dist_exec_plan_with_metrics.transform_down(|plan| {
69        // Transform all stages using NetworkShuffleExec and NetworkCoalesceExec as barriers.
70        if let Some(network_boundary) = plan.as_network_boundary() {
71            let Stage::Local(stage) = network_boundary.input_stage() else {
72                return plan_err!("Stage was not in Local state");
73            };
74            // This transform is a bit inefficient because we traverse the plan nodes twice
75            // For now, we are okay with trading off performance for simplicity.
76            let plan_with_metrics =
77                stage_metrics_rewriter(stage, Arc::clone(&metrics_collection), format)?;
78            let network_boundary = network_boundary.with_input_stage(Stage::Local(LocalStage {
79                query_id: stage.query_id,
80                num: stage.num,
81                plan: plan_with_metrics,
82                tasks: stage.tasks,
83                metrics_set: stage.metrics_set.clone(),
84            }))?;
85            let network_boundary =
86                MetricsWrapperExec::new(network_boundary, plan.metrics().unwrap_or_default());
87            return Ok(Transformed::yes(Arc::new(network_boundary)));
88        }
89
90        Ok(Transformed::no(plan))
91    })?;
92    plan.with_new_children(vec![transformed.data])
93}
94
95/// Extra information for rewriting local plans.
96#[derive(Default)]
97pub struct RewriteCtx {
98    /// Used to rename metrics for the current task.
99    pub task_id: Option<u64>,
100}
101
102impl RewriteCtx {
103    pub(crate) fn from_task_id(task_id: u64) -> RewriteCtx {
104        RewriteCtx {
105            task_id: Some(task_id),
106        }
107    }
108
109    /// Rewrites the [MetricsSet] depending on the context.
110    pub(crate) fn maybe_rewrite_node_metics(&self, node_metrics: MetricsSet) -> MetricsSet {
111        if let Some(task_id) = self.task_id {
112            return annotate_metrics_set_with_task_id(node_metrics, task_id);
113        }
114        node_metrics
115    }
116}
117
118/// Adds task id labels to all metrics in the provided [MetricsSet].
119///
120/// TODO: This re-allocates the vec of metrics by creating a new [MetricsSet]. It also
121/// reallocates the labels vec for each metric. Can we avoid this?
122/// See https://github.com/apache/datafusion/issues/19959
123pub fn annotate_metrics_set_with_task_id(metrics_set: MetricsSet, task_id: u64) -> MetricsSet {
124    let mut result = MetricsSet::new();
125
126    for metric in metrics_set.iter() {
127        let mut labels = metric.labels().to_vec();
128        labels.push(Label::new(
129            DISTRIBUTED_DATAFUSION_TASK_ID_LABEL,
130            task_id.to_string(),
131        ));
132        result.push(Arc::new(Metric::new_with_labels(
133            metric.value().clone(),
134            metric.partition(),
135            labels,
136        )));
137    }
138
139    result
140}
141
142/// Rewrites a local plan with metrics, stopping at network boundaries.
143///
144/// Example:
145///
146/// AggregateExec [output_rows = 1, elapsed_compute = 100]
147///  └── ProjectionExec [output_rows = 2, elapsed_compute = 200]
148///      └── NetworkShuffleExec [bytes_transferred = 100, max_mem_used = 100]
149///
150/// The result will be:
151///
152/// MetricsWrapperExec (wrapped: AggregateExec) [output_rows = 1, elapsed_compute = 100]
153///  └── MetricsWrapperExec (wrapped: ProjectionExec) [output_rows = 2, elapsed_compute = 200]
154///      └── MetricsWrapperExec (wrapped: NetworkShuffleExec) [bytes_transferred = 100, max_mem_used = 100]
155pub fn rewrite_local_plan_with_metrics(
156    ctx: RewriteCtx,
157    plan: Arc<dyn ExecutionPlan>,
158    metrics: Vec<MetricsSet>,
159) -> Result<Arc<dyn ExecutionPlan>> {
160    let mut idx = 0;
161    Ok(plan
162        .transform_down(|node| {
163            if idx >= metrics.len() {
164                return internal_err!("not enough metrics provided to rewrite plan");
165            }
166            let mut node_metrics = metrics[idx].clone();
167
168            node_metrics = ctx.maybe_rewrite_node_metics(node_metrics);
169
170            idx += 1;
171            Ok(Transformed::new(
172                Arc::new(MetricsWrapperExec::new(node.clone(), node_metrics)),
173                true,
174                if node.is_network_boundary() {
175                    TreeNodeRecursion::Jump
176                } else {
177                    TreeNodeRecursion::Continue
178                },
179            ))
180        })?
181        .data)
182}
183
184/// Enriches a stage with metrics from each task by re-writing the plan using
185/// [MetricsWrapperExec] nodes.
186///
187/// Example:
188///
189/// For a stage with 2 tasks:
190///
191/// Task 1:
192/// AggregateExec [output_rows = 1, elapsed_compute = 100]
193///  └── ProjectionExec [output_rows = 2, elapsed_compute = 200]
194///      └── NetworkShuffleExec [bytes_transferred = 100, max_mem_used = 100]
195///
196/// Task 2:
197/// AggregateExec [output_rows = 3, elapsed_compute = 300]
198///  └── ProjectionExec [output_rows = 4, elapsed_compute = 400]
199///      └── NetworkShuffleExec [bytes_transferred = 200, max_mem_used = 200]
200///
201/// The result will be:
202///
203/// MetricsWrapperExec (wrapped: AggregateExec) [output_rows = 1, output_rows = 3, elapsed_compute = 100, elapsed_compute = 300]
204///  └── MetricsWrapperExec (wrapped: ProjectionExec) [output_rows = 2, output_rows = 4, elapsed_compute = 200, elapsed_compute = 400]
205///      └── MetricsWrapperExec (wrapped: NetworkShuffleExec) [bytes_transferred = 100, bytes_transferred = 200, max_mem_used = 100, max_mem_used = 200]
206///
207/// Note: Metrics may be aggregated by name (ex. output_rows) automatically by various datafusion utils.
208pub fn stage_metrics_rewriter(
209    stage: &LocalStage,
210    metrics_collection: Arc<MetricsStore>,
211    format: DistributedMetricsFormat,
212) -> Result<Arc<dyn ExecutionPlan>> {
213    // Phase 1 — accumulate per-task metrics into a map keyed by node identity.
214    //
215    // For each task, the plan is traversed with `apply_with_dt_ctx`, which visits nodes in pre-order
216    // traversal, ignoring branches that do not belong to the recursed DistributedTaskContext
217    // (e.g., because of the presence of ChildrenIsolatorUnionExec).
218    //
219    // The raw allocation address of each `Arc<dyn ExecutionPlan>` as the node key.
220    // The planning plan is not modified between traversals, so these addresses are stable.
221    let mut node_metrics_map: HashMap<usize, MetricsSet> = HashMap::new();
222
223    for task_id in 0..stage.tasks {
224        let d_ctx = DistributedTaskContext {
225            task_index: task_id,
226            task_count: stage.tasks,
227        };
228        let task_key = TaskKey {
229            query_id: stage.query_id,
230            stage_id: stage.num,
231            task_number: task_id,
232        };
233        let Some(task_metrics) = metrics_collection.get(&task_key) else {
234            return internal_err!(
235                "not enough metrics provided to rewrite task: missing metrics for task {} in stage {}",
236                task_id,
237                stage.num
238            );
239        };
240
241        let mut per_task_counter = 0usize;
242        stage.plan.apply_with_dt_ctx(d_ctx, |node, _ctx| {
243            if per_task_counter >= task_metrics.pre_order_plan_metrics.len() {
244                return internal_err!(
245                    "not enough metrics provided to rewrite task: {} metrics provided",
246                    task_metrics.pre_order_plan_metrics.len()
247                );
248            }
249
250            let mut node_metrics = task_metrics.pre_order_plan_metrics[per_task_counter].clone();
251            let rewrite_ctx = format.to_rewrite_ctx(task_id as u64);
252            node_metrics = rewrite_ctx.maybe_rewrite_node_metics(node_metrics);
253
254            let id = Arc::as_ptr(node) as *const () as usize;
255            let entry = node_metrics_map.entry(id).or_default();
256            for metric in node_metrics.iter().map(Arc::clone) {
257                entry.push(metric);
258            }
259
260            per_task_counter += 1;
261            Ok(TreeNodeRecursion::Continue)
262        })?;
263    }
264
265    // Phase 2 — rewrite: wrap every node with its accumulated metrics.
266    // Nodes that were inactive for all tasks (never visited in phase 1) get empty metrics.
267    Arc::clone(&stage.plan)
268        .transform_down(|plan| {
269            let id = Arc::as_ptr(&plan) as *const () as usize;
270            let metrics = node_metrics_map.remove(&id).unwrap_or_default();
271            Ok(Transformed::new(
272                Arc::new(MetricsWrapperExec::new(plan.clone(), metrics)),
273                true,
274                match plan.is_network_boundary() {
275                    true => TreeNodeRecursion::Jump,
276                    false => TreeNodeRecursion::Continue,
277                },
278            ))
279        })
280        .map(|v| v.data)
281}
282
283#[cfg(test)]
284mod tests {
285    use crate::DistributedExt;
286    use crate::coordinator::MetricsStore;
287    use crate::metrics::DISTRIBUTED_DATAFUSION_TASK_ID_LABEL;
288    use crate::metrics::task_metrics_rewriter::MetricsWrapperExec;
289    use crate::metrics::task_metrics_rewriter::{
290        annotate_metrics_set_with_task_id, stage_metrics_rewriter,
291    };
292    use crate::metrics::{DistributedMetricsFormat, rewrite_distributed_plan_with_metrics};
293    use crate::stage::LocalStage;
294    use crate::test_utils::in_memory_channel_resolver::{
295        InMemoryChannelResolver, InMemoryWorkerResolver,
296    };
297    use crate::test_utils::metrics::make_test_metrics_set_from_seed;
298    use crate::test_utils::plans::count_plan_nodes_up_to_network_boundary;
299    use crate::test_utils::session_context::register_temp_parquet_table;
300    use crate::{DistributedExec, SessionStateBuilderExt, TaskKey, TaskMetrics};
301    use datafusion::arrow::array::{Int32Array, StringArray};
302    use datafusion::arrow::datatypes::{DataType, Field, Schema};
303    use datafusion::arrow::record_batch::RecordBatch;
304    use datafusion::execution::SessionStateBuilder;
305    use datafusion::physical_plan::empty::EmptyExec;
306    use datafusion::physical_plan::metrics::{Count, Label, Metric, MetricValue, MetricsSet};
307    use datafusion::physical_plan::{ExecutionPlan, collect};
308    use datafusion::prelude::SessionConfig;
309    use datafusion::prelude::SessionContext;
310    use itertools::Itertools;
311    use std::sync::Arc;
312    use test_case::test_case;
313    use uuid::Uuid;
314
315    async fn make_test_ctx() -> SessionContext {
316        make_test_ctx_inner(false).await
317    }
318
319    async fn make_test_distributed_ctx() -> SessionContext {
320        make_test_ctx_inner(true).await
321    }
322
323    /// Creates a non-distributed session context and registers two tables:
324    /// - table1 (id: int, name: string)
325    /// - table2 (id: int, name: string, phone: string, balance: float64)
326    async fn make_test_ctx_inner(distributed: bool) -> SessionContext {
327        let config = SessionConfig::new().with_target_partitions(4);
328        let mut builder = SessionStateBuilder::new()
329            .with_default_features()
330            .with_config(config);
331
332        if distributed {
333            builder = builder
334                .with_distributed_worker_resolver(InMemoryWorkerResolver::new(10))
335                .with_distributed_channel_resolver(InMemoryChannelResolver::default())
336                .with_distributed_metrics_collection(true)
337                .unwrap()
338                .with_distributed_planner()
339                .with_distributed_desired_task_count_handler(2)
340        }
341
342        let state = builder.build();
343        let ctx = SessionContext::from(state);
344
345        // Create test data for table1
346        let schema1 = Arc::new(Schema::new(vec![
347            Field::new("id", DataType::Int32, false),
348            Field::new("name", DataType::Utf8, false),
349        ]));
350
351        let batches1 = vec![
352            RecordBatch::try_new(
353                schema1.clone(),
354                vec![
355                    Arc::new(Int32Array::from(vec![1, 2, 3])),
356                    Arc::new(StringArray::from(vec!["a", "b", "c"])),
357                ],
358            )
359            .unwrap(),
360        ];
361
362        // Create test data for table2 with extended schema
363        let schema2 = Arc::new(Schema::new(vec![
364            Field::new("id", DataType::Int32, false),
365            Field::new("name", DataType::Utf8, false),
366            Field::new("phone", DataType::Utf8, false),
367            Field::new("balance", DataType::Float64, false),
368        ]));
369
370        let batches2 = vec![
371            RecordBatch::try_new(
372                schema2.clone(),
373                vec![
374                    Arc::new(Int32Array::from(vec![1, 2, 3])),
375                    Arc::new(StringArray::from(vec![
376                        "customer1",
377                        "customer2",
378                        "customer3",
379                    ])),
380                    Arc::new(StringArray::from(vec![
381                        "13-123-4567",
382                        "31-456-7890",
383                        "23-789-0123",
384                    ])),
385                    Arc::new(datafusion::arrow::array::Float64Array::from(vec![
386                        100.5, 250.0, 50.25,
387                    ])),
388                ],
389            )
390            .unwrap(),
391        ];
392
393        // Register the test data as parquet tables
394        let _ = register_temp_parquet_table("table1", schema1, batches1, &ctx)
395            .await
396            .unwrap();
397
398        let _ = register_temp_parquet_table("table2", schema2, batches2, &ctx)
399            .await
400            .unwrap();
401
402        ctx
403    }
404
405    fn make_test_stage(plan: Arc<dyn ExecutionPlan>) -> LocalStage {
406        LocalStage {
407            query_id: Uuid::new_v4(),
408            num: 2,
409            plan,
410            tasks: 4,
411            metrics_set: Default::default(),
412        }
413    }
414
415    fn collect_metrics_from_plan(plan: &Arc<dyn ExecutionPlan>, metrics: &mut Vec<MetricsSet>) {
416        metrics.extend(plan.metrics());
417        for child in plan.children() {
418            collect_metrics_from_plan(child, metrics);
419        }
420    }
421
422    fn metrics_set_eq(a: &MetricsSet, b: &MetricsSet) -> bool {
423        println!("a: {a:?}");
424        println!("b: {b:?}");
425        a.iter().count() == b.iter().count()
426            && a.iter().zip(b.iter()).all(|(a, b)| {
427                a.value() == b.value() && a.partition() == b.partition() && a.labels() == b.labels()
428            })
429    }
430
431    /// Asserts that we successfully re-write the metrics of a plan generated from the provided SQL query.
432    /// Also asserts that the order which metrics are collected from a plan matches the order which
433    /// they are re-written (ie. ensures we don't assign metrics to the wrong nodes)
434    ///
435    /// Only tests single node plans since the [TaskMetricsRewriter] stops on [NetworkBoundary].
436    async fn run_stage_metrics_rewriter_test(sql: &str, format: DistributedMetricsFormat) {
437        // Generate the plan
438        let ctx = make_test_ctx().await;
439        let plan = ctx
440            .sql(sql)
441            .await
442            .unwrap()
443            .create_physical_plan()
444            .await
445            .unwrap();
446
447        let stage = make_test_stage(plan.clone());
448
449        let num_metrics_per_task_per_node = 4;
450
451        // Generate metrics for each task and store them in the map.
452        let metrics_collection = MetricsStore::from_entries((0..stage.tasks).map(|task_id| {
453            let task_key = TaskKey {
454                query_id: stage.query_id,
455                stage_id: stage.num,
456                task_number: task_id,
457            };
458            let metrics = (0..count_plan_nodes_up_to_network_boundary(&plan))
459                .map(|node_id| {
460                    make_test_metrics_set_from_seed(
461                        (node_id * task_id) as u64,
462                        num_metrics_per_task_per_node,
463                    )
464                })
465                .collect::<Vec<MetricsSet>>();
466            let task_metrics = TaskMetrics {
467                task_metrics: MetricsSet::new(),
468                pre_order_plan_metrics: metrics,
469            };
470            (task_key, task_metrics)
471        }));
472        let metrics_collection = Arc::new(metrics_collection);
473
474        // Rewrite the plan.
475        let rewritten_plan =
476            stage_metrics_rewriter(&stage, metrics_collection.clone(), format).unwrap();
477
478        // Collect metrics from the plan.
479        let mut actual_metrics = vec![];
480        collect_metrics_from_plan(&rewritten_plan, &mut actual_metrics);
481        assert_eq!(
482            actual_metrics.len(),
483            count_plan_nodes_up_to_network_boundary(&plan)
484        );
485
486        // Assert that metrics from all tasks are present.
487        // actual_stage_node_metrics_set contains metrics for all task ex. [output_rows=1, elapsed_compute=1, output_rows=2, elapsed_compute=2...]
488        for (node_id, actual_stage_node_metrics_set) in actual_metrics.iter().enumerate() {
489            // actual_task_node_metrics_set contains metrics for one task ex. [output_rows=1, elapsed_compute=1]
490            for (task_id, actual_task_node_metrics_set) in actual_stage_node_metrics_set
491                .iter()
492                .chunks(num_metrics_per_task_per_node)
493                .into_iter()
494                .enumerate()
495            {
496                let expected_task_node_metrics = metrics_collection
497                    .get(&TaskKey {
498                        query_id: stage.query_id,
499                        stage_id: stage.num,
500                        task_number: task_id,
501                    })
502                    .unwrap()
503                    .pre_order_plan_metrics[node_id]
504                    .clone();
505
506                let mut actual_metrics_set = MetricsSet::new();
507                actual_task_node_metrics_set
508                    .for_each(|metric| actual_metrics_set.push(metric.clone()));
509
510                let mut expected_metrics_set = expected_task_node_metrics;
511
512                if format == DistributedMetricsFormat::PerTask {
513                    // Add task ids labels. We expect the actual metrics to be annotated by the
514                    // rewriter when using DistributedMetricsFormat::PerTask
515                    expected_metrics_set =
516                        annotate_metrics_set_with_task_id(expected_metrics_set, task_id as u64);
517                }
518                assert!(metrics_set_eq(&actual_metrics_set, &expected_metrics_set));
519            }
520        }
521    }
522
523    #[test_case(DistributedMetricsFormat::Aggregated ; "aggregated_metrics")]
524    #[test_case(DistributedMetricsFormat::PerTask ; "per_task_metrics")]
525    #[tokio::test]
526    async fn test_stage_metrics_rewriter_1(format: DistributedMetricsFormat) {
527        run_stage_metrics_rewriter_test(
528            "SELECT sum(balance) / 7.0 as avg_yearly from table2 group by name",
529            format,
530        )
531        .await;
532    }
533
534    #[test_case(DistributedMetricsFormat::Aggregated ; "aggregated_metrics")]
535    #[test_case(DistributedMetricsFormat::PerTask ; "per_task_metrics")]
536    #[tokio::test]
537    async fn test_stage_metrics_rewriter_2(format: DistributedMetricsFormat) {
538        run_stage_metrics_rewriter_test("SELECT id, COUNT(*) as count FROM table1 WHERE id > 1 GROUP BY id ORDER BY id LIMIT 10", format).await;
539    }
540
541    #[test_case(DistributedMetricsFormat::Aggregated ; "aggregated_metrics")]
542    #[test_case(DistributedMetricsFormat::PerTask ; "per_task_metrics")]
543    #[tokio::test]
544    async fn test_stage_metrics_rewriter_3(format: DistributedMetricsFormat) {
545        run_stage_metrics_rewriter_test(
546            "SELECT sum(balance) / 7.0 as avg_yearly
547            FROM table2
548            WHERE name LIKE 'customer%'
549              AND balance < (
550                SELECT 0.2 * avg(balance)
551                FROM table2 t2_inner
552                WHERE t2_inner.id = table2.id
553              )",
554            format,
555        )
556        .await;
557    }
558
559    #[tokio::test]
560    async fn test_rewrite_unexecuted_distributed_plan_with_metrics_err() {
561        let ctx = make_test_distributed_ctx().await;
562        let plan = ctx
563            .sql("SELECT id, COUNT(*) as count FROM table1 WHERE id > 1 GROUP BY id ORDER BY id LIMIT 10")
564            .await
565            .unwrap()
566            .create_physical_plan()
567            .await
568            .unwrap();
569        assert!(plan.is::<DistributedExec>());
570        assert!(
571            rewrite_distributed_plan_with_metrics(plan, DistributedMetricsFormat::Aggregated)
572                .await
573                .is_err()
574        );
575    }
576
577    // Assert every plan node has at least one metric except partition isolators, network boundary nodes, and the root DistributedExec node.
578    fn assert_metrics_present_in_plan(plan: &Arc<dyn ExecutionPlan>) {
579        if let Some(metrics) = plan.metrics() {
580            assert!(metrics.iter().count() > 0);
581        } else {
582            assert!(plan.is::<DistributedExec>());
583        }
584        for child in plan.children() {
585            assert_metrics_present_in_plan(child);
586        }
587    }
588
589    #[tokio::test]
590    async fn test_executed_distributed_plan_has_metrics() {
591        let ctx = make_test_distributed_ctx().await;
592        let plan = ctx
593            .sql("SELECT id, COUNT(*) as count FROM table1 WHERE id > 1 GROUP BY id ORDER BY id LIMIT 10")
594            .await
595            .unwrap()
596            .create_physical_plan()
597            .await
598            .unwrap();
599        collect(plan.clone(), ctx.task_ctx()).await.unwrap();
600        assert!(plan.is::<DistributedExec>());
601        let rewritten_plan =
602            rewrite_distributed_plan_with_metrics(plan, DistributedMetricsFormat::Aggregated)
603                .await
604                .unwrap();
605        assert_metrics_present_in_plan(&rewritten_plan);
606    }
607
608    #[test]
609    // An important feature of DF execution plans which we want to preserve is the ability
610    // to traverse a plan and collect metrics from specific nodes. To do this, the wrapper must
611    // allow access to the inner node. This test asserts that we support this.
612    fn test_wrapped_node_is_accessible() {
613        let example_node = Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![Field::new(
614            "id",
615            DataType::Int32,
616            false,
617        )]))));
618
619        let wrapped = MetricsWrapperExec::new(example_node, MetricsSet::new());
620        assert_eq!(wrapped.name(), "EmptyExec");
621        assert!(wrapped.inner().is::<EmptyExec>());
622    }
623
624    #[test]
625    fn test_annotate_metrics_set_with_task_id_output_rows() {
626        // Create a MetricsSet with an OutputRows metric
627        let mut metrics_set = MetricsSet::new();
628        let count = Count::new();
629        count.add(1234);
630        let labels = vec![Label::new("operator", "scan")];
631        metrics_set.push(Arc::new(Metric::new_with_labels(
632            MetricValue::OutputRows(count),
633            Some(0),
634            labels,
635        )));
636
637        let task_id = 42;
638        let annotated = annotate_metrics_set_with_task_id(metrics_set, task_id);
639
640        // Verify we have one metric
641        assert_eq!(annotated.iter().count(), 1);
642
643        let metric = annotated.iter().next().unwrap();
644
645        // Verify metric type is preserved (OutputRows)
646        match metric.value() {
647            MetricValue::OutputRows(count) => {
648                assert_eq!(count.value(), 1234);
649            }
650            other => panic!("Expected OutputRows, got {:?}", other.name()),
651        }
652
653        // Verify partition is preserved
654        assert_eq!(metric.partition(), Some(0));
655
656        // Verify original labels are preserved and task_id label is added
657        let labels: Vec<_> = metric.labels().iter().collect();
658        assert_eq!(labels.len(), 2);
659        assert_eq!(labels[0].name(), "operator");
660        assert_eq!(labels[0].value(), "scan");
661        assert_eq!(labels[1].name(), DISTRIBUTED_DATAFUSION_TASK_ID_LABEL);
662        assert_eq!(labels[1].value(), "42");
663    }
664}