Skip to content

[Refactor] Crate organization by stage: per-crate functionality, cross-stage code into asap-accuracy / asap-cost / asap-types, enforced crate boundaries, public vs private #1054

Open

Description

@zzylol

Example

Run stage_pipeline on one query, quantile_over_time(0.9, latency[5m]), evaluated every minute with max_error 0.01. On main (cef05d2), the crate that should only select a plan (asap-plan-selection, Stage 3) runs every other stage itself:

Step Stage it belongs to Where it runs today
hash-cons the roots Stage 0 plan-selection/src/search/pipeline.rs:197 → LogicalMemo::from_roots
per-computation candidates Stage 1 Pass 1 pipeline.rs:198 → generate_per_computation_candidates_with_memo; route.rs:872 → try_populate_pass1
whole-result δ-sized candidates Stage 1 Pass 1 plan-selection/src/search/candidates/accuracy_configurations.rs:8
pane candidates and shared summary states Stage 1 Pass 2 route.rs:889 → extend_local_window_composition; route.rs:906 → expand_concrete_summary_state_groups_before
physical candidates and materializations Stage 2 plan-selection/src/search/candidates/{inventory,configurations}.rs
realize each plan the search proposes Stage 2 search/refinement/planner.rs:1050 → physical.rs:82 → optimize_physical_asap_chosen
resize sketches for the failure probability and recompile the DAG Stage 1 sizing + Stage 2 plan-selection/src/lib.rs:2243 sized_for_failure_probability
check accuracy Stage 3, but using Pass 1 code lib.rs:275-284 imports 8 guarantee functions from pass1::per_computation_candidates

The other crates have the same problem:

  • asap-types holds algorithms used by one stage only.
  • asap-physical-asap-optimization (Stage 2) depends on the Stage 1 crate for one field and one re-export.
  • Almost every module of asap-logical-asap-optimization is pub.

What should happen instead:

  • Each stage crate does only its stage.
  • Stage crates never depend on each other.
  • Shared models (accuracy, cost) and shared types live in non-stage crates.
  • Each crate makes public only what other crates use.

Status (2026-10-31)

  • code: decided 2026-10-31, Pass 1 sizes each approximate input for δ even when the query's root composes no guarantee (e.g. a ratio of quantiles under avg, or topk … or quantile). This follows the sizing table literally; such plans are rejected anyway when guarantees are enforced.

Done on refactor/stage-boundaries, apart from the open items below. Each stage crate depends only on asap-types, asap-accuracy and asap-cost. asap-plan-selection has no edge to logical or physical, and physical has none to logical. A CI test reads cargo metadata and checks the graph (integration-tests/tests/crate_graph.rs, #1132). The test allows two exceptions:

Where the code differs from the text decided on 2026-10-29, the text below says so with "code:". Stage 3 internals are out of scope: the #1095 redesign was removed in #1133, so the old engine is Stage 3's only engine behind the frozen select_plan interface (#1047).

Open (Stages 0–2):

  • Memo layout across crates ([Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 D1). MemoCandidates is a public struct with public fields, and it is the public memo field of the Pass 1 and Pass 2 outputs.
  • Low-value visibility leftovers in asap-types:
    • validate_at_ingestion and planned_data_state are pub, but only integration-tests use them.
    • Some IR compilation types (PhysicalASAPNodeIdentityMap, PhysicalASAPDAGCompilation, ProducerStorageMetrics, …) have only test users outside the crate. Check each one against the public signatures before narrowing it.
  • Naming leftover: Stage 0's hash-consing lives under the logical crate's pass2/ module (pass2/cse.rs, pass2/memo.rs). Optional.

Open, allowed but not needed yet (no work planned):

  • asap-cost lower and upper cost bounds over all materializations, for early pruning in Stages 1–2. With them, cost-dominance pruning in Stages 1–2 would follow the pruning rule below.

Outside this issue: planner → executor and the deployment items in asap-types (#1016).

Concrete maintenance problem

Current crate graph ([dependencies], not dev-dependencies)

flowchart TD
  planner["asap-planner<br/>(facade)"]
  fsql["frontend-sql"]; fpromql["frontend-promql"]; fmql["frontend-metricsql"]; fcommon["frontend-common"]
  ps["asap-plan-selection<br/>(Stage 3, plus S1/S2/facade code)"]
  phys["asap-physical-asap-optimization<br/>(Stage 2)"]
  logical["asap-logical-asap-optimization<br/>(S0 hash-cons, Pass 1, Pass 2, accuracy)"]
  exec["asap-executor"]
  types["asap-types<br/>(IR, workload, plus single-stage algorithms)"]
  planner --> fsql & fpromql & fmql
  planner --> ps
  planner --> logical
  planner -- "✗ #1016" --> exec
  fsql & fpromql & fmql --> fcommon
  fcommon --> types
  ps -- "✗ X1, X3: calls Stage 2" --> phys
  ps -- "✗ X2, X4: runs Pass 1/2, accuracy" --> logical
  phys -- "✗ one field + one re-export" --> logical
  logical --> types
  phys --> types
  ps --> types
  exec --> types
  classDef stage fill:#e8f0fe,stroke:#4a6fa5
  classDef bad fill:#fdecea,stroke:#c0392b
  class fsql,fpromql,fmql,fcommon,logical,phys stage
  class ps bad
Loading

Edges marked ✗ are the ones this issue removes or redirects. Not drawn: devtools and integration-tests (they depend on almost everything); dev-dependencies (executor → logical, plan-selection); the optional logical → frontend-promql under test-support; external crates (asap_sketchlib, highs, parsers).

The graph has no cycles. The problem is which edges exist and what each crate holds:

Edge Why it exists
plan-selection → logical Stage 3 runs Pass 1 and Pass 2 and uses Pass 1's accuracy functions (table above)
plan-selection → physical Stage 3 realizes each proposed plan through Stage 2 (refinement/planner.rs:1050), and calls summary_materialization_eligibility while pricing (costing/direct_profile.rs:196)
physical → logical only DeploymentCapabilities (one field, query_time_results_retained, physical/src/lib.rs:308) and pass1::realization::ranked_column, which merely re-exports asap_types::ir::schema::topk::ranked_column (realization.rs:172)
planner → executor the executor's supported-summary list and the compile checks (installable.rs:380-390, :505-600); see #1016

What each crate holds today (production lines)

Crate Lines Content, and what does not belong
asap-types ~15.5k IR (operators, schema, scalars, guarantees, timing) and workload types.
Single-stage algorithms:
• ir/cse.rs (Stage 0 hash-consing)
• ir/canonicalize.rs (frontends only)
• physical/summary_window.rs plan_pane_phase / validate_pane_coverage and timing.rs split_shared_by_timing (Stage 2 only)
• workload/resources/* (Stage 3 only)
Deployment items: materialized_view.rs, SupportedSummary, agg_has_exact_per_entity_kernel
asap-frontend-*, frontend-common ~10k Stage 0: query text → QueryRoot
asap-logical-asap-optimization ~15.4k LogicalMemo hash-consing, Pass 1, Pass 2 (memo rules plus the whole-workload path), accuracy/.
Of pass1/per_computation_candidates.rs (3,879 lines), only about 600 generate candidates (498-1110). About 2,000 lines are the shared accuracy model:
• guarantee propagation (1615-3031)
• guarantee helpers that Stage 3 calls (1328-1516)
• sized (3661-3711)
The rest is whole-workload composition and substitution.
Pass 1 depends on Pass 2 (pcc:613-625, :1690).
DeploymentCapabilities is defined here but read by Stages 1, 2 and 3
asap-physical-asap-optimization ~1.2k Stage 2, plus the whole-workload optimize_physical_asap
asap-plan-selection ~23.4k About 60% is Stage 3 (checks, pricing, cost models, MILP, report). The rest is:
• Stage 2 catalog construction (search/candidates/{inventory,configurations,physical,reconstruction}.rs, ~1.3k lines)
• Pass 1 sizing and pruning (query_pruning.rs, accuracy_configurations.rs, lib.rs:2243-2364)
• facade orchestration (search/pipeline.rs; search/route.rs except :954-1194)
• the public API (OptimizationPass, optimize, PlanningModels in search/mod.rs)
• accuracy glue (accuracy.rs, lib.rs:2651-2936)
asap-planner ~1.1k Frontend entry, InstallablePlan. The selected plan's DAG is exported four times: Stage 2 (physical/src/lib.rs:664), Stage 3 after resizing (plan-selection/src/lib.rs:2260), and installable_plan_of before and after pane expansion (installable.rs:606, :622)

Data structures and functions used across stages

Item Defined in Used by
IR: OperatorNode, QueryRoot, schema, AccuracyTarget, AccuracyGuarantee, Materialization, ExecutionTiming; workload types types all stages
memo and stage-boundary types (#1047) logical, plan-selection two or more stages each
accuracy model: accuracy/*, the guarantee propagation and sizing in per_computation_candidates.rs, AccuracyModel (plan-selection/src/accuracy.rs) logical, plan-selection Pass 1, Pass 2, Stage 3
bounds derived from DataWorkload: rows, group count G, source rates (plan-selection/src/lib.rs:1852-2242, :2937-3211) plan-selection pricing and accuracy sizing
DeploymentCapabilities logical Pass 1, Stage 2, Stage 3, planner
CandidateGenerationTimeout logical all stages
PhysicalASAPDAG, compile_physical_asap_* types Stage 2, Stage 3 (again), planner (twice), executor

Visibility

  • asap-logical-asap-optimization: pub mod accuracy / pass1 / pass2 and every submodule are public. About 40 pub items of asap_aware_cse are used outside the crate only by devtools.
  • asap-plan-selection:
    • PlanSelector and its methods, CalibrationFit, cost::analytical and cost::operator_statistics (~20 types), and six cost constants are pub with no user outside the crate.
    • pub use search as pass; (lib.rs:297) gives one module two public paths.
  • asap-types: single-stage functions are public crate-wide.

Before and after structure

Decided 2026-10-29.

Target crate graph

flowchart TD
  planner["asap-planner (facade)<br/>runs: frontends → S0 → P1 → per round: P2 → S2 → S3 → build plan → InstallablePlan<br/>splits deployment inputs; injects the Stage 2 realizer into Stage 3"]
  fe["asap-frontend-*<br/>(Stage 0 lowering)"]
  logical["asap-logical-asap-optimization<br/>(S0 hash-cons, Pass 1 incl. all sizing, Pass 2)"]
  phys["asap-physical-asap-optimization<br/>(Stage 2, realizer)"]
  ps["asap-plan-selection<br/>(Stage 3 only)"]
  acc["asap-accuracy (new)<br/>guarantees, estimators, sizing, AccuracyModel"]
  cost["asap-cost (new)<br/>resource usage, cost models 1/2/3, CostInputs"]
  types["asap-types<br/>IR, workload, workload-derived bounds,<br/>stage-boundary types (asap_types::stages)"]
  planner --> fe & logical & phys & ps
  fe --> types
  logical --> acc
  logical -.->|early pruning| cost
  phys -.-> acc
  phys -.->|early pruning| cost
  ps --> acc
  ps --> cost
  logical & phys & ps --> types
  acc --> types
  cost --> types
  classDef stage fill:#e8f0fe,stroke:#4a6fa5
  classDef shared fill:#e9f7ef,stroke:#2e8b57
  class fe,logical,phys,ps stage
  class acc,cost shared
Loading

Blue boxes are stage crates; there is no edge between any two of them. Green boxes are shared models; any stage may depend on them, and dashed edges are allowed but not needed yet. planner also depends on asap-accuracy, asap-cost and asap-types to assemble deployment inputs (not drawn).

Rules:

  1. Stage crates never depend on each other. The stage crates are frontend-*, logical, physical and plan-selection. A stage reads the previous stage's output type from asap-types and calls no other stage. Any stage may use asap-accuracy and asap-cost.
  2. asap-accuracy and asap-cost depend only on asap-types. Neither depends on the other: the workload-derived bounds both need live in asap-types.
  3. The facade is the only crate that depends on every stage. devtools and integration-tests are unrestricted.
  4. CI checks the graph. A test reads cargo metadata and fails on any [dependencies] edge outside the allowed list.

Per-crate functionality after the change

Crate Owns Receives from
asap-types IR, workload facts, workload-derived bounds (rows, group count G, source rates, confirmed_weights / with_confirmed_weights, #1083), the stage-boundary types of #1047 in asap_types::stages (DagOfLogicalMemoGroups, PerComputationLogicalMemoCandidates with ComputationTarget / FusedComputation / HavingEntry / HavingPlacement / HowToComputeAggregateMeasure (#1146), MemoCandidates, LogicalASAPMemoCandidates, PhysicalASAPMemoCandidates, PrunedCandidate / PruneReason, AssignmentRealizer, RealizedPlan), CandidateGenerationTimeout. code: EXACT_ACCUMULATOR_STATE_BYTES stays here, because the executor reads it and must not depend on asap-cost plan-selection (bounds), logical (memo types, timeout). Gives away its single-stage algorithms (below)
asap-accuracy (new) Guarantee composition and propagation; estimators (target → sketch parameters); accuracy budget split; δ union bound; size_for_all_readers; merged-error checks; AccuracyModel, AccuracyEvidenceProvider, OperandDomain, satisfies logical accuracy/* and the accuracy parts of per_computation_candidates.rs; plan-selection accuracy.rs and lib.rs:2651-2936
asap-cost (new) (1) Resource-usage estimation: analytical model, operator statistics, sketch byte models, measured operation costs, calibration. (2) Cost models: cost model 1/2/3, ResourceUnitCost, Pareto, and CostInputs (was PlanSelectionSettings). It gives the usage for a given timing and materialization, and lower/upper bounds over all materializations, so earlier stages can prune by cost (code: the bounds are not written yet; see Status) plan-selection cost/*, lib.rs pricing (3212-4591) and byte models (4873-5184); types workload/resources/*
asap-frontend-* Stage 0 lowering types canonicalize, resolve_group_keys_promql
asap-logical-asap-optimization Stage 0 hash-consing (generate_dag_of_logical_memo_groups); Pass 1, including all sizing (accuracy budget parts, whole-result δ, G); Pass 2 memo rules, including pane widths and shared summary-state sizes. code: DeploymentCapabilities is still defined here and re-exported by the planner; the facade splits it, so Stage 2 receives two bools and Stage 3 reads none of it types cse.rs; plan-selection accuracy_configurations.rs, resize_for_failure_probability, Pass 1 pruning
asap-physical-asap-optimization Stage 2: physical candidates, allowed timing and materializations (recording may_keep_across_evaluations), pane phase and coverage, building the selected plan; the realizer used by Stage 3 (Stage2AssignmentRealizer). code: also the whole-workload optimize_physical_asap, compiled only with the reference-pricer feature (#1135) plan-selection search/candidates/{inventory,configurations,physical,reconstruction}.rs, per_entity_aggregate_violation (guess: executor legality); types summary_window.rs, split_shared_by_timing (#1145)
asap-plan-selection Stage 3 only: checks (accuracy, latency, raw-data retention), MILP search, search-specific cost machinery (lower bounds, proposals, direct profiles, MILP price options), Selection —
asap-planner Orchestration, including pane-width rounds (#1048) (run_planning, PlanningRun); public API (OptimizationPass, optimize, check_contract); deployment-input assembly and dispatch; InstallablePlan plan-selection search/pipeline.rs, the round loop of route.rs, search/mod.rs
asap-devtools stage_pipeline, viewer data, fuzzing, and the reference pricer, kept to check the MILP's choice against enumeration (--price-enumerated, --exhaustive, planner_fuzz; #1002, #1004). code: the pricer's parts stay in their stage crates behind a reference-pricer feature (physical's optimize_physical_asap, plan-selection's whole-plan selector), and devtools enables it, so no production build compiles them (#1135) —

Deployment items in asap-types (materialized_view.rs, SupportedSummary, the executor kernel list) and the planner → executor edge are decided in #1016.

Resolving the four cross-stage calls

# Today After
X1 Stage 3 calls Stage 2 for each proposed plan (refinement/planner.rs:1050): the MILP proposes with local legality and cost lower bounds; an evaluator realizes the plan through Stage 2, checks it and prices it exactly, and turns the result into a cut. The catalog does not carry combination-level legality (timing copies, pane phase), combined accuracy, or consumer-dependent cost Short term: the trait AssignmentRealizer lives in asap_types::stages. The Stage 2 crate implements it, and the facade passes that implementation to Stage 3. There is no crate edge, and physicalization stays lazy. Longer term (under consideration): Stage 2 precomputes the combination-level work, so realizing a plan is only assembling it from the catalog plus IR validation, and the realizer goes away. First measure how often and why Stage 2 rejects an assignment today
X2 Stage 3 resizes sketches for δ and recompiles the DAG (lib.rs:2243-2364) All sizing is in Stage 1 (below); Stage 3 only checks
X3 Pricing calls summary_materialization_eligibility (direct_profile.rs:196) Stage 2 records may_keep_across_evaluations on each configuration
X4 Stage 3 imports 8 Pass 1 guarantee functions and the HLL/UnivMon estimators They move to asap-accuracy

Sizing: every size is decided in Stage 1, Stage 3 only checks

Sizes are decided in four places today:

  1. Pass 1 per budget share (groups.rs:204-215 → compose_alone → sized).
  2. add_scoped_candidates in plan-selection.
  3. Pass 2 shared sizes (size_for_all_readers, shared_size, state groups).
  4. Stage 3 δ resizing (lib.rs:2243).

After the change:

Decision Stage Function (asap-accuracy unless noted)
Whole-window build size: ε, budget share n, δ split over G Pass 1 (absorbs places 2 and 4) estimators, budget split, δ union bound
Pane width w and count n = W / w, chain sharing Pass 2 window rule; the facade gives each round's width domain (#1048) — (structural rule in logical)
Sketch size inside panes = the whole-window build's size Pass 2 (inherited, never resized) merged_error: Exact and AsIfBuiltOverUnion keep the guarantee at equal parameters; Approximate gives none
Shared build or chain size = the strictest reader Pass 2 state groups size_for_all_readers
Pane phase and alignment Stage 2 plan_pane_phase, validate_pane_coverage
Composed accuracy meets the target Stage 3 (check only) satisfies, guarantee composition
Memory of n panes = n × sketch bytes Stage 3 pricing asap-cost byte models

Docs to fix (done: #1098 and the final docs pass #1148): planner-layering.md ("Summary nodes are created unsized") and per_computation_candidates.rs:22 should say that Pass 1 creates sized candidates, one per accuracy-budget split. Smaller sketches per pane are out of scope; they need a sketchlib bound that depends on the number of merges.

Pruning

#1047's rule becomes: a stage removes only what its own knowledge proves invalid, or what is provably dominated under the shared cost model. Each stage records PrunedCandidate<C> with one PruneReason enum (asap_types::stages). Cost-dominance pruning in Stages 1–2 is allowed but not implemented yet.

  • Dominated means the pruned candidate's cost lower bound is ≥ the kept candidate's upper bound, for the same output, a guarantee at least as strong, and the same sharing.
  • This is the memo design's "keep the cheapest per sharing signature".

Visibility rules

  • Public: a crate's entry functions, its boundary and IR types, and what the facade and devtools use.
  • pub(crate): used by other modules of the same crate.
  • Private: used only in its own module.
  • #[cfg(test)] or a test-support feature: used only by tests.
  • No pub mod that leaks internals, and no second public path to the same item.
Crate Change
logical pass1, pass2, accuracy modules become pub(crate), with named pub use for the entry points and output types. asap_aware_cse items become pub(crate) (devtools labels move to the memo's origin). computation_candidates_for_intent, ClassicHllConfidence and DEFAULT_DELTA become private. Remove the realization::ranked_column re-export. code (#1145): every module is crate-private; the crate root re-exports what the planner and devtools use; other crates' tests reach internals through test_support (feature test-support)
plan-selection PlanSelector (+ methods), CalibrationFit, cost::analytical, cost::operator_statistics and the six cost constants become pub(crate) (most move to asap-cost; #1136 narrowed asap-cost's own API). Keep one path (pass) instead of pub use search as pass. code: confirmed_weights and with_confirmed_weights moved to asap_types::workload::bounds and stay pub, since the planner and plan-selection call them in production
physical optimize_physical_asap_assigned goes under test-support; physical_candidate_count and materializes_any go under #[cfg(test)]. code (#1135): catalog is private with root re-exports; no re-exports of asap-types items; test-only catalog items under test_support::catalog; materializes_any moved to asap-types with PhysicalWorkloadPlan, under cfg(any(test, feature = "test-support"))
types Single-stage functions move to their stage. These become pub(crate): structural_hash, is_shareable_when_equal, SummaryCoverage::{derive, definition_id}, optimistic_floor, approximate_layer_count, expand_pane_chains, whole_window_build, compile_physical_asap_dag_with_node_ids, *_before, flatten_before, view_window. code: is_shareable_when_equal stays pub (logical's hash-consing calls it); the test-only ones are under test-support; split_shared_by_timing moved to physical (#1145), for which asap-types exposes timing_under_consumer and MaterializationAssignment::get

Dead and test-only public code found by this review is listed in #1046.

Why the proposed structure is minimally complex

There are two new crates, each shared by several stages:

  • asap-accuracy: Pass 1, Pass 2, Stage 3.
  • asap-cost: Stage 3 now; earlier stages for cost-based pruning later.

Everything else is moving code to the crate whose stage uses it, and narrowing visibility. Removing the edges between stage crates turns the "no calls across stages" rule (#1047) from a review convention into a compile error.

Existing unit and end-to-end protection

  • Planner snapshots in devtools/tests/corpus_snapshots/ must stay unchanged in every PR.
  • cargo test --workspace.
  • The new cargo metadata graph test.

Compatibility or migration impact

Suggested order (one concern per PR; move PRs only move code):

Related

Activity

  1. zzylol commented on Oct 29, 2026

    @zzylol
    ContributorAuthor

    Progress 2026-10-29:

  2. zzylol commented on Oct 29, 2026

    @zzylol
    ContributorAuthor

    Decided 2026-10-29: the realizer is option D. A trait AssignmentRealizer lives in asap_types::stages; the Stage 2 crate implements it, and the facade passes that implementation to Stage 3. The search algorithm is unchanged. The MILP algorithm itself may change later (an efficiency discussion is pending). Option B, where Stage 2 precomputes combination-level work so that realizing is only assembly, stays a candidate. Data point: in a profile of the 6 memo-example workloads, Stage 2 rejected no assignments in 5 of them; ratio-of-quantiles had 46 rejections, all no_accuracy_guarantee.

  3. zzylol commented on Oct 30, 2026

    @zzylol
    ContributorAuthor

    Progress:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    optimizerWorkload optimization and plan selectionrefactor

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions