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#[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    /// Create a new DistAnalyzeExec
60    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    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
77    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
94/// Returns verbose analyze metrics as JSON values using the same `JsonMetrics` shape
95/// as `EXPLAIN ANALYZE VERBOSE FORMAT JSON`.
96///
97/// This reads metrics directly from a running physical plan for the experimental
98/// HTTP analyze stream. It is a best-effort diagnostic live snapshot, not a
99/// transactionally consistent snapshot; metric values may change while this
100/// function traverses the plan.
101pub 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    /// Return a reference to Any that can be used for downcasting
154    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    /// AnalyzeExec is handled specially so this value is ignored
167    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        // Wrap the input plan using `CoalescePartitionsExec` to poll multiple
197        // partitions in parallel
198        let coalesce_partition_plan = CoalescePartitionsExec::new(self.input.clone());
199
200        // Create future that computes thefinal output
201        let captured_input = self.input.clone();
202        let captured_schema = self.schema.clone();
203
204        // Finish the input stream and create the output
205        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
224/// Build the result [`DfRecordBatch`] of `ANALYZE`
225struct 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
268/// Creates the output of AnalyzeExec as a RecordBatch
269fn 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    // Treat the current stage as stage 0. Fetch its metrics
279    let mut collector = MetricCollector::new(verbose);
280    // Safety: metric collector won't return error
281    accept(input.as_ref(), &mut collector).unwrap();
282    let stage_0_metrics = collector.record_batch_metrics;
283
284    // Append the metrics of the current stage
285    builder.append_metric(0, 0, metrics_to_string(stage_0_metrics, format)?);
286
287    // Find merge scan and append its sub_stage_metrics
288    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            // might have multiple merge scans, so continue
295            return Ok(TreeNodeRecursion::Continue);
296        }
297        Ok(TreeNodeRecursion::Continue)
298    })?;
299
300    // Write total rows
301    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    // well-known metrics
322    output_rows: usize,
323    // busy time in nanoseconds
324    elapsed_compute: usize,
325
326    // other metrics
327    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        // Unreachable path. Each metrics should contains at least one level 0.
347        Self::default()
348    }
349
350    /// Convert a [`PlanMetrics`] to a [`JsonMetrics`] without children.
351    ///
352    /// Returns the level of the plan and the [`JsonMetrics`].
353    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}