Skip to main content

query/
analyze.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Customized `ANALYZE` plan that aware of [MergeScanExec].
16//!
17//! The code skeleton is taken from `datafusion/physical-plan/src/analyze.rs`
18
19use 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
49/// Fixed output schema of [`DistAnalyzeExec`], for `Describe` handlers:
50/// execution rewrites the plan in `optimize_physical_plan`, so this schema
51/// differs from the logical `Analyze` plan's.
52pub 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    /// Create a new DistAnalyzeExec
71    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    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
84    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
101/// Returns verbose analyze metrics as JSON values using the same `JsonMetrics` shape
102/// as `EXPLAIN ANALYZE VERBOSE FORMAT JSON`.
103///
104/// This reads metrics directly from a running physical plan for the experimental
105/// HTTP analyze stream. It is a best-effort diagnostic live snapshot, not a
106/// transactionally consistent snapshot; metric values may change while this
107/// function traverses the plan.
108pub 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    /// AnalyzeExec is handled specially so this value is ignored
175    fn input_distribution_requirements(&self) -> InputDistributionRequirements {
176        // AnalyzeExec is handled specially so this value is ignored.
177        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        // Wrap the input plan using `CoalescePartitionsExec` to poll multiple
220        // partitions in parallel
221        let coalesce_partition_plan = CoalescePartitionsExec::new(self.input.clone());
222
223        // Create future that computes thefinal output
224        let captured_input = self.input.clone();
225        let captured_schema = self.schema.clone();
226
227        // Finish the input stream and create the output
228        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
247/// Build the result [`DfRecordBatch`] of `ANALYZE`
248struct 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
291/// Creates the output of AnalyzeExec as a RecordBatch
292fn 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    // Treat the current stage as stage 0. Fetch its metrics
302    let mut collector = MetricCollector::new(verbose);
303    // Safety: metric collector won't return error
304    accept(input.as_ref(), &mut collector).unwrap();
305    let stage_0_metrics = collector.record_batch_metrics;
306
307    // Append the metrics of the current stage
308    builder.append_metric(0, 0, metrics_to_string(stage_0_metrics, format)?);
309
310    // Find merge scan and append its sub_stage_metrics
311    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            // might have multiple merge scans, so continue
318            return Ok(TreeNodeRecursion::Continue);
319        }
320        Ok(TreeNodeRecursion::Continue)
321    })?;
322
323    // Write total rows
324    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    // well-known metrics
345    output_rows: usize,
346    // busy time in nanoseconds
347    elapsed_compute: usize,
348
349    // other metrics
350    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        // Unreachable path. Each metrics should contains at least one level 0.
374        Self::default()
375    }
376
377    /// Convert a [`PlanMetrics`] to a [`JsonMetrics`] without children.
378    ///
379    /// Returns the level of the plan and the [`JsonMetrics`].
380    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}