Skip to main content

mz_adapter/explain/
optimizer_trace.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Tracing utilities for explainable plans.
11
12use std::fmt::{Debug, Display};
13use std::sync::Arc;
14
15use mz_catalog::memory::objects::Cluster;
16use mz_compute_types::dataflows::DataflowDescription;
17use mz_compute_types::plan::LirRelationExpr;
18use mz_expr::explain::ExplainContext;
19use mz_expr::{MirRelationExpr, MirScalarExpr, OptimizedMirRelationExpr, RowSetFinishing};
20use mz_ore::collections::CollectionExt;
21use mz_repr::explain::tracing::{PlanTrace, TraceEntry};
22use mz_repr::explain::{
23    Explain, ExplainConfig, ExplainError, ExplainFormat, ExprHumanizer, UsedIndexes,
24};
25use mz_repr::optimize::OptimizerFeatures;
26use mz_repr::{Datum, Row};
27use mz_sql::ast::display::AstDisplay;
28use mz_sql::plan::{self, HirRelationExpr, HirScalarExpr};
29use mz_sql_parser::ast::{ExplainStage, NamedPlan};
30use mz_transform::dataflow::DataflowMetainfo;
31use mz_transform::notice::RawOptimizerNotice;
32use smallvec::SmallVec;
33use tracing::dispatcher;
34use tracing_subscriber::prelude::*;
35
36use crate::AdapterError;
37use crate::coord::peek::FastPathPlan;
38use crate::explain::Explainable;
39use crate::explain::insights::{self, PlanInsightsContext};
40
41/// Provides functionality for tracing plans generated by the execution of an
42/// optimization pipeline.
43///
44/// Internally, this will create a layered [`tracing::subscriber::Subscriber`]
45/// consisting of one layer for each supported plan type `T` and wrap it into a
46/// [`dispatcher::Dispatch`] instance.
47///
48/// Use [`OptimizerTrace::as_guard`] to activate the [`dispatcher::Dispatch`]
49/// and collect a trace.
50///
51/// Use [`OptimizerTrace::into_rows`] or [`OptimizerTrace::into_plan_insights`]
52/// to cleanly destroy the [`OptimizerTrace`] instance and obtain the tracing
53/// result.
54pub struct OptimizerTrace(dispatcher::Dispatch);
55
56impl std::fmt::Debug for OptimizerTrace {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_tuple("OptimizerTrace").finish() // Skip the dispatch field
59    }
60}
61
62impl OptimizerTrace {
63    /// Create a new [`OptimizerTrace`].
64    ///
65    /// The instance will only accumulate [`TraceEntry`] instances along
66    /// the prefix of the given `path` if `path` is present, or it will
67    /// accumulate all [`TraceEntry`] instances otherwise.
68    pub fn new(filter: Option<SmallVec<[NamedPlan; 4]>>) -> OptimizerTrace {
69        let filter = || filter.clone();
70        if let Some(global_subscriber) = mz_ore::tracing::GLOBAL_SUBSCRIBER.get() {
71            let subscriber = Arc::clone(global_subscriber)
72                // Collect `explain_plan` types that are not used in the regular explain
73                // path, but are useful when instrumenting code for debugging purposes.
74                .with(PlanTrace::<String>::new(filter()))
75                .with(PlanTrace::<HirScalarExpr>::new(filter()))
76                .with(PlanTrace::<MirScalarExpr>::new(filter()))
77                // Collect `explain_plan` types that are used in the regular explain path.
78                .with(PlanTrace::<HirRelationExpr>::new(filter()))
79                .with(PlanTrace::<MirRelationExpr>::new(filter()))
80                .with(PlanTrace::<DataflowDescription<OptimizedMirRelationExpr>>::new(filter()))
81                .with(PlanTrace::<DataflowDescription<LirRelationExpr>>::new(
82                    filter(),
83                ))
84                // Don't filter for FastPathPlan entries (there can be at most one).
85                .with(PlanTrace::<FastPathPlan>::new(None))
86                .with(PlanTrace::<UsedIndexes>::new(None))
87                // All optimizer spans are `TRACE` and up. Technically this slows down the system
88                // by skipping the tracing fast path DURING an `EXPLAIN`, but we haven't
89                // seen this be a problem (yet).
90                //
91                // Note that we typically do NOT use global filters like this, preferring
92                // per-layer ones, but we are forced to because per-layer filters
93                // require an `Arc<dyn Subscriber + LookupSpan>`, which isn't a trait
94                // exposed by tracing, for now.
95                .with(tracing::level_filters::LevelFilter::TRACE);
96
97            OptimizerTrace(dispatcher::Dispatch::new(subscriber))
98        } else {
99            // This codepath should not be taken except in tests, and is left here as a
100            // convenience.
101            let subscriber = tracing_subscriber::registry()
102                .with(PlanTrace::<String>::new(filter()))
103                .with(PlanTrace::<HirScalarExpr>::new(filter()))
104                .with(PlanTrace::<MirScalarExpr>::new(filter()))
105                .with(PlanTrace::<HirRelationExpr>::new(filter()))
106                .with(PlanTrace::<MirRelationExpr>::new(filter()))
107                .with(PlanTrace::<DataflowDescription<OptimizedMirRelationExpr>>::new(filter()))
108                .with(PlanTrace::<DataflowDescription<LirRelationExpr>>::new(
109                    filter(),
110                ))
111                .with(PlanTrace::<FastPathPlan>::new(None))
112                .with(PlanTrace::<UsedIndexes>::new(None))
113                .with(tracing::level_filters::LevelFilter::TRACE);
114
115            OptimizerTrace(dispatcher::Dispatch::new(subscriber))
116        }
117    }
118
119    /// Enter this [`OptimizerTrace`]'s tracing [`dispatcher::Dispatch`], returning a guard.
120    ///
121    /// Linked to this [`OptimizerTrace`] with a lifetime to ensure
122    /// [`OptimizerTrace::into_rows`] isn't called until the guard is dropped.
123    pub fn as_guard<'s>(&'s self) -> DispatchGuard<'s> {
124        let dispatch = self.0.clone();
125        let tracing_guard = tracing::dispatcher::set_default(&dispatch);
126
127        DispatchGuard {
128            _tracing_guard: tracing_guard,
129            _life: std::marker::PhantomData,
130        }
131    }
132
133    /// Convert the optimizer trace into a vector or rows that can be returned
134    /// to the client.
135    pub async fn into_rows(
136        self,
137        format: ExplainFormat,
138        config: &ExplainConfig,
139        features: &OptimizerFeatures,
140        humanizer: &dyn ExprHumanizer,
141        row_set_finishing: Option<RowSetFinishing>,
142        target_cluster: Option<&Cluster>,
143        dataflow_metainfo: DataflowMetainfo,
144        stage: ExplainStage,
145        stmt_kind: plan::ExplaineeStatementKind,
146        insights_ctx: Option<Box<PlanInsightsContext>>,
147    ) -> Result<Vec<Row>, AdapterError> {
148        let collect_all = |format| {
149            self.collect_all(
150                format,
151                config,
152                features,
153                humanizer,
154                row_set_finishing.clone(),
155                target_cluster.map(|c| c.name.as_str()),
156                dataflow_metainfo.clone(),
157            )
158        };
159
160        let rows = match stage {
161            ExplainStage::Trace => {
162                // For the `Trace` (pseudo-)stage, return the entire trace as
163                // triples of (time, path, plan) values.
164                let rows = collect_all(format)?
165                    .0
166                    .into_iter()
167                    .map(|entry| {
168                        // The trace would have to take over 584 years to overflow a u64.
169                        let span_duration = u64::try_from(entry.span_duration.as_nanos());
170                        Row::pack_slice(&[
171                            Datum::from(span_duration.unwrap_or(u64::MAX)),
172                            Datum::from(entry.path.as_str()),
173                            Datum::from(entry.plan.as_str()),
174                        ])
175                    })
176                    .collect();
177                rows
178            }
179            ExplainStage::PlanInsights => {
180                if format != ExplainFormat::Json {
181                    coord_bail!("EXPLAIN PLAN INSIGHTS only supports JSON format");
182                }
183
184                let mut text_traces = collect_all(ExplainFormat::Text)?;
185                let mut json_traces = collect_all(ExplainFormat::Json)?;
186                let global_plan = self.collect_global_plan();
187                let fast_path_plan = self.collect_fast_path_plan();
188
189                // Plans can be very large and exhaust the json serialization recursion limit.
190                // Convert those into error objects.
191                let mut get_plan = |name: NamedPlan| {
192                    let text_plan = match text_traces.remove(name.path()) {
193                        None => "<unknown>".into(),
194                        Some(entry) => entry.plan,
195                    };
196                    let json_plan = match json_traces.remove(name.path()) {
197                        None => serde_json::Value::Null,
198                        Some(entry) => serde_json::from_str(&entry.plan).unwrap_or_else(|e| {
199                            serde_json::json!({
200                                "error": format!("internal error: {e}"),
201                            })
202                        }),
203                    };
204                    serde_json::json!({
205                        "text": text_plan,
206                        "json": json_plan,
207                    })
208                };
209
210                let is_fast_path = fast_path_plan.is_some();
211                let mut plan_insights =
212                    insights::plan_insights(humanizer, global_plan, fast_path_plan);
213                let mut redacted_sql = None;
214                if let Some(insights_ctx) = insights_ctx {
215                    redacted_sql = insights_ctx
216                        .stmt
217                        .as_ref()
218                        .map(|s| Some(s.to_ast_string_redacted()));
219                    if let (Some(plan_insights), false) = (plan_insights.as_mut(), is_fast_path) {
220                        if insights_ctx.enable_re_optimize {
221                            plan_insights
222                                .compute_fast_path_clusters(humanizer, insights_ctx)
223                                .await;
224                        }
225                    }
226                }
227                let cluster = target_cluster.map(|c| {
228                    serde_json::json!({
229                        "name": c.name,
230                        "id": c.id,
231                    })
232                });
233
234                let output = serde_json::json!({
235                    "plans": {
236                        "raw": get_plan(NamedPlan::Raw),
237                        "optimized": {
238                            "global": get_plan(NamedPlan::Global),
239                            "fast_path": get_plan(NamedPlan::FastPath),
240                        }
241                    },
242                    "insights": plan_insights,
243                    "cluster": cluster,
244                    "redacted_sql": redacted_sql,
245                });
246                let output = serde_json::to_string_pretty(&output).expect("JSON string");
247                vec![Row::pack_slice(&[Datum::from(output.as_str())])]
248            }
249            _ => {
250                // For everything else, return the plan for the stage identified
251                // by the corresponding path.
252
253                let path = stage
254                    .paths()
255                    .map(|path| path.into_element().path())
256                    .ok_or_else(|| {
257                        AdapterError::Internal("explain stage unexpectedly missing path".into())
258                    })?;
259                let mut traces = collect_all(format)?;
260
261                // For certain stages we want to return the resulting fast path
262                // plan instead of the selected stage if it is present.
263                let plan = if stage.show_fast_path() && !config.no_fast_path {
264                    traces
265                        .remove(NamedPlan::FastPath.path())
266                        .or_else(|| traces.remove(path))
267                } else {
268                    traces.remove(path)
269                };
270
271                let row = plan
272                    .map(|entry| Row::pack_slice(&[Datum::from(entry.plan.as_str())]))
273                    .ok_or_else(|| {
274                        if !stmt_kind.supports(&stage) {
275                            // Print a nicer error for unsupported stages.
276                            AdapterError::Unstructured(anyhow::anyhow!(format!(
277                                "cannot EXPLAIN {stage} FOR {stmt_kind}"
278                            )))
279                        } else {
280                            // We don't expect this stage to be missing.
281                            AdapterError::Internal(format!(
282                                "stage `{path}` not present in the collected optimizer trace",
283                            ))
284                        }
285                    })?;
286                vec![row]
287            }
288        };
289
290        // We assume that any `Dispatch` cloned from this `OptimizerTrace` has long been dropped
291        // (`as_guard` tries to ensure this.). We rebuild the tracing interest cache, as
292        // this `OptimizerTrace` is acting like a reload-layer, and tracing needs to
293        // recalculate what the max level is, using this often-unknown
294        // API. Note that the reference to the `Dispatch` in self MUST be dropped before
295        // re-calculating interest.
296        //
297        // Before this is dropped and rebuilt, there is small extra cost to all `DEBUG` spans and
298        // events, if the other layers (otel and stderr) are only interested in `INFO`.
299        drop(self);
300        tracing_core::callsite::rebuild_interest_cache();
301        Ok(rows)
302    }
303
304    /// Collect a [`insights::PlanInsights`] with insights about the the
305    /// optimized plans rendered as a JSON `String`.
306    pub async fn into_plan_insights(
307        self,
308        features: &OptimizerFeatures,
309        humanizer: &dyn ExprHumanizer,
310        row_set_finishing: Option<RowSetFinishing>,
311        target_cluster: Option<&Cluster>,
312        dataflow_metainfo: DataflowMetainfo,
313        insights_ctx: Option<Box<PlanInsightsContext>>,
314    ) -> Result<String, AdapterError> {
315        let rows = self
316            .into_rows(
317                ExplainFormat::Json,
318                &ExplainConfig::default(),
319                features,
320                humanizer,
321                row_set_finishing,
322                target_cluster,
323                dataflow_metainfo,
324                ExplainStage::PlanInsights,
325                plan::ExplaineeStatementKind::Select,
326                insights_ctx,
327            )
328            .await?;
329
330        // When using `ExplainStage::PlanInsights`, we're guaranteed that the
331        // output is a single row containing a single column containing the plan
332        // insights as a string.
333        Ok(rows.into_element().into_element().unwrap_str().into())
334    }
335
336    /// Collect all traced plans for all plan types `T` that are available in
337    /// the wrapped [`dispatcher::Dispatch`].
338    fn collect_all(
339        &self,
340        format: ExplainFormat,
341        config: &ExplainConfig,
342        features: &OptimizerFeatures,
343        humanizer: &dyn ExprHumanizer,
344        row_set_finishing: Option<RowSetFinishing>,
345        target_cluster: Option<&str>,
346        dataflow_metainfo: DataflowMetainfo,
347    ) -> Result<TraceEntries<String>, ExplainError> {
348        let mut results = vec![];
349
350        // First, create an ExplainContext without `used_indexes`. We'll use this to, e.g., collect
351        // HIR plans.
352        let mut context = ExplainContext {
353            config,
354            features,
355            humanizer,
356            cardinality_stats: Default::default(), // empty stats
357            used_indexes: Default::default(),
358            finishing: row_set_finishing.clone(),
359            duration: Default::default(),
360            target_cluster,
361            optimizer_notices: RawOptimizerNotice::explain(
362                &dataflow_metainfo.optimizer_notices,
363                humanizer,
364                config.redacted,
365            )?,
366        };
367
368        // Collect trace entries of types produced by local optimizer stages.
369        results.extend(itertools::chain!(
370            self.collect_explainable_entries::<HirRelationExpr>(&format, &mut context)?,
371            self.collect_explainable_entries::<MirRelationExpr>(&format, &mut context)?,
372        ));
373
374        // Collect trace entries of types produced by global optimizer stages.
375        let mut context = ExplainContext {
376            config,
377            features,
378            humanizer,
379            cardinality_stats: Default::default(), // empty stats
380            used_indexes: Default::default(),
381            finishing: row_set_finishing,
382            duration: Default::default(),
383            target_cluster,
384            optimizer_notices: RawOptimizerNotice::explain(
385                &dataflow_metainfo.optimizer_notices,
386                humanizer,
387                config.redacted,
388            )?,
389        };
390        results.extend(itertools::chain!(
391            self.collect_explainable_entries::<DataflowDescription<OptimizedMirRelationExpr>>(
392                &format,
393                &mut context,
394            )?,
395            self.collect_explainable_entries::<DataflowDescription<LirRelationExpr>>(
396                &format,
397                &mut context
398            )?,
399            self.collect_explainable_entries::<FastPathPlan>(&format, &mut context)?,
400        ));
401
402        // Collect trace entries of type String, HirScalarExpr, MirScalarExpr
403        // which are useful for ad-hoc debugging.
404        results.extend(itertools::chain!(
405            self.collect_scalar_entries::<HirScalarExpr>(),
406            self.collect_scalar_entries::<MirScalarExpr>(),
407            self.collect_string_entries(),
408        ));
409
410        // sort plans by instant (TODO: this can be implemented in a more
411        // efficient way, as we can assume that each of the runs that are used
412        // to `*.extend` the `results` vector is already sorted).
413        results.sort_by_key(|x| x.instant);
414
415        Ok(TraceEntries(results))
416    }
417
418    /// Collects the global optimized plan from the trace, if it exists.
419    fn collect_global_plan(&self) -> Option<DataflowDescription<OptimizedMirRelationExpr>> {
420        self.0
421            .downcast_ref::<PlanTrace<DataflowDescription<OptimizedMirRelationExpr>>>()
422            .and_then(|trace| trace.find(NamedPlan::Global.path()))
423            .map(|entry| entry.plan)
424    }
425
426    /// Collects the fast path plan from the trace, if it exists.
427    fn collect_fast_path_plan(&self) -> Option<FastPathPlan> {
428        self.0
429            .downcast_ref::<PlanTrace<FastPathPlan>>()
430            .and_then(|trace| trace.find(NamedPlan::FastPath.path()))
431            .map(|entry| entry.plan)
432    }
433
434    /// Collect all trace entries of a plan type `T` that implements
435    /// [`Explainable`].
436    fn collect_explainable_entries<T>(
437        &self,
438        format: &ExplainFormat,
439        context: &mut ExplainContext,
440    ) -> Result<Vec<TraceEntry<String>>, ExplainError>
441    where
442        T: Clone + Debug + 'static,
443        for<'a> Explainable<'a, T>: Explain<'a, Context = ExplainContext<'a>>,
444    {
445        if let Some(trace) = self.0.downcast_ref::<PlanTrace<T>>() {
446            // Get a handle of the associated `PlanTrace<UsedIndexes>`.
447            let used_indexes_trace = self.0.downcast_ref::<PlanTrace<UsedIndexes>>();
448
449            trace
450                .collect_as_vec()
451                .into_iter()
452                .map(|mut entry| {
453                    // Update the context with the current time.
454                    context.duration = entry.full_duration;
455
456                    // Try to find the UsedIndexes instance for this entry.
457                    let used_indexes = used_indexes_trace.map(|t| t.used_indexes_for(&entry.path));
458
459                    // Render the EXPLAIN output string for this entry.
460                    let plan = if let Some(mut used_indexes) = used_indexes {
461                        // Temporary swap the found UsedIndexes with the default
462                        // one in the ExplainContext while explaining the plan
463                        // for this entry.
464                        std::mem::swap(&mut context.used_indexes, &mut used_indexes);
465                        let plan = Explainable::new(&mut entry.plan).explain(format, context)?;
466                        std::mem::swap(&mut context.used_indexes, &mut used_indexes);
467                        plan
468                    } else {
469                        // No UsedIndexes instance for this entry found - use
470                        // the default UsedIndexes in the ExplainContext.
471                        Explainable::new(&mut entry.plan).explain(format, context)?
472                    };
473
474                    Ok(TraceEntry {
475                        instant: entry.instant,
476                        span_duration: entry.span_duration,
477                        full_duration: entry.full_duration,
478                        path: entry.path,
479                        plan,
480                    })
481                })
482                .collect()
483        } else {
484            unreachable!("collect_explainable_entries called with wrong plan type T");
485        }
486    }
487
488    /// Collect all trace entries of a plan type `T`.
489    fn collect_scalar_entries<T>(&self) -> Vec<TraceEntry<String>>
490    where
491        T: Clone + Debug + 'static,
492        T: Display,
493    {
494        if let Some(trace) = self.0.downcast_ref::<PlanTrace<T>>() {
495            trace
496                .collect_as_vec()
497                .into_iter()
498                .map(|entry| TraceEntry {
499                    instant: entry.instant,
500                    span_duration: entry.span_duration,
501                    full_duration: entry.full_duration,
502                    path: entry.path,
503                    plan: entry.plan.to_string(),
504                })
505                .collect()
506        } else {
507            vec![]
508        }
509    }
510
511    /// Collect all trace entries with plans of type [`String`].
512    fn collect_string_entries(&self) -> Vec<TraceEntry<String>> {
513        if let Some(trace) = self.0.downcast_ref::<PlanTrace<String>>() {
514            trace.collect_as_vec()
515        } else {
516            vec![]
517        }
518    }
519}
520
521/// A wrapper around a `tracing::subscriber::DefaultGuard`.
522pub struct DispatchGuard<'a> {
523    _tracing_guard: tracing::subscriber::DefaultGuard,
524    _life: std::marker::PhantomData<&'a ()>,
525}
526
527/// A collection of optimizer trace entries with convenient accessor methods.
528pub struct TraceEntries<T>(pub Vec<TraceEntry<T>>);
529
530impl<T> TraceEntries<T> {
531    // Removes the first (and by assumption the only) trace that matches the
532    // given path from the collected trace.
533    pub fn remove(&mut self, path: &'static str) -> Option<TraceEntry<T>> {
534        let index = self.0.iter().position(|entry| entry.path == path);
535        index.map(|index| self.0.remove(index))
536    }
537}