Skip to content

feat(asap-aware-mapping)!: deployment capabilities (supported summaries, raw-data and query-time retention) - #730

Merged
zzylol merged 2 commits into
mainfrom
g9-deployment-inputs
Oct 16, 2026
Merged

zzylol merged 2 commits into
mainfrom
g9-deployment-inputs

Conversation

@zzylol

@zzylol zzylol commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Why

#509 lists three deployment inputs: the cost model, the accuracy model and the executor's capabilities. Only the first two existed (the cost model's unit costs, resource speed and settings landed in #723). So the planner could offer a summary the executor cannot build or answer, it could not tell whether raw data is kept for query-time scans, and it had no way to plan Example 4's B3 (tumbling panes built at query time and kept across evaluations). Old drafts #609, #610 and #625 predate the current stage code. This PR reuses their ideas, not their code.

What

  • DeploymentCapabilities (new, in asap-aware-mapping, field PlanningModels::deployment_capabilities). It holds three inputs, all data:
    • supported_summaries: the sketch families the executor supports, each with the readouts it supports, in the family-capability table's vocabulary (SketchAlgorithm, StatisticKind). "Supports" means the executor can build the family's state and answer that statistic from it. None (the default) supports everything the planner can build. Exact accumulators and plain exact execution are always supported.
    • raw_data_retained (default true).
    • query_time_results_retained (default false).
  • Stage 1. Pass 1 does not offer an unsupported summary. It moves the summary to the target's pruned list with the new PruneReason::UnsupportedByDeployment, and a fused computation moves its unsupported heaps to its own new pruned list (with none left, it is not offered). Pass 2 then offers none either. Its rules share builds, split them into panes or reuse them, and none of them adds a family or a readout. A test checks this on Pass 2 output.
  • Raw data.
    • When retained, nothing changes: Stage 3 already counts raw data on disk once per source, at the longest window a query-time scan reads (feat(planner)!: Stage 3 resource usage and cost models 1-3 (usage over time, provisioned capacity, own cluster) #723).
    • When not retained, raw samples are not kept after they arrive. A candidate with any query-time scan is then invalid, and the reason lists every such scan, e.g. scans 300000 ms of raw a, 60000 ms of raw b at query time, but the deployment does not retain raw data. Only ingestion-time work reads raw rows.
    • Stored data (AtRest or Mixed) with raw_data_retained: false is a contradiction. Plan selection refuses it as bad input (SelectionError::InvalidDeploymentCapabilities) before measuring any candidate.
  • B3: a query-time result kept across evaluations. Materialization gains MatAtQueryKeptAcrossEvaluations(Medium). Stage 2 offers it for a tumbling-pane chain that builds exactly one new pane per evaluation (builds_one_pane_per_evaluation): every reader repeats at a fixed interval equal to the pane width, and readers with a known evaluation phase share one phase. With query_time_results_retained:
    • Stage 2 offers the kept option even with one reader. A chain two or more queries read keeps its MatAtQuery option (shared within one evaluation) beside it, and Stage 3 prices both, so the cheaper wins.
    • Stage 3 prices the kept option as kept. Each evaluation scans one pane of raw data and builds the newest pane. The N − 1 older panes, which complete the readers' look-back, are held every second (in memory, all N at the peak; on disk, N − 1).
    • With the switch off, Stage 2 never offers the kept option, so candidates, ids and costs equal main's.
  • Legacy hook removed. CostModel::summary_support_evidence is removed, because supported_summaries replaces it. No non-test code overrode it, and the legacy search's summary arm now returns its old default (None). The CostModel trait itself stays.
  • Per-node usage removed. NodeCost.usage, NodeCost.per_second and the NodeUsage type are deleted: nothing outside price() and tests read them, and stage_pipeline never wrote them. price() adds each node's use straight into the candidate's ResourceUsage. A node's rate is still in its detail (x 0.0167/s).
  • stage_pipeline --deployment-capabilities '<json>' reads the inputs (unknown fields are refused) and writes them read-only as the document's top-level deployment_capabilities. Stage 1 gains pass1_pruned: per target with any pruned candidate, its index, the query it is listed under, and every pruned candidate of its own (pruned) and of its fused computation (fused_pruned) with the reason. A user can now see why DDSketch disappeared.

How

  • PerComputationCandidates::without_unsupported_summaries filters the Pass 1 inventory. Each target's readout is statistic(intent).kind(), the statistic its estimate reads. It runs in run_stage1_to_stage3_from_pass1 and in the devtools pipeline before enumeration, so the Cartesian product shrinks; nothing is enumerated and then discarded.
  • run_stage2_and_stage3 does not prune. Its doc now says so: callers must pass an inventory already pruned for models.deployment_capabilities, with stage1 enumerated from it. Pruning there would come too late, because each Stage 1 choice indexes the inventory it was enumerated from.
  • Stage 2: MaterializationCandidate::may_keep_across_evaluations adds the MatAtQueryKeptAcrossEvaluations option after the existing ones, which keeps the ids under default inputs.
  • builds_one_pane_per_evaluation also compares known phases modulo the interval. Its doc states the co-scheduling assumption: readers with no known phase run at the same instants as the others, as Stage 3's evaluation rate already assumes.
  • Stage 3:
    • The node map (node_map) and the kept chains (kept_pane_chains, now just the chains with the new variant) are built once per candidate and passed to both raw_data_violation and price. Stage 3 no longer recomputes the readers to decide what is kept.
    • windows_read counts one pane for those chains, so the scan, its latency and the raw data kept on disk cover one pane.
    • The kept arm charges N − 1 panes as average memory and N as peak memory, or N − 1 panes on disk.
    • DeploymentCapabilities::validate runs at the top of select_plan, beside the settings check.
  • fixed_interval_ms moved from stage1_to_stage3 to Stage 2, so Stage 2 and Stage 3 share it.

Decisions:

  1. DeploymentCapabilities is a new struct next to PlanSelectionSettings, not a set of extra fields on it, because Stages 1 and 2 read it too and PlanSelectionSettings is Stage 3's.
  2. The capability vocabulary covers sketch families only. Exact is always supported.
  3. "Raw data not retained" means nothing is kept. Every query-time raw scan is then invalid, B3's one-pane scan included. The bool stayed global.
  4. query_time_results_retained defaults to false, so that the default inputs keep today's candidates and ids.
  5. The kept option is its own Materialization variant (overriding the earlier "no IR field" decision), so the physical DAG says which chains an executor keeps. A separate variant leaves the serialized MatAtQuery and every default document unchanged.
  6. ResourceCalibration::cost_per_retained_byte is not deleted. The legacy physical_plan_cost_model still reads it through ResourceEstimate::calibrated_cost, and these inputs do not replace it.
  7. The value_operation_* hooks stay. They gate exact-operation placement, which these inputs do not cover.

Key code interfaces added / modified

crates/asap-aware-mapping/src/deployment_capabilities.rs (new module)

/// What the deployment's executor can run and keep. Planning reads it in
/// Stage 1 (`supported_summaries`), Stage 2 (`query_time_results_retained`)
/// and Stage 3 (`raw_data_retained`); `stage_pipeline` reads it from
/// `--deployment-capabilities <json>`. New type.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct DeploymentCapabilities {
    /// The sketch families the executor can build, each with the readouts it can
    /// answer from one; `None` (default) supports every family and readout.
    pub supported_summaries: Option<Vec<SupportedSummary>>,   // new
    /// Whether raw samples are kept for query-time scans; default `true`.
    pub raw_data_retained: bool,                               // new
    /// Whether a query-time result may be kept across evaluations
    /// (`MatAtQueryKeptAcrossEvaluations`); default `false`.
    pub query_time_results_retained: bool,                     // new
}

/// One sketch family the executor can build, with the readouts it can answer
/// from the family's state. New type.
pub struct SupportedSummary {
    /// The family the executor can build.
    pub family: SketchAlgorithm,          // new
    /// The statistics it can answer from one state of the family.
    pub readouts: Vec<StatisticKind>,     // new
}

impl DeploymentCapabilities {
    /// Whether the executor can build `family` and answer `readout` from it.
    pub fn supports(&self, family: &SketchAlgorithm, readout: StatisticKind) -> bool; // new
    /// Checks these capabilities against the data workload: stored data (at rest
    /// or mixed) contradicts `raw_data_retained: false`. Plan selection calls it
    /// before measuring any candidate.
    pub fn validate(&self, data: &DataWorkload) -> Result<(), DeploymentCapabilitiesError>; // new
}

/// Deployment capabilities that contradict the other planning inputs; always bad
/// input. New type.
pub enum DeploymentCapabilitiesError {
    /// Bad input: the data workload declares stored raw data (`AtRest` or
    /// `Mixed`), but the deployment says it retains none.
    StoredDataNotRetained {
        /// The declared arrival that stores raw data.
        arrival: DataArrival,
    },                                     // new
}

crates/types/src/post_asap/execution_data_state.rs

/// The materialization option of one sub-DAG. The type stays; it gains a variant.
pub enum Materialization {
    /// Runs at query time for each execution; its output is discarded.
    NotMaterialized,                          // unchanged
    /// Runs as data arrives; its output is stored until a query reads it.
    MatAtIngestion(Medium),                   // unchanged
    /// Runs when a query first needs it; its output is reused by the other
    /// queries of the same evaluation and discarded after it.
    MatAtQuery(Medium),                       // unchanged (doc corrected)
    /// Runs at query time and keeps its output across evaluations: a tumbling-pane
    /// chain whose readers evaluate together every pane width builds its newest
    /// pane each evaluation and keeps the older ones (B3). Offered only when the
    /// deployment keeps query-time results.
    MatAtQueryKeptAcrossEvaluations(Medium),  // new
}

crates/asap-aware-mapping/src/pass/mod.rs, crates/asap-aware-mapping/src/pass/stage1_to_stage3.rs

/// Planning logic the stages read. The type stays; it gains `deployment_capabilities`.
pub struct PlanningModels<'a> {
    /// The legacy cost-model hooks (not read by the stage pipeline).
    pub cost: &'a dyn CostModel,                              // unchanged
    /// The accuracy model plan selection reads.
    pub accuracy: &'a dyn AccuracyModel,                      // unchanged
    /// The deployment's empirical accuracy evidence.
    pub evidence: &'a dyn AccuracyEvidenceProvider,           // unchanged
    /// The deployment's plan selection settings (cost model, speed, horizon).
    pub plan_selection: &'a PlanSelectionSettings,            // unchanged
    /// What the deployment's executor can run and keep; default
    /// `DeploymentCapabilities::default()`.
    pub deployment_capabilities: &'a DeploymentCapabilities,  // new
}
impl<'a> PlanningModels<'a> {
    /// These models with the deployment's capabilities.
    pub fn with_deployment_capabilities(
        self,
        deployment_capabilities: &'a DeploymentCapabilities,
    ) -> Self;                                                // new
}

/// Stage 2 and Stage 3 over already-enumerated Stage 1 candidates. Signature
/// unchanged; its doc now says it does not prune, so `per_computation` must
/// already be pruned for `models.deployment_capabilities`.
pub fn run_stage2_and_stage3<Id: Clone + std::fmt::Debug>(
    per_computation: PerComputationCandidates<Id>,
    stage1: Vec<Stage1Candidate>,
    entries: &[QueryWorkloadEntry],
    data: &DataWorkload,
    models: PlanningModels<'_>,
) -> Stage1ToStage3Run<Id>;                                   // unchanged

crates/asap-aware-mapping/src/per_computation_candidates.rs

/// Why Pass 1 proved a candidate unable to realize its target. The type stays.
pub enum PruneReason {
    /// The measure requires an exact answer and the summary is approximate.
    RequiresExact,             // unchanged
    /// `count` counts rows, so a possibly-null `avg` input changes the result.
    NullableAvgInput,          // unchanged
    /// The deployment's executor cannot build the summary's family or answer the
    /// measure's readout from it (a deployment restriction, not bad input).
    UnsupportedByDeployment,   // new
}

/// A target's measure computed together with the target beneath it. The type
/// stays; it gains `pruned`.
pub struct FusedComputation {
    /// The index of the inner target it absorbs.
    pub absorbs: usize,                                       // unchanged
    /// How it is computed: exact or a summary; empty when Pass 1 pruned every
    /// one, and the fusion is then not offered.
    pub candidates: Vec<ComputationCandidate>,                // unchanged (may now be empty)
    /// The candidates Pass 1 pruned from it, with their reasons; empty by default.
    pub pruned: Vec<(ComputationCandidate, PruneReason)>,     // new
}

impl<Id> PerComputationCandidates<Id> {
    /// This inventory without the summary candidates `deployment` does not
    /// support (moved to the pruned lists with `UnsupportedByDeployment`).
    /// Unchanged when every summary is supported.
    pub fn without_unsupported_summaries(self, deployment: &DeploymentCapabilities) -> Self; // new
}

crates/asap-aware-mapping/src/stage2_physical_asap_optimization.rs

/// Stage 2 for one logical candidate; `deployment` says whether a query-time
/// result may be kept across evaluations.
pub fn optimize_physical_asap(
    from_logical: &str,
    roots: &[QueryRoot],
    entries: &[QueryWorkloadEntry],
    data: &DataWorkload,
    deployment: &DeploymentCapabilities,   // new parameter
) -> Result<Vec<PhysicalCandidate>, PhysicalASAPOptimizationError>;
/// The number of physical candidates `optimize_physical_asap` returns.
pub fn physical_candidate_count(
    roots: &[QueryRoot],
    entries: &[QueryWorkloadEntry],
    data: &DataWorkload,
    deployment: &DeploymentCapabilities,   // new parameter
) -> Result<usize, PhysicalASAPOptimizationError>;
/// Whether a tumbling-pane chain of `pane_width_ms` panes read by queries
/// repeating as `readers` builds exactly one new pane per evaluation: every
/// reader repeats at the pane width and the known evaluation phases are equal
/// (co-scheduling assumed for readers with no known phase).
pub(crate) fn builds_one_pane_per_evaluation<'a>(
    pane_width_ms: u64,
    readers: impl IntoIterator<Item = &'a QueryRecurrence>,
) -> bool;                                                       // new

crates/asap-aware-mapping/src/stage3_plan_selection.rs

/// Why plan selection failed. The type stays; it gains a variant.
pub enum SelectionError {
    /// Bad input: the deployment's plan selection settings.
    InvalidSettings(AnalyticalCostError),                         // unchanged
    /// Bad input: the deployment's capabilities contradict the data workload
    /// (`DeploymentCapabilities::validate`).
    InvalidDeploymentCapabilities(DeploymentCapabilitiesError),   // new
    /// Bad input: a candidate's root count differs from the entry count.
    EntryCount { candidate: String, roots: usize, entries: usize }, // unchanged
    /// Internal: a node could not be measured.
    Cost { candidate: String, node: PhysicalASAPNodeId, error: AnalyticalCostError }, // unchanged
    /// Unsatisfiable: every candidate was rejected.
    NoValidCandidate(Vec<Rejection>),                             // unchanged
}

/// One node's price per second of wall time, with how it was measured. The type stays.
pub struct NodeCost {
    pub usage: NodeUsage,                // removed: nothing read it; the candidate's ResourceUsage is accumulated directly
    /// Its price per second under cost model 1; `None` under cost models 2 and 3.
    pub cost_per_second: Option<f64>,    // unchanged
    /// The number of consumers it is computed for (1 when materialized).
    pub multiplier: u64,                 // unchanged
    pub per_second: f64,                 // removed: test-only; the rate stays in `detail`
    /// How it was measured: one computation's work, its rate and what it keeps.
    pub detail: String,                  // unchanged
}

pub struct NodeUsage { /* ... */ }       // removed: per-node usage nobody read

crates/types/src/post_asap/family_capability.rs

/// The kind of a `SummaryEstimate` statistic: the readout vocabulary of the
/// family-capability table and of a deployment's supported summaries. The type
/// stays; it gains `Serialize, Deserialize`.
pub enum StatisticKind {
    /// The total of the updates.
    Total,              // unchanged
    /// One item's frequency.
    ItemFrequency,      // unchanged
    /// The value at any quantile rank.
    Quantile,           // unchanged
    /// The number of distinct items.
    Distinct,           // unchanged
    /// The L2 norm of the item frequencies.
    FrequencyL2,        // unchanged
    /// The entropy of the item frequencies.
    FrequencyEntropy,   // unchanged
    /// The heaviest items.
    TopK,               // unchanged
}

crates/asap-aware-mapping/src/cost_model.rs

/// The legacy search's cost-model hooks. The trait stays; one hook is removed.
pub trait CostModel {
    /// Physical feasibility evidence for a complete summary candidate.
    fn summary_support_evidence(&self, _summary: &OperatorNode) -> Option<bool>; // removed: replaced by DeploymentCapabilities::supported_summaries
}

crates/devtools/src/planning_stages.rs, crates/devtools/src/candidate_labels.rs, crates/devtools/src/bin/stage_pipeline.rs

/// The four stages over `roots`; `deployment` restricts Pass 1 and sets Stage 2/3 retention.
pub fn run_planning_stages_over(
    roots: Vec<QueryRoot>,
    entries: &[QueryWorkloadEntry],
    data: &DataWorkload,
    max_combinations: Option<usize>,
    plan_selection: &PlanSelectionSettings,
    deployment: &DeploymentCapabilities,   // new parameter
) -> Result<PlanningStagesRun, PlanningStagesError>;
/// The query each target of `inventory` is listed under (the first that reads it).
pub fn query_of_each_target<Id>(inventory: &PerComputationCandidates<Id>) -> Vec<usize>; // new
/// A Pass 1 per-computation candidate's label, e.g. "exact", "Sum acc" or "Kll".
pub fn computation_label(candidate: &ComputationCandidate) -> String;                  // new
// stage_pipeline: new flag `--deployment-capabilities '<json>'`; new top-level
// document key `deployment_capabilities` (the inputs used, defaults filled in);
// new `stage1_logical_asap_optimization.pass1_pruned`
// ([{target, query, pruned: [{candidate, reason}], fused_pruned: [...]}]);
// a kept chain is labelled "query time, kept".

Usage, end to end (Example 3's Pattern B; every number is checked by deployment_capabilities_flag_sets_the_capabilities_and_shows_them, query_time_results_retained_still_offers_the_chain_shared_within_one_evaluation and the Example 4 tests at this commit):

// 1. What the user writes: Pattern B, the per-series p99 latency over the last 5 min,
//    every minute, over 1M series ingesting 66,667 samples/s (`--example planner-layering-3b`).
// `workload`: the queries and data facts; Stage 0 lowers it and Stages 1-3 read its entries.
let workload = pattern_b();
// `roots`: one PromQL plan root per query, from the query-language frontends (Stage 0).
let roots = lower_promql_roots(&workload)?;
// `entries`: each query's accuracy target, latency bound and 1-min recurrence.
let entries: Vec<_> = workload.query_workload.entries().collect();
// `data`: the data workload (arrival rate, series); Stages 2 and 3 read it.
let data = workload.data_workload.clone().unwrap_or_default();

// 2. What the deployment declares. Same as
//    --deployment-capabilities '{"supported_summaries": [{"family": "Kll", "readouts": ["Quantile"]}],
//                                "query_time_results_retained": true}'
// `deployment_capabilities`: an executor that builds only KLL (answering quantiles), keeps raw
//    data (default) and may keep query-time results across evaluations.
let deployment_capabilities = DeploymentCapabilities {
    supported_summaries: Some(vec![SupportedSummary {
        family: SketchAlgorithm::Kll,
        readouts: vec![StatisticKind::Quantile],
    }]),
    query_time_results_retained: true,
    ..DeploymentCapabilities::default()
};

// 3. Stages 1-3. `run` holds every stage's candidates and Stage 3's selection.
//    Stage 0 → 3, validation: the data is ingesting, so `validate` passes.
//    Stage 1 / Pass 1: `without_unsupported_summaries` prunes DDSketch
//    (UnsupportedByDeployment), so only L1 exact, L2 whole-window KLL and L3 KLL in
//    five 1-min panes remain (was L1-L5). The document's `pass1_pruned` shows
//    [{"target": 0, "query": "Q1", "pruned": [{"candidate": "DDSketch",
//      "reason": "UnsupportedByDeployment"}], "fused_pruned": []}].
//    Stage 2: L3's chain has one reader repeating every minute, the pane width, so
//    `builds_one_pane_per_evaluation` holds and the chain gets three options:
//    P3 (B2, rebuilt), P3-m1 (B1, at ingestion), P3-m2 (B3,
//    MatAtQueryKeptAcrossEvaluations, labelled "query time, kept").
//    Stage 3: `kept_pane_chains` = {P3-m2's chain}; its scan reads one pane (4M samples,
//    128 MB of raw data on disk instead of 640 MB), and it holds 4 of 5 panes
//    (71,072,000,000 bytes) between evaluations: 8,884.53 cost/s, 31.8 ms per evaluation.
let run = run_planning_stages_over(roots, &entries, &data, None,
                                   &PlanSelectionSettings::default(), &deployment_capabilities)?;
// `selection`: Stage 3's result: the selected id, each valid candidate's cost, the rejections.
let selection = run.stage3_plan_selection?;

// 4. What the user gets back. The whole-window KLL is still cheapest under cost model 1:
//    P2, 2.100 cost/s; P3-m2 is valid, "costlier: 8884.530 vs 2.100", and P3-m1 (B1) is
//    13,326.530. The document's `deployment_capabilities` echoes the inputs above.
assert_eq!(selection.selected, "P2");
//    With a p50 panel added beside the p99 (both every minute, 191 physical candidates),
//    the 1-min KLL chain has two readers and Stage 2 offers both query-time options:
//    shared within one evaluation (P29-m2, 26.55 cost/s) and kept (P29-m3, 8,884.65 cost/s).
//    Stage 3 prices both; the kept one is not selected.
//    With --deployment-capabilities '{"raw_data_retained": false}' instead, P1, P2 and P3
//    are invalid ("scans 300000 ms of raw latency_ms at query time, but the deployment
//    does not retain raw data"), and B1 (P3-m1, 13,326.53 cost/s) is selected.

Before this PR

  • The planner offered every summary family it could build, whatever the executor supports. The only support hook was the legacy CostModel::summary_support_evidence, which the stage pipeline never read.
  • Raw-data retention was always assumed, and nothing was checked.
  • MatAtQuery was offered only for a build shared by two or more queries, and it was reused within one evaluation. Example 4's B3 tests were #[ignore]d.

After this PR

  • A deployment states its capabilities once (PlanningModels::with_deployment_capabilities, or stage_pipeline --deployment-capabilities), and the stage document shows them and every Pass 1 pruning with its reason.
  • Unsupported summaries are never offered.
  • A deployment that keeps no raw data gets only plans whose raw-data reads happen at ingestion time, and stored data with no raw data retained is refused.
  • A deployment that keeps query-time results gets B3 priced, beside (not instead of) the option shared within one evaluation.
Pattern B (3b) default deployment query_time_results_retained: true raw_data_retained: false
physical candidates 7 9 (P3-m2 and P5-m2 are B3) 7
B2 rebuilt (P3) 2.183/s 2.183/s invalid: scans 300000 ms of raw data
B1 at ingestion (P3-m1) 13,326.530/s 13,326.530/s 13,326.530/s, selected
B3 kept (P3-m2) not offered 8,884.530/s, 71.1 GB held, 128 MB raw on disk —
selected P2, 2.100/s P2, 2.100/s P3-m1
Pattern B + p50, retention on before this fix now
2-reader KLL chain shared within one evaluation not offered (priced as kept) P29-m2, 26.55/s
2-reader KLL chain kept across evaluations P29-m2, 8,884.65/s P29-m3, 8,884.65/s

With the default deployment, nothing changes:

  • Example 1's committed fixture gains only deployment_capabilities and an empty pass1_pruned.
  • Examples 1, 2, 3a and 3b still select P1 at 7.94021/s, P1 at 6.0800009/s, P485 at 39,828.80/s and P2 at 2.100/s; the example tests that pin selections and costs pass. Example 2's pass1_pruned is not empty: it lists the pruning that already happened on main (Cms, CountSketch and UnivMon pruned with RequiresExact for its Q2 and Q3 targets).
  • Removing the per-node usage changes no output: the four example documents are byte-identical before and after that refactor.

Verification

  • New and changed tests:
    • Each restriction removes exactly the right candidates: a_family_the_deployment_cannot_build_is_pruned, a_readout_the_deployment_cannot_answer_prunes_its_family, a_fused_computation_loses_its_unsupported_heaps (now also checks the fused pruned reasons), and restricting_summaries_removes_exactly_the_candidates_that_use_them (Examples 1, 2 and Pattern B: the restricted Stage 1 candidates equal, in order, the unrestricted ones whose every family and readout is supported).
    • Both query-time options: query_time_results_retained_still_offers_the_chain_shared_within_one_evaluation (the Pattern B + p50 workload; it failed before the fix with only the kept option offered).
    • Phases: readers_at_different_phases_do_not_build_one_pane_per_evaluation (failed before the fix on phases 0 and 30 s).
    • Raw data: without_raw_data_every_query_time_scan_is_listed (failed before: only the first scan was listed), stored_data_without_raw_data_retained_is_bad_input (failed before: AtRest was accepted), without_raw_data_only_ingestion_time_scans_are_valid.
    • B3 pricing: query_time_results_retained_keeps_a_pane_chain_across_evaluations (now also checks that on disk the kept chain charges N − 1 panes; that check failed before the fix).
    • Usage accumulation: node_prices_add_up_to_the_candidates_usage replaces the test that summed node usages; state_on_disk_is_counted_as_disk_not_memory and partial_copies_of_the_newest_window_add_to_peak_memory now check the candidate's usage.
    • CLI: deployment_capabilities_flag_sets_the_capabilities_and_shows_them (flag, document key, pass1_pruned, raw-data rejection, unknown field refused).
  • Example 4: stage2_..._b_tumbling_kll_has_three_materialization_options and stage2_..._b_b3_keeps_query_time_windows run (no longer ignored). stage3_plan_selection_b_rebuilding_every_window_costs_most stays ignored with the numbers in its reason: the built-in model makes B2 (2.183/s) cheaper than B3 (8,884.5/s) and B1 (13,326.5/s).
  • Commands run on this head:
    • cargo nextest run -p asap-aware-mapping --lib filtered to Stage 2, Stage 3, deployment and Pass 1 tests: 118 passed; -p asap-aware-mapping --test per_computation_candidates: 29 passed.
    • cargo nextest run -p asap-integration-tests --test deployment_capabilities --test planner_layering_example1 --test planner_layering_example2 --test planner_layering_example3 --test planner_layering_example4 --test reuse_rules and -p asap-devtools --test stage_pipeline: all passed.
    • cargo fmt --all -- --check clean; cargo clippy -p asap-aware-mapping -p asap-devtools -p asap-integration-tests -p asap-types --all-targets --all-features --locked -- -D warnings clean.
    • Not run: the full workspace suite, workspace clippy and the external-consumer script.

Limitations and follow-ups

  • The IR now records which pane chains are kept across evaluations (MatAtQueryKeptAcrossEvaluations), but no executor in this repo keeps panes across evaluations yet. The kept option is planned and priced only.

  • The executor does not declare its own capability set yet. Planning defaults to "everything", so a deployment must pass --deployment-capabilities or with_deployment_capabilities.

  • B3 is priced in steady state; cold start, before the first N panes exist, is not.

  • Readers with no known phase are assumed co-scheduled with the others.

  • The DAG viewer does not render deployment_capabilities or pass1_pruned yet.

  • Raw retention is one global bool, with "not retained" meaning nothing is kept.

DeploymentCapabilities states which sketch families and readouts the
executor supports, whether raw data is retained and whether a query-time
result may be kept across evaluations. It sits in PlanningModels, and
stage_pipeline reads it from --deployment <json> and shows it read-only
under the document's top-level deployment.

- Stage 1: Pass 1 offers no unsupported summary
  (PruneReason::UnsupportedByDeployment); Pass 2 adds none.
- Stage 2: with query_time_retention, a tumbling-pane chain whose readers
  repeat at its pane width is also offered MatAtQuery (Example 4's B3).
- Stage 3: such a chain is priced as kept across evaluations (one pane
  scanned and built per evaluation, N - 1 panes held); without raw data
  retained, a query-time scan makes a candidate invalid.
- Remove CostModel::summary_support_evidence, replaced by the supported
  summaries.
- Example 4's B3 tests run with query-time retention and are no longer
  ignored.

The default deployment plans exactly as before.
…; surface Pass 1 pruning

- Materialization gains MatAtQueryKeptAcrossEvaluations. With
  query_time_results_retained, Stage 2 offers it beside MatAtQuery
  (shared within one evaluation) and Stage 3 prices each, so a chain two
  panels read is no longer always priced as kept.
- builds_one_pane_per_evaluation requires equal known evaluation phases.
- Stored data with raw_data_retained false is bad input
  (DeploymentCapabilities::validate); the raw-data rejection lists every
  query-time scan.
- Rename query_time_retention to query_time_results_retained and
  PlanningModels::deployment to deployment_capabilities (with
  with_deployment_capabilities, --deployment-capabilities and the
  document key).
- stage_pipeline shows Pass 1 pruned candidates per target
  (pass1_pruned); FusedComputation records its pruned candidates.
- Remove NodeCost.usage, NodeCost.per_second and NodeUsage; price()
  accumulates the candidate's ResourceUsage directly. The node map and
  kept chains are built once per candidate.
- The kept arm charges N - 1 panes on disk; drop StatisticKind's Hash.
@zzylol zzylol added binding Logical-to-physical binding and implementation selection cost-model Cost formulas, statistics, ranking, and selection enhancement New feature or request optimizer Workload optimization and plan selection labels Oct 16, 2026
@zzylol
zzylol merged commit 66533b3 into main Oct 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

binding Logical-to-physical binding and implementation selection cost-model Cost formulas, statistics, ranking, and selection 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