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::internal_err;
16use datafusion::physical_plan::metrics::{Label, Metric, MetricsSet};
17use datafusion::physical_plan::{ChildrenPropertiesMode, ExecutionPlan, ReplaceChildrenOptions};
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.replace_children(
93 vec![transformed.data],
94 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
95 )
96}
97
98#[derive(Default)]
100pub struct RewriteCtx {
101 pub task_id: Option<u64>,
103}
104
105impl RewriteCtx {
106 pub(crate) fn from_task_id(task_id: u64) -> RewriteCtx {
107 RewriteCtx {
108 task_id: Some(task_id),
109 }
110 }
111
112 pub(crate) fn maybe_rewrite_node_metics(&self, node_metrics: MetricsSet) -> MetricsSet {
114 if let Some(task_id) = self.task_id {
115 return annotate_metrics_set_with_task_id(node_metrics, task_id);
116 }
117 node_metrics
118 }
119}
120
121pub fn annotate_metrics_set_with_task_id(metrics_set: MetricsSet, task_id: u64) -> MetricsSet {
127 let mut result = MetricsSet::new();
128
129 for metric in metrics_set.iter() {
130 let mut labels = metric.labels().to_vec();
131 labels.push(Label::new(
132 DISTRIBUTED_DATAFUSION_TASK_ID_LABEL,
133 task_id.to_string(),
134 ));
135 result.push(Arc::new(Metric::new_with_labels(
136 metric.value().clone(),
137 metric.partition(),
138 labels,
139 )));
140 }
141
142 result
143}
144
145pub fn rewrite_local_plan_with_metrics(
159 ctx: RewriteCtx,
160 plan: Arc<dyn ExecutionPlan>,
161 metrics: Vec<MetricsSet>,
162) -> Result<Arc<dyn ExecutionPlan>> {
163 let mut idx = 0;
164 Ok(plan
165 .transform_down(|node| {
166 if idx >= metrics.len() {
167 return internal_err!("not enough metrics provided to rewrite plan");
168 }
169 let mut node_metrics = metrics[idx].clone();
170
171 node_metrics = ctx.maybe_rewrite_node_metics(node_metrics);
172
173 idx += 1;
174 Ok(Transformed::new(
175 Arc::new(MetricsWrapperExec::new(node.clone(), node_metrics)),
176 true,
177 if node.is_network_boundary() {
178 TreeNodeRecursion::Jump
179 } else {
180 TreeNodeRecursion::Continue
181 },
182 ))
183 })?
184 .data)
185}
186
187pub fn stage_metrics_rewriter(
212 stage: &LocalStage,
213 metrics_collection: Arc<MetricsStore>,
214 format: DistributedMetricsFormat,
215) -> Result<Arc<dyn ExecutionPlan>> {
216 let mut node_metrics_map: HashMap<usize, MetricsSet> = HashMap::new();
225
226 for task_id in 0..stage.tasks {
227 let d_ctx = DistributedTaskContext {
228 task_index: task_id,
229 task_count: stage.tasks,
230 };
231 let task_key = TaskKey {
232 query_id: stage.query_id,
233 stage_id: stage.num,
234 task_number: task_id,
235 };
236 let Some(task_metrics) = metrics_collection.get(&task_key) else {
237 return internal_err!(
238 "not enough metrics provided to rewrite task: missing metrics for task {} in stage {}",
239 task_id,
240 stage.num
241 );
242 };
243
244 let mut per_task_counter = 0usize;
245 stage.plan.apply_with_dt_ctx(d_ctx, |node, _ctx| {
246 if per_task_counter >= task_metrics.pre_order_plan_metrics.len() {
247 return internal_err!(
248 "not enough metrics provided to rewrite task: {} metrics provided",
249 task_metrics.pre_order_plan_metrics.len()
250 );
251 }
252
253 let mut node_metrics = task_metrics.pre_order_plan_metrics[per_task_counter].clone();
254 let rewrite_ctx = format.to_rewrite_ctx(task_id as u64);
255 node_metrics = rewrite_ctx.maybe_rewrite_node_metics(node_metrics);
256
257 let id = Arc::as_ptr(node) as *const () as usize;
258 let entry = node_metrics_map.entry(id).or_default();
259 for metric in node_metrics.iter().map(Arc::clone) {
260 entry.push(metric);
261 }
262
263 per_task_counter += 1;
264 Ok(TreeNodeRecursion::Continue)
265 })?;
266 }
267
268 Arc::clone(&stage.plan)
271 .transform_down(|plan| {
272 let id = Arc::as_ptr(&plan) as *const () as usize;
273 let metrics = node_metrics_map.remove(&id).unwrap_or_default();
274 Ok(Transformed::new(
275 Arc::new(MetricsWrapperExec::new(plan.clone(), metrics)),
276 true,
277 match plan.is_network_boundary() {
278 true => TreeNodeRecursion::Jump,
279 false => TreeNodeRecursion::Continue,
280 },
281 ))
282 })
283 .map(|v| v.data)
284}
285
286#[cfg(test)]
287mod tests {
288 use crate::DistributedExt;
289 use crate::coordinator::MetricsStore;
290 use crate::metrics::DISTRIBUTED_DATAFUSION_TASK_ID_LABEL;
291 use crate::metrics::task_metrics_rewriter::MetricsWrapperExec;
292 use crate::metrics::task_metrics_rewriter::{
293 annotate_metrics_set_with_task_id, stage_metrics_rewriter,
294 };
295 use crate::metrics::{DistributedMetricsFormat, rewrite_distributed_plan_with_metrics};
296 use crate::stage::LocalStage;
297 use crate::test_utils::in_memory_channel_resolver::{
298 InMemoryChannelResolver, InMemoryWorkerResolver,
299 };
300 use crate::test_utils::metrics::make_test_metrics_set_from_seed;
301 use crate::test_utils::plans::count_plan_nodes_up_to_network_boundary;
302 use crate::test_utils::session_context::register_temp_parquet_table;
303 use crate::{DistributedExec, SessionStateBuilderExt, TaskKey, TaskMetrics};
304 use datafusion::arrow::array::{Int32Array, StringArray};
305 use datafusion::arrow::datatypes::{DataType, Field, Schema};
306 use datafusion::arrow::record_batch::RecordBatch;
307 use datafusion::execution::SessionStateBuilder;
308 use datafusion::physical_plan::empty::EmptyExec;
309 use datafusion::physical_plan::metrics::{Count, Label, Metric, MetricValue, MetricsSet};
310 use datafusion::physical_plan::{ExecutionPlan, collect};
311 use datafusion::prelude::SessionConfig;
312 use datafusion::prelude::SessionContext;
313 use itertools::Itertools;
314 use std::sync::Arc;
315 use test_case::test_case;
316 use uuid::Uuid;
317
318 async fn make_test_ctx() -> SessionContext {
319 make_test_ctx_inner(false).await
320 }
321
322 async fn make_test_distributed_ctx() -> SessionContext {
323 make_test_ctx_inner(true).await
324 }
325
326 async fn make_test_ctx_inner(distributed: bool) -> SessionContext {
330 let config = SessionConfig::new().with_target_partitions(4);
331 let mut builder = SessionStateBuilder::new()
332 .with_default_features()
333 .with_config(config);
334
335 if distributed {
336 builder = builder
337 .with_distributed_worker_resolver(InMemoryWorkerResolver::new(10))
338 .with_distributed_channel_resolver(InMemoryChannelResolver::default())
339 .with_distributed_metrics_collection(true)
340 .unwrap()
341 .with_distributed_planner()
342 .with_distributed_desired_task_count_handler(2)
343 }
344
345 let state = builder.build();
346 let ctx = SessionContext::from(state);
347
348 let schema1 = Arc::new(Schema::new(vec![
350 Field::new("id", DataType::Int32, false),
351 Field::new("name", DataType::Utf8, false),
352 ]));
353
354 let batches1 = vec![
355 RecordBatch::try_new(
356 schema1.clone(),
357 vec![
358 Arc::new(Int32Array::from(vec![1, 2, 3])),
359 Arc::new(StringArray::from(vec!["a", "b", "c"])),
360 ],
361 )
362 .unwrap(),
363 ];
364
365 let schema2 = Arc::new(Schema::new(vec![
367 Field::new("id", DataType::Int32, false),
368 Field::new("name", DataType::Utf8, false),
369 Field::new("phone", DataType::Utf8, false),
370 Field::new("balance", DataType::Float64, false),
371 ]));
372
373 let batches2 = vec![
374 RecordBatch::try_new(
375 schema2.clone(),
376 vec![
377 Arc::new(Int32Array::from(vec![1, 2, 3])),
378 Arc::new(StringArray::from(vec![
379 "customer1",
380 "customer2",
381 "customer3",
382 ])),
383 Arc::new(StringArray::from(vec![
384 "13-123-4567",
385 "31-456-7890",
386 "23-789-0123",
387 ])),
388 Arc::new(datafusion::arrow::array::Float64Array::from(vec![
389 100.5, 250.0, 50.25,
390 ])),
391 ],
392 )
393 .unwrap(),
394 ];
395
396 let _ = register_temp_parquet_table("table1", schema1, batches1, &ctx)
398 .await
399 .unwrap();
400
401 let _ = register_temp_parquet_table("table2", schema2, batches2, &ctx)
402 .await
403 .unwrap();
404
405 ctx
406 }
407
408 fn make_test_stage(plan: Arc<dyn ExecutionPlan>) -> LocalStage {
409 LocalStage {
410 query_id: Uuid::new_v4(),
411 num: 2,
412 plan,
413 tasks: 4,
414 metrics_set: Default::default(),
415 }
416 }
417
418 fn collect_metrics_from_plan(plan: &Arc<dyn ExecutionPlan>, metrics: &mut Vec<MetricsSet>) {
419 metrics.extend(plan.metrics());
420 for child in plan.children() {
421 collect_metrics_from_plan(child, metrics);
422 }
423 }
424
425 fn metrics_set_eq(a: &MetricsSet, b: &MetricsSet) -> bool {
426 println!("a: {a:?}");
427 println!("b: {b:?}");
428 a.iter().count() == b.iter().count()
429 && a.iter().zip(b.iter()).all(|(a, b)| {
430 a.value() == b.value() && a.partition() == b.partition() && a.labels() == b.labels()
431 })
432 }
433
434 async fn run_stage_metrics_rewriter_test(sql: &str, format: DistributedMetricsFormat) {
440 let ctx = make_test_ctx().await;
442 let plan = ctx
443 .sql(sql)
444 .await
445 .unwrap()
446 .create_physical_plan()
447 .await
448 .unwrap();
449
450 let stage = make_test_stage(plan.clone());
451
452 let num_metrics_per_task_per_node = 4;
453
454 let metrics_collection = MetricsStore::from_entries((0..stage.tasks).map(|task_id| {
456 let task_key = TaskKey {
457 query_id: stage.query_id,
458 stage_id: stage.num,
459 task_number: task_id,
460 };
461 let metrics = (0..count_plan_nodes_up_to_network_boundary(&plan))
462 .map(|node_id| {
463 make_test_metrics_set_from_seed(
464 (node_id * task_id) as u64,
465 num_metrics_per_task_per_node,
466 )
467 })
468 .collect::<Vec<MetricsSet>>();
469 let task_metrics = TaskMetrics {
470 task_metrics: MetricsSet::new(),
471 pre_order_plan_metrics: metrics,
472 };
473 (task_key, task_metrics)
474 }));
475 let metrics_collection = Arc::new(metrics_collection);
476
477 let rewritten_plan =
479 stage_metrics_rewriter(&stage, metrics_collection.clone(), format).unwrap();
480
481 let mut actual_metrics = vec![];
483 collect_metrics_from_plan(&rewritten_plan, &mut actual_metrics);
484 assert_eq!(
485 actual_metrics.len(),
486 count_plan_nodes_up_to_network_boundary(&plan)
487 );
488
489 for (node_id, actual_stage_node_metrics_set) in actual_metrics.iter().enumerate() {
492 for (task_id, actual_task_node_metrics_set) in actual_stage_node_metrics_set
494 .iter()
495 .chunks(num_metrics_per_task_per_node)
496 .into_iter()
497 .enumerate()
498 {
499 let expected_task_node_metrics = metrics_collection
500 .get(&TaskKey {
501 query_id: stage.query_id,
502 stage_id: stage.num,
503 task_number: task_id,
504 })
505 .unwrap()
506 .pre_order_plan_metrics[node_id]
507 .clone();
508
509 let mut actual_metrics_set = MetricsSet::new();
510 actual_task_node_metrics_set
511 .for_each(|metric| actual_metrics_set.push(metric.clone()));
512
513 let mut expected_metrics_set = expected_task_node_metrics;
514
515 if format == DistributedMetricsFormat::PerTask {
516 expected_metrics_set =
519 annotate_metrics_set_with_task_id(expected_metrics_set, task_id as u64);
520 }
521 assert!(metrics_set_eq(&actual_metrics_set, &expected_metrics_set));
522 }
523 }
524 }
525
526 #[test_case(DistributedMetricsFormat::Aggregated ; "aggregated_metrics")]
527 #[test_case(DistributedMetricsFormat::PerTask ; "per_task_metrics")]
528 #[tokio::test]
529 async fn test_stage_metrics_rewriter_1(format: DistributedMetricsFormat) {
530 run_stage_metrics_rewriter_test(
531 "SELECT sum(balance) / 7.0 as avg_yearly from table2 group by name",
532 format,
533 )
534 .await;
535 }
536
537 #[test_case(DistributedMetricsFormat::Aggregated ; "aggregated_metrics")]
538 #[test_case(DistributedMetricsFormat::PerTask ; "per_task_metrics")]
539 #[tokio::test]
540 async fn test_stage_metrics_rewriter_2(format: DistributedMetricsFormat) {
541 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;
542 }
543
544 #[test_case(DistributedMetricsFormat::Aggregated ; "aggregated_metrics")]
545 #[test_case(DistributedMetricsFormat::PerTask ; "per_task_metrics")]
546 #[tokio::test]
547 async fn test_stage_metrics_rewriter_3(format: DistributedMetricsFormat) {
548 run_stage_metrics_rewriter_test(
549 "SELECT sum(balance) / 7.0 as avg_yearly
550 FROM table2
551 WHERE name LIKE 'customer%'
552 AND balance < (
553 SELECT 0.2 * avg(balance)
554 FROM table2 t2_inner
555 WHERE t2_inner.id = table2.id
556 )",
557 format,
558 )
559 .await;
560 }
561
562 #[tokio::test]
563 async fn test_rewrite_unexecuted_distributed_plan_with_metrics_err() {
564 let ctx = make_test_distributed_ctx().await;
565 let plan = ctx
566 .sql("SELECT id, COUNT(*) as count FROM table1 WHERE id > 1 GROUP BY id ORDER BY id LIMIT 10")
567 .await
568 .unwrap()
569 .create_physical_plan()
570 .await
571 .unwrap();
572 assert!(plan.is::<DistributedExec>());
573 assert!(
574 rewrite_distributed_plan_with_metrics(plan, DistributedMetricsFormat::Aggregated)
575 .await
576 .is_err()
577 );
578 }
579
580 fn assert_metrics_present_in_plan(plan: &Arc<dyn ExecutionPlan>) {
582 if let Some(metrics) = plan.metrics() {
583 assert!(metrics.iter().count() > 0);
584 } else {
585 assert!(plan.is::<DistributedExec>());
586 }
587 for child in plan.children() {
588 assert_metrics_present_in_plan(child);
589 }
590 }
591
592 #[tokio::test]
593 async fn test_executed_distributed_plan_has_metrics() {
594 let ctx = make_test_distributed_ctx().await;
595 let plan = ctx
596 .sql("SELECT id, COUNT(*) as count FROM table1 WHERE id > 1 GROUP BY id ORDER BY id LIMIT 10")
597 .await
598 .unwrap()
599 .create_physical_plan()
600 .await
601 .unwrap();
602 collect(plan.clone(), ctx.task_ctx()).await.unwrap();
603 assert!(plan.is::<DistributedExec>());
604 let rewritten_plan =
605 rewrite_distributed_plan_with_metrics(plan, DistributedMetricsFormat::Aggregated)
606 .await
607 .unwrap();
608 assert_metrics_present_in_plan(&rewritten_plan);
609 }
610
611 #[test]
612 fn test_wrapped_node_is_accessible() {
616 let example_node = Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![Field::new(
617 "id",
618 DataType::Int32,
619 false,
620 )]))));
621
622 let wrapped = MetricsWrapperExec::new(example_node, MetricsSet::new());
623 assert_eq!(wrapped.name(), "EmptyExec");
624 assert!(wrapped.inner().is::<EmptyExec>());
625 }
626
627 #[test]
628 fn test_annotate_metrics_set_with_task_id_output_rows() {
629 let mut metrics_set = MetricsSet::new();
631 let count = Count::new();
632 count.add(1234);
633 let labels = vec![Label::new("operator", "scan")];
634 metrics_set.push(Arc::new(Metric::new_with_labels(
635 MetricValue::OutputRows(count),
636 Some(0),
637 labels,
638 )));
639
640 let task_id = 42;
641 let annotated = annotate_metrics_set_with_task_id(metrics_set, task_id);
642
643 assert_eq!(annotated.iter().count(), 1);
645
646 let metric = annotated.iter().next().unwrap();
647
648 match metric.value() {
650 MetricValue::OutputRows(count) => {
651 assert_eq!(count.value(), 1234);
652 }
653 other => panic!("Expected OutputRows, got {:?}", other.name()),
654 }
655
656 assert_eq!(metric.partition(), Some(0));
658
659 let labels: Vec<_> = metric.labels().iter().collect();
661 assert_eq!(labels.len(), 2);
662 assert_eq!(labels[0].name(), "operator");
663 assert_eq!(labels[0].value(), "scan");
664 assert_eq!(labels[1].name(), DISTRIBUTED_DATAFUSION_TASK_ID_LABEL);
665 assert_eq!(labels[1].value(), "42");
666 }
667}