Skip to content

refactor(plan-selection): delete the unused physical-DAG, storage and handoff cost estimators - #833

Merged
zzylol merged 1 commit into
mainfrom
cleanup-dead-cost
Oct 21, 2026
Merged

zzylol merged 1 commit into
mainfrom
cleanup-dead-cost

Conversation

@zzylol

@zzylol zzylol commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

Stage 3 prices each node of a physical candidate with estimate_operator. A second, whole-DAG cost path still sits beside it: lowering a query to a PhysicalDAGNode DAG, pricing that DAG with cache, storage-request and handoff profiles, and comparing two DAGs over one ComparisonScope. Nothing reads it any more. #667 removed its summary-maintenance caller, #686 removed the dag_export devtool that used the lowering and the profiles, and #734 removed the Stage 3 comparison adapter. About 5,000 lines of code and 60 tests keep compiling and passing against an API nothing calls, and two develop docs still describe it as available. This follows the delete-unused-fields rule: delete public code that no non-test code reads.

What

  • plan-selection: delete cost/query_physical_lowering.rs, cost/storage_io.rs and cost/physical_handoff_cost.rs, plus their test files. From analytical_cost.rs, delete the physical-DAG estimator and the cache resolver. From physical_operator_statistics.rs, delete ComparisonScope, ScanSelection and the statistics-provider trait. Delete the AnalyticalCostError variants that only the deleted code produced.
  • types: delete workload::resources::{cache, physical_handoff, storage}, whose only readers were the deleted estimators. Schema now deserializes with a plain derive, so it no longer reads the old columns and bare-dtype layouts. AggIntent::Cardinality's cols key is now required. The #[serde(default)] on the Option col fields is dropped as redundant: serde already reads a missing Option field as None, so a measure without col still loads.
  • executor: delete the dag compatibility re-export module. The 8 test files that used it (the 6 the audit found plus 2 with a multi-line import) now import the real modules.
  • docs: delete develop_docs/physical-handoff-costs.md and storage-operation-costs.md. Both documented only deleted estimators. Add a status note to the analytical-resource-cost proposal and the physical-plan integration doc, and fix the vocabulary-migration record, the develop-docs index and the architecture map.

Planning output does not change.

How

These are plain deletions. Once the estimator chain is gone, clippy (-D warnings, all targets) found every private helper that had lost its only caller (validate_operator_statistics, validate_promql_child_edges, expected_input_arity / OperatorInputArity), and those are deleted too. Public items and enum variants never trigger dead-code warnings, so each one was checked with grep across the workspace and in ASAPQuery and ASAPCollector. ASAPCollector does not depend on ASAPPlanner crates. ASAPQuery-backend main depends on asap-planner and asap-executor at pinned rev 05fb36e4; none of the deleted items appears in it, so a later pin bump is unaffected.

Before dropping the legacy wire forms, every committed JSON file was scanned (23 files: examples, workloads, settings). None has a columns schema, a bare-string dtype, or a fields schema without closed. The one "kind": "sum" measure in tools/dag-viewer/examples/planner-layering-example1.json carries "col": null. Serialization always writes closed, col and cols, so only hand-written legacy JSON is affected. The separate --table JSON format is unchanged.

Deleted items

The grep column is run on origin/main (e2f9c81) as git grep -n <name> origin/main -- crates ':!<the three deleted cost modules>' ':!crates/*/tests/**' ':!*.md', plus git grep on origin/main of ASAPQuery, ASAPQuery-backend and ASAPCollector, which found 0 hits in all three for every item.

Item Why it is dead (no non-test reader) PR that removed its last caller
cost/query_physical_lowering.rs (3,028 lines, 17 unit tests) Only hits: the pub mod line and module doc in cost/mod.rs. #686 (deleted dag_export, the last lower_query_physical_dag caller), and #734 (removed the Stage 3 adapter's query_physical_lowering import)
cost/storage_io.rs (StorageIoProfile, estimate_storage_io; 3 unit tests) and tests/storage_io.rs (8 tests) StorageIoProfile: 0 hits. storage_io: only the cost/mod.rs lines. #734 (removed crate::storage_io::estimate_storage_io from the physical-plan adapter); #686 removed the devtool field
cost/physical_handoff_cost.rs (PhysicalHandoffProfile, estimate_physical_handoffs) and tests/physical_handoff_cost.rs (9 tests) PhysicalHandoffProfile: 0 hits. physical_handoff_cost: only the cost/mod.rs lines. #734; #686 removed the devtool field
analytical_cost::estimate_physical_dag, estimate_physical_dag_with_cache, estimate_physical_dag_comparison Only hits: their own definitions, each other, and the analytical_cost.rs test module. #667 (summary maintenance's estimate_physical_dag calls), #734 (estimate_physical_dag_comparison in the adapter)
analytical_cost::cache_hit_ratios, resolve_cache_profile, ResolvedCacheProfile Only hits: definitions, the deleted estimator, and tests. #686 (dag_export's cache_hit_ratios call)
PhysicalDAGNode, PhysicalNodeEvidence, EvidenceBackedPhysicalDAG, ExecutionMultiplicity, PhysicalDAGEstimateRequest, PhysicalDAGComparisonEstimate, ANALYTICAL_COST_MODEL_VERSION Only hits: analytical_cost.rs definitions, the deleted estimator, and tests. ANALYTICAL_COST_MODEL_VERSION: definition only. #667, #686, #734 (last EvidenceBackedPhysicalDAG users)
validate_operator_statistics, validate_promql_child_edges, expected_input_arity, OperatorInputArity (private) Their only caller was estimate_physical_dag_with_cache; clippy dead_code flags them after it goes. this PR (their caller)
ComparisonScope (with ::from_workload and ::validate), ScanSelection, validate_comparison_scopes, evaluations_in_horizon, OperatorStatisticsProvider Only hits: physical_operator_statistics.rs definitions, the deleted estimator, and tests. from_workload: definition only. #667 (last ComparisonScope::from_workload callers), #734
types::workload::resources::CacheProfile, CacheEvidence, CacheCapacityEvidence (resources/cache.rs, 2 tests) Only hits: the re-export in analytical_cost.rs and the deleted resolver. #734 (adapter's cache_profile field); #686 (devtool field)
types::workload::resources::StorageResources (resources/storage.rs, 2 tests) Only hits: its file and the storage_io re-export. #734
types::workload::resources::PhysicalHandoffBytes, PhysicalHandoffKind (resources/physical_handoff.rs, 2 tests; planner_vocabulary.rs::renamed_handoff_apis_are_public) Only hits: its file, the physical_handoff_cost re-export, and that test. #734
AnalyticalCostError::{UnsupportedDataArrival, NoEvaluationsInHorizon, InvalidCacheEvidence, UnsupportedQueryOperator, MissingComparisonScope, MissingScanSelection, ScanOutsideComparisonScope, ComparisonScopeMismatch, MissingOperatorStatistics, ConflictingEdgeStatistics, InvalidPhysicalDAG} After the deletions above, each is constructed nowhere (grep -rnw <variant> crates hits only the enum). this PR (their producers)
SchemaWire, FieldWire, FieldDataTypeWire (the columns and bare-dtype branches); tests legacy_pre_asap_schema_deserializes, legacy_pre_asap_schema_without_closed_or_table_is_open, legacy_summary_schema_deserializes_as_lifted No committed JSON uses those layouts (scan above), and serialization never writes them. none: compatibility for plans saved by builds older than the unified Schema
#[serde(default)] on AggIntent's col (9 variants; redundant, a missing Option still reads as None) and cols (now required); test agg_intent_serde_reads_pre_115_payloads No committed payload omits cols, and serialization always writes it. none: compatibility for payloads written before #115
asap_executor::dag (executor/src/dag/mod.rs) git grep 'asap_executor::dag' origin/main -- crates ':!crates/*/tests/**': 0 hits. Test files and a README doctest only. none: a compatibility re-export
analytical_cost.rs tests: 26 physical_dag_* / cache_* / comparison / scope tests, operator_arity_distinguishes_source_statistics_from_dag_children, and the comparison_scope / cache_test_scan / cache_profile helpers They exercise only the items above. with their subjects

Not deleted (out of scope; already dead on main before this PR): AnalyticalCostError::{InvalidIngestionRate, InconsistentBootstrapEvidence, InvalidOperationCost, ParameterMismatch, UnsupportedWithoutDistribution, UnsupportedCandidate, UnsupportedSummaryOperation} are declared but never constructed on main either.

Key code interfaces added / modified

Removed only. No interface is added or changed. Files deleted outright are linked at the base commit.

crates/plan-selection/src/cost/mod.rs (removed: query_physical_lowering.rs, storage_io.rs, physical_handoff_cost.rs)

/// Plan selection's analytical cost modules. The module stays; three submodules are removed.
pub mod analytical_cost;              // unchanged
pub mod physical_operator_statistics; // unchanged
pub mod physical_handoff_cost;        // removed
pub mod query_physical_lowering;      // removed
pub mod storage_io;                   // removed

crates/plan-selection/src/cost/analytical_cost.rs

/// Version label for the estimator over evidenced physical plans; nothing read it.
pub const ANALYTICAL_COST_MODEL_VERSION: &str; // removed
/// Cache hit ratios from a cache profile; only the deleted estimator read it.
pub fn cache_hit_ratios(profile: &CacheProfile, evaluation_count: u64, data_arrival: DataArrival) -> Result<(f64, f64), AnalyticalCostError>; // removed
/// One node of a lowered physical DAG; removed with the lowering.
pub struct PhysicalDAGNode { /* ... */ } // removed
/// Per-node statistics evidence for a lowered DAG.
pub struct PhysicalNodeEvidence { /* ... */ } // removed
/// A lowered DAG with its evidence.
pub struct EvidenceBackedPhysicalDAG { /* ... */ } // removed
/// Whether a lowered node runs once or once per evaluation.
pub enum ExecutionMultiplicity { Once, PerEvaluation } // removed
/// The inputs of one side of a DAG comparison.
pub struct PhysicalDAGEstimateRequest<'a> { /* ... */ } // removed
/// The raw and candidate estimates of a comparison.
pub struct PhysicalDAGComparisonEstimate { /* ... */ } // removed
/// Prices two lowered DAGs over one comparison scope.
pub fn estimate_physical_dag_comparison(raw: PhysicalDAGEstimateRequest<'_>, candidate: PhysicalDAGEstimateRequest<'_>) -> Result<PhysicalDAGComparisonEstimate, AnalyticalCostError>; // removed
/// Prices a lowered DAG with no cache.
pub fn estimate_physical_dag(nodes: &[PhysicalDAGNode], root: &str, scope: &ComparisonScope, statistics: &(impl OperatorStatisticsProvider + ?Sized)) -> Result<ResourceEstimate, AnalyticalCostError>; // removed
/// Prices a lowered DAG under a cache profile.
pub fn estimate_physical_dag_with_cache(nodes: &[PhysicalDAGNode], root: &str, scope: &ComparisonScope, statistics: &(impl OperatorStatisticsProvider + ?Sized), cache_profile: &CacheProfile) -> Result<ResourceEstimate, AnalyticalCostError>; // removed
/// Prices one physical operator from its statistics: Stage 3's live path.
pub fn estimate_operator(operator: PhysicalOperator, statistics: OperatorStatistics) -> Result<ResourceEstimate, AnalyticalCostError>; // unchanged

/// Errors of the analytical model. The type stays; variants only the deleted code produced are removed.
pub enum AnalyticalCostError {
    /// An arrival mode the DAG estimator did not model (unsatisfiable).
    UnsupportedDataArrival(DataArrival), // removed
    /// A recurrence with no evaluations in the horizon (bad input).
    NoEvaluationsInHorizon, // removed
    /// Bad cache evidence (bad input).
    InvalidCacheEvidence(&'static str), // removed
    /// A query operator with no physical lowering (unsatisfiable).
    UnsupportedQueryOperator, // removed
    /// A comparison-scope field is missing (bad input).
    MissingComparisonScope(&'static str), // removed
    /// A lowered scan without a scan selection (bad input).
    MissingScanSelection(String), // removed
    /// A lowered scan outside its comparison scope (bad input).
    ScanOutsideComparisonScope(String), // removed
    /// Two comparison scopes differ (bad input).
    ComparisonScopeMismatch(&'static str), // removed
    /// No statistics for a lowered node (bad input).
    MissingOperatorStatistics(String), // removed
    /// A parent's input statistics differ from its child's output (bad input).
    ConflictingEdgeStatistics { parent: String, child: String, input_index: usize }, // removed
    /// A malformed lowered DAG (internal bug).
    InvalidPhysicalDAG(&'static str), // removed
    /// A negative or non-finite ingestion rate (bad input).
    InvalidIngestionRate(f64), // unchanged
    /// Bootstrap rows and bytes disagree on zero (bad input).
    InconsistentBootstrapEvidence, // unchanged
    /// A non-positive summary operation cost (bad input).
    InvalidOperationCost(&'static str, f64), // unchanged
    /// A required input is missing or zero (bad input).
    MissingOrZero(&'static str), // unchanged
    /// Required evidence is missing or stale (bad input).
    MissingOrStale(&'static str), // unchanged
    /// The recurrence cannot be resolved over the horizon (bad input).
    InvalidRecurrence, // unchanged
    /// A negative or non-finite calibration value (bad input).
    InvalidCalibration(&'static str, f64), // unchanged
    /// Every calibration coefficient is zero (bad input).
    ZeroCalibration, // unchanged
    /// A sketch algorithm and its parameters disagree (internal bug).
    ParameterMismatch(SketchAlgorithm, SketchParams), // unchanged
    /// A family needs a value-distribution model (unsatisfiable).
    UnsupportedWithoutDistribution(&'static str), // unchanged
    /// Arithmetic overflow (input too large).
    Overflow, // unchanged
    /// No supported exact or sketch state (unsatisfiable).
    UnsupportedCandidate, // unchanged
    /// Statistics contradict the operator (bad input).
    InconsistentOperatorStatistics(&'static str), // unchanged
    /// A summary operation with no cost formula (unsatisfiable).
    UnsupportedSummaryOperation(&'static str), // unchanged
    /// Invalid statistics for a node (bad input).
    InvalidOperatorStatistics { node: String, reason: &'static str }, // unchanged
    /// Two measured entries for one operator, phase and family (bad input).
    DuplicateOperatorCost(String), // unchanged
    /// A measured entry names several families (bad input).
    AmbiguousOperatorCost(String), // unchanged
    /// Measured costs without a positive ms_per_cpu_op (bad input).
    MeasuredCostsWithoutCpuSpeed(f64), // unchanged
}

crates/plan-selection/src/cost/physical_operator_statistics.rs

/// The workload scope two lowered DAGs were compared over (with from_workload and validate).
pub struct ComparisonScope { /* ... */ } // removed
/// One scan's exact source selection.
pub struct ScanSelection { /* ... */ } // removed
/// Requires two comparison scopes to be equal.
pub fn validate_comparison_scopes(raw: &ComparisonScope, candidate: &ComparisonScope) -> Result<u64, AnalyticalCostError>; // removed
/// Resolves per-node statistics for the DAG estimator.
pub trait OperatorStatisticsProvider {
    fn statistics(&self, node_id: &str) -> Result<OperatorStatistics, AnalyticalCostError>;
} // removed

crates/types/src/workload/resources.rs

// Shared resource dimensions. The module stays; three submodules are removed.
pub use cache::{CacheCapacityEvidence, CacheEvidence, CacheProfile}; // removed
pub use physical_handoff::{PhysicalHandoffBytes, PhysicalHandoffKind}; // removed
pub use storage::StorageResources; // removed
pub use cpu::{MeasuredCpu, ModeledCpu}; // unchanged
pub use measurement::Measurement; // unchanged
pub use physical::{MeasuredResources, PhysicalResources}; // unchanged

crates/types/src/ir/schema/mod.rs, crates/types/src/ir/operator/agg_intent.rs

/// Per-edge schema. The type stays; its JSON form is now exactly its fields.
#[derive(Serialize, Deserialize)] // was #[serde(try_from = "SchemaWire")]
pub struct Schema {
    /// Fields in positional order; required (the legacy `columns` key is no longer read).
    pub fields: Vec<Field>, // unchanged
    /// Index of the time column; default None.
    pub time_index: Option<ColumnId>, // unchanged
    /// Unique-key sets; default empty.
    pub unique_keys: Vec<Vec<ColumnId>>, // unchanged
    /// Whether the schema lists every column; default false (was true when `fields` came without `closed`).
    pub closed: bool, // unchanged
}
/// The legacy deserialization form.
struct SchemaWire { /* ... */ } // removed

/// An aggregate measure. The type stays; `cols` is now a required JSON key (`col` stays optional: a missing `Option` reads as `None`).
pub enum AggIntent<C = ColumnId> {
    /// Sum of `col` (None: the sample value). Min, Max, Avg, StdDev, Variance, Quantile, FrequencyL2 and FrequencyEntropy change the same way.
    Sum { col: Option<C> }, // was #[serde(default)] col
    /// Distinct count over `cols`.
    Cardinality { cols: Vec<C>, accuracy: AccuracyTarget }, // was #[serde(default = "Vec::new")] cols
}

The other AggIntent variants do not change.

crates/executor/src/lib.rs

/// Compatibility re-export of plan, runtime, operators, physical_planner (as `planner`) and sources (as `scan`); import those modules directly.
pub mod dag; // removed

Usage

Nothing user-facing changes. The live pricing path is the same as before: a workload enters stage_pipeline, Stage 3 prices each node of each physical candidate with estimate_operator, and cost model 1 turns the result into cost per second. Run at this head:

# One approximate PromQL query: a p99 over 5-minute windows within 1% error,
# failing at most 0.1% of the time.
stage_pipeline --promql 'quantile_over_time(0.99, latency[5m])' \
  --max-error 0.01 --failure-probability 0.001 --out u.json

stage3_plan_selection in u.json:

selected  P2                       # the DDSketch candidate
accepted  P2: "metric not set; accepted relative (DDSketch)"
rejected  []
cost      0.0005172 cost/s, source "analytical-cost-v2 (illustrative statistics)"
per node (each price_source "analytical", from estimate_operator):
  0 scan 300000 samples; keeps 9600000 bytes of raw data on disk   0.00035
  1 time range 300s: keep 300000 of 300000 rows                    8.33e-05
  2 build DDSketch into 1000 states: 300000 rows x depth 1          8.33e-05
  3 estimate 1000 groups x 1                                        2.78e-07
  4 operator: pass 1000 rows                                        2.78e-07
usage     cpu_ops_per_second 250.6, peak_memory_bytes 8296096, disk_bytes 9600000, query_ms 1.862

Before this PR

The crate exported a second cost path, from lowering through DAG estimation to the cache, storage and handoff profiles. It compiled and had about 60 tests, but no stage, devtool or downstream repository called it. Its develop docs said it was "kept" and waiting for a consumer. Schema and AggIntent also accepted JSON layouts that no build has written since the schema unification and #115. The executor kept a dag alias module that only tests used.

After this PR

Stage 3's per-node analytical estimate is the only analytical cost path. Cost-model code is about 5,000 lines smaller. The docs say which parts of the analytical-cost design were deleted. Legacy columns schemas and cardinality measures without cols now fail to deserialize with serde's error instead of loading.

Verification

All runs below are local, at this head. GitHub CI was not waited on.

  • cargo fmt --all -- --check: clean.
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings: clean apart from two lints already on main in the untouched plan-selection/src/pass/components.rs (redundant_closure at :78, neg_cmp_op_on_partial_ord at :310). The local clippy 0.1.98 reports them; they were allowed for this run.
  • cargo check -p asap-planner --tests --locked: ok.
  • cargo test --workspace --locked: 1769 passed, 0 failed, 36 ignored. After rebasing onto main (9bf3834; test: drop duplicate and stale tests, keep slow variants out of debug CI, run the DAG viewer tests in CI #834 dropped tests): 1776 passed, 0 failed, 33 ignored; the promql-only run: 51 passed.
  • cargo test -p asap-planner --no-default-features --features promql --locked: 48 passed, 0 failed, 2 ignored.
  • JSON fixture scan for legacy schema and measure layouts: 0 hits in 23 committed JSON files (38 JSON/JSONL files after the rebase, also 0 hits).
  • Screenshots: not applicable.
  • Overlap: PR 1 of this cleanup (test cleanup) also edits crates/executor/tests/physical_dag.rs and weighted_topk_binding.rs. Whichever PR merges second needs a small rebase.

@zzylol zzylol added cost-model Cost formulas, statistics, ranking, and selection documentation Improvements or additions to documentation labels Oct 21, 2026
@zzylol
zzylol force-pushed the cleanup-dead-cost branch from f798a76 to 6080bea Compare October 21, 2026 19:57
… handoff cost estimators

No planning stage, devtool or downstream repository reads the whole-DAG
cost path any more: #667, #686 and #734 removed its last callers. Delete
it, the types only it used, and the legacy wire compatibility no
committed payload needs.

- plan-selection: delete cost/query_physical_lowering.rs, storage_io.rs
  and physical_handoff_cost.rs; in analytical_cost.rs delete
  estimate_physical_dag{,_with_cache,_comparison}, the cache profile
  resolver, PhysicalDAGNode/EvidenceBackedPhysicalDAG and their
  validators; in physical_operator_statistics.rs delete ComparisonScope,
  ScanSelection, validate_comparison_scopes and OperatorStatisticsProvider;
  drop the error variants only they produced.
- types: delete resources::{cache, physical_handoff, storage}; read Schema
  with a plain derive (no `columns` / bare-dtype layouts) and require
  AggIntent's `col` / `cols` keys.
- executor: delete the `dag` compatibility re-export; tests import the
  real module paths.
- docs: delete the storage and handoff develop docs; mark the deleted
  path in the analytical-cost proposal and physical-plan integration doc.
@zzylol
zzylol force-pushed the cleanup-dead-cost branch from 6080bea to 5b04d65 Compare October 21, 2026 20:34
@zzylol
zzylol merged commit 14394ad into main Oct 21, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cost-model Cost formulas, statistics, ranking, and selection documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant