From c141cda97f761541405002b9ba6fd660ea9f2de3 Mon Sep 17 00:00:00 2001 From: discord9 Date: Fri, 6 Nov 2026 20:46:49 +0800 Subject: [PATCH] feat(promql): draw the fused node's tree in a bracketed block The tree lines carried their own absolute indentation, so a nested fused node's tree sat in the same column as the node's real children: in the early logical snapshots of EXPLAIN VERBOSE the region fused node's tree root read as a sibling of the node and its TableScan as a child of the window above it. The tree now opens a native_tree=[ ... ] block that starts at column zero, the same shape MergeScan uses for its remote input, so only the lines after the closing bracket are plan children. Signed-off-by: discord9 --- src/promql/src/extension_plan/fused_series.rs | 8 +- .../src/extension_plan/fused_series/tests.rs | 22 +++--- src/promql/src/native/display.rs | 17 ++-- src/promql/src/native/display/tests.rs | 78 +++++++++---------- src/query/src/promql/native_compile/tests.rs | 40 +++++----- src/query/src/promql/planner/test.rs | 15 +++- 6 files changed, 96 insertions(+), 84 deletions(-) diff --git a/src/promql/src/extension_plan/fused_series.rs b/src/promql/src/extension_plan/fused_series.rs index ea2ef3f2de2..ef81bfe7b24 100644 --- a/src/promql/src/extension_plan/fused_series.rs +++ b/src/promql/src/extension_plan/fused_series.rs @@ -1075,7 +1075,7 @@ impl UserDefinedLogicalNodeCore for FusedSeries { "{separator}(state routing: canonical labels except {excluded:?})" )?; } - write!(f, "\n{}", self.tree) + write!(f, "\nnative_tree=[\n{}\n]", self.tree) } fn with_exprs_and_inputs( @@ -2593,7 +2593,11 @@ impl DisplayAs for FusedSeriesExec { if self.format == FusedOutputFormat::SeriesFrame { write!(f, ": (output: series_frame)")?; } - write!(f, "\n{}", self.tree.display_with(&self.node_metrics())) + write!( + f, + "\nnative_tree=[\n{}\n]", + self.tree.display_with(&self.node_metrics()) + ) } } diff --git a/src/promql/src/extension_plan/fused_series/tests.rs b/src/promql/src/extension_plan/fused_series/tests.rs index 28bb4526648..cda3e43ccdd 100644 --- a/src/promql/src/extension_plan/fused_series/tests.rs +++ b/src/promql/src/extension_plan/fused_series/tests.rs @@ -2439,10 +2439,10 @@ impl Display for Explain<'_> { fn fmt_for_explain_prints_the_native_tree() { let text = Explain(&fused(rate_tree())).to_string(); assert!( - text.starts_with("FusedSeries\n Window: rate [300000ms]"), + text.starts_with("FusedSeries\nnative_tree=[\nWindow: rate [300000ms]"), "{text}" ); - assert!(text.contains(" Input: raw samples source=0"), "{text}"); + assert!(text.contains("\n Input: raw samples source=0"), "{text}"); assert!(!text.contains("metrics="), "{text}"); } @@ -2456,16 +2456,16 @@ fn fmt_as_prints_the_native_tree() { assert!(!text.contains("metrics="), "{text}"); assert!( - text.starts_with("FusedSeriesExec\n Window: rate"), + text.starts_with("FusedSeriesExec\nnative_tree=[\nWindow: rate"), "the node line precedes its tree: {text}" ); // A distinctive line of the compiled tree: the window's bound range, offset, anchor and grid. assert!( - text.contains(" Window: rate [300000ms] offset 0 anchor - on grid 300000/60000x5"), + text.contains("\nWindow: rate [300000ms] offset 0 anchor - on grid 300000/60000x5"), "the node line carries the tree it runs: {text}" ); assert!( - text.contains(" Input: raw samples"), + text.contains("\n Input: raw samples"), "the node line carries the tree's source: {text}" ); } @@ -2480,7 +2480,7 @@ async fn fmt_as_reports_node_metrics_after_execution() { let text = displayable(exec.as_ref()).indent(false).to_string(); println!("=== physical_plan after execution ===\n{text}"); assert!( - text.starts_with("FusedSeriesExec\n Window: rate"), + text.starts_with("FusedSeriesExec\nnative_tree=[\nWindow: rate"), "{text}" ); assert!(text.contains("metrics=["), "{text}"); @@ -5832,7 +5832,7 @@ fn the_explain_text_names_the_frame_format_only() { assert!( Explain(&rows) .to_string() - .starts_with("FusedSeries\n Window:") + .starts_with("FusedSeries\nnative_tree=[\nWindow:") ); let framed = FusedSeries::try_new_with_format( rows.tree().clone(), @@ -5845,21 +5845,21 @@ fn the_explain_text_names_the_frame_format_only() { assert!( Explain(&framed) .to_string() - .starts_with("FusedSeries: (output: series_frame)\n Window:"), + .starts_with("FusedSeries: (output: series_frame)\nnative_tree=[\nWindow:"), "a frame output names its format: {}", Explain(&framed) ); let framed_exec = plan_of(framed, memory_exec(Vec::new())).unwrap(); let text = displayable(framed_exec.as_ref()).indent(false).to_string(); assert!( - text.starts_with("FusedSeriesExec: (output: series_frame)\n Window:"), + text.starts_with("FusedSeriesExec: (output: series_frame)\nnative_tree=[\nWindow:"), "{text}" ); let canonical = canonical_producer_node(); let text = Explain(&canonical).to_string(); assert!( text.starts_with( - "FusedSeries: (output: series_frame, per-partition canonical merge)\n Merge:" + "FusedSeries: (output: series_frame, per-partition canonical merge)\nnative_tree=[\nMerge:" ), "{text}" ); @@ -5867,7 +5867,7 @@ fn the_explain_text_names_the_frame_format_only() { let plain = plan(rate_tree(), memory_exec(Vec::new())).expect("the runtime executes the tree"); let text = displayable(plain.as_ref()).indent(false).to_string(); assert!( - text.starts_with("FusedSeriesExec\n Window:"), + text.starts_with("FusedSeriesExec\nnative_tree=[\nWindow:"), "the row-table node line has no parameters: {text}" ); } diff --git a/src/promql/src/native/display.rs b/src/promql/src/native/display.rs index a3cb7b21681..1797ed6f92d 100644 --- a/src/promql/src/native/display.rs +++ b/src/promql/src/native/display.rs @@ -519,8 +519,7 @@ impl NativeTree { report_node(self.root(), ROOT_PATH, metrics) } - /// This tree rendered under its operator line, with each node's own - /// figures when `metrics` holds them. + /// This tree rendered from column zero, with each node's own figures when `metrics` holds them. pub fn display_with<'a>(&'a self, metrics: &'a NodeMetricsSnapshot) -> NativeTreeDisplay<'a> { NativeTreeDisplay { tree: self, @@ -538,9 +537,10 @@ pub struct NativeTreeDisplay<'a> { impl fmt::Display for NativeTreeDisplay<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // The root sits one level in from the operator line it is drawn under, and each child one - // level below its consumer. - write_report(f, &self.tree.report(self.metrics), 1) + // The root starts at column zero: the block the caller brackets the tree in carries the + // indentation, so each line here is depth-relative to that block rather than to the + // caller's own place in the plan. + write_report(f, &self.tree.report(self.metrics), 0) } } @@ -567,10 +567,11 @@ fn write_report( } impl fmt::Display for NativeTree { - /// One line per node, outermost first, two spaces per depth, without figures: this is the - /// tree's own shape, which is what a display that holds no metrics snapshot can show. + /// One line per node, outermost first, from column zero with two spaces per depth and without + /// figures: this is the tree's own shape, which is what a display that holds no metrics + /// snapshot can show. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write_report(f, &self.report(&NodeMetricsSnapshot::default()), 1) + write_report(f, &self.report(&NodeMetricsSnapshot::default()), 0) } } diff --git a/src/promql/src/native/display/tests.rs b/src/promql/src/native/display/tests.rs index affba89d745..84dc72483c8 100644 --- a/src/promql/src/native/display/tests.rs +++ b/src/promql/src/native/display/tests.rs @@ -125,7 +125,7 @@ fn shown(lines: &[&str]) -> String { fn a_tree_is_plan_lines_only() { let tree = NativeTree::try_new(raw_input()).unwrap(); - assert_eq!(tree.to_string(), shown(&[" Input: raw samples source=0"])); + assert_eq!(tree.to_string(), shown(&["Input: raw samples source=0"])); assert_eq!( NativeTree::try_new(window_over( raw_input(), @@ -138,8 +138,8 @@ fn a_tree_is_plan_lines_only() { .unwrap() .to_string(), shown(&[ - " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", - " Input: raw samples source=0", + "Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", + " Input: raw samples source=0", ]) ); } @@ -161,8 +161,8 @@ fn a_window_over_raw_samples_is_shown_once_per_node() { assert_eq!( tree.to_string(), shown(&[ - " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", - " Input: raw samples source=0", + "Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", + " Input: raw samples source=0", ]) ); } @@ -187,9 +187,9 @@ fn a_map_nests_below_the_window_it_maps() { assert_eq!( tree.to_string(), shown(&[ - " Map: abs, round 3", - " Window: rate [300000ms] offset 60000 anchor - on grid 0/60000x4", - " Input: raw samples source=0", + "Map: abs, round 3", + " Window: rate [300000ms] offset 60000 anchor - on grid 0/60000x4", + " Input: raw samples source=0", ]) ); } @@ -211,8 +211,8 @@ fn a_window_names_its_constant_argument_and_its_anchor() { assert_eq!( tree.to_string(), shown(&[ - " Window: quantile_over_time 0.9 [60000ms] offset 0 anchor 42 on grid 1000/60000x4", - " Input: raw samples source=0", + "Window: quantile_over_time 0.9 [60000ms] offset 0 anchor 42 on grid 1000/60000x4", + " Input: raw samples source=0", ]) ); } @@ -236,7 +236,7 @@ fn an_input_reports_whether_it_is_raw_and_what_ordering_it_guarantees() { let tree = NativeTree::try_new(evaluated_input).unwrap(); assert_eq!( tree.to_string(), - shown(&[" Input: evaluated samples source=0, contiguous",]) + shown(&["Input: evaluated samples source=0, contiguous",]) ); let interleaved = NativeType::Samples(Arc::new(SeriesSchema { @@ -254,7 +254,7 @@ fn an_input_reports_whether_it_is_raw_and_what_ordering_it_guarantees() { let tree = NativeTree::try_new(raw_input).unwrap(); assert_eq!( tree.to_string(), - shown(&[" Input: raw samples source=0, interleaved"]) + shown(&["Input: raw samples source=0, interleaved"]) ); } @@ -268,7 +268,7 @@ fn a_scalar_and_an_instant_line_show_what_they_hold() { output: NativeType::Scalar(Arc::clone(&out)), }; let tree = NativeTree::try_new(scalar).unwrap(); - assert_eq!(tree.to_string(), shown(&[" Scalar: 2"])); + assert_eq!(tree.to_string(), shown(&["Scalar: 2"])); let instant = NativeNode { expr: NativeExpr::Instant { @@ -287,8 +287,8 @@ fn a_scalar_and_an_instant_line_show_what_they_hold() { assert_eq!( tree.to_string(), shown(&[ - " Instant: lookback 300000ms offset 60000 on grid 0/60000x4", - " Input: raw samples source=0", + "Instant: lookback 300000ms offset 60000 on grid 0/60000x4", + " Input: raw samples source=0", ]) ); } @@ -320,9 +320,9 @@ fn an_aggregate_and_a_binary_name_their_binding() { assert_eq!( tree.to_string(), shown(&[ - " Aggregate: sum by [job] complete", - " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", - " Input: raw samples source=0", + "Aggregate: sum by [job] complete", + " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", + " Input: raw samples source=0", ]) ); @@ -356,10 +356,10 @@ fn an_aggregate_and_a_binary_name_their_binding() { assert_eq!( tree.to_string(), shown(&[ - " Binary: + ignoring [] one-to-one", - " Scalar: 2", - " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", - " Input: raw samples source=0", + "Binary: + ignoring [] one-to-one", + " Scalar: 2", + " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", + " Input: raw samples source=0", ]) ); @@ -375,7 +375,7 @@ fn an_aggregate_and_a_binary_name_their_binding() { .to_string() .lines() .next(), - Some(" Binary: + ignoring [] one-to-one fill_right 0.5") + Some("Binary: + ignoring [] one-to-one fill_right 0.5") ); } @@ -409,15 +409,15 @@ fn a_set_operation_names_its_operator_and_labels() { }; assert_eq!( root_line(SetOp::And, MatchingLabels::On(vec!["job".into()])), - " Set: and on [job]" + "Set: and on [job]" ); assert_eq!( root_line(SetOp::Or, MatchingLabels::Ignoring(vec!["job".into()])), - " Set: or ignoring [job]" + "Set: or ignoring [job]" ); assert_eq!( root_line(SetOp::Unless, MatchingLabels::Ignoring(Vec::new())), - " Set: unless ignoring []" + "Set: unless ignoring []" ); } @@ -451,9 +451,9 @@ fn a_without_grouping_does_not_read_as_by() { assert_eq!( tree.to_string(), shown(&[ - " Aggregate: sum without [job] complete", - " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", - " Input: raw samples source=0", + "Aggregate: sum without [job] complete", + " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4", + " Input: raw samples source=0", ]) ); } @@ -501,22 +501,22 @@ fn a_vector_operation_names_its_binding() { top: false, group: Grouping::Without(Arc::new(["job".into()])), }, - " Vector: bottomk 3 without [job]", + "Vector: bottomk 3 without [job]", ), ( BoundVectorOp::LimitK { k: 2.0, group: Grouping::By(Arc::new([])), }, - " Vector: limitk 2 by []", + "Vector: limitk 2 by []", ), ( BoundVectorOp::LimitRatio { ratio: -0.5 }, - " Vector: limit_ratio -0.5", + "Vector: limit_ratio -0.5", ), ( BoundVectorOp::Sort(SortOrder::ByLabelDesc(vec!["job".into(), "le".into()])), - " Vector: sort_by_label_desc [job, le]", + "Vector: sort_by_label_desc [job, le]", ), ( BoundVectorOp::Absent { @@ -529,14 +529,14 @@ fn a_vector_operation_names_its_binding() { ) .unwrap(), }, - r#" Vector: absent {job="x"}"#, + r#"Vector: absent {job="x"}"#, ), ( BoundVectorOp::CountValues { label: "value".into(), group: Grouping::Without(Arc::new(["job".into()])), }, - r#" Vector: count_values "value" without [job]"#, + r#"Vector: count_values "value" without [job]"#, ), ]; for (op, line) in lines { @@ -573,7 +573,7 @@ fn a_modified_window_names_its_modifier() { assert_eq!( tree.to_string().lines().next(), Some( - " Window: rate [300000ms] smoothed lookback 300000ms offset 0 anchor - on grid \ + "Window: rate [300000ms] smoothed lookback 300000ms offset 0 anchor - on grid \ 0/60000x4" ) ); @@ -624,10 +624,10 @@ fn each_node_shows_only_the_figures_it_recorded() { assert_eq!( tree.display_with(&metrics).to_string(), shown(&[ - " Aggregate: sum by [job] complete, metrics=[groups: 120]", - " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4, \ + "Aggregate: sum by [job] complete, metrics=[groups: 120]", + " Window: rate [300000ms] offset 0 anchor - on grid 0/60000x4, \ metrics=[output_batches: 4, output_values: 7200]", - " Input: raw samples source=0, metrics=[input_batches: 1, input_samples: 1486080]", + " Input: raw samples source=0, metrics=[input_batches: 1, input_samples: 1486080]", ]) ); } diff --git a/src/query/src/promql/native_compile/tests.rs b/src/query/src/promql/native_compile/tests.rs index 92eaaa5a96d..59c0f57ce61 100644 --- a/src/query/src/promql/native_compile/tests.rs +++ b/src/query/src/promql/native_compile/tests.rs @@ -1039,8 +1039,8 @@ fn the_display_of_a_compiled_tree_is_the_tree() { assert_eq!( tree.to_string(), [ - " Window: rate [300000ms] offset 0 anchor - on grid 0/5000x21", - " Input: raw samples source=0", + "Window: rate [300000ms] offset 0 anchor - on grid 0/5000x21", + " Input: raw samples source=0", ] .join("\n") ); @@ -1055,29 +1055,29 @@ fn the_display_of_a_tree_the_batch_added_to_is_the_query() { ( "sum by(job)(rate(x[5m]) * 100)", &[ - " Aggregate: sum by [job] complete", - " Binary: * ignoring [] one-to-one", - " Window: rate [300000ms] offset 0 anchor - on grid 0/5000x21", - " Input: raw samples source=0", - " Scalar: 100", + "Aggregate: sum by [job] complete", + " Binary: * ignoring [] one-to-one", + " Window: rate [300000ms] offset 0 anchor - on grid 0/5000x21", + " Input: raw samples source=0", + " Scalar: 100", ], ), ( "-x", &[ - " Binary: * ignoring [] one-to-one", - " Scalar: -1", - " Instant: lookback 5000ms offset 0 on grid 0/5000x21", - " Input: raw samples source=0", + "Binary: * ignoring [] one-to-one", + " Scalar: -1", + " Instant: lookback 5000ms offset 0 on grid 0/5000x21", + " Input: raw samples source=0", ], ), ( "0 < x", &[ - " Binary: < ignoring [] one-to-one", - " Scalar: 0", - " Instant: lookback 5000ms offset 0 on grid 0/5000x21", - " Input: raw samples source=0", + "Binary: < ignoring [] one-to-one", + " Scalar: 0", + " Instant: lookback 5000ms offset 0 on grid 0/5000x21", + " Input: raw samples source=0", ], ), ]; @@ -1289,7 +1289,7 @@ fn the_supported_math_functions_map_to_their_kernels() { let root = display.lines().next().expect("a compiled tree has a root"); assert_eq!( root, - format!(" Map: {op}"), + format!("Map: {op}"), "`{query}` bound the wrong kernel" ); } @@ -2853,7 +2853,7 @@ fn the_aggregates_that_are_not_a_fold_bind_a_vector_operation() { .unwrap_or_else(|error| panic!("`{query}` was refused: {error}")); let display = tree.to_string(); let root = display.lines().next().expect("a compiled tree has a root"); - assert_eq!(root, format!(" Vector: {op}"), "`{query}`"); + assert_eq!(root, format!("Vector: {op}"), "`{query}`"); } let error = compile( @@ -4287,7 +4287,7 @@ fn whole_vector_functions_bind_a_vector_operation() { .unwrap_or_else(|error| panic!("`{query}` was refused: {error}")); let display = tree.to_string(); let root = display.lines().next().expect("a compiled tree has a root"); - assert_eq!(root, format!(" Vector: {op}"), "`{query}`"); + assert_eq!(root, format!("Vector: {op}"), "`{query}`"); } } @@ -4830,7 +4830,7 @@ fn label_functions_bind_their_change_into_a_map() { .unwrap_or_else(|error| panic!("`{query}` was refused: {error}")); let display = tree.to_string(); let root = display.lines().next().expect("a compiled tree has a root"); - assert_eq!(root, format!(" Map: {op}"), "`{query}`"); + assert_eq!(root, format!("Map: {op}"), "`{query}`"); } } @@ -4853,7 +4853,7 @@ fn histogram_functions_bind_their_kernel_into_a_map() { .unwrap_or_else(|error| panic!("`{query}` was refused: {error}")); let display = tree.to_string(); let root = display.lines().next().expect("a compiled tree has a root"); - assert_eq!(root, format!(" Map: {name}"), "`{query}`"); + assert_eq!(root, format!("Map: {name}"), "`{query}`"); } } diff --git a/src/query/src/promql/planner/test.rs b/src/query/src/promql/planner/test.rs index 9afa4390021..703c74c7cb7 100644 --- a/src/query/src/promql/planner/test.rs +++ b/src/query/src/promql/planner/test.rs @@ -2620,7 +2620,10 @@ async fn native_fused_rate_plans_to_a_fused_series_node() { let plan = build_native_fused_plan("rate(some_metric[5m])", true).await; let display = plan.display_indent().to_string(); - assert!(display.contains(" Window: rate [300000ms]"), "{display}"); + assert!( + display.contains("native_tree=[\nWindow: rate [300000ms]"), + "{display}" + ); // The node reads the selector's own scan; nothing between them packs the frame it reads. assert!(display.contains("TableScan: some_metric"), "{display}"); // The node keeps the compiled tree, not the query text. @@ -4225,14 +4228,18 @@ async fn native_split_explain_renders_each_tree_including_remote_input() { let (final_text, remote_text) = text .split_once("MergeScan [is_placeholder=false, remote_input=[") .expect("the partial is inside a merge scan"); - assert!(final_text.starts_with("FusedSeries\n Binary:"), "{text}"); + assert!( + final_text.starts_with("FusedSeries\nnative_tree=[\nBinary:"), + "{text}" + ); assert!( final_text.contains("Aggregate: avg by [tag_0] final"), "{text}" ); assert!( - remote_text - .contains("FusedSeries: (output: series_frame)\n Aggregate: avg by [tag_0] partial"), + remote_text.contains( + "FusedSeries: (output: series_frame)\nnative_tree=[\nAggregate: avg by [tag_0] partial" + ), "{text}" ); assert!(remote_text.contains("Window: irate [60000ms]"), "{text}");