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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum DistributedMetricsFormat {
23 Aggregated,
25
26 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
40pub 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 let dist_exec_plan_with_metrics = rewrite_local_plan_with_metrics(
63 format.to_rewrite_ctx(0), distributed_exec.plan_for_viz()?,
65 task_metrics,
66 )?;
67
68 let transformed = dist_exec_plan_with_metrics.transform_down(|plan| {
69 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 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#[derive(Default)]
97pub struct RewriteCtx {
98 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 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
118pub 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
142pub 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
184pub fn stage_metrics_rewriter(
209 stage: &LocalStage,
210 metrics_collection: Arc<MetricsStore>,
211 format: DistributedMetricsFormat,
212) -> Result<Arc<dyn ExecutionPlan>> {
213 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 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 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 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 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 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 async fn run_stage_metrics_rewriter_test(sql: &str, format: DistributedMetricsFormat) {
437 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 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 let rewritten_plan =
476 stage_metrics_rewriter(&stage, metrics_collection.clone(), format).unwrap();
477
478 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 for (node_id, actual_stage_node_metrics_set) in actual_metrics.iter().enumerate() {
489 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 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 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 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 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 assert_eq!(annotated.iter().count(), 1);
642
643 let metric = annotated.iter().next().unwrap();
644
645 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 assert_eq!(metric.partition(), Some(0));
655
656 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}