Skip to content

perf(plan-selection): accuracy pair checks reuse built outputs; shared input contexts measured (#1095 §8) - #1156

Merged
zzylol merged 2 commits into
feat/1095-group-milpfrom
feat/1095-shared-input-contexts
Oct 31, 2026
Merged

zzylol merged 2 commits into
feat/1095-group-milpfrom
feat/1095-shared-input-contexts

Conversation

@zzylol

@zzylol zzylol commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Why

#1095 §8's first open point: the m contexts are 32–63 % of the prototype's variables, and "candidates of one operator node (e.g. MatAtQuery(Memory) and MatAtQuery(Disk)) could share one input context". #1155 recommended trying this next, because rangeheavy-t8-t5's largest round (114,225 variables, 63 % in shareable contexts) spends 44–52 s per build, in four or more rounds.

This PR measures first. The measurements show that §8's sharing has nothing to share on the §6.1 workloads, and that the 44 s build is not model building: 96 % of it goes to the accuracy pair checks in groups/accuracy_positions.rs. So this PR fixes the pair checks and writes the shared-context result down as data for #1095 §8. It does not implement shared contexts.

What

  • Step 1, breakdown (no code in the branch). Below: the size of each context kind, the operator nodes that have two or more shareable candidates, and the build split into phases. I measured with a temporary debug build that is not in the branch.
  • Exactness argument for shared input contexts, as a [Feature] Stage 3: no execution-scope copies, exact candidate-level MILP, one cost formula #1095 §3 change proposal. The proposal is "no change for now": no workload has a set of candidates the exact form could share.
  • Build fix. The accuracy checks keep what they build across checks and queries. Weights and excluded pairs are unchanged. On t8-t5's 114,225-variable round the build drops from 44.3 s to 7.6 s.

How

A candidate's outputs in a check depend only on the members of the approximate set that lie at or below it. Checker::built reaches its producers only through reaches, which reads below[candidate] ∩ S, and below[producer] ⊆ below[candidate]. So the outputs are stored per (candidate, members at or below it) in BuiltBefore, which lives for the whole accuracy_positions call, together with the reader_key table. A pair check then builds only the candidates above both members and reuses everything else from the single checks. Two exceptions:

  • A result built across a cycle of links (the walk hit a candidate it was still building) depends on where the walk entered the cycle. Such a result stays in the check's own Walk and is not kept.
  • At the top of a check, among a reference's output candidates that reach no member, only the first exact stand-in that is not a summary state is built. The later ones have the same reader_key ("exact"), so distinct_for_readers would drop them anyway. This turns each pair check's scan over every output candidate (about 560 on t8-t5) from hash lookups into a bit test.

Key code interfaces added / modified

None public. Private to crates/plan-selection/src/groups/accuracy_positions.rs:

/// What the checks built, kept across checks and queries (new type).
struct BuiltBefore {
    /// Per (candidate, approximate members at or below it, ascending): its outputs. // new
    outputs: HashMap<(usize, Vec<usize>), Built>,
    /// The reader keys of every node built so far (was one table per check). // new
    keys: Keys,
}

/// One check's walk (new type; replaces the per-check `memo`, `keys` and `visiting` arguments).
struct Walk {
    /// Per candidate built in this check. // new
    built: HashMap<usize, BuiltInWalk>,
    /// The candidates being built (was the `visiting` argument). // new
    visiting: Vec<usize>,
    /// How many reads of a candidate still being built the walk has made. // new
    cycle_reads: usize,
}

/// A candidate's outputs in one check, and whether they were built across a cycle of links (new type).
struct BuiltInWalk {
    /// Its outputs. // new
    outputs: Built,
    /// Built across a cycle: not kept in `BuiltBefore`. // new
    across_cycle: bool,
}

impl Checker<'_> {
    /// `before` holds what earlier checks built, for every query.
    fn check(&self, approximate: &[usize], before: &mut BuiltBefore) -> Outcome; // was check(&self, approximate)
    fn built(&self, class: usize, approximate: &[usize], walk: &mut Walk, before: &mut BuiltBefore) -> Built;
    // was built(&self, class, approximate, memo: &mut HashMap<usize, Built>, keys: &mut Keys, visiting: &mut Vec<usize>)
}

/// Tests only: check with no kept builds (the store cleared before each check) and no skipped stand-in. // new
#[cfg(test)]
pub(crate) static FRESH_BUILDS: Cell<bool>; // thread_local

Usage (the §6.1 devtool run; nothing changes for callers):

cargo build --release -p asap-devtools --bin stage_pipeline
# Stage 3 on the new search. prepare_milp_inputs_with → accuracy_positions runs once per pane-width round,
# before build_selection_milp; ASAP_STAGE3_REPORT gets one JSON line per round.
ASAP_STAGE3_SEARCH=milp ASAP_STAGE3_REPORT=t8-t5.report \
  target/release/stage_pipeline --workload tools/dag-viewer/memo-examples/promql/rangeheavy-t8-t5-1h.json \
  --cost-model aws-fargate-us-east-1 --enforce-accuracy-guarantees --milp-time-limit-s 300 --no-memo \
  --deployment-capabilities '{"raw_data_retained": true, "query_time_results_retained": false}' --out t8-t5.json
# t8-t5.report, 114,225-variable rounds: "build_ms": 7556 (was 44257)
# t8-t5.json: the selected plan (round 1), cost 6.4822e-6 per second, plan_cost = objective, as before.

Step 1: where the size and the build time go

Measured on #1155's head with a temporary debug build that dumps each shareable context's owner (operator node, timing, materialization, producers, options) and times each build phase.

Context sizes. "Options" counts the p variables of a context.

t8-t5, largest round (114,225 vars) workload-01, largest round (39,907 vars)
query contexts 3; 16,845 options; scope: 30,045 vars 9; 5,862 options; 19,787 vars
I 1 option 8 options
shareable contexts 4,388 contexts; 22,730 options; scope: 71,676 vars / 27,118 binaries / 160,705 rows 1,219; 4,861 options; 18,932 vars
… query-time (all MatAtQuery(Memory)) 3,668 contexts, 22,010 options (9,565 computed there) 492 contexts, 3,844 options
… ingestion-time (MatAtIngestion(Memory) pane chains) 720 contexts, 720 options (boundary reads only) 727 contexts, 1,017 options
operator nodes with ≥ 2 shareable candidates 720, every one {PaneChain MatAtIngestion(Memory), PaneChain MatAtQuery(Memory)}: 1,440 contexts, 2,160 options 197, the same pair: 394 contexts, 796 options
… of which have the same timing (shareable under §8) 0 0
p removable by §8's sharing 0 0
for comparison: query-time contexts with the same producer list but different operator nodes 2,756 of 22,010 options (12.5 %) 564 of 3,844 (14.7 %)

The other five §6.1 workloads look the same. Every operator node with two shareable candidates is a pane chain at ingestion time plus at query time. example1 and ratio-of-quantiles have none. Stage 2 emits no Disk medium and no pair of MatAtQuery and MatAtQueryKeptAcrossEvaluations on one node in these workloads.

What the shareable contexts are. On t8-t5's 39,505-variable round, the owners of the 1,268 query-time contexts are Project (344), SummaryEstimate (344), SummaryMerge (320), PaneChain (240) and a few others. They form nested chains such as Project → SummaryEstimate → SummaryAgg → TimeRange → Scan. A MatAtQuery candidate of each level gets its own context, and that context holds a copy of the NotMaterialized stack below it (2–9 options each). The copies belong to different operator nodes, each with its own result. Sharing within one operator node, as §8 proposes, cannot remove them.

Build phases (t8-t5, 114,225 variables, base):

Phase ms
candidate links, contexts, candidate_data 115
dominance, contexts 201
accuracy_positions 42,453 (96 %)
choice_variables / structural_rows / acyclic rows 18 / 45 / 10
reach_rows / reader_constraint_rows / latency_rows / accuracy_rows / encode_objective 336 / 110 / 30 / 12 / 29
total build_ms 44,228

On the 80,601-variable round, accuracy_positions ran 1,212 single checks (0.43 s) and 28,724 pair checks (15.8 s). Those checks built 16.5 M candidate outputs between them, and only 1.1 s of the 15.8 s was guarantee_check. Each pair check rebuilt every candidate from scratch and looked up all ~560 output candidates of the query. The number of pairs grows with the square of the pane-chain count, so the build grows faster than the model: 0.03 s at 4k variables, 3 s at 40k, 17 s at 81k, 44 s at 114k. The model building itself (choice_variables through encode_objective) takes 0.6 s at 114k. No phase there is worth fixing now.

Exactness argument for shared input contexts (#1095 §3 change proposal)

Suppose shareable candidates m1 and m2 of one operator node share one input context S. Both are read across a boundary, and a reader context picks at most one of them because they fill one slot.

  1. Same choices. Sharing is exact only if S's options and the producers of each input are the same for both. That needs the same timing and the same compat per input (producer_candidates_per_input). Contexts of different timings compute different things. A query-time context computes NotMaterialized query-time candidates. An ingestion-time context computes nothing and only reads I and other shareable candidates across its boundary. So an ingestion-time and a query-time pane chain can never share.
  2. Can both be selected in an optimal plan? Yes. Take m1 = MatAtQuery(Memory) and m2 = MatAtQuery(Disk), with readers R1 and R2, where R1's compat or latency bound admits only m1, and m2 has the lower per-execution cost. When the schedule-group rates add (different intervals), selecting both costs r1(E + e1) + r2(E + e2). Moving R2 onto m1 costs (r1 + r2)(E + e1), which is higher by r2(e1 − e2). With different readers, or different held time (H[m] follows the slowest reader), both copies can be selected in an optimal plan.
  3. Does a shared context undercount? If both are selected, plan_cost charges two copies of the input subtree, one at each candidate's rate. The current charge (context runs once, at the owner's rate) would charge one. Decode would also produce one input subtree read by two materialized nodes, which is a different plan.
  4. Options.
    • (a) Share only among candidates with the same timing, producers and rate source (MatAtQuery Memory vs Disk of one node), and add Σ_{m ∈ set} x[m] ≤ 1. Then context activity is Σ x[m], reach and rates read the selected owner, and decode is unchanged. This loses exactly the plans in point 2, where both media serve disjoint readers. That is an O5-style loss: one shared result per operator node, with the second copy useful only when readers differ in compat, latency or per-execution cost. Unlike O5's case, this one differs in cost, so O5's cost-model-1 argument does not cover it, and it would have to be listed as a fourth O4 class.
    • (b) Exact: charge S's runs once per selected owner. Use V_i ≥ E[S] − Ē (x[m_i] − z_i) per owner i and rate component, also for owners at a fixed rate. Keep a separate z per owner for its own run, and decode S into one copy per selected owner. This touches rows 3–6, row 7 (L[m_i] per owner over one context), y, W/H, the G4 ranks, the warm start, completion and decode.
  5. Decision. The simplest exact design that would apply is (b), and (a) is the simplest lossy one. On every §6.1 workload the sets (a) and (b) would share are empty (step 1): no operator node has two shareable candidates of one timing. Either one would be dead code today, so this PR implements neither. Proposed §8 text: "Shared input contexts within one operator node: measured, nothing to share (the only nodes with two shareable candidates are pane chains at ingestion and at query time, which cannot share a context). Revisit if Stage 2 emits several media or keep flags per node; then use (b), or (a) with its loss listed under O4. The size of the m contexts comes from nested MatAtQuery candidates of different nodes, each copying the NotMaterialized stack below it (Project → SummaryEstimate → SummaryAgg → TimeRange → Scan). That is a 3b or 3c question, not a sharing one."

Major algorithm changes

Accuracy pair checks reuse built outputs (accuracy_positions, Checker::check, Checker::built).

  • Problem: on rangeheavy-t8-t5's 114,225-variable round, accuracy_positions takes 42.5 s of the 44.2 s build, and the build repeats in every 114k round.
  • Before: per query, per check (each operator node's group, then each pair that meets): a fresh memo and keys. Build every output candidate of every reference, depth first, and rebuild every candidate that reaches a member. Then distinct_for_readers, the roots, and guarantee_check.
  • After: 1. One BuiltBefore per accuracy_positions call. 2. In built: a candidate that reaches no member returns its exact stand-in right away. Otherwise, look up (candidate, members at or below it) in BuiltBefore; on a miss, build it as before and store the result unless the walk read a candidate it was still building. 3. Per reference, skip the exact non-state stand-ins after the first one. 4. The roots and guarantee_check are unchanged.
  • Correctness: built is a function of (candidate, S ∩ below[candidate]) when no cycle is involved. Results that involve a cycle stay per check, as before. Stand-ins and outputs keep their addresses for the whole call because Keys holds them, so reader_key gives the same values. A skipped stand-in has the key distinct_for_readers would have dropped. Test kept_builds_give_the_weights_of_fresh_builds checks weights and excluded pairs against checks with no kept builds and no skipped stand-in (FRESH_BUILDS). It covers t8-t5's shape (a quotient plus both operands), a quotient of three quantiles (which has excluded pairs), and a scalar ratio of two output references whose approximate values sit under one operand only. The last one fails if the skip also drops the first stand-in: the ratio then has no root to check, and every approximate candidate would weigh nothing. Test outputs_built_across_a_cycle_are_not_kept builds a synthetic two-candidate link cycle above an approximate member, for one member and for a pair. It checks that the output cut off by the cycle read is not kept, and that a later walk from the cycle's other candidate equals a build with no kept builds. Mutations that skip every stand-in, panic on a cycle read, or keep outputs built across a cycle each fail one of these tests. On all seven workloads every round's plan and objective is unchanged (After).
  • Complexity: pair check time goes from O(candidates reaching a member + output candidates), with hashing per node, to O(candidates above both members) plus a bit test per output candidate. The pair count is unchanged. Measured in separate instrumented runs on t8-t5 (20 s budget). At 80,601 variables, pair checks took 15.8 s on the base, 9.0 s with the store alone, and 4.5 s once stand-ins also skip the store. At 54,449 variables, with all of this PR, pair checks went from 5.9 s to 0.85 s and the build from 6.5 s to 1.36 s. Memory: the store adds about 130 MB at 80k variables (peak RSS 226 → 359 MB in runs with a 20 s budget).
  • Store lifetime (decision): the store lives for the whole accuracy_positions call and is shared by every query, because candidates below several queries are reused across them. It is not cleared per query and its size is not reported in ASAP_STAGE3_REPORT. It grows with the number of pair checks, about 130 MB at 80k variables, and is dropped before the MILP is built. If larger workloads make it matter, clearing it per query is a one-line change.
  • Limits: the remaining 7.6 s at 114k is still mostly pair checks (their guarantee_check and the per-check scan), and their number is quadratic. Pairs are not deduplicated by guarantee.

Before this PR

#1155's table: t8-t5's largest round (114,225 variables) builds in 44–52 s. In the 300 s budget that leaves each 114k round about 0.5 s of solve and no dual bound. The incumbent stays 2.0661e-5, and round 1's plan (6.4822e-6) is selected.

After this PR

Same host, same §6.1 settings (budget 300 s, accuracy enforced, aws-fargate-us-east-1, raw data retained, workload-01 under ulimit -v 8388608). Base = #1155's head, then this branch, run one after the other. Rows show the selected round, and the largest round where it differs. "Build" is build_ms (input preparation plus model build, without the warm start).

Workload Vars / binaries Build (before → after) Solve (before → after) Status, gap (before → after) Cost per second (before → after) plan_cost = objective Rounds; total build Wall Peak RSS
example1 501 / 323 0.07 → 0.07 s 0.03 → 0.03 s optimal → optimal 4.3770e+00 → 4.3770e+00 yes 14; 1.0 s → 14; 1.0 s 2 → 2 s 54 → 54 MB
rangeheavy-t5-q05-q09 6,982 / 3,908 0.15 → 0.09 s 1.63 → 1.66 s optimal → optimal 3.9901e-06 → 3.9901e-06 yes 16; 1.7 s → 16; 1.1 s 38 → 38 s 119 → 119 MB
rangeheavy-t7-t4 4,490 / 2,627 0.03 → 0.03 s 2.49 → 2.52 s optimal → optimal 2.1493e-07 → 2.1493e-07 yes 16; 0.3 s → 16; 0.3 s 29 → 29 s 105 → 105 MB
rangeheavy-t8-t5 4,013 / 1,749 0.03 → 0.02 s 1.46 → 1.46 s optimal → optimal 6.4822e-06 → 6.4822e-06 yes 12; 311.3 s → 16; 84.5 s 330 → 301 s 301 → 1580 MB
  largest round (5 → 9×) 114,225 / 50,601 44.3, 44.4, 44.4, 44.3, 44.2 → 7.6, 7.6, 7.6, 7.6, 7.5, 7.5, 7.5, 7.5, 7.6 s 0.54 → 145.86 s deadline, no dual bound → deadline, 78.7 % incumbent 2.0661e-05 → 6.4825e-06
ratio-of-quantiles 52 / 29 0.00 → 0.00 s 0.05 → 0.05 s optimal → optimal 2.4071e-06 → 2.4071e-06 yes 16; 0.0 s → 16; 0.0 s 1 → 1 s 35 → 34 MB
rollupheavy-t11 8,788 / 4,990 0.17 → 0.16 s 3.69 → 3.67 s optimal → optimal 1.4159e-06 → 1.4159e-06 yes 16; 1.8 s → 16; 1.8 s 62 → 63 s 145 → 145 MB
workload-01 39,907 / 13,147 0.25 → 0.27 s 191.51 → 194.56 s optimal → optimal 1.4171e-06 → 1.4171e-06 yes 15; 2.9 s → 15; 3.0 s 292 → 294 s 896 → 898 MB
  • Every cost equals the base, and plan_cost = objective everywhere. Variables, binaries and the selected round are unchanged on all seven workloads. The weights and excluded pairs, and so the models, are identical.
  • t8-t5: each 114,225-variable round builds in 7.5–7.6 s instead of 44.2–44.4 s. The rounds' total build time falls from 311 s to 85 s, and the facade gets through 16 rounds instead of 12 in the same budget. The last 114k round now gets 146 s of HiGHS. It improves that round's incumbent from 2.0661e-5 to 6.4825e-6 and gives the round's first dual bound (1.3817e-6, gap 78.7 %). Round 1's optimal 6.4822e-6 is still selected.
  • Peak RSS on t8-t5 rises from 301 MB to 1,580 MB. Most of that comes from HiGHS's branch and bound on that 114k round, which the base never reached (0.5 s solves). The accuracy store accounts for about 130 MB (measured above). Still far below the 8 GB limit.
  • rangeheavy-t5-q05-q09's rounds build in 1.1 s instead of 1.7 s in total. Every other workload is unchanged: accuracy_positions is tens of ms there.

Verification

  • cargo test -p asap-plan-selection --lib: 316 passed, including the new groups::tests::kept_builds_give_the_weights_of_fresh_builds and groups::accuracy_positions::tests::outputs_built_across_a_cycle_are_not_kept. The three mutations above each fail them.
  • cargo test -p asap-devtools --test devtools stage_pipeline: 51 passed. An earlier run that overlapped a clippy build on a full disk failed scans_projecting_different_labels_are_shared once; it passed alone and in this clean full run.
  • cargo fmt --all -- --check: clean. cargo clippy -p asap-plan-selection --all-targets -- -D warnings: clean.
  • The follow-up commit (tests, &mut BuiltBefore, named walk entry) changes no production behaviour, so the measurements in "After this PR" were not repeated.
  • Measurements: base and branch stage_pipeline binaries on the same host, all seven workloads. The step-1 breakdown and phase timings come from a temporary debug build (context dump and phase timers) that is not in the branch.

🤖 Generated with Claude Code

…ks (#1095 §8)

The selection MILP's input preparation spent 42–44 s of the 44–46 s
build of rangeheavy-t8-t5's 114,225-variable round in accuracy pair
checks: each of ~30k pair checks rebuilt every candidate from scratch and
walked every output candidate of the query. A candidate's outputs depend
only on the approximate members at or below it, so they are now kept
across checks and queries (except outputs built across a cycle of
links), and repeated exact stand-ins among a reference's output
candidates are skipped where `distinct_for_readers` would drop them.
Weights and excluded pairs are unchanged (new test against fresh builds).
@zzylol zzylol added enhancement New feature or request optimizer Workload optimization and plan selection search Candidate-plan search space and algorithms labels Oct 31, 2026
…f kept accuracy builds (#1095)

- `FRESH_BUILDS` now checks with no kept builds and no skipped stand-in,
  so the comparison test also covers the skip; a scalar ratio whose
  approximate values sit under one operand only fails if the first exact
  stand-in of the other reference is skipped.
- A synthetic two-candidate link cycle checks that outputs built across
  the cycle are not kept, for one member and for a pair.
- `check` takes `&mut BuiltBefore` (no `RefCell`); the walk's
  per-candidate entry is a named struct.
@zzylol
zzylol merged commit 574dbee into feat/1095-group-milp Oct 31, 2026
1 of 2 checks passed
zzylol added a commit that referenced this pull request Nov 2, 2026
…ries (#1095 §8) (#1156)

Measured that candidates of one operator node never share an input context on the measured workloads (no same-timing pairs), so the §8 shared-input-context step has nothing to share. The 44 s build of rangeheavy-t8-t5 largest round was 96% accuracy checks; a candidate built outputs are now kept across pair checks and queries (except outputs built across a cycle of links), and duplicate exact stand-ins are skipped. Weights, excluded pairs, plans and costs unchanged; that round builds in 7.6 s.
zzylol added a commit that referenced this pull request Nov 2, 2026
…ries (#1095 §8) (#1156)

Measured that candidates of one operator node never share an input context on the measured workloads (no same-timing pairs), so the §8 shared-input-context step has nothing to share. The 44 s build of rangeheavy-t8-t5 largest round was 96% accuracy checks; a candidate built outputs are now kept across pair checks and queries (except outputs built across a cycle of links), and duplicate exact stand-ins are skipped. Weights, excluded pairs, plans and costs unchanged; that round builds in 7.6 s.
zzylol added a commit that referenced this pull request Nov 2, 2026
… Stage 3's default search (#1095) (#1144)

* feat(plan-selection): group selection MILP over execution contexts behind ASAP_STAGE3_SEARCH=milp (#1095)

Step 1 of #1095's solving strategy: one MILP with a warm start. The
owner-model pieces from tag stage3-owner-milp-redesign are restored and
the owner/instance model is replaced by execution contexts: each query
evaluation, the ingestion pipeline I (NotMaterialized ingestion-time
candidates) and each shareable (materialized) candidate's own inputs.

- groups.rs (was owners.rs): RunKind, ResultSlot (group roots split by
  self-read depth; private nodes their own slot), execution contexts and
  their options; dominance, per-candidate usage and accuracy weights kept.
  Scan windows are costed at every union of the window kinds read within
  one context, as plan_cost prices them.
- milp/model.rs: p[ctx, c], x[m], a[q, m], rows 1-9, slot links for
  inputs over several result slots, order ranks within and across
  contexts, latency in scaled units.
- milp/cost_terms.rs: run terms (plain, producer-dependent, exact scan
  window sets), schedule-group rates, held memory, raw retention and
  pane retention levels.
- milp/warm_start.rs: each query's cheapest plan, then greedy sharing,
  costed exactly by the completion.
- milp/decode.rs: one node per (context, computed candidate) and per
  selected shareable candidate.
- asap-cost: a NotMaterialized ingestion-time node runs once per second
  for every consumer; a NotMaterialized node a materialized node computes
  is held until that node's slowest reader finishes.

The old engine stays the default.

* fix(plan-selection): a shareable candidate reads a scan at its own window; window-independent scans keep their usage per window (#1095)

* fix(plan-selection): adopt main's Stage 1 exports and cost_inputs name after rebasing onto the Stages 0–2 milestone (#1095)

* feat(plan-selection): selection MILP solved per independent part, LP gap diagnosis, tighter big-M bounds (#1095 §5 3a, §6.1) (#1155)

Each connected component of the selection MILP is solved alone and joined before the single decode, realization and plan_cost check. An lp-diagnosis feature (devtools only) attributes the LP gap by row and variable family. Two big-M bounds are tightened. No selected plan or cost changes.

* perf(plan-selection): keep accuracy builds across pair checks and queries (#1095 §8) (#1156)

Measured that candidates of one operator node never share an input context on the measured workloads (no same-timing pairs), so the §8 shared-input-context step has nothing to share. The 44 s build of rangeheavy-t8-t5 largest round was 96% accuracy checks; a candidate built outputs are now kept across pair checks and queries (except outputs built across a cycle of links), and duplicate exact stand-ins are skipped. Weights, excluded pairs, plans and costs unchanged; that round builds in 7.6 s.

* feat(plan-selection): one ingestion context per refresh window in the selection MILP (#1095) (#1157)

Brings feat/1095-stage3 (#1151 refresh groups and cost rule) into the prototype and drops its once-per-second ingestion hook. The MILP has one ingestion context I_r per refresh window (unbounded views alone), so a shared stateless ingestion node is charged once per refresh window, as asap-cost charges it. workload-01 costs 1.6% more: its old plan under-charged a scan read by chains of three refresh windows. Part of #1095.

* fix(plan-selection): use the tumbling-window names (#1163) in the group MILP after rebasing onto main

The group MILP commits were written before #1163 renamed PaneChain to
SummaryTumblingWindowChain and pane_width_ms / pane_count / newest_panes /
pane_retention_ms to their tumbling-window names; rename their uses, and
rustfmt the lines the rebase resolved.

* feat(plan-selection): the group MILP is Stage 3's default search (#1095 C6)

select_plan runs the group MILP unless ASAP_STAGE3_SEARCH=old. Inputs the
MILP does not price yet (cost models 2 and 3, or a measured operation cost
table without a frozen calibration factor) are an UnsupportedCostModel
error; there is no fallback to the old engine.

* fix(plan-selection): use the remaining tumbling-window names (#1163 part 2) in the group MILP

#1176 renamed the remaining pane helpers (pane_candidates, kept_pane_chains,
panes_of, retained_panes, the pane test helper, ...); rename their uses in
the group MILP's code and tests.
@zzylol
zzylol deleted the feat/1095-shared-input-contexts branch November 3, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request optimizer Workload optimization and plan selection search Candidate-plan search space and algorithms

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant