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::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/// 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        .as_any()
114        .downcast_ref::<DistAnalyzeExec>()
115        .map(|exec| exec.input().clone())
116        .unwrap_or_else(|| plan.clone());
117
118    let mut stages = Vec::new();
119    let mut collector = MetricCollector::new(verbose);
120    accept(input.as_ref(), &mut collector).unwrap();
121    stages.push(json!({
122        "stage": 0,
123        "node": 0,
124        "plan": JsonMetrics::from_record_batch_metrics(collector.record_batch_metrics),
125    }));
126
127    let _ = input.apply(|plan| {
128        if let Some(merge_scan) = plan.as_any().downcast_ref::<MergeScanExec>() {
129            for (node, metric) in merge_scan.sub_stage_metrics().into_iter().enumerate() {
130                stages.push(json!({
131                    "stage": 1,
132                    "node": node,
133                    "plan": JsonMetrics::from_record_batch_metrics(metric),
134                }));
135            }
136        }
137        Ok(TreeNodeRecursion::Continue)
138    });
139
140    Ok(Value::Array(stages))
141}
142
143impl DisplayAs for DistAnalyzeExec {
144    fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
145        match t {
146            DisplayFormatType::Default
147            | DisplayFormatType::Verbose
148            | DisplayFormatType::TreeRender => {
149                write!(f, "DistAnalyzeExec",)
150            }
151        }
152    }
153}
154
155impl ExecutionPlan for DistAnalyzeExec {
156    fn name(&self) -> &'static str {
157        "DistAnalyzeExec"
158    }
159
160    /// Return a reference to Any that can be used for downcasting
161    fn as_any(&self) -> &dyn Any {
162        self
163    }
164
165    fn properties(&self) -> &Arc<PlanProperties> {
166        &self.properties
167    }
168
169    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
170        vec![&self.input]
171    }
172
173    /// AnalyzeExec is handled specially so this value is ignored
174    fn required_input_distribution(&self) -> Vec<Distribution> {
175        vec![]
176    }
177
178    fn with_new_children(
179        self: Arc<Self>,
180        mut children: Vec<Arc<dyn ExecutionPlan>>,
181    ) -> DfResult<Arc<dyn ExecutionPlan>> {
182        assert_eq_or_internal_err!(
183            children.len(),
184            1,
185            "DistAnalyzeExec requires exactly one child"
186        );
187        Ok(Arc::new(Self::new(
188            children.swap_remove(0),
189            self.verbose,
190            self.format,
191        )))
192    }
193
194    fn execute(
195        &self,
196        partition: usize,
197        context: Arc<TaskContext>,
198    ) -> DfResult<DfSendableRecordBatchStream> {
199        if 0 != partition {
200            return internal_err!("AnalyzeExec invalid partition. Expected 0, got {partition}");
201        }
202
203        // Wrap the input plan using `CoalescePartitionsExec` to poll multiple
204        // partitions in parallel
205        let coalesce_partition_plan = CoalescePartitionsExec::new(self.input.clone());
206
207        // Create future that computes thefinal output
208        let captured_input = self.input.clone();
209        let captured_schema = self.schema.clone();
210
211        // Finish the input stream and create the output
212        let format = self.format;
213        let verbose = self.verbose;
214        let mut input_stream = coalesce_partition_plan.execute(0, context)?;
215        let output = async move {
216            let mut total_rows = 0;
217            while let Some(batch) = input_stream.next().await.transpose()? {
218                total_rows += batch.num_rows();
219            }
220
221            create_output_batch(total_rows, captured_input, captured_schema, format, verbose)
222        };
223
224        Ok(Box::pin(RecordBatchStreamAdapter::new(
225            self.schema.clone(),
226            futures::stream::once(output),
227        )))
228    }
229}
230
231/// Build the result [`DfRecordBatch`] of `ANALYZE`
232struct AnalyzeOutputBuilder {
233    stage_builder: UInt32Builder,
234    node_builder: UInt32Builder,
235    plan_builder: StringBuilder,
236    schema: SchemaRef,
237}
238
239impl AnalyzeOutputBuilder {
240    fn new(schema: SchemaRef) -> Self {
241        Self {
242            stage_builder: UInt32Builder::with_capacity(4),
243            node_builder: UInt32Builder::with_capacity(4),
244            plan_builder: StringBuilder::with_capacity(1, 1024),
245            schema,
246        }
247    }
248
249    fn append_metric(&mut self, stage: u32, node: u32, content: String) {
250        self.stage_builder.append_value(stage);
251        self.node_builder.append_value(node);
252        self.plan_builder.append_value(content);
253    }
254
255    fn append_total_rows(&mut self, total_rows: usize) {
256        self.stage_builder.append_null();
257        self.node_builder.append_null();
258        self.plan_builder
259            .append_value(format!("Total rows: {}", total_rows));
260    }
261
262    fn finish(mut self) -> DfResult<DfRecordBatch> {
263        DfRecordBatch::try_new(
264            self.schema,
265            vec![
266                Arc::new(self.stage_builder.finish()),
267                Arc::new(self.node_builder.finish()),
268                Arc::new(self.plan_builder.finish()),
269            ],
270        )
271        .map_err(DataFusionError::from)
272    }
273}
274
275/// Creates the output of AnalyzeExec as a RecordBatch
276fn create_output_batch(
277    total_rows: usize,
278    input: Arc<dyn ExecutionPlan>,
279    schema: SchemaRef,
280    format: AnalyzeFormat,
281    verbose: bool,
282) -> DfResult<DfRecordBatch> {
283    let mut builder = AnalyzeOutputBuilder::new(schema);
284
285    // Treat the current stage as stage 0. Fetch its metrics
286    let mut collector = MetricCollector::new(verbose);
287    // Safety: metric collector won't return error
288    accept(input.as_ref(), &mut collector).unwrap();
289    let stage_0_metrics = collector.record_batch_metrics;
290
291    // Append the metrics of the current stage
292    builder.append_metric(0, 0, metrics_to_string(stage_0_metrics, format)?);
293
294    // Find merge scan and append its sub_stage_metrics
295    input.apply(|plan| {
296        if let Some(merge_scan) = plan.as_any().downcast_ref::<MergeScanExec>() {
297            let sub_stage_metrics = merge_scan.sub_stage_metrics();
298            for (node, metric) in sub_stage_metrics.into_iter().enumerate() {
299                builder.append_metric(1, node as _, metrics_to_string(metric, format)?);
300            }
301            // might have multiple merge scans, so continue
302            return Ok(TreeNodeRecursion::Continue);
303        }
304        Ok(TreeNodeRecursion::Continue)
305    })?;
306
307    // Write total rows
308    builder.append_total_rows(total_rows);
309
310    builder.finish()
311}
312
313fn metrics_to_string(metrics: RecordBatchMetrics, format: AnalyzeFormat) -> DfResult<String> {
314    match format {
315        AnalyzeFormat::JSON => Ok(JsonMetrics::from_record_batch_metrics(metrics).to_string()),
316        AnalyzeFormat::TEXT => Ok(metrics.to_string()),
317        format => Err(DataFusionError::NotImplemented(format!(
318            "AnalyzeFormat {format}",
319        ))),
320    }
321}
322
323#[derive(Debug, Default, Serialize)]
324struct JsonMetrics {
325    name: String,
326    param: String,
327
328    // well-known metrics
329    output_rows: usize,
330    // busy time in nanoseconds
331    elapsed_compute: usize,
332
333    // other metrics
334    metrics: HashMap<String, usize>,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    memory_usage: Option<usize>,
337    children: Vec<JsonMetrics>,
338}
339
340impl JsonMetrics {
341    fn from_record_batch_metrics(record_batch_metrics: RecordBatchMetrics) -> Self {
342        let mut layers: HashMap<usize, Vec<Self>> = HashMap::default();
343
344        let memory_usage = record_batch_metrics.memory_usage;
345        for plan_metrics in record_batch_metrics.plan_metrics.into_iter().rev() {
346            let (level, mut metrics) = Self::from_plan_metrics(plan_metrics);
347            if let Some(next_layer) = layers.remove(&(level + 1)) {
348                metrics.children = next_layer;
349            }
350            if level == 0 {
351                metrics.memory_usage = Some(memory_usage);
352                return metrics;
353            }
354            layers.entry(level).or_default().push(metrics);
355        }
356
357        // Unreachable path. Each metrics should contains at least one level 0.
358        Self::default()
359    }
360
361    /// Convert a [`PlanMetrics`] to a [`JsonMetrics`] without children.
362    ///
363    /// Returns the level of the plan and the [`JsonMetrics`].
364    fn from_plan_metrics(plan_metrics: PlanMetrics) -> (usize, Self) {
365        let raw_name = plan_metrics.plan.trim_end();
366        let mut elapsed_compute = 0;
367        let mut output_rows = 0;
368        let mut other_metrics = HashMap::default();
369        let (name, param) = raw_name.split_once(": ").unwrap_or((raw_name, ""));
370
371        for (name, value) in plan_metrics.metrics.into_iter() {
372            if name == "elapsed_compute" {
373                elapsed_compute = value;
374            } else if name == "output_rows" {
375                output_rows = value;
376            } else {
377                other_metrics.insert(name, value);
378            }
379        }
380
381        (
382            plan_metrics.level,
383            Self {
384                name: name.to_string(),
385                param: param.to_string(),
386                output_rows,
387                elapsed_compute,
388                metrics: other_metrics,
389                memory_usage: None,
390                children: vec![],
391            },
392        )
393    }
394}
395
396impl Display for JsonMetrics {
397    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398        write!(f, "{}", serde_json::to_string(self).unwrap())
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use datafusion::physical_plan::empty::EmptyExec;
405
406    use super::*;
407
408    fn empty_plan(name: &str) -> Arc<dyn ExecutionPlan> {
409        Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![Field::new(
410            name,
411            DataType::Utf8,
412            true,
413        )]))))
414    }
415
416    #[test]
417    fn qbs_dist_analyze_rejects_zero_children() {
418        let analyze = Arc::new(DistAnalyzeExec::new(
419            empty_plan("original"),
420            false,
421            AnalyzeFormat::TEXT,
422        ));
423
424        assert!(ExecutionPlan::with_new_children(analyze, vec![]).is_err());
425    }
426
427    #[test]
428    fn qbs_dist_analyze_rejects_multiple_children() {
429        let analyze = Arc::new(DistAnalyzeExec::new(
430            empty_plan("original"),
431            false,
432            AnalyzeFormat::TEXT,
433        ));
434
435        let result = ExecutionPlan::with_new_children(
436            analyze,
437            vec![empty_plan("first"), empty_plan("second")],
438        );
439
440        if let Ok(plan) = result {
441            let retained = plan
442                .as_any()
443                .downcast_ref::<DistAnalyzeExec>()
444                .unwrap()
445                .input()
446                .schema()
447                .field(0)
448                .name()
449                .clone();
450            panic!("expected an arity error for multiple children, but retained `{retained}`");
451        }
452    }
453
454    #[test]
455    fn qbs_dist_analyze_accepts_exactly_one_child() {
456        let analyze = Arc::new(DistAnalyzeExec::new(
457            empty_plan("original"),
458            false,
459            AnalyzeFormat::TEXT,
460        ));
461        let replacement = empty_plan("replacement");
462
463        let rebuilt = ExecutionPlan::with_new_children(analyze, vec![replacement]).unwrap();
464        let rebuilt = rebuilt.as_any().downcast_ref::<DistAnalyzeExec>().unwrap();
465
466        assert_eq!(rebuilt.input().schema().field(0).name(), "replacement");
467    }
468
469    #[test]
470    fn qbs_analyze_json_preserves_plan_name_without_parameters() {
471        let input = empty_plan("input");
472        let mut collector = MetricCollector::new(false);
473        accept(input.as_ref(), &mut collector).unwrap();
474
475        let plan_metrics = &collector.record_batch_metrics.plan_metrics;
476        assert_eq!(plan_metrics.len(), 1);
477        assert_eq!(plan_metrics[0].level, 0);
478        assert_eq!(plan_metrics[0].plan, input.name());
479
480        let analyze: Arc<dyn ExecutionPlan> = Arc::new(DistAnalyzeExec::new(
481            input.clone(),
482            false,
483            AnalyzeFormat::JSON,
484        ));
485        let metrics = analyze_plan_metrics_to_json_value(&analyze, false).unwrap();
486        let plan_name = metrics[0]["plan"]["name"].as_str().unwrap();
487        let plan_param = metrics[0]["plan"]["param"].as_str().unwrap();
488
489        assert!(!plan_name.is_empty());
490        assert_eq!(plan_name, input.name());
491        assert!(plan_param.is_empty());
492    }
493
494    #[test]
495    fn qbs_analyze_json_splits_plan_name_and_parameters() {
496        let (_, metrics) = JsonMetrics::from_plan_metrics(PlanMetrics {
497            plan: "FilterExec: predicate".to_string(),
498            plan_name: "FilterExec".to_string(),
499            level: 0,
500            metrics: vec![],
501        });
502
503        assert_eq!(metrics.name, "FilterExec");
504        assert_eq!(metrics.param, "predicate");
505    }
506
507    #[test]
508    fn qbs_analyze_json_includes_memory_usage_only_at_root() {
509        let metrics = JsonMetrics::from_record_batch_metrics(RecordBatchMetrics {
510            memory_usage: 42,
511            plan_metrics: vec![
512                PlanMetrics {
513                    plan: "RootExec".to_string(),
514                    plan_name: "RootExec".to_string(),
515                    level: 0,
516                    metrics: vec![("mem_used".to_string(), 24)],
517                },
518                PlanMetrics {
519                    plan: "ChildExec".to_string(),
520                    plan_name: "ChildExec".to_string(),
521                    level: 1,
522                    metrics: vec![("mem_used".to_string(), 18)],
523                },
524            ],
525            ..Default::default()
526        });
527        let value = serde_json::to_value(metrics).unwrap();
528
529        assert_eq!(value["memory_usage"], 42);
530        assert!(value["children"][0].get("memory_usage").is_none());
531        assert_eq!(value["metrics"]["mem_used"], 24);
532        assert_eq!(value["children"][0]["metrics"]["mem_used"], 18);
533    }
534}