Skip to content

perf(planner): compile each stored output's build over the nodes below it - #808

Merged
zzylol merged 2 commits into
mainfrom
perf/installable-dag-checks
Oct 21, 2026
Merged

zzylol merged 2 commits into
mainfrom
perf/installable-dag-checks

Conversation

@zzylol

@zzylol zzylol commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

plan_installable compiled every stored output's build against the whole plan. The executor's compile checks the whole plan it is given before lowering anything (preflight_depth, logical::restore, PhysicalASAPDAG::validate) and indexes and sorts every node and edge. For a plan of N nodes with S stored outputs, that is O(S × N).

A pane-expanded plan of a long window has thousands of panes, and each is a stored output. On ASAPQuery's seed-862-long workloads the plan has about 5,300 nodes and 1,700 stored outputs.

Since #805, Stage 3 runs the same check (InstallablePlan::check_compiles) on every candidate that may be selected. check_compiles also looked up each input node and walked the query run's inputs by scanning the whole node and edge lists, which is quadratic as well. So the long workloads now take 260–420 s end to end, almost all of it in these compiles.

Part of #673.

What

  • One whole-plan compile per plan. The first build is still compiled in the whole plan, so the executor's whole-plan checks run once and report as they did before.
  • Every other build compiles only the nodes below it. The sub-DAG is the stored output's node and every node below it, with their edges, in the plan's order.
  • No more list scans. check_compiles looks up nodes and inputs through an index built once.
  • Unchanged: which plans compile, which are rejected, and the errors reported.

How

  • IndexedDag (built once per plan) also keeps each node's position and each consumer's input edges.
  • IndexedDag::dag_below(root) collects the closure below root and returns it as a PhysicalASAPDAG rooted at root.
  • executor_builds (in plan_installable) and check_compiles compile the first stored output over the whole plan, and every other over its sub-DAG.

The executor only lowers the nodes it reaches from the roots, stopping at bound inputs, and every node it reaches is in the sub-DAG. So each build compiles exactly as before, and the whole-plan checks still run once per plan.

Key code interfaces added / modified

None (private functions in crates/planner/src/installable.rs).

Usage

A user plans one of ASAPQuery's long-window workloads for a deployment that keeps no raw data (seed-862-long workload-02, its 9 PromQL queries, coverage converter with --no-raw --failure-probability 0.3):

// The user's workload: sums over 15 m, per-series p99/p95/p75 averaged
// over 300 s windows every second, top-k over 3 m and 60 m.
let plan = plan_installable(UserInput::new(&workload, FrontendInput::Promql { histograms: None },
    PlanningModels::builtin().with_deployment_capabilities(&no_raw).with_plan_selection(&settings))).await?;
// Stage 3's executor compile check (#805) installs each candidate it may
// select and compiles it: the first stored output's build in the whole
// plan, the other ~1,700 (one per one-second pane) each over the few nodes
// below it. plan_installable then installs the selected plan the same way.
assert_eq!(plan.selection.unwrap().selected_candidate_id, "P13-m127+P1-m3"); // as before
// 2.5 s end to end (was 419 s).

(The numbers come from running the coverage converter. The converter isn't part of this PR.)

Before this PR and after this PR

The 10 seed-862-long workloads (9 PromQL queries each), MVP flags, plan_installable end to end, release build. Both columns use main with #806 applied locally, so the search itself is fast and what remains is the installable plan's compiles:

Workload Before After Selected (both)
seed-862-long workload-01 260 s / 90 MB 2.54 s / 90 MB P41-m87+P1-m3
seed-862-long workload-02 419 s / 88 MB 2.51 s / 88 MB P13-m127+P1-m3
seed-862-long workload-03 400 s / 89 MB 2.84 s / 89 MB P77-m87+P1-m3
seed-862-long workload-04 278 s / 86 MB 2.49 s / 86 MB P41-m87+P1-m3
seed-862-long workload-05 263 s / 88 MB 2.38 s / 88 MB P25-m127+P1-m3
seed-862-long workload-06 349 s / 89 MB 2.42 s / 89 MB P13-m127+P1-m3
seed-862-long workload-07 191 s / 89 MB 2.47 s / 89 MB P77-m87+P1-m3
seed-862-long workload-08 273 s / 86 MB 2.31 s / 86 MB P39-m87+P1-m3
seed-862-long workload-09 288 s / 91 MB 2.51 s / 91 MB P41-m87+P1-m3
seed-862-long workload-10 184 s / 87 MB 2.32 s / 87 MB P41-m91+P1-m3

Each workload selects the same plan at the same cost before and after. With this PR, the 20 other ASAPQuery workloads (seed-862 and seed-862-x5) plan in 1.45–1.75 s, at 89–94 MB each.

Verification

  • cargo test --workspace --locked: passes.
  • New installable::tests::stored_output_builds_compile_only_the_nodes_below_them: a p99 panel over a 300 s window every second, with no raw data kept. It plans a DAG of 910 nodes with 300 stored outputs (one-second panes) and counts the nodes handed to the executor's compiler by the stored outputs' builds, in both plan_installable and Stage 3's compile check (check_compiles).
    • With plan_installable's builds reverted to the whole plan: 1,092,000 nodes in 35 s (debug build). The test fails.
    • With only check_compiles reverted to the whole plan: 281,424 nodes; the test fails.
    • With this change: 10,530 nodes (under 40 × the plan's size), in about 1 s. The test passes.
  • Measurements: /usr/bin/time around the coverage converter. Selections and costs were compared before and after.
  • cargo fmt --check: passes.
  • Not run: clippy, GitHub CI.

@zzylol zzylol added enhancement New feature or request optimizer Workload optimization and plan selection labels Oct 21, 2026
…w it

plan_installable (and Stage 3's executor compile check, through
check_compiles) compiled every stored output's build with the whole
plan: the executor's compile runs its whole-plan checks (depth,
restore, validate) and indexes every node and edge before lowering the
build, so a plan of N nodes with S stored outputs cost O(S x N). A
pane-expanded plan of a long window has thousands of panes, each a
stored output.

The first build is still compiled in the whole plan, which runs the
executor's whole-plan checks once; every other build is compiled over
the sub-DAG of its node and every node below it. check_compiles also
looks up nodes and inputs through the index instead of scanning the
node and edge lists for each one.
@zzylol
zzylol force-pushed the perf/installable-dag-checks branch from c9befd4 to 4ca2a94 Compare October 21, 2026 10:06
@zzylol
zzylol merged commit 05fb36e into main Oct 21, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request optimizer Workload optimization and plan selection

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant