1use crate::coordinator::{DistributedExec, MetricsStore};
2use crate::execution_plans::{DistributedLeafExec, NetworkCoalesceExec};
3use crate::metrics::DISTRIBUTED_DATAFUSION_TASK_ID_LABEL;
4use datafusion::common::{HashMap, Statistics, config_err};
5use datafusion::common::{exec_err, plan_err};
6use datafusion::error::Result;
7use datafusion::execution::{SendableRecordBatchStream, TaskContext};
8use datafusion::physical_plan::display::DisplayableExecutionPlan;
9use datafusion::physical_plan::metrics::{Label, Metric, MetricsSet};
10use datafusion::physical_plan::{
11 ColumnStatistics, ExecutionPlan, ExecutionPlanProperties, displayable,
12};
13use itertools::Either;
14use std::collections::VecDeque;
15use std::sync::Arc;
16use url::Url;
17use uuid::Uuid;
18
19#[derive(Debug, Clone)]
73pub enum Stage {
74 Local(LocalStage),
75 Remote(RemoteStage),
76}
77
78#[derive(Debug, Clone)]
79pub struct LocalStage {
80 pub query_id: Uuid,
82 pub num: usize,
84 pub plan: Arc<dyn ExecutionPlan>,
87 pub tasks: usize,
89 pub metrics_set: MetricsSet,
91}
92
93impl LocalStage {
94 pub fn execute(
95 &self,
96 partition: usize,
97 context: Arc<TaskContext>,
98 ) -> Result<SendableRecordBatchStream> {
99 if self.tasks > 1 {
100 return exec_err!("Cannot execute a local stage with more than 1 task");
101 }
102 self.plan.execute(partition, context)
103 }
104}
105
106#[derive(Debug, Clone)]
107pub struct RemoteStage {
108 pub query_id: Uuid,
110 pub num: usize,
112 pub workers: Vec<Url>,
114 pub runtime_stats: Option<Arc<Statistics>>,
116}
117
118impl Stage {
119 pub fn query_id(&self) -> Uuid {
120 match &self {
121 Self::Local(v) => v.query_id,
122 Self::Remote(v) => v.query_id,
123 }
124 }
125
126 pub fn num(&self) -> usize {
127 match &self {
128 Self::Local(v) => v.num,
129 Self::Remote(v) => v.num,
130 }
131 }
132
133 pub fn task_count(&self) -> usize {
134 match &self {
135 Self::Local(v) => v.tasks,
136 Self::Remote(v) => v.workers.len(),
137 }
138 }
139
140 pub fn local_plan(&self) -> Option<&Arc<dyn ExecutionPlan>> {
141 match &self {
142 Self::Local(v) => Some(&v.plan),
143 Self::Remote(_) => None,
144 }
145 }
146
147 pub fn metrics(&self) -> MetricsSet {
148 match &self {
149 Self::Local(v) => v.metrics_set.clone(),
150 Self::Remote(_) => MetricsSet::new(),
151 }
152 }
153
154 pub fn partition_statistics(
155 &self,
156 partition: Option<usize>,
157 partition_count: usize,
158 schema: SchemaRef,
159 ) -> Result<Arc<Statistics>> {
160 match self {
161 Stage::Local(local) => local.plan.partition_statistics(partition),
162 Stage::Remote(remote) => {
163 let Some(runtime_stats) = &remote.runtime_stats else {
164 return Ok(Arc::new(Statistics::new_unknown(&schema)));
165 };
166 match partition {
167 None => Ok(Arc::clone(runtime_stats)),
168 Some(_) => Ok(Arc::new(multiply_stats(
169 runtime_stats,
170 1.0 / partition_count as f32,
171 ))),
172 }
173 }
174 }
175 }
176}
177
178fn multiply_stats(stats: &Statistics, f: f32) -> Statistics {
179 Statistics {
180 num_rows: multiply_precision(stats.num_rows, f),
181 total_byte_size: multiply_precision(stats.total_byte_size, f),
182 column_statistics: stats
183 .column_statistics
184 .iter()
185 .map(|col| ColumnStatistics {
186 null_count: multiply_precision(col.null_count, f),
187 max_value: Precision::Absent,
188 min_value: Precision::Absent,
189 sum_value: Precision::Absent,
190 distinct_count: multiply_precision(col.distinct_count, f),
191 byte_size: multiply_precision(col.byte_size, f),
192 })
193 .collect(),
194 }
195}
196
197fn multiply_precision(p: Precision<usize>, f: f32) -> Precision<usize> {
198 match p {
199 Precision::Exact(v) => Precision::Exact((v as f32 * f) as usize),
200 Precision::Inexact(v) => Precision::Inexact((v as f32 * f) as usize),
201 Precision::Absent => Precision::Absent,
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq)]
206pub struct DistributedTaskContext {
207 pub task_index: usize,
208 pub task_count: usize,
209}
210
211impl DistributedTaskContext {
212 pub fn from_ctx(ctx: &Arc<TaskContext>) -> Arc<Self> {
213 ctx.session_config()
214 .get_extension::<Self>()
215 .unwrap_or(Arc::new(DistributedTaskContext {
216 task_index: 0,
217 task_count: 1,
218 }))
219 }
220}
221
222use crate::{
223 DistributedMetricsFormat, NetworkShuffleExec, TaskKey, rewrite_distributed_plan_with_metrics,
224};
225use crate::{NetworkBoundary, NetworkBoundaryExt};
226use datafusion::arrow::datatypes::SchemaRef;
227use datafusion::common::DataFusionError;
228use datafusion::common::stats::Precision;
229use datafusion::physical_expr::Partitioning;
230use std::fmt::Write;
243
244pub async fn explain_analyze(
246 executed: Arc<dyn ExecutionPlan>,
247 format: DistributedMetricsFormat,
248) -> Result<String, DataFusionError> {
249 match executed.downcast_ref::<DistributedExec>() {
250 None => Ok(DisplayableExecutionPlan::with_metrics(executed.as_ref())
251 .indent(true)
252 .to_string()),
253 Some(_) => {
254 let executed = rewrite_distributed_plan_with_metrics(executed.clone(), format).await?;
255 Ok(display_plan_ascii(executed.as_ref(), true))
256 }
257 }
258}
259
260const LTCORNER: &str = "┌"; const LDCORNER: &str = "└"; const VERTICAL: &str = "│"; const HORIZONTAL: &str = "─"; pub fn display_plan_ascii(plan: &dyn ExecutionPlan, show_metrics: bool) -> String {
266 if let Some(plan) = plan.downcast_ref::<DistributedExec>() {
267 let mut f = String::new();
268 display_ascii(plan, Either::Left(plan), 0, show_metrics, &mut f).unwrap();
269 f
270 } else {
271 match show_metrics {
272 true => DisplayableExecutionPlan::with_metrics(plan)
273 .indent(true)
274 .to_string(),
275 false => displayable(plan).indent(true).to_string(),
276 }
277 }
278}
279
280fn display_ascii(
281 root: &DistributedExec,
282 stage: Either<&DistributedExec, &Stage>,
283 depth: usize,
284 show_metrics: bool,
285 f: &mut String,
286) -> std::fmt::Result {
287 let plan = match stage {
288 Either::Left(distributed_exec) => distributed_exec.children().first().unwrap(),
289 Either::Right(stage) => {
290 let Some(plan) = stage.local_plan() else {
291 return write!(f, "StageExec: encoded input plan");
292 };
293 plan
294 }
295 };
296 match stage {
297 Either::Left(dist_exec) => {
298 write!(
301 f,
302 "{}{}{} DistributedExec",
303 " ".repeat(depth),
304 LTCORNER,
305 HORIZONTAL.repeat(5),
306 )?;
307 if show_metrics && let Some(metrics) = dist_exec.metrics() {
308 writeln!(
309 f,
310 " {} {}",
311 HORIZONTAL.repeat(2),
312 format_metrics_by_task(&metrics)
313 )?;
314 } else {
315 writeln!(f)?;
316 }
317 }
318 Either::Right(stage) => {
319 write!(
320 f,
321 "{}{}{} Stage {} {} {}",
322 " ".repeat(depth),
323 LTCORNER,
324 HORIZONTAL.repeat(5),
325 stage.num(),
326 HORIZONTAL.repeat(2),
327 format_tasks_for_stage(stage.task_count(), plan)
328 )?;
329 if show_metrics && let Some(metrics_store) = &root.metrics_store {
330 let metrics = gather_stage_header_metrics(stage, metrics_store);
331 write!(f, " ")?;
332 writeln!(f, "{}", format_metrics_by_task(&metrics))?;
333 } else {
334 writeln!(f)?;
335 }
336 }
337 }
338
339 let mut plan_str = String::new();
340 display_inner_ascii(plan, 0, show_metrics, &mut plan_str)?;
341 let plan_str = plan_str
342 .split('\n')
343 .filter(|v| !v.is_empty())
344 .collect::<Vec<_>>()
345 .join(&format!("\n{}{}", " ".repeat(depth), VERTICAL));
346 writeln!(f, "{}{}{}", " ".repeat(depth), VERTICAL, plan_str)?;
347 writeln!(
348 f,
349 "{}{}{}",
350 " ".repeat(depth),
351 LDCORNER,
352 HORIZONTAL.repeat(50)
353 )?;
354 for input_stage in find_input_stages(plan.as_ref()) {
355 display_ascii(root, Either::Right(input_stage), depth + 1, show_metrics, f)?;
356 }
357 Ok(())
358}
359
360fn display_inner_ascii(
361 plan: &Arc<dyn ExecutionPlan>,
362 indent: usize,
363 show_metrics: bool,
364 f: &mut String,
365) -> std::fmt::Result {
366 if plan.is::<DistributedLeafExec>() {
367 return display_inner_distributed_leaf(plan, indent, show_metrics, f);
368 }
369
370 let node_str = displayable(plan.as_ref()).one_line().to_string();
371 let metrics_str = match show_metrics {
372 true => metrics_suffix(plan.metrics().map(|m| format_metrics_by_task(&m))),
373 false => String::new(),
374 };
375 writeln!(
376 f,
377 "{} {}{metrics_str}",
378 " ".repeat(indent),
379 node_str.trim_end() )?;
381
382 if plan.is_network_boundary() {
383 return Ok(());
384 }
385
386 for child in plan.children() {
387 display_inner_ascii(child, indent + 2, show_metrics, f)?;
388 }
389 Ok(())
390}
391
392fn display_inner_distributed_leaf(
393 plan: &Arc<dyn ExecutionPlan>,
394 indent: usize,
395 show_metrics: bool,
396 f: &mut String,
397) -> std::fmt::Result {
398 let Some(leaf) = plan.downcast_ref::<DistributedLeafExec>() else {
399 return Ok(());
400 };
401 let indent = " ".repeat(indent);
402
403 if let Some(by_task) = show_metrics
408 .then(|| plan.metrics())
409 .flatten()
410 .map(|m| metrics_by_task_id(&m))
411 && !by_task.is_empty()
412 {
413 writeln!(f, "{indent} DistributedLeafExec:")?;
414 for (task_i, variant) in leaf.variants.iter().enumerate() {
415 let variant = displayable(variant.as_ref()).one_line().to_string();
416 let metrics = match by_task.is_empty() {
417 true => String::new(),
418 false => metrics_suffix(by_task.get(&task_i).map(format_metrics_by_task)),
419 };
420 writeln!(f, "{indent} t{task_i}: {}{metrics}", variant.trim_end())?;
421 }
422 } else {
423 let header = match show_metrics {
424 true => metrics_suffix(plan.metrics().map(|m| format_metrics_by_task(&m))),
425 false => String::new(),
426 };
427 writeln!(f, "{indent} DistributedLeafExec:{header}")?;
428 for (task_i, variant) in leaf.variants.iter().enumerate() {
429 let variant = displayable(variant.as_ref()).one_line().to_string();
430 writeln!(f, "{indent} t{task_i}: {}", variant.trim_end())?;
431 }
432 }
433 Ok(())
434}
435
436fn gather_stage_header_metrics(stage: &Stage, metrics_store: &MetricsStore) -> MetricsSet {
439 let mut task_key = TaskKey {
440 query_id: stage.query_id(),
441 stage_id: stage.num(),
442 task_number: 0,
443 };
444 let mut all_metrics = stage.metrics();
445 while let Some(metrics_set) = metrics_store.get(&task_key).map(|v| v.task_metrics) {
446 for metric in metrics_set.iter() {
447 let mut labels = metric.labels().to_vec();
448 labels.push(Label::new(
449 DISTRIBUTED_DATAFUSION_TASK_ID_LABEL,
450 task_key.task_number.to_string(),
451 ));
452 all_metrics.push(Arc::new(Metric::new_with_labels(
453 metric.value().clone(),
454 metric.partition(),
455 labels,
456 )));
457 }
458 task_key.task_number += 1;
459 }
460 all_metrics
461}
462
463fn aggregate_by_task_id(metrics: &MetricsSet) -> MetricsSet {
470 let mut map: HashMap<(String, Option<String>), Metric> = HashMap::new();
472
473 for metric in metrics.iter() {
474 let name = metric.value().name().to_string();
475 let task_id = metric
476 .labels()
477 .iter()
478 .find(|l| l.name() == DISTRIBUTED_DATAFUSION_TASK_ID_LABEL)
479 .map(|l| l.value().to_string());
480
481 let key = (name, task_id.clone());
482
483 map.entry(key)
484 .and_modify(|accum| {
485 accum.value_mut().aggregate(metric.value());
486 })
487 .or_insert_with(|| {
488 let labels = task_id
489 .map(|id| vec![Label::new(DISTRIBUTED_DATAFUSION_TASK_ID_LABEL, id)])
490 .unwrap_or_default();
491 let mut accum = Metric::new_with_labels(
492 metric.value().new_empty(),
493 None, labels,
495 );
496 accum.value_mut().aggregate(metric.value());
497 accum
498 });
499 }
500
501 let mut result = MetricsSet::new();
502 for (_, metric) in map {
503 result.push(Arc::new(metric));
504 }
505 result
506}
507
508fn sorted_for_display_by_task_id(metrics: MetricsSet) -> MetricsSet {
514 let mut vec: Vec<Arc<Metric>> = metrics.iter().cloned().collect();
515 vec.sort_unstable_by_key(|metric| {
516 let task_id = metric
517 .labels()
518 .iter()
519 .find(|l| l.name() == DISTRIBUTED_DATAFUSION_TASK_ID_LABEL)
520 .and_then(|l| l.value().parse::<u64>().ok());
521 (
522 metric.value().display_sort_key(),
523 metric.value().name().to_owned(),
524 task_id,
525 )
526 });
527 let mut result = MetricsSet::new();
528 for m in vec {
529 result.push(m);
530 }
531 result
532}
533
534fn format_metrics_by_task(metrics: &MetricsSet) -> String {
545 let aggregated = aggregate_by_task_id(metrics);
546 let sorted = sorted_for_display_by_task_id(aggregated).timestamps_removed();
547
548 let mut groups: Vec<(String, bool, Vec<String>)> = Vec::new();
552 for m in sorted.iter() {
553 let name = m.value().name().to_string();
554 let task_id = m
555 .labels()
556 .iter()
557 .find(|l| l.name() == DISTRIBUTED_DATAFUSION_TASK_ID_LABEL)
558 .map(|l| l.value());
559 let entry = match task_id {
560 Some(id) => format!("{id}:{}", m.value()),
561 None => m.value().to_string(),
562 };
563 match groups.last_mut() {
564 Some((n, _, entries)) if *n == name => entries.push(entry),
565 _ => groups.push((name, task_id.is_some(), vec![entry])),
566 }
567 }
568
569 groups
570 .into_iter()
571 .map(|(name, has_task_id, entries)| match has_task_id {
572 true => format!("{name}={{{}}}", entries.join(", ")),
573 false => format!("{name}={}", entries.join(", ")),
574 })
575 .collect::<Vec<_>>()
576 .join(", ")
577}
578
579fn metrics_suffix(formatted: Option<String>) -> String {
582 match formatted.unwrap_or_default() {
583 s if s.is_empty() => ", metrics=[]".to_string(),
584 s => format!(", metrics=[{s}]"),
585 }
586}
587
588fn metrics_by_task_id(metrics: &MetricsSet) -> HashMap<usize, MetricsSet> {
592 let mut map: HashMap<usize, MetricsSet> = HashMap::new();
593 for metric in metrics.iter() {
594 let Some(task_id) = metric
595 .labels()
596 .iter()
597 .find(|l| l.name() == DISTRIBUTED_DATAFUSION_TASK_ID_LABEL)
598 .and_then(|l| l.value().parse::<usize>().ok())
599 else {
600 continue;
601 };
602 map.entry(task_id).or_default().push(Arc::clone(metric));
603 }
604 map
605}
606
607fn format_tasks_for_stage(n_tasks: usize, head: &Arc<dyn ExecutionPlan>) -> String {
608 let partitioning = head.properties().output_partitioning();
609 let input_partitions = partitioning.partition_count();
610 let hash_shuffle = matches!(partitioning, Partitioning::Hash(_, _));
611 let partitions = match hash_shuffle {
615 true => input_partitions,
616 false => n_tasks * input_partitions,
617 };
618 format!("tasks={n_tasks}, partitions={partitions}")
619}
620
621const NUM_COLORS: usize = 6;
624const COLOR_SCHEME: &str = "spectral6";
625
626pub fn display_plan_graphviz(plan: Arc<dyn ExecutionPlan>) -> Result<String> {
633 let mut f = String::new();
634
635 writeln!(
636 f,
637 "digraph G {{
638 rankdir=BT
639 edge[colorscheme={COLOR_SCHEME}, penwidth=2.0]
640 splines=false
641"
642 )?;
643
644 if plan.is::<DistributedExec>() {
645 let mut max_num = 0;
646 let mut all_stages = find_all_stages(&plan)
647 .into_iter()
648 .inspect(|v| max_num = max_num.max(v.num()))
649 .collect::<Vec<_>>();
650 let head_stage = Stage::Local(LocalStage {
651 query_id: Default::default(),
652 num: max_num + 1,
653 plan: plan.clone(),
654 tasks: 1,
655 metrics_set: MetricsSet::new(),
656 });
657 all_stages.insert(0, &head_stage);
658
659 for stage in &all_stages {
661 for i in 0..stage.task_count() {
662 let p = display_single_task(stage, i)?;
663 writeln!(f, "{p}")?;
664 }
665 }
666 for stage in &all_stages {
668 let Some(plan) = stage.local_plan() else {
669 continue;
670 };
671 for input_stage in find_input_stages(plan.as_ref()) {
672 for task_i in 0..stage.task_count() {
673 for input_task_i in 0..input_stage.task_count() {
674 let edges =
675 display_inter_task_edges(stage, task_i, input_stage, input_task_i)?;
676 writeln!(
677 f,
678 "// edges from child stage {} task {} to stage {} task {}\n {}",
679 input_stage.num(),
680 input_task_i,
681 stage.num(),
682 task_i,
683 edges
684 )?;
685 }
686 }
687 }
688 }
689 } else {
690 writeln!(f, "node[shape=none]")?;
692 let p = display_plan(&plan, 0, 1, 0)?;
693 writeln!(f, "{p}")?;
694 }
695
696 writeln!(f, "}}")?;
697
698 Ok(f)
699}
700
701fn display_single_task(stage: &Stage, task_i: usize) -> Result<String> {
702 let Some(plan) = stage.local_plan() else {
703 return config_err!("plan not present");
704 };
705 let partition_group =
706 build_partition_group(task_i, plan.output_partitioning().partition_count());
707
708 let mut f = String::new();
709 writeln!(
710 f,
711 "
712 subgraph \"cluster_stage_{}_task_{}_margin\" {{
713 style=invis
714 margin=20.0
715 subgraph \"cluster_stage_{}_task_{}\" {{
716 color=blue
717 style=dotted
718 label = \"Stage {} Task {} Partitions {}\"
719 labeljust=r
720 labelloc=b
721
722 node[shape=none]
723
724",
725 stage.num(),
726 task_i,
727 stage.num(),
728 task_i,
729 stage.num(),
730 task_i,
731 format_pg(&partition_group)
732 )?;
733
734 writeln!(
735 f,
736 "{}",
737 display_plan(plan, task_i, stage.task_count(), stage.num())?
738 )?;
739 writeln!(f, " }}")?;
740 writeln!(f, " }}")?;
741
742 Ok(f)
743}
744
745fn display_plan(
746 plan: &Arc<dyn ExecutionPlan>,
747 task_i: usize,
748 _n_tasks: usize,
749 stage_num: usize,
750) -> Result<String> {
751 let mut queue = VecDeque::from([plan]);
756 let mut node_index = 0;
757
758 let mut f = String::new();
759 while let Some(plan) = queue.pop_front() {
760 node_index += 1;
761 let p = display_single_plan(plan.as_ref(), stage_num, task_i, node_index)?;
762 writeln!(f, "{p}")?;
763
764 if plan.is_network_boundary() {
765 continue;
766 }
767 for child in plan.children().iter() {
768 queue.push_back(child);
769 }
770 }
771
772 type PlanWithParent<'a> = (
774 &'a Arc<dyn ExecutionPlan>,
775 Option<&'a Arc<dyn ExecutionPlan>>,
776 usize,
777 );
778 let mut queue: VecDeque<PlanWithParent> = VecDeque::from([(plan, None, 0usize)]);
779 node_index = 0;
780 while let Some((plan, maybe_parent, parent_idx)) = queue.pop_front() {
781 node_index += 1;
782 if let Some(parent) = maybe_parent {
783 let output_partitions = plan.output_partitioning().partition_count();
784
785 for i in 0..output_partitions {
786 let style = "";
787
788 writeln!(
789 f,
790 " {}_{}_{}_{}:t{}:n -> {}_{}_{}_{}:b{}:s {}[color={}]",
791 plan.name(),
792 stage_num,
793 task_i,
794 node_index,
795 i,
796 parent.name(),
797 stage_num,
798 task_i,
799 parent_idx,
800 i,
801 style,
802 i % NUM_COLORS + 1
803 )?;
804 }
805 }
806
807 if plan.as_ref().is_network_boundary() {
808 continue;
809 }
810
811 for child in plan.children() {
812 queue.push_back((child, Some(plan), node_index));
813 }
814 }
815 Ok(f)
816}
817
818pub fn display_single_plan(
862 plan: &(dyn ExecutionPlan + 'static),
863 stage_num: usize,
864 task_i: usize,
865 node_index: usize,
866) -> Result<String> {
867 let mut f = String::new();
868 let output_partitions = plan.output_partitioning().partition_count();
869 let input_partitions = if plan.is_network_boundary() {
870 output_partitions
871 } else if let Some(child) = plan.children().first() {
872 child.output_partitioning().partition_count()
873 } else {
874 1
875 };
876
877 writeln!(
878 f,
879 "
880 {}_{}_{}_{} [label=<
881 <TABLE BORDER='0' CELLBORDER='0' CELLSPACING='0' CELLPADDING='0'>
882 <TR>
883 <TD CELLBORDER='0'>
884 <TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'>
885 <TR>",
886 plan.name(),
887 stage_num,
888 task_i,
889 node_index
890 )?;
891
892 for i in 0..output_partitions {
893 writeln!(f, " <TD PORT='t{i}'></TD>")?;
894 }
895
896 writeln!(
897 f,
898 " </TR>
899 </TABLE>
900 </TD>
901 </TR>
902 <TR>
903 <TD BORDER='0' CELLPADDING='0' CELLSPACING='0'>
904 <TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'>
905 <TR>
906 <TD>{}</TD>
907 </TR>
908 </TABLE>
909 </TD>
910 </TR>
911 <TR>
912 <TD CELLBORDER='0'>
913 <TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'>
914 <TR>",
915 plan.name()
916 )?;
917
918 for i in 0..input_partitions {
919 writeln!(f, " <TD PORT='b{i}'></TD>")?;
920 }
921
922 writeln!(
923 f,
924 " </TR>
925 </TABLE>
926 </TD>
927 </TR>
928 </TABLE>
929 >];
930"
931 )?;
932 Ok(f)
933}
934
935fn display_inter_task_edges(
936 stage: &Stage,
937 task_i: usize,
938 input_stage: &Stage,
939 input_task_i: usize,
940) -> Result<String> {
941 let Some(plan) = stage.local_plan() else {
942 return plan_err!("The inner plan of a stage was encoded.");
943 };
944 let Some(input_plan) = input_stage.local_plan() else {
945 return plan_err!("The inner plan of a stage was encoded.");
946 };
947 let mut f = String::new();
948
949 let mut queue = VecDeque::from([plan]);
950 let mut index = 0;
951 while let Some(plan) = queue.pop_front() {
952 index += 1;
953 if let Some(node) = plan.downcast_ref::<NetworkShuffleExec>() {
954 if node.input_stage().num() != input_stage.num() {
955 continue;
956 }
957 let output_partitions = plan.output_partitioning().partition_count();
959 for p in 0..output_partitions {
960 writeln!(
961 f,
962 " {}_{}_{}_{}:t{}:n -> {}_{}_{}_{}:b{}:s [color={}]",
963 input_plan.name(),
964 input_stage.num(),
965 input_task_i,
966 1, p + (task_i * output_partitions),
968 plan.name(),
969 stage.num(),
970 task_i,
971 index,
972 p,
973 p % NUM_COLORS + 1
974 )?;
975 }
976 continue;
977 } else if let Some(node) = plan.downcast_ref::<NetworkCoalesceExec>() {
978 if node.input_stage().num() != input_stage.num() {
979 continue;
980 }
981 let output_partitions = plan.output_partitioning().partition_count();
983 let input_partitions_per_task = output_partitions / input_stage.task_count();
984 for p in 0..input_partitions_per_task {
985 writeln!(
986 f,
987 " {}_{}_{}_{}:t{}:n -> {}_{}_{}_{}:b{}:s [color={}]",
988 input_plan.name(),
989 input_stage.num(),
990 input_task_i,
991 1, p,
993 plan.name(),
994 stage.num(),
995 task_i,
996 index,
997 p + (input_task_i * input_partitions_per_task),
998 p % NUM_COLORS + 1
999 )?;
1000 }
1001 continue;
1002 }
1003
1004 for child in plan.children() {
1005 queue.push_back(child);
1006 }
1007 }
1008
1009 Ok(f)
1010}
1011
1012fn format_pg(partition_group: &[usize]) -> String {
1013 partition_group
1014 .iter()
1015 .map(|pg| format!("{pg}"))
1016 .collect::<Vec<_>>()
1017 .join("_")
1018}
1019
1020fn build_partition_group(task_i: usize, partitions: usize) -> Vec<usize> {
1021 ((task_i * partitions)..((task_i + 1) * partitions)).collect::<Vec<_>>()
1022}
1023
1024fn find_input_stages(plan: &dyn ExecutionPlan) -> Vec<&Stage> {
1025 let mut result = vec![];
1026 for child in plan.children() {
1027 if let Some(plan) = child.as_network_boundary() {
1028 result.push(plan.input_stage());
1029 } else {
1030 result.extend(find_input_stages(child.as_ref()));
1031 }
1032 }
1033 result
1034}
1035
1036pub(crate) fn find_all_stages(plan: &Arc<dyn ExecutionPlan>) -> Vec<&Stage> {
1037 let mut result = vec![];
1038 if let Some(plan) = plan.as_network_boundary() {
1039 result.push(plan.input_stage());
1040 }
1041 for child in plan.children() {
1042 result.extend(find_all_stages(child));
1043 }
1044 result
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050 use crate::test_utils::mock_exec::MockExec;
1051 use datafusion::arrow::datatypes::{DataType, Field, Schema};
1052 use datafusion::physical_expr::expressions::Column;
1053 use datafusion::physical_expr::{Partitioning, PhysicalExpr};
1054 use datafusion::physical_plan::metrics::{Count, MetricValue};
1055 use datafusion::physical_plan::repartition::RepartitionExec;
1056
1057 fn output_rows(rows: usize, task_id: Option<u64>) -> Arc<Metric> {
1059 let count = Count::new();
1060 count.add(rows);
1061 let labels = task_id
1062 .map(|id| {
1063 vec![Label::new(
1064 DISTRIBUTED_DATAFUSION_TASK_ID_LABEL,
1065 id.to_string(),
1066 )]
1067 })
1068 .unwrap_or_default();
1069 Arc::new(Metric::new_with_labels(
1070 MetricValue::OutputRows(count),
1071 None,
1072 labels,
1073 ))
1074 }
1075
1076 fn metrics_set(metrics: impl IntoIterator<Item = Arc<Metric>>) -> MetricsSet {
1077 let mut set = MetricsSet::new();
1078 for m in metrics {
1079 set.push(m);
1080 }
1081 set
1082 }
1083
1084 #[test]
1085 fn format_metrics_by_task_collapses_per_task_values_into_a_map() {
1086 let set = metrics_set([
1087 output_rows(100, Some(0)),
1088 output_rows(150, Some(1)),
1089 output_rows(200, Some(2)),
1090 ]);
1091 assert_eq!(
1092 format_metrics_by_task(&set),
1093 "output_rows={0:100, 1:150, 2:200}"
1094 );
1095 }
1096
1097 #[test]
1098 fn format_metrics_by_task_keeps_task_ids_explicit_when_non_contiguous() {
1099 let set = metrics_set([output_rows(100, Some(0)), output_rows(200, Some(2))]);
1103 assert_eq!(format_metrics_by_task(&set), "output_rows={0:100, 2:200}");
1104 }
1105
1106 #[test]
1107 fn format_metrics_by_task_without_task_ids_stays_scalar() {
1108 let set = metrics_set([output_rows(100, None)]);
1109 assert_eq!(format_metrics_by_task(&set), "output_rows=100");
1110 }
1111
1112 fn single_column_schema() -> Arc<Schema> {
1113 Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]))
1114 }
1115
1116 #[test]
1117 fn format_tasks_for_stage_non_hash_counts_every_task_slice() {
1118 let plan: Arc<dyn ExecutionPlan> = Arc::new(MockExec::new_partitioned(
1120 vec![vec![], vec![], vec![]],
1121 single_column_schema(),
1122 ));
1123 assert_eq!(format_tasks_for_stage(2, &plan), "tasks=2, partitions=6");
1124 }
1125
1126 #[test]
1127 fn format_tasks_for_stage_hash_shares_partitions_across_tasks() {
1128 let mock: Arc<dyn ExecutionPlan> = Arc::new(MockExec::new(vec![], single_column_schema()));
1130 let expr: Arc<dyn PhysicalExpr> = Arc::new(Column::new("id", 0));
1131 let hashed: Arc<dyn ExecutionPlan> =
1132 Arc::new(RepartitionExec::try_new(mock, Partitioning::Hash(vec![expr], 8)).unwrap());
1133 assert_eq!(format_tasks_for_stage(3, &hashed), "tasks=3, partitions=8");
1134 }
1135}