1use std::any::Any;
20use std::fmt::Display;
21use std::sync::Arc;
22
23use ahash::HashMap;
24use arrow::array::{StringBuilder, UInt32Builder};
25use arrow_schema::{DataType, Field, Schema, SchemaRef};
26use common_recordbatch::adapter::{MetricCollector, PlanMetrics, RecordBatchMetrics};
27use common_recordbatch::{DfRecordBatch, DfSendableRecordBatchStream};
28use datafusion::error::Result as DfResult;
29use datafusion::execution::TaskContext;
30use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec;
31use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
32use datafusion::physical_plan::{
33 DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties, accept,
34};
35use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
36use datafusion_common::{DataFusionError, assert_eq_or_internal_err, internal_err};
37use datafusion_physical_expr::{Distribution, EquivalenceProperties, Partitioning};
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
49#[derive(Debug)]
50pub struct DistAnalyzeExec {
51 input: Arc<dyn ExecutionPlan>,
52 schema: SchemaRef,
53 properties: Arc<PlanProperties>,
54 verbose: bool,
55 format: AnalyzeFormat,
56}
57
58impl DistAnalyzeExec {
59 pub fn new(input: Arc<dyn ExecutionPlan>, verbose: bool, format: AnalyzeFormat) -> Self {
61 let schema = SchemaRef::new(Schema::new(vec![
62 Field::new(STAGE, DataType::UInt32, true),
63 Field::new(NODE, DataType::UInt32, true),
64 Field::new(PLAN, DataType::Utf8, true),
65 ]));
66 let properties = Arc::new(Self::compute_properties(&input, schema.clone()));
67 Self {
68 input,
69 schema,
70 properties,
71 verbose,
72 format,
73 }
74 }
75
76 fn compute_properties(input: &Arc<dyn ExecutionPlan>, schema: SchemaRef) -> PlanProperties {
78 let eq_properties = EquivalenceProperties::new(schema);
79 let output_partitioning = Partitioning::UnknownPartitioning(1);
80 let properties = input.properties();
81 PlanProperties::new(
82 eq_properties,
83 output_partitioning,
84 properties.emission_type,
85 properties.boundedness,
86 )
87 }
88
89 pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
90 &self.input
91 }
92}
93
94pub fn analyze_plan_metrics_to_json_value(
102 plan: &Arc<dyn ExecutionPlan>,
103 verbose: bool,
104) -> serde_json::Result<Value> {
105 let input = plan
106 .as_any()
107 .downcast_ref::<DistAnalyzeExec>()
108 .map(|exec| exec.input().clone())
109 .unwrap_or_else(|| plan.clone());
110
111 let mut stages = Vec::new();
112 let mut collector = MetricCollector::new(verbose);
113 accept(input.as_ref(), &mut collector).unwrap();
114 stages.push(json!({
115 "stage": 0,
116 "node": 0,
117 "plan": JsonMetrics::from_record_batch_metrics(collector.record_batch_metrics),
118 }));
119
120 let _ = input.apply(|plan| {
121 if let Some(merge_scan) = plan.as_any().downcast_ref::<MergeScanExec>() {
122 for (node, metric) in merge_scan.sub_stage_metrics().into_iter().enumerate() {
123 stages.push(json!({
124 "stage": 1,
125 "node": node,
126 "plan": JsonMetrics::from_record_batch_metrics(metric),
127 }));
128 }
129 }
130 Ok(TreeNodeRecursion::Continue)
131 });
132
133 Ok(Value::Array(stages))
134}
135
136impl DisplayAs for DistAnalyzeExec {
137 fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
138 match t {
139 DisplayFormatType::Default
140 | DisplayFormatType::Verbose
141 | DisplayFormatType::TreeRender => {
142 write!(f, "DistAnalyzeExec",)
143 }
144 }
145 }
146}
147
148impl ExecutionPlan for DistAnalyzeExec {
149 fn name(&self) -> &'static str {
150 "DistAnalyzeExec"
151 }
152
153 fn as_any(&self) -> &dyn Any {
155 self
156 }
157
158 fn properties(&self) -> &Arc<PlanProperties> {
159 &self.properties
160 }
161
162 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
163 vec![&self.input]
164 }
165
166 fn required_input_distribution(&self) -> Vec<Distribution> {
168 vec![]
169 }
170
171 fn with_new_children(
172 self: Arc<Self>,
173 mut children: Vec<Arc<dyn ExecutionPlan>>,
174 ) -> DfResult<Arc<dyn ExecutionPlan>> {
175 assert_eq_or_internal_err!(
176 children.len(),
177 1,
178 "DistAnalyzeExec requires exactly one child"
179 );
180 Ok(Arc::new(Self::new(
181 children.swap_remove(0),
182 self.verbose,
183 self.format,
184 )))
185 }
186
187 fn execute(
188 &self,
189 partition: usize,
190 context: Arc<TaskContext>,
191 ) -> DfResult<DfSendableRecordBatchStream> {
192 if 0 != partition {
193 return internal_err!("AnalyzeExec invalid partition. Expected 0, got {partition}");
194 }
195
196 let coalesce_partition_plan = CoalescePartitionsExec::new(self.input.clone());
199
200 let captured_input = self.input.clone();
202 let captured_schema = self.schema.clone();
203
204 let format = self.format;
206 let verbose = self.verbose;
207 let mut input_stream = coalesce_partition_plan.execute(0, context)?;
208 let output = async move {
209 let mut total_rows = 0;
210 while let Some(batch) = input_stream.next().await.transpose()? {
211 total_rows += batch.num_rows();
212 }
213
214 create_output_batch(total_rows, captured_input, captured_schema, format, verbose)
215 };
216
217 Ok(Box::pin(RecordBatchStreamAdapter::new(
218 self.schema.clone(),
219 futures::stream::once(output),
220 )))
221 }
222}
223
224struct AnalyzeOutputBuilder {
226 stage_builder: UInt32Builder,
227 node_builder: UInt32Builder,
228 plan_builder: StringBuilder,
229 schema: SchemaRef,
230}
231
232impl AnalyzeOutputBuilder {
233 fn new(schema: SchemaRef) -> Self {
234 Self {
235 stage_builder: UInt32Builder::with_capacity(4),
236 node_builder: UInt32Builder::with_capacity(4),
237 plan_builder: StringBuilder::with_capacity(1, 1024),
238 schema,
239 }
240 }
241
242 fn append_metric(&mut self, stage: u32, node: u32, content: String) {
243 self.stage_builder.append_value(stage);
244 self.node_builder.append_value(node);
245 self.plan_builder.append_value(content);
246 }
247
248 fn append_total_rows(&mut self, total_rows: usize) {
249 self.stage_builder.append_null();
250 self.node_builder.append_null();
251 self.plan_builder
252 .append_value(format!("Total rows: {}", total_rows));
253 }
254
255 fn finish(mut self) -> DfResult<DfRecordBatch> {
256 DfRecordBatch::try_new(
257 self.schema,
258 vec![
259 Arc::new(self.stage_builder.finish()),
260 Arc::new(self.node_builder.finish()),
261 Arc::new(self.plan_builder.finish()),
262 ],
263 )
264 .map_err(DataFusionError::from)
265 }
266}
267
268fn create_output_batch(
270 total_rows: usize,
271 input: Arc<dyn ExecutionPlan>,
272 schema: SchemaRef,
273 format: AnalyzeFormat,
274 verbose: bool,
275) -> DfResult<DfRecordBatch> {
276 let mut builder = AnalyzeOutputBuilder::new(schema);
277
278 let mut collector = MetricCollector::new(verbose);
280 accept(input.as_ref(), &mut collector).unwrap();
282 let stage_0_metrics = collector.record_batch_metrics;
283
284 builder.append_metric(0, 0, metrics_to_string(stage_0_metrics, format)?);
286
287 input.apply(|plan| {
289 if let Some(merge_scan) = plan.as_any().downcast_ref::<MergeScanExec>() {
290 let sub_stage_metrics = merge_scan.sub_stage_metrics();
291 for (node, metric) in sub_stage_metrics.into_iter().enumerate() {
292 builder.append_metric(1, node as _, metrics_to_string(metric, format)?);
293 }
294 return Ok(TreeNodeRecursion::Continue);
296 }
297 Ok(TreeNodeRecursion::Continue)
298 })?;
299
300 builder.append_total_rows(total_rows);
302
303 builder.finish()
304}
305
306fn metrics_to_string(metrics: RecordBatchMetrics, format: AnalyzeFormat) -> DfResult<String> {
307 match format {
308 AnalyzeFormat::JSON => Ok(JsonMetrics::from_record_batch_metrics(metrics).to_string()),
309 AnalyzeFormat::TEXT => Ok(metrics.to_string()),
310 format => Err(DataFusionError::NotImplemented(format!(
311 "AnalyzeFormat {format}",
312 ))),
313 }
314}
315
316#[derive(Debug, Default, Serialize)]
317struct JsonMetrics {
318 name: String,
319 param: String,
320
321 output_rows: usize,
323 elapsed_compute: usize,
325
326 metrics: HashMap<String, usize>,
328 children: Vec<JsonMetrics>,
329}
330
331impl JsonMetrics {
332 fn from_record_batch_metrics(record_batch_metrics: RecordBatchMetrics) -> Self {
333 let mut layers: HashMap<usize, Vec<Self>> = HashMap::default();
334
335 for plan_metrics in record_batch_metrics.plan_metrics.into_iter().rev() {
336 let (level, mut metrics) = Self::from_plan_metrics(plan_metrics);
337 if let Some(next_layer) = layers.remove(&(level + 1)) {
338 metrics.children = next_layer;
339 }
340 if level == 0 {
341 return metrics;
342 }
343 layers.entry(level).or_default().push(metrics);
344 }
345
346 Self::default()
348 }
349
350 fn from_plan_metrics(plan_metrics: PlanMetrics) -> (usize, Self) {
354 let raw_name = plan_metrics.plan.trim_end();
355 let mut elapsed_compute = 0;
356 let mut output_rows = 0;
357 let mut other_metrics = HashMap::default();
358 let (name, param) = raw_name.split_once(": ").unwrap_or((raw_name, ""));
359
360 for (name, value) in plan_metrics.metrics.into_iter() {
361 if name == "elapsed_compute" {
362 elapsed_compute = value;
363 } else if name == "output_rows" {
364 output_rows = value;
365 } else {
366 other_metrics.insert(name, value);
367 }
368 }
369
370 (
371 plan_metrics.level,
372 Self {
373 name: name.to_string(),
374 param: param.to_string(),
375 output_rows,
376 elapsed_compute,
377 metrics: other_metrics,
378 children: vec![],
379 },
380 )
381 }
382}
383
384impl Display for JsonMetrics {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 write!(f, "{}", serde_json::to_string(self).unwrap())
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use datafusion::physical_plan::empty::EmptyExec;
393
394 use super::*;
395
396 fn empty_plan(name: &str) -> Arc<dyn ExecutionPlan> {
397 Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![Field::new(
398 name,
399 DataType::Utf8,
400 true,
401 )]))))
402 }
403
404 #[test]
405 fn qbs_dist_analyze_rejects_zero_children() {
406 let analyze = Arc::new(DistAnalyzeExec::new(
407 empty_plan("original"),
408 false,
409 AnalyzeFormat::TEXT,
410 ));
411
412 assert!(ExecutionPlan::with_new_children(analyze, vec![]).is_err());
413 }
414
415 #[test]
416 fn qbs_dist_analyze_rejects_multiple_children() {
417 let analyze = Arc::new(DistAnalyzeExec::new(
418 empty_plan("original"),
419 false,
420 AnalyzeFormat::TEXT,
421 ));
422
423 let result = ExecutionPlan::with_new_children(
424 analyze,
425 vec![empty_plan("first"), empty_plan("second")],
426 );
427
428 if let Ok(plan) = result {
429 let retained = plan
430 .as_any()
431 .downcast_ref::<DistAnalyzeExec>()
432 .unwrap()
433 .input()
434 .schema()
435 .field(0)
436 .name()
437 .clone();
438 panic!("expected an arity error for multiple children, but retained `{retained}`");
439 }
440 }
441
442 #[test]
443 fn qbs_dist_analyze_accepts_exactly_one_child() {
444 let analyze = Arc::new(DistAnalyzeExec::new(
445 empty_plan("original"),
446 false,
447 AnalyzeFormat::TEXT,
448 ));
449 let replacement = empty_plan("replacement");
450
451 let rebuilt = ExecutionPlan::with_new_children(analyze, vec![replacement]).unwrap();
452 let rebuilt = rebuilt.as_any().downcast_ref::<DistAnalyzeExec>().unwrap();
453
454 assert_eq!(rebuilt.input().schema().field(0).name(), "replacement");
455 }
456
457 #[test]
458 fn qbs_analyze_json_preserves_plan_name_without_parameters() {
459 let input = empty_plan("input");
460 let mut collector = MetricCollector::new(false);
461 accept(input.as_ref(), &mut collector).unwrap();
462
463 let plan_metrics = &collector.record_batch_metrics.plan_metrics;
464 assert_eq!(plan_metrics.len(), 1);
465 assert_eq!(plan_metrics[0].level, 0);
466 assert_eq!(plan_metrics[0].plan, input.name());
467
468 let analyze: Arc<dyn ExecutionPlan> = Arc::new(DistAnalyzeExec::new(
469 input.clone(),
470 false,
471 AnalyzeFormat::JSON,
472 ));
473 let metrics = analyze_plan_metrics_to_json_value(&analyze, false).unwrap();
474 let plan_name = metrics[0]["plan"]["name"].as_str().unwrap();
475 let plan_param = metrics[0]["plan"]["param"].as_str().unwrap();
476
477 assert!(!plan_name.is_empty());
478 assert_eq!(plan_name, input.name());
479 assert!(plan_param.is_empty());
480 }
481
482 #[test]
483 fn qbs_analyze_json_splits_plan_name_and_parameters() {
484 let (_, metrics) = JsonMetrics::from_plan_metrics(PlanMetrics {
485 plan: "FilterExec: predicate".to_string(),
486 plan_name: "FilterExec".to_string(),
487 level: 0,
488 metrics: vec![],
489 });
490
491 assert_eq!(metrics.name, "FilterExec");
492 assert_eq!(metrics.param, "predicate");
493 }
494}