feat(asap-aware-mapping): make CSE costing recurrence-aware - #295
Merged
Conversation
…ting (#287) Add a generic recurrence-aware cost context that carries RepeatingEntry intervals and DataCharacteristics-derived ingest rates into the CSE share-vs-recompute decision, without adding scheduling or a runtime execution loop. New `recurrence` module: - UpdateRate/EvaluationRate/CostRate/Horizon: distinct newtypes so a steady-state cost rate (cost units/second) and a one-shot Cost can never be combined except explicitly via `total_cost(rate, horizon, one_shot)` -- enforced by the type system (no `impl Add<Cost> for CostRate`). - `evaluation_rate_of`: sum(1/interval_i) over repeating consumers' intervals, validating zero/invalid intervals. - `update_rate_from_data_characteristics`: series_count * samples_per_sec_per_series proxy. - `RecurrenceProfile`: aggregated per-target evaluation_rate, one_shot_consumers, update_rate; `is_empty()` is the "no metadata" case that preserves pre-#287 behavior exactly. - `RecurrenceCostExplanation`: selected alternative, both compared cost rates, every input, units, and provenance, for a downstream consumer (e.g. issue #286's DAG-viewer annotations). CostModel trait (cost_model.rs): - New hooks `maintenance_cost_per_update`/`summary_read_cost`/ `raw_recompute_cost`, all with defaults delegating to existing hooks. - `cse_share_decision_with_recurrence`: the recurrence-aware Share/RecomputeIndependently decision. Falls back to `cse_share_decision` (structural consumer_count) when `RecurrenceProfile::is_empty()`; otherwise compares maintained_cost_rate vs recompute_cost_rate, requiring an explicit Horizon whenever one-shot and repeating work are mixed (RecurrenceError::MissingHorizon otherwise). PlanSpace (replacement.rs): - `PlanSpace::recurrence_profiles`: walks every root's reachable sub-DAG (mirroring discover_targets' own traversal) and folds each root's RootRecurrence (Repeating(interval) or OneShot) into every site reachable from it -- so a summary shared by consumers with different intervals gets one profile combining all of them. Keeps `Id` fully opaque (positional, no Eq/Hash/Clone bound needed). Tests cover: mixed intervals, a pure one-shot consumer (with and without an explicit horizon), multiple roots sharing a sub-DAG via a real PlanSpace, invalid/zero intervals, CostRate/Cost unit consistency, a deterministic CostModel showing high evaluation frequency selects Share and low frequency selects RecomputeIndependently, and that update_rate affects only maintained_cost_rate while evaluation_rate affects both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses a code review of PR #295 that found real correctness bugs in the recurrence-aware cost context. All fixes on the same branch. 1. Batch-only workloads no longer silently prefer Share (recurrence.rs): `decide` now charges a one-time `summary_build_cost` (new CostModel hook, defaults to `raw_recompute_cost`) whenever a maintained total is computed, not just `summary_read_cost * one_shot_consumers`. A single one-shot consumer now strictly prefers RecomputeIndependently (build + one read costs more than one direct recompute); many one-shot consumers still amortize the build cost correctly. 2. `update_rate` no longer stamped on sites unreachable from any root (replacement.rs): `PlanSpace::recurrence_profiles` now tracks which sites its BFS actually reaches from a real root and only attaches the caller-supplied `update_rate` to those — a site only ever produced by a Replacement::Rewrite candidate (e.g. AvgToSumOverCountStrategy's invented sum/count sub-DAG) now falls back to RecurrenceProfile::EMPTY instead of getting an ingest-driven maintenance cost charged against a zero evaluation signal. 3. Fixed `DefaultCostModel::maintenance_cost_per_update`'s unit mismatch (cost_model.rs): it no longer reuses `cse_shared_maintenance_cost`'s life-of-the-workload weight (~1-6) as a per-update-event rate multiplier, which made maintained_cost_rate blow up at any realistic update rate. Default is now a small nominal `Cost(0.01)`, independent of that table. 4. `RecurrenceProfileMap` now holds owned `Rc<QueryExpr>` clones alongside each profile (replacement.rs), not just raw pointers, so it's safe to outlive the `PlanSpace` it was built from — no more risk of a stale profile matching an unrelated node whose allocation reused a freed address. 5. `UpdateRate` is now validated (recurrence.rs): new `validate_update_rate`/`RecurrenceError::InvalidUpdateRate`, applied in `RecurrenceProfile::with_update_rate`, `update_rate_from_data_characteristics`, `PlanSpace::recurrence_profiles`'s own parameter, and as a backstop inside `decide` itself (so a profile built via a direct struct literal can't bypass it). NaN/negative/infinite rates are now rejected instead of silently corrupting the comparison. 6. `PlanSpace::recurrence_profiles`'s root-count mismatch is now a `RecurrenceError::RootCountMismatch`, not a panic — consistent with the method's own `Result`-returning signature. 7. `RecurrenceCostExplanation`'s rate/cost fields (maintained_cost_rate, recompute_cost_rate, maintenance_cost_per_update, summary_read_cost, raw_recompute_cost) are now `Option<...>`, so the structural-fallback path reports "not computed" (`None`) instead of a literal zero that a downstream consumer (e.g. #286's DAG-viewer annotations) could misread as "free". Also fixed (lower priority, time permitting): - Edge multiplicity: a root referencing the same shared subtree twice from one parent (e.g. `BinaryOp{lhs: X, rhs: X}`) now credits X with 2 contributions per repeating root, matching how `MemoGroup::consumer_count` already counts that occurrence — the per-root BFS in `recurrence_profiles` now mirrors `discover_targets`' own "count every occurrence, recurse into children once" pattern instead of a plain reachability set. - `Horizon` is now validated the same way (new `RecurrenceError::InvalidHorizon`): a zero, negative, or non-finite horizon is rejected in `decide` instead of silently distorting (or inverting) the recurring-cost-rate term. - Removed an unnecessary `Vec<RepetitionInterval>` clone in the final per-site aggregation loop (iterates `&Vec` directly instead). Not fixed (documented, not attempted): the full O(roots * nodes) per-root BFS in `recurrence_profiles` still walks each root independently rather than a single O(N+E) topological pass — flagged as a real but lower-priority performance concern in the review; a correctness-focused fix pass was judged not the right place to also restructure the traversal's complexity class, and the current approach is still correct and adequately fast for realistic workload sizes. New/updated tests: batch-only-workload (both DeterministicUnitCostModel and DefaultCostModel), single-one-shot-consumer regression, UpdateRate NaN/negative/infinite rejection (builder, update_rate_from_data_characteristics, recurrence_profiles, and the decide() backstop), Horizon zero/negative/NaN/infinite rejection, RootCountMismatch as Err not panic, update_rate-not-stamped-on-an-unreachable-site (via a real AvgToSumOverCountStrategy rewrite fixture), and edge-multiplicity credit via a BinaryOp{lhs: X, rhs: X} fixture. cargo build --workspace, cargo test --workspace, cargo clippy --workspace --all-targets -- -D warnings, and cargo fmt --all -- --check all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
Author
|
Question: Is subDAG recurrence considered? |
Contributor
Author
|
Yes. Each workload root’s recurrence ( The sub-DAG structure itself remains unchanged; recurrence is stored in a separate |
milindsrivastava1997
approved these changes
Aug 28, 2026
milindsrivastava1997
left a comment
Collaborator
There was a problem hiding this comment.
Thanks, LGTM!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #287.
This PR makes CSE share-vs-recompute costing aware of how often workload roots execute, rather than relying only on structural consumer counts.
It carries repeating-query intervals and workload ingest characteristics into ASAP-aware cost context, aggregates recurrence across shared sub-DAGs, and exposes recurrence-aware variants of both local candidate ranking and whole-plan selection.
Scheduling, Prometheus rule-group behavior, runtime execution loops, temporal panes, retention, and watermark semantics remain out of scope.
Before vs. after: recurrence-aware E2E selection
The workload associates each DAG root with recurrence metadata:
Repeating(interval)orOneShot. The relational DAG structure itself does not repeat or change. The planner propagates each root annotation through its reachable sub-DAG and builds a separateRecurrenceProfilefor costing.The E2E test intentionally uses a deterministic test cost model. Its numbers are fixed test inputs, not production measurements:
The compared rates are therefore:
Before this PR, both workloads below have the same DAG and the same structural consumer count of two. Structural costing cannot distinguish them, so it gives both the same ranking. After this PR, only the root recurrence metadata differs:
1 / 0.010 s = 100times/second, so the shared sub-DAG is evaluated100 + 100 = 200times/second. Maintaining it costs10 updates/s * 1 = 10units/s, and reading it costs200 evaluations/s * 1 = 200units/s, for 210 units/s total. Recomputing costs200 evaluations/s * 50 = 10,000units/s.210 < 10,000.1 / 100 s = 0.01times/second, so the shared sub-DAG is evaluated0.01 + 0.01 = 0.02times/second. Maintaining it still costs10 updates/s * 1 = 10units/s because ingest continues even when queries are infrequent; reading it costs0.02 evaluations/s * 1 = 0.02units/s, for 10.02 units/s total. Recomputing costs0.02 evaluations/s * 50 = 1unit/s.1 < 10.02.The important crossover is that maintained state pays an ingest-driven cost continuously, whereas recomputation pays only when a query evaluates. Frequent queries amortize maintenance; infrequent queries may be cheaper to recompute.
plan_selection_uses_recurrence_profiles_for_cse_choicesverifies both local candidate ordering and whole-plan selection end to end: the high-frequency workload selectsCseShare, while the structurally identical low-frequency workload selectsCseRecompute.Cost model
The new recurrence model keeps steady-state rates and one-time costs in distinct types:
UpdateRate: raw-data updates per second;EvaluationRate: consumer evaluations per second;CostRate: cost units per second;Horizon: seconds used to combine recurring and one-shot costs.Maintained and recomputed alternatives are compared as:
Mixed repeating and one-shot workloads require an explicit horizon:
Here
maintained_cost_rateandrecompute_cost_rateare the two recurring-rate expressions defined immediately above; there is no separate undefinedrecurring_cost_rateinput.CostModelgains overridable hooks for maintenance updates, summary reads, raw recomputation, and initial summary construction. Missing recurrence metadata delegates to the existing structuralcse_share_decision, preserving previous behavior.Core algorithm
Recurrence handling is not a new
ReplacementStrategyand does not generate replacements. It augments the cost context after the existing workload-wide replacement search has discovered the legal alternatives.In short, replacement strategies determine which alternatives are legal, while recurrence profiles help determine whether sharing or recomputing a common sub-DAG is cheaper for this workload. The recurrence-aware logic currently participates specifically in
SharedSubtreeStrategyCSE selection betweenCseShareandCseRecompute; it does not uniformly rerank candidates produced by every other replacement strategy. Those non-CSE candidates retain their existing ranking behavior.The recurrence-aware CSE decision then has two stages.
1. Build a recurrence profile for each candidate target
For every workload root
r:That contribution is propagated through the root's reachable query DAG. Edge and ancestor path multiplicities are preserved, so if an independently evaluated parent reaches a descendant twice, the descendant receives twice the root's rate or one-shot contribution.
For each target
v, the contributions are folded into:This supports shared sub-DAGs consumed by roots with different intervals without exposing or constraining caller query identifiers.
2. Compare maintained state with raw recomputation
For a CSE share/recompute pair at target
v:For a purely repeating workload, the planner selects:
If one-shot consumers are present, the comparison uses an explicit horizon
Hwhenever recurring work is also present:The lower total wins. A one-shot-only profile uses the same formula with a zero recurring-rate term, so no caller-supplied horizon is needed. An empty profile falls back to the original structural consumer-count decision.
cost_sorted_with_recurrenceuses this comparison to order the local CSE candidates.global_selection_with_recurrenceapplies it during whole-plan traversal while retaining the existing effective structural consumer accounting and selected-rewrite dependency propagation.Workload integration
PlanSpace::recurrence_profilesassociates each workload root with either a repeating interval or a one-shot execution and folds those contributions into every reachable target.The implementation:
sum(1 / interval_i);Rcs insideRecurrenceProfileMap, preventing stale pointer identity.Recurrence is connected to real planning through:
PlanSpace::cost_sorted_with_recurrence; andPlanSpace::global_selection_with_recurrence.Both use recurrence-aware CSE decisions while preserving existing ranking behavior for non-CSE candidates. The original
cost_sortedandglobal_selectionAPIs remain unchanged for callers without recurrence metadata.Explanation output
RecurrenceCostExplanationreports:Structural fallback values are represented as
None, rather than misleading numeric zeroes.Tests
Coverage includes:
cost_sortedandglobal_selectionend to end;Verification:
All workspace tests pass;
asap-aware-mappinghas 174 passing tests.