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.

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), the stage-boundary types of #1047 in asap_types::stages (DagOfLogicalMemoGroups, PerComputationLogicalMemoCandidates, LogicalASAPMemoCandidates, PhysicalASAPMemoCandidates, PrunedCandidate), CandidateGenerationTimeout 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 plan-selection cost/*, lib.rs pricing (3212-4591) and byte models (4873-5184); types workload/resources/* and EXACT_ACCUMULATOR_STATE_BYTES
asap-frontend-* Stage 0 lowering types canonicalize, resolve_group_keys_promql
asap-logical-asap-optimization Stage 0 hash-consing; Pass 1, including all sizing (budget shares, whole-result δ, G); Pass 2 memo rules, including pane widths and shared summary-state sizes 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 plan-selection search/candidates/{inventory,configurations,physical,reconstruction}.rs, per_entity_aggregate_violation (guess: executor legality); types summary_window.rs, split_shared_by_timing
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); 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: the whole-workload optimize_physical_asap + select_plan, kept to check the MILP's choice against enumeration (--price-enumerated, planner_fuzz; #1002, #1004) —

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: 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.

  • 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
plan-selection PlanSelector (+ methods), CalibrationFit, cost::analytical, cost::operator_statistics and the six cost constants become pub(crate) (most move to asap-cost). Keep one path (pass) instead of pub use search as pass. confirmed_weights and with_confirmed_weights go under test-support
physical optimize_physical_asap_assigned goes under test-support; physical_candidate_count and materializes_any go under #[cfg(test)]
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

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):

  1. Create asap-accuracy; move the accuracy model (removes X4).
  2. Create asap-cost; move resource-usage estimation and the cost models.
  3. Move the single-stage algorithms out of asap-types, and the workload-derived bounds into it.
  4. Move the boundary types into asap_types::stages ([Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P3–P5).
  5. Move the Stage 2 catalog into physical ([Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P5); fix X3. Drop physical → logical.
  6. Move all sizing into Pass 1 (removes X2), then the realizer trait (removes X1).
  7. Move orchestration and the public API to the planner (Reorganize by stage: one memo-based Pass 2, Stage 1 finishes the memo, deployment inputs split by stage, asap-planner as the only entry point #958 step 6, [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P7). Drop plan-selection → logical / physical.
  8. Visibility pass per crate, then the CI graph test.

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.

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