Skip to content

feat(devtools): stage_pipeline --text prints every planner stage's output as text - #1159

Open
zzylol wants to merge 2 commits into
mainfrom
feat/print-planning-stages
Open

zzylol wants to merge 2 commits into
mainfrom
feat/print-planning-stages

Conversation

@zzylol

@zzylol zzylol commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Why

Reading what each planner stage hands the next took either the DAG viewer or raw JSON. The JSON document has no listing of Stage 2's PhysicalASAPMemoCandidates (Stage 3's input), only its pruned candidates. A text dump is easier to diff, grep and paste into issues; #1160 was written from it.

The text comes from the same binary as the viewer document, so both outputs share one planning run, one set of inputs and flags, and cannot drift apart. This consolidates #1158 (a Python printer of the JSON document) into Rust.

What

stage_pipeline --text writes the run as text instead of the viewer document. --out - writes to standard output. The text has:

  • Stage 0: the frontends' DAG.

  • The memo after each step:

    • its groups (DagOfLogicalMemoGroups);
    • Pass 1's PerComputationLogicalMemoCandidates, with computation targets and pruned ways to compute;
    • after Pass 2's local window composition;
    • Pass 2's LogicalASAPMemoCandidates, Stage 2's input.

    Each group is one line with the groups it reads; each candidate shows its origin, new when the step added it, its own operator chain and its input groups. The selected memo choice follows.

  • Stage 2's PhysicalASAPMemoCandidates: every operator node (the candidates it implements, and whether it is an implementation root or private), every physical candidate (timing, materialization, whether it may produce a query output, and the producer candidates per input), the reader constraints and the pruned candidates.

  • Stage 3's Selection: search status and MILP size, the selected DAG (each node's candidate, operator and producer nodes), the cost per second with each plan node's charge and detail, and each query's accepted accuracy.

  • With --enumerated: the first --max-enumerated memo choices, each with its composed DAG and the selected one marked.

Shared label change: node_label moves from stage_pipeline into candidate_labels. It now names a pane chain's layout (PaneChain pane_count=6 pane_width_ms=10000) and prints a time range as 60s, so the viewer document's memo labels change the same way.

How

stage_pipeline already plans with PlanSearchSettings::inspect, which keeps every stage's memo. With --text, it passes the resulting PlanningStagesRun to planning_stages_text and returns before building the JSON document. The text module prints aligned columns (write_rows) from the run's own types, not from the JSON document.

Key code interfaces added / modified

crates/devtools/src/planning_stages_text.rs (new module)

/// Every stage's output of `stages` (planned with `PlanSearchSettings::inspect`)
/// as text, one section per stage boundary; with `enumerated`, also the first
/// that many memo choices over Stage 2's input memo, each with its composed DAG.
pub fn planning_stages_text(stages: &PlanningStagesRun, enumerated: Option<usize>) -> String; // new

crates/devtools/src/candidate_labels.rs

/// One node's operator kind with its sketch family and parameters, its summary
/// query, its time range (`60s`) and its pane layout.
pub fn node_label(node: &asap_types::ir::flat::FlatNode) -> String; // was private in bin/stage_pipeline.rs; now names pane_width_ms / pane_count, and a range as 60s

crates/devtools/src/bin/stage_pipeline.rs: new flags --text, --enumerated (needs --text), and --out -.

Usage

A workload file rate1m.json with one exact panel, every 10 s, over a counter with 1,000,000 series at 66,667 rows/s:

{"query_workload": {"language": "promql", "query_batch": null, "repeating_queries": [
   {"query": "sum by (job) (rate(http_requests_total[1m]))", "demand": {"fixed_interval": 10000},
    "requirements": {"accuracy": {"max_error": 0.0, "failure_probability": 0.0}},
    "predictability": {"predictable": {"known_at": null}}}]},
 "data_workload": {"arrival": {"ingesting": {"rows_per_second": 66666.67}},
   "sources": {"http_requests_total": {"series_count": 1000000, "sample_interval": 15000, "metric_type": "counter"}}}}
cargo run -p asap-devtools --bin stage_pipeline -- --workload rate1m.json --text --out -

Excerpt of its 468 lines at this commit:

==================== Stage 1 Pass 2, after local window composition (pane candidates) ====================
G0:  Scan
    [0]  Pass1    Scan  inputs=[]
G1:  TimeRange 60s     <- G0
    [0]  Pass1    TimeRange 60s  inputs=[G0]
G2:  Aggregate rate()  <- G1
    [0]   Pass1       FinalizeExactAccumulator ← SummaryAgg                                                 inputs=[G1]
    [1]   Panes  new  FinalizeExactAccumulator ← SummaryMerge ← PaneChain pane_count=6 pane_width_ms=10000  inputs=[G0]
    [2]   Panes  new  FinalizeExactAccumulator ← SummaryMerge ← PaneChain pane_count=24 pane_width_ms=2500  inputs=[G0]
    …
==================== Stage 2 output = Stage 3 input: PhysicalASAPMemoCandidates ====================
operator_nodes_of_candidates_for_memo_groups (81):
    O2:   FinalizeExactAccumulator                    implements=[G2[0]]   implementation root
    O30:  PaneChain pane_count=6 pane_width_ms=10000  implements=[G2[1]]   private
candidates (107):
    C29:   O28  QueryTime      NotMaterialized         output  <- {C106}
    C31:   O30  QueryTime      NotMaterialized                 <- {C0}
    C32:   O30  IngestionTime  MatAtIngestion(Memory)          <- {C1}
    C33:   O31  QueryTime      NotMaterialized                 <- {C31, C32}
==================== Stage 3 output: Selection ====================
search: Optimal, 899 MILP variables, 2231 constraints, 356 ms solving
node0:  C0    Scan
node1:  C2    TimeRange 60s             <- node0
node4:  C30   SummaryAgg                <- node1
…
cost_per_second: 3.467880
    #0  Scan        QueryTime  2.320000  scan 4000000 samples; x 0.1000/s; keeps 192000000 bytes of raw data on disk; holds 48 bytes for 34.2 ms
    #2  SummaryAgg  QueryTime  0.423940  build exact Rate accumulator into 1000000 states: 4000000 rows x depth 1; x 0.1000/s; holds 56000000 bytes for 34.2 ms
    …
accepted accuracy:
    q0: metric not set; accepted by the deployment's accuracy model regardless of guarantee

Major algorithm changes

None. This is a devtool output mode plus label changes; planning is unchanged.

Before this PR

Stage boundaries were visible only through the viewer document. #1158 proposed a separate Python script that reads that document, so it could not show Stage 2's physical candidates, and a second implementation had to track the document format.

After this PR

One binary plans the workload and writes either the viewer document (the default) or the text (--text). Both outputs label pane chains with their layout and time ranges as 60s.

Verification

  • cargo test -p asap-devtools --lib --bins: passed. This includes planning_stages_text::tests::prints_every_stage_of_a_repeating_rate_query, which checks every section, the exact group and pane-candidate lines, the new marker, the ingestion-time pane chain, the selected choice and a two-choice enumeration.
  • cargo test -p asap-devtools --test devtools -- text_output enumerated_needs_text: 2 passed. These cover --text --out - on Example 1, and the usage error for --enumerated without --text.
  • cargo test -p asap-devtools --test devtools -- stage_pipeline:: viewer_contract::: 50 passed, run after the node_label change to 60s ranges.
  • cargo clippy -p asap-devtools --all-targets and cargo fmt: clean.

Follow-up

🤖 Generated with Claude Code

…tput as text

Adds asap_devtools::planning_stages_text and the print_planning_stages
bin: the frontends' roots, the memo groups, Pass 1's
PerComputationLogicalMemoCandidates, the memo after Pass 2's local window
composition, LogicalASAPMemoCandidates, PhysicalASAPMemoCandidates and
Stage 3's Selection for one PromQL workload. node_label moves from
stage_pipeline to candidate_labels so both share it, and now names a
pane chain's layout (pane_width_ms, pane_count).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One binary plans the workload; --text writes the run as text instead of the
viewer document, so both outputs come from the same inputs and flags.
The text output, now in the aligned format (G1:  TimeRange 60s  <- G0), also
covers what tools/dag-viewer/print_stages.py (#1158) prints: candidates new
at each memo step, the selected memo choice, Stage 3's search outcome, cost
per plan node and accepted accuracy per query, and with --enumerated the
memo choices with their DAGs. --out - writes to standard output.
node_label prints a time range as 60s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol zzylol changed the title feat(devtools): print_planning_stages prints every planner stage's output as text feat(devtools): stage_pipeline --text prints every planner stage's output as text Nov 2, 2026
@zzylol

zzylol commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Float parameters in candidate labels print with full round-trip precision. Running stage_pipeline --text at 0e8c928 on crates/devtools/tests/sketch_plan_snapshots/workloads/quantile-single.json (quantile_over_time(0.99, data[5m]), max_error 0.05, failure_probability 0.3) prints:

G2:  Aggregate quantile(q=0.99)  <- G1
    [1]  Pass1    Project ← SummaryEstimate Quantile(q=0.99) ← SummaryAgg Kll(failure_probability=0.000029999999999999997 k=86)  inputs=[G1]

The same string repeats on every KLL pane candidate and on every Stage 2 operator node that uses this KLL, so the lines get very long and hard to compare.

The value is a computed f64, so serde_json::Value's Display prints its shortest round-trip form. In find_details's compact closure (crates/devtools/src/candidate_labels.rs:306 and :313), numbers are formatted with {v}. Formatting Value::Number floats to a few significant digits (e.g. 3e-5, or {:.4e} with trailing zeros trimmed) would print Kll(failure_probability=3e-5 k=86). Integers like k=86 and simple values like alpha=0.05 should still print as they do now.

The viewer document uses the same labels (node_label), so the fix applies to it too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-tools Developer tools, diagnostics, and demos enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant