1use std::fmt::Display;
20use std::sync::Arc;
21
22use ahash::HashMap;
23use arrow::array::{StringBuilder, UInt32Builder};
24use arrow_schema::{DataType, Field, Schema, SchemaRef};
25use common_recordbatch::adapter::{MetricCollector, PlanMetrics, RecordBatchMetrics};
26use common_recordbatch::{DfRecordBatch, DfSendableRecordBatchStream};
27use datafusion::error::Result as DfResult;
28use datafusion::execution::TaskContext;
29use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec;
30use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
31use datafusion::physical_plan::{
32 ChildrenPropertiesMode, DisplayAs, DisplayFormatType, ExecutionPlan,
33 InputDistributionRequirements, PlanProperties, ReplaceChildrenOptions, accept,
34};
35use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
36use datafusion_common::{DataFusionError, assert_eq_or_internal_err, internal_err};
37use datafusion_physical_expr::{EquivalenceProperties, Partitioning, PhysicalExpr};
38use futures::StreamExt;
39use serde::Serialize;
40use serde_json::{Value, json};
41use sqlparser::ast::AnalyzeFormat;
42
43use crate::dist_plan::MergeScanExec;
44
45const STAGE: &str = "stage";
46const NODE: &str = "node";
47const PLAN: &str = "plan";
48
49pub fn dist_analyze_output_schema() -> SchemaRef {
53 SchemaRef::new(Schema::new(vec![
54 Field::new(STAGE, DataType::UInt32, true),
55 Field::new(NODE, DataType::UInt32, true),
56 Field::new(PLAN, DataType::Utf8, true),
57 ]))
58}
59
60#[derive(Debug)]
61pub struct DistAnalyzeExec {
62 input: Arc<dyn ExecutionPlan>,
63 schema: SchemaRef,
64 properties: Arc<PlanProperties>,
65 verbose: bool,
66 format: AnalyzeFormat,
67}
68
69impl DistAnalyzeExec {
70 pub fn new(input: Arc<dyn ExecutionPlan>, verbose: bool, format: AnalyzeFormat) -> Self {
72 let schema = dist_analyze_output_schema();
73 let properties = Arc::new(Self::compute_properties(&input, schema.clone()));
74 Self {
75 input,
76 schema,
77 properties,
78 verbose,
79 format,
80 }
81 }
82
83 fn compute_properties(input: &Arc<dyn ExecutionPlan>, schema: SchemaRef) -> PlanProperties {
85 let eq_properties = EquivalenceProperties::new(schema);
86 let output_partitioning = Partitioning::UnknownPartitioning(1);
87 let properties = input.properties();
88 PlanProperties::new(
89 eq_properties,
90 output_partitioning,
91 properties.emission_type,
92 properties.boundedness,
93 )
94 }
95
96 pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
97 &self.input
98 }
99}
100
101pub fn analyze_plan_metrics_to_json_value(
109 plan: &Arc<dyn ExecutionPlan>,
110 verbose: bool,
111) -> serde_json::Result<Value> {
112 let input = plan
113 .downcast_ref::<DistAnalyzeExec>()
114 .map(|exec| exec.input().clone())
115 .unwrap_or_else(|| plan.clone());
116
117 let mut stages = Vec::new();
118 let mut collector = MetricCollector::new(verbose);
119 accept(input.as_ref(), &mut collector).unwrap();
120 stages.push(json!({
121 "stage": 0,
122 "node": 0,
123 "plan": JsonMetrics::from_record_batch_metrics(collector.record_batch_metrics),
124 }));
125
126 let _ = input.apply(|plan| {
127 if let Some(merge_scan) = plan.downcast_ref::<MergeScanExec>() {
128 for (node, metric) in merge_scan.sub_stage_metrics().into_iter().enumerate() {
129 stages.push(json!({
130 "stage": 1,
131 "node": node,
132 "plan": JsonMetrics::from_record_batch_metrics(metric),
133 }));
134 }
135 }
136 Ok(TreeNodeRecursion::Continue)
137 });
138
139 Ok(Value::Array(stages))
140}
141
142impl DisplayAs for DistAnalyzeExec {
143 fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
144 match t {
145 DisplayFormatType::Default
146 | DisplayFormatType::Verbose
147 | DisplayFormatType::TreeRender => {
148 write!(f, "DistAnalyzeExec",)
149 }
150 }
151 }
152}
153
154impl ExecutionPlan for DistAnalyzeExec {
155 fn name(&self) -> &'static str {
156 "DistAnalyzeExec"
157 }
158
159 fn properties(&self) -> &Arc<PlanProperties> {
160 &self.properties
161 }
162
163 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
164 vec![&self.input]
165 }
166
167 fn apply_expressions(
168 &self,
169 _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> DfResult<TreeNodeRecursion>,
170 ) -> DfResult<TreeNodeRecursion> {
171 Ok(TreeNodeRecursion::Continue)
172 }
173
174 fn input_distribution_requirements(&self) -> InputDistributionRequirements {
176 InputDistributionRequirements::new(vec![
178 datafusion_physical_expr::Distribution::UnspecifiedDistribution,
179 ])
180 }
181
182 fn replace_children(
183 self: Arc<Self>,
184 mut children: Vec<Arc<dyn ExecutionPlan>>,
185 _options: ReplaceChildrenOptions,
186 ) -> DfResult<Arc<dyn ExecutionPlan>> {
187 assert_eq_or_internal_err!(
188 children.len(),
189 1,
190 "DistAnalyzeExec requires exactly one child"
191 );
192 Ok(Arc::new(Self::new(
193 children.swap_remove(0),
194 self.verbose,
195 self.format,
196 )))
197 }
198
199 #[allow(deprecated)]
200 fn with_new_children(
201 self: Arc<Self>,
202 children: Vec<Arc<dyn ExecutionPlan>>,
203 ) -> DfResult<Arc<dyn ExecutionPlan>> {
204 self.replace_children(
205 children,
206 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
207 )
208 }
209
210 fn execute(
211 &self,
212 partition: usize,
213 context: Arc<TaskContext>,
214 ) -> DfResult<DfSendableRecordBatchStream> {
215 if 0 != partition {
216 return internal_err!("AnalyzeExec invalid partition. Expected 0, got {partition}");
217 }
218
219 let coalesce_partition_plan = CoalescePartitionsExec::new(self.input.clone());
222
223 let captured_input = self.input.clone();
225 let captured_schema = self.schema.clone();
226
227 let format = self.format;
229 let verbose = self.verbose;
230 let mut input_stream = coalesce_partition_plan.execute(0, context)?;
231 let output = async move {
232 let mut total_rows = 0;
233 while let Some(batch) = input_stream.next().await.transpose()? {
234 total_rows += batch.num_rows();
235 }
236
237 create_output_batch(total_rows, captured_input, captured_schema, format, verbose)
238 };
239
240 Ok(Box::pin(RecordBatchStreamAdapter::new(
241 self.schema.clone(),
242 futures::stream::once(output),
243 )))
244 }
245}
246
247struct AnalyzeOutputBuilder {
249 stage_builder: UInt32Builder,
250 node_builder: UInt32Builder,
251 plan_builder: StringBuilder,
252 schema: SchemaRef,
253}
254
255impl AnalyzeOutputBuilder {
256 fn new(schema: SchemaRef) -> Self {
257 Self {
258 stage_builder: UInt32Builder::with_capacity(4),
259 node_builder: UInt32Builder::with_capacity(4),
260 plan_builder: StringBuilder::with_capacity(1, 1024),
261 schema,
262 }
263 }
264
265 fn append_metric(&mut self, stage: u32, node: u32, content: String) {
266 self.stage_builder.append_value(stage);
267 self.node_builder.append_value(node);
268 self.plan_builder.append_value(content);
269 }
270
271 fn append_total_rows(&mut self, total_rows: usize) {
272 self.stage_builder.append_null();
273 self.node_builder.append_null();
274 self.plan_builder
275 .append_value(format!("Total rows: {}", total_rows));
276 }
277
278 fn finish(mut self) -> DfResult<DfRecordBatch> {
279 DfRecordBatch::try_new(
280 self.schema,
281 vec![
282 Arc::new(self.stage_builder.finish()),
283 Arc::new(self.node_builder.finish()),
284 Arc::new(self.plan_builder.finish()),
285 ],
286 )
287 .map_err(DataFusionError::from)
288 }
289}
290
291fn create_output_batch(
293 total_rows: usize,
294 input: Arc<dyn ExecutionPlan>,
295 schema: SchemaRef,
296 format: AnalyzeFormat,
297 verbose: bool,
298) -> DfResult<DfRecordBatch> {
299 let mut builder = AnalyzeOutputBuilder::new(schema);
300
301 let mut collector = MetricCollector::new(verbose);
303 accept(input.as_ref(), &mut collector).unwrap();
305 let stage_0_metrics = collector.record_batch_metrics;
306
307 builder.append_metric(0, 0, metrics_to_string(stage_0_metrics, format)?);
309
310 input.apply(|plan| {
312 if let Some(merge_scan) = plan.downcast_ref::<MergeScanExec>() {
313 let sub_stage_metrics = merge_scan.sub_stage_metrics();
314 for (node, metric) in sub_stage_metrics.into_iter().enumerate() {
315 builder.append_metric(1, node as _, metrics_to_string(metric, format)?);
316 }
317 return Ok(TreeNodeRecursion::Continue);
319 }
320 Ok(TreeNodeRecursion::Continue)
321 })?;
322
323 builder.append_total_rows(total_rows);
325
326 builder.finish()
327}
328
329fn metrics_to_string(metrics: RecordBatchMetrics, format: AnalyzeFormat) -> DfResult<String> {
330 match format {
331 AnalyzeFormat::JSON => Ok(JsonMetrics::from_record_batch_metrics(metrics).to_string()),
332 AnalyzeFormat::TEXT => Ok(metrics.to_string()),
333 format => Err(DataFusionError::NotImplemented(format!(
334 "AnalyzeFormat {format}",
335 ))),
336 }
337}
338
339#[derive(Debug, Default, Serialize)]
340struct JsonMetrics {
341 name: String,
342 param: String,
343
344 output_rows: usize,
346 elapsed_compute: usize,
348
349 metrics: HashMap<String, usize>,
351 #[serde(skip_serializing_if = "Option::is_none")]
352 memory_usage: Option<usize>,
353 children: Vec<JsonMetrics>,
354}
355
356impl JsonMetrics {
357 fn from_record_batch_metrics(record_batch_metrics: RecordBatchMetrics) -> Self {
358 let mut layers: HashMap<usize, Vec<Self>> = HashMap::default();
359
360 let memory_usage = record_batch_metrics.memory_usage;
361 for plan_metrics in record_batch_metrics.plan_metrics.into_iter().rev() {
362 let (level, mut metrics) = Self::from_plan_metrics(plan_metrics);
363 if let Some(next_layer) = layers.remove(&(level + 1)) {
364 metrics.children = next_layer;
365 }
366 if level == 0 {
367 metrics.memory_usage = Some(memory_usage);
368 return metrics;
369 }
370 layers.entry(level).or_default().push(metrics);
371 }
372
373 Self::default()
375 }
376
377 fn from_plan_metrics(plan_metrics: PlanMetrics) -> (usize, Self) {
381 let raw_name = plan_metrics.plan.trim_end();
382 let mut elapsed_compute = 0;
383 let mut output_rows = 0;
384 let mut other_metrics = HashMap::default();
385 let (name, param) = raw_name.split_once(": ").unwrap_or((raw_name, ""));
386
387 for (name, value) in plan_metrics.metrics.into_iter() {
388 if name == "elapsed_compute" {
389 elapsed_compute = value;
390 } else if name == "output_rows" {
391 output_rows = value;
392 } else {
393 other_metrics.insert(name, value);
394 }
395 }
396
397 (
398 plan_metrics.level,
399 Self {
400 name: name.to_string(),
401 param: param.to_string(),
402 output_rows,
403 elapsed_compute,
404 metrics: other_metrics,
405 memory_usage: None,
406 children: vec![],
407 },
408 )
409 }
410}
411
412impl Display for JsonMetrics {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 write!(f, "{}", serde_json::to_string(self).unwrap())
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use datafusion::physical_plan::empty::EmptyExec;
421
422 use super::*;
423
424 fn empty_plan(name: &str) -> Arc<dyn ExecutionPlan> {
425 Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![Field::new(
426 name,
427 DataType::Utf8,
428 true,
429 )]))))
430 }
431
432 #[test]
433 fn qbs_dist_analyze_rejects_zero_children() {
434 let analyze = Arc::new(DistAnalyzeExec::new(
435 empty_plan("original"),
436 false,
437 AnalyzeFormat::TEXT,
438 ));
439
440 assert!(
441 ExecutionPlan::replace_children(
442 analyze,
443 vec![],
444 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
445 )
446 .is_err()
447 );
448 }
449
450 #[test]
451 fn qbs_dist_analyze_rejects_multiple_children() {
452 let analyze = Arc::new(DistAnalyzeExec::new(
453 empty_plan("original"),
454 false,
455 AnalyzeFormat::TEXT,
456 ));
457
458 let result = ExecutionPlan::replace_children(
459 analyze,
460 vec![empty_plan("first"), empty_plan("second")],
461 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
462 );
463
464 if let Ok(plan) = result {
465 let retained = plan
466 .downcast_ref::<DistAnalyzeExec>()
467 .unwrap()
468 .input()
469 .schema()
470 .field(0)
471 .name()
472 .clone();
473 panic!("expected an arity error for multiple children, but retained `{retained}`");
474 }
475 }
476
477 #[test]
478 fn qbs_dist_analyze_accepts_exactly_one_child() {
479 let analyze = Arc::new(DistAnalyzeExec::new(
480 empty_plan("original"),
481 false,
482 AnalyzeFormat::TEXT,
483 ));
484 let replacement = empty_plan("replacement");
485
486 let rebuilt = ExecutionPlan::replace_children(
487 analyze,
488 vec![replacement],
489 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
490 )
491 .unwrap();
492 let rebuilt = rebuilt.downcast_ref::<DistAnalyzeExec>().unwrap();
493
494 assert_eq!(rebuilt.input().schema().field(0).name(), "replacement");
495 }
496
497 #[test]
498 fn qbs_analyze_json_preserves_plan_name_without_parameters() {
499 let input = empty_plan("input");
500 let mut collector = MetricCollector::new(false);
501 accept(input.as_ref(), &mut collector).unwrap();
502
503 let plan_metrics = &collector.record_batch_metrics.plan_metrics;
504 assert_eq!(plan_metrics.len(), 1);
505 assert_eq!(plan_metrics[0].level, 0);
506 assert_eq!(plan_metrics[0].plan, input.name());
507
508 let analyze: Arc<dyn ExecutionPlan> = Arc::new(DistAnalyzeExec::new(
509 input.clone(),
510 false,
511 AnalyzeFormat::JSON,
512 ));
513 let metrics = analyze_plan_metrics_to_json_value(&analyze, false).unwrap();
514 let plan_name = metrics[0]["plan"]["name"].as_str().unwrap();
515 let plan_param = metrics[0]["plan"]["param"].as_str().unwrap();
516
517 assert!(!plan_name.is_empty());
518 assert_eq!(plan_name, input.name());
519 assert!(plan_param.is_empty());
520 }
521
522 #[test]
523 fn qbs_analyze_json_splits_plan_name_and_parameters() {
524 let (_, metrics) = JsonMetrics::from_plan_metrics(PlanMetrics {
525 plan: "FilterExec: predicate".to_string(),
526 plan_name: "FilterExec".to_string(),
527 level: 0,
528 metrics: vec![],
529 });
530
531 assert_eq!(metrics.name, "FilterExec");
532 assert_eq!(metrics.param, "predicate");
533 }
534
535 #[test]
536 fn qbs_analyze_json_includes_memory_usage_only_at_root() {
537 let metrics = JsonMetrics::from_record_batch_metrics(RecordBatchMetrics {
538 memory_usage: 42,
539 plan_metrics: vec![
540 PlanMetrics {
541 plan: "RootExec".to_string(),
542 plan_name: "RootExec".to_string(),
543 level: 0,
544 metrics: vec![("mem_used".to_string(), 24)],
545 },
546 PlanMetrics {
547 plan: "ChildExec".to_string(),
548 plan_name: "ChildExec".to_string(),
549 level: 1,
550 metrics: vec![("mem_used".to_string(), 18)],
551 },
552 ],
553 ..Default::default()
554 });
555 let value = serde_json::to_value(metrics).unwrap();
556
557 assert_eq!(value["memory_usage"], 42);
558 assert!(value["children"][0].get("memory_usage").is_none());
559 assert_eq!(value["metrics"]["mem_used"], 24);
560 assert_eq!(value["children"][0]["metrics"]["mem_used"], 18);
561 }
562}