From 17c5ec1ed9c421e24bb905b658dd4fe3478ef0fa Mon Sep 17 00:00:00 2001 From: zzylol <50204836+zzylol@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:41:03 +0000 Subject: [PATCH 1/2] =?UTF-8?q?perf(plan-selection):=20accuracy=20checks?= =?UTF-8?q?=20reuse=20built=20outputs=20across=20checks=20(#1095=20=C2=A78?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../src/groups/accuracy_positions.rs | 117 ++++++++++++++---- crates/plan-selection/src/groups/tests.rs | 43 +++++++ 2 files changed, 137 insertions(+), 23 deletions(-) diff --git a/crates/plan-selection/src/groups/accuracy_positions.rs b/crates/plan-selection/src/groups/accuracy_positions.rs index abf0b154e..e2fb18144 100644 --- a/crates/plan-selection/src/groups/accuracy_positions.rs +++ b/crates/plan-selection/src/groups/accuracy_positions.rs @@ -17,8 +17,9 @@ //! fails through one reader is unusable through every reader (the largest //! weight counts, as before). The candidates of one operator node (other //! timings or materializations) build the same node, so they are checked -//! once, together. -use std::cell::OnceCell; +//! once, together. What a check builds is kept for the later checks +//! ([`BuiltBefore`]). +use std::cell::{OnceCell, RefCell}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::rc::Rc; @@ -77,6 +78,7 @@ pub(crate) fn accuracy_positions( let exact_stand_ins: Vec>> = (0..physical.candidates.len()) .map(|_| OnceCell::new()) .collect(); + let built_before = RefCell::new(BuiltBefore::default()); let mut positions = AccuracyPositions::default(); for (query, root) in physical.query_roots.iter().enumerate() { let references: Vec = (0..output_refs.len()) @@ -95,6 +97,7 @@ pub(crate) fn accuracy_positions( let checker = Checker { physical, exact_stand_ins: &exact_stand_ins, + built_before: &built_before, producer_candidates_per_input, output_candidates, below: &below, @@ -282,6 +285,8 @@ struct Checker<'a> { physical: &'a PhysicalASAPMemoCandidates, /// Per candidate, its node with an exact guarantee, built once. exact_stand_ins: &'a [OnceCell>], + /// What earlier checks built, for every query. + built_before: &'a RefCell, producer_candidates_per_input: &'a [Vec>], output_candidates: &'a [Vec], below: &'a [BTreeSet], @@ -296,21 +301,69 @@ struct Checker<'a> { /// The outputs one candidate is built as, or why they are not checked. type Built = Result>, UnusableReason>; +/// What the checks built, kept across checks and queries. A candidate's +/// outputs depend only on the members of the approximate set at or below +/// it, so a pair check rebuilds only the candidates above both members +/// and reuses the rest from the single checks. Outputs built across a +/// cycle of links depend on where the walk entered the cycle and are not +/// kept. +#[derive(Default)] +struct BuiltBefore { + /// Per (candidate, approximate members at or below it, ascending): its + /// outputs. + outputs: HashMap<(usize, Vec), Built>, + /// The [`reader_key`]s of every node built so far. + keys: Keys, +} + +#[cfg(test)] +thread_local! { + /// Set by a test to build every check from scratch, as before the + /// outputs were kept across checks ([`BuiltBefore`]). + pub(crate) static FRESH_BUILDS: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +/// One check's walk: per candidate built, its outputs and whether they +/// were built across a cycle; the candidates being built; and how many +/// cycle reads the walk has made. +#[derive(Default)] +struct Walk { + built: HashMap, + visiting: Vec, + cycle_reads: usize, +} + impl Checker<'_> { /// Checks the query's outputs with exactly `approximate` approximate. fn check(&self, approximate: &[usize]) -> Outcome { - let mut memo: HashMap = HashMap::new(); - let mut keys = Keys::new(); + #[cfg(test)] + if FRESH_BUILDS.with(std::cell::Cell::get) { + *self.built_before.borrow_mut() = BuiltBefore::default(); + } + let mut walk = Walk::default(); + let before = &mut *self.built_before.borrow_mut(); let mut per_reference: Vec>> = Vec::new(); for &reference in self.references { let mut outputs: Vec> = Vec::new(); + // Exact stand-ins that are not summary states are the same + // for readers ([`reader_key`]): only the first is kept. + let mut exact_value_stand_in = false; for &class in &self.output_candidates[reference] { - match self.built(class.0, approximate, &mut memo, &mut keys, &mut Vec::new()) { + let reaches = approximate + .iter() + .any(|member| self.below[class.0].contains(member)); + if !reaches + && self.physical.physical_node(class).result_kind != OperatorResultKind::State + && std::mem::replace(&mut exact_value_stand_in, true) + { + continue; + } + match self.built(class.0, approximate, &mut walk, before) { Ok(built) => outputs.extend(built), Err(reason) => return Outcome::Fails(reason), } } - per_reference.push(distinct_for_readers(outputs, &mut keys)); + per_reference.push(distinct_for_readers(outputs, &mut before.keys)); } let Some(roots) = self.roots(&per_reference) else { return Outcome::Fails(UnusableReason::NotChecked); @@ -374,49 +427,63 @@ impl Checker<'_> { } /// The outputs `class` is built as with `approximate` approximate - /// (module documentation). `visiting` holds the candidates being built, - /// so a cycle of links builds nothing. + /// (module documentation). `walk.visiting` holds the candidates being + /// built, so a cycle of links builds nothing. fn built( &self, class: usize, approximate: &[usize], - memo: &mut HashMap, - keys: &mut Keys, - visiting: &mut Vec, + walk: &mut Walk, + before: &mut BuiltBefore, ) -> Built { - if let Some(done) = memo.get(&class) { + if let Some((done, across_cycle)) = walk.built.get(&class) { + walk.cycle_reads += usize::from(*across_cycle); return done.clone(); } - if visiting.contains(&class) { + if walk.visiting.contains(&class) { + walk.cycle_reads += 1; return Ok(Vec::new()); } - let node = self.physical.physical_node(PhysicalCandidateId(class)); let reaches = |candidate: usize| { approximate .iter() .any(|member| self.below[candidate].contains(member)) }; + if !reaches(class) { + return Ok(vec![self.exact_stand_in(class)]); + } + let mut members: Vec = approximate + .iter() + .copied() + .filter(|member| self.below[class].contains(member)) + .collect(); + members.sort_unstable(); + let key = (class, members); + if let Some(done) = before.outputs.get(&key) { + walk.built.insert(class, (done.clone(), false)); + return done.clone(); + } + let cycle_reads = walk.cycle_reads; + let node = self.physical.physical_node(PhysicalCandidateId(class)); let producers = &self.producer_candidates_per_input[class]; let below_inputs = producers .iter() .flatten() .any(|producer| reaches(producer.0)); - let built = if !reaches(class) { - Ok(vec![self.exact_stand_in(class)]) - } else if !below_inputs { + let built = if !below_inputs { // `class` is in `approximate`, over exact inputs: its node with // every guarantee below it composed, as the realizer does. with_guarantees(node, &mut HashMap::new()) .map(|node| vec![node]) .map_err(|_| UnusableReason::NotChecked) } else { - visiting.push(class); + walk.visiting.push(class); let mut per_input = Vec::with_capacity(producers.len()); let mut failed = None; for input in producers { let mut outputs = Vec::new(); for producer in input.iter().filter(|producer| reaches(producer.0)) { - match self.built(producer.0, approximate, memo, keys, visiting) { + match self.built(producer.0, approximate, walk, before) { Ok(built) => outputs.extend(built), Err(reason) => failed = Some(reason), } @@ -430,15 +497,19 @@ impl Checker<'_> { if let Some(exact) = exact { outputs.push(self.exact_stand_in(exact.0)); } - per_input.push(distinct_for_readers(outputs, keys)); + per_input.push(distinct_for_readers(outputs, &mut before.keys)); } - visiting.pop(); + walk.visiting.pop(); match failed { Some(reason) => Err(reason), - None => rebuilt(node, &per_input, keys), + None => rebuilt(node, &per_input, &mut before.keys), } }; - memo.insert(class, built.clone()); + let across_cycle = walk.cycle_reads != cycle_reads; + if !across_cycle { + before.outputs.insert(key, built.clone()); + } + walk.built.insert(class, (built.clone(), across_cycle)); built } } diff --git a/crates/plan-selection/src/groups/tests.rs b/crates/plan-selection/src/groups/tests.rs index 80c9c2574..f000507e1 100644 --- a/crates/plan-selection/src/groups/tests.rs +++ b/crates/plan-selection/src/groups/tests.rs @@ -823,3 +823,46 @@ fn one_pane_chain_serves_a_ratio_and_its_operands() { ); } } + +// The checks keep what they build for later checks: on RangeHeavy T8+T5's +// shape (a quotient and both operands alone) and on a quotient of three +// quantiles (where pairs are excluded), the weights and excluded pairs +// equal those of building every check from scratch. +#[test] +fn kept_builds_give_the_weights_of_fresh_builds() { + kept_builds_match_fresh_builds(&[ + "quantile_over_time(0.9, m[5m]) / quantile_over_time(0.5, m[5m])", + "quantile_over_time(0.9, m[5m])", + "quantile_over_time(0.5, m[5m])", + ]); + let three = kept_builds_match_fresh_builds(&[ + "quantile_over_time(0.9, m[5m]) / quantile_over_time(0.5, m[5m]) / quantile_over_time(0.1, m[5m])", + ]); + assert!(!three.accuracy_excluded_pairs[0].is_empty()); +} + +/// Asserts that `queries`' accuracy weights and excluded pairs are the +/// same with builds kept across checks and with fresh builds; returns the +/// former. +fn kept_builds_match_fresh_builds(queries: &[&str]) -> MilpInputs { + let stages = through_stages_1_and_2_over(queries, PaneWidthDomain::SeedGrids); + let prepare = |fresh: bool| { + accuracy_positions::FRESH_BUILDS.with(|cell| cell.set(fresh)); + let inputs = prepare_milp_inputs( + &stages.physical, + &stages.entries, + &stages.data, + &CostInputs::default(), + ); + accuracy_positions::FRESH_BUILDS.with(|cell| cell.set(false)); + inputs + }; + let (kept, fresh) = (prepare(false), prepare(true)); + assert!(kept + .accuracy_weights + .iter() + .any(|weights| !weights.is_empty())); + assert_eq!(kept.accuracy_weights, fresh.accuracy_weights); + assert_eq!(kept.accuracy_excluded_pairs, fresh.accuracy_excluded_pairs); + kept +} From 7b337a7e32b33ee801e46c33b592e07e4b3f805e Mon Sep 17 00:00:00 2001 From: zzylol <50204836+zzylol@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:31:37 +0000 Subject: [PATCH 2/2] test(plan-selection): pin the stand-in skip and the cycle exception of 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. --- .../src/groups/accuracy_positions.rs | 198 ++++++++++++++++-- crates/plan-selection/src/groups/tests.rs | 87 ++++++-- 2 files changed, 240 insertions(+), 45 deletions(-) diff --git a/crates/plan-selection/src/groups/accuracy_positions.rs b/crates/plan-selection/src/groups/accuracy_positions.rs index e2fb18144..710ea029e 100644 --- a/crates/plan-selection/src/groups/accuracy_positions.rs +++ b/crates/plan-selection/src/groups/accuracy_positions.rs @@ -19,7 +19,7 @@ //! timings or materializations) build the same node, so they are checked //! once, together. What a check builds is kept for the later checks //! ([`BuiltBefore`]). -use std::cell::{OnceCell, RefCell}; +use std::cell::OnceCell; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::rc::Rc; @@ -78,7 +78,7 @@ pub(crate) fn accuracy_positions( let exact_stand_ins: Vec>> = (0..physical.candidates.len()) .map(|_| OnceCell::new()) .collect(); - let built_before = RefCell::new(BuiltBefore::default()); + let mut built_before = BuiltBefore::default(); let mut positions = AccuracyPositions::default(); for (query, root) in physical.query_roots.iter().enumerate() { let references: Vec = (0..output_refs.len()) @@ -97,7 +97,6 @@ pub(crate) fn accuracy_positions( let checker = Checker { physical, exact_stand_ins: &exact_stand_ins, - built_before: &built_before, producer_candidates_per_input, output_candidates, below: &below, @@ -120,7 +119,7 @@ pub(crate) fn accuracy_positions( let mut weights = BTreeMap::new(); let mut usable: Vec<&[usize]> = Vec::new(); for group in groups.values() { - let weight = match checker.check(group) { + let weight = match checker.check(group, &mut built_before) { Outcome::Exact => continue, Outcome::Meets => { usable.push(group); @@ -168,7 +167,7 @@ pub(crate) fn accuracy_positions( .chain(usable[second]) .copied() .collect(); - if let Outcome::Fails(_) = checker.check(&both) { + if let Outcome::Fails(_) = checker.check(&both, &mut built_before) { for &one in usable[first] { for &other in usable[second] { excluded.push(( @@ -285,8 +284,6 @@ struct Checker<'a> { physical: &'a PhysicalASAPMemoCandidates, /// Per candidate, its node with an exact guarantee, built once. exact_stand_ins: &'a [OnceCell>], - /// What earlier checks built, for every query. - built_before: &'a RefCell, producer_candidates_per_input: &'a [Vec>], output_candidates: &'a [Vec], below: &'a [BTreeSet], @@ -318,30 +315,42 @@ struct BuiltBefore { #[cfg(test)] thread_local! { - /// Set by a test to build every check from scratch, as before the - /// outputs were kept across checks ([`BuiltBefore`]). + /// Set by a test to check with no kept builds: every check builds + /// from scratch ([`BuiltBefore`] cleared) and builds every output + /// candidate (no exact stand-in skipped). pub(crate) static FRESH_BUILDS: std::cell::Cell = const { std::cell::Cell::new(false) }; } -/// One check's walk: per candidate built, its outputs and whether they -/// were built across a cycle; the candidates being built; and how many -/// cycle reads the walk has made. +/// One check's walk. #[derive(Default)] struct Walk { - built: HashMap, + /// Per candidate built in this check. + built: HashMap, + /// The candidates being built. visiting: Vec, + /// How many reads of a candidate still being built the walk has made. cycle_reads: usize, } +/// A candidate's outputs in one check, and whether they were built across +/// a cycle of links (then they are not kept in [`BuiltBefore`]). +struct BuiltInWalk { + outputs: Built, + across_cycle: bool, +} + impl Checker<'_> { /// Checks the query's outputs with exactly `approximate` approximate. - fn check(&self, approximate: &[usize]) -> Outcome { + /// `before` holds what earlier checks built, for every query. + fn check(&self, approximate: &[usize], before: &mut BuiltBefore) -> Outcome { #[cfg(test)] - if FRESH_BUILDS.with(std::cell::Cell::get) { - *self.built_before.borrow_mut() = BuiltBefore::default(); + let fresh = FRESH_BUILDS.with(std::cell::Cell::get); + #[cfg(not(test))] + let fresh = false; + if fresh { + *before = BuiltBefore::default(); } let mut walk = Walk::default(); - let before = &mut *self.built_before.borrow_mut(); let mut per_reference: Vec>> = Vec::new(); for &reference in self.references { let mut outputs: Vec> = Vec::new(); @@ -352,7 +361,8 @@ impl Checker<'_> { let reaches = approximate .iter() .any(|member| self.below[class.0].contains(member)); - if !reaches + if !fresh + && !reaches && self.physical.physical_node(class).result_kind != OperatorResultKind::State && std::mem::replace(&mut exact_value_stand_in, true) { @@ -436,9 +446,9 @@ impl Checker<'_> { walk: &mut Walk, before: &mut BuiltBefore, ) -> Built { - if let Some((done, across_cycle)) = walk.built.get(&class) { - walk.cycle_reads += usize::from(*across_cycle); - return done.clone(); + if let Some(done) = walk.built.get(&class) { + walk.cycle_reads += usize::from(done.across_cycle); + return done.outputs.clone(); } if walk.visiting.contains(&class) { walk.cycle_reads += 1; @@ -460,7 +470,13 @@ impl Checker<'_> { members.sort_unstable(); let key = (class, members); if let Some(done) = before.outputs.get(&key) { - walk.built.insert(class, (done.clone(), false)); + walk.built.insert( + class, + BuiltInWalk { + outputs: done.clone(), + across_cycle: false, + }, + ); return done.clone(); } let cycle_reads = walk.cycle_reads; @@ -509,7 +525,13 @@ impl Checker<'_> { if !across_cycle { before.outputs.insert(key, built.clone()); } - walk.built.insert(class, (built.clone(), across_cycle)); + walk.built.insert( + class, + BuiltInWalk { + outputs: built.clone(), + across_cycle, + }, + ); built } } @@ -636,3 +658,133 @@ fn key_and_settled(node: &Rc, keys: &mut Keys) -> (u64, bool) { keys.insert(Rc::as_ptr(node), (known.0, known.1, node.clone())); known } + +#[cfg(test)] +mod tests { + use super::*; + use crate::groups::tests::{candidate, catalog, reader_of, relative, root, values_node}; + use asap_logical_asap_optimization::test_support::entry_with; + use asap_types::ir::properties::AccuracyTarget; + use asap_types::workload::{QueryRecurrence, RepeatedDemand, RepetitionInterval}; + + /// The depth of each output of `built`. + fn depths(built: &Built) -> Vec { + fn depth(node: &OperatorNode) -> usize { + 1 + node + .children() + .into_iter() + .map(|child| depth(child)) + .max() + .unwrap_or(0) + } + built + .as_ref() + .expect("built") + .iter() + .map(|node| depth(node)) + .collect() + } + + // A two-candidate link cycle above an approximate member: `A` reads + // the leaf or `B`; `B` reads `A` or a second leaf. Walking from the + // top enters the cycle at `A`, so `B` is built there with `A` cut off + // (a cycle read), over the second leaf only. Such outputs are not + // kept: a later walk that starts at `B` builds it in full, over `A`, + // as with no kept builds, for one member and for a pair. + #[test] + fn outputs_built_across_a_cycle_are_not_kept() { + let approximate_leaf = |value: f64, error: f64| { + Rc::new( + (*values_node(value)) + .clone() + .with_guarantee(Some(relative(error, 0.01))), + ) + }; + let leaf = approximate_leaf(1.0, 0.01); + let other_leaf = approximate_leaf(2.0, 0.02); + let a = reader_of(&leaf); + let b = reader_of(&leaf); + let top = reader_of(&a); + let physical = catalog( + vec![(leaf, 0), (a, 1), (b, 2), (top, 3), (other_leaf, 4)], + vec![ + candidate(0, &[], false), + candidate(1, &[&[0, 2]], false), + candidate(2, &[&[1, 4]], false), + candidate(3, &[&[1]], true), + candidate(4, &[], false), + ], + vec![root(3)], + ); + let producers: Vec>> = physical + .candidates + .iter() + .map(|candidate| { + candidate + .producer_candidates_per_input + .iter() + .map(|producers| producers.iter().copied().collect()) + .collect() + }) + .collect(); + // Leaves 0 and 4 stand for approximate candidates. + let below: Vec> = vec![ + BTreeSet::from([0]), + BTreeSet::from([0, 4]), + BTreeSet::from([0, 4]), + BTreeSet::from([0, 4]), + BTreeSet::from([4]), + ]; + let exact_stand_ins: Vec>> = (0..physical.candidates.len()) + .map(|_| OnceCell::new()) + .collect(); + let entry = entry_with( + AccuracyTarget::max_error(0.01).failure_probability(0.01), + QueryRecurrence::Repeated(RepeatedDemand::FixedInterval(RepetitionInterval(60_000))), + ); + let data = DataWorkload::default(); + let cost_inputs = asap_cost::CostInputs::default(); + let models = SelectionModels { + accuracy: &asap_accuracy::BuiltinAccuracyModel, + evidence: &asap_accuracy::NoAccuracyEvidence, + cost_inputs: &cost_inputs, + }; + let output_candidates = vec![vec![PhysicalCandidateId(3)]]; + let checker = Checker { + physical: &physical, + exact_stand_ins: &exact_stand_ins, + producer_candidates_per_input: &producers, + output_candidates: &output_candidates, + below: &below, + query: 0, + root: &physical.query_roots[0], + references: &[0], + entry: &entry, + data: &data, + models: &models, + }; + for approximate in [&[0][..], &[0, 4]] { + let mut kept = BuiltBefore::default(); + // From the top: `B` is built with `A` cut off, over the second + // leaf alone (depth 2). + let mut walk = Walk::default(); + checker + .built(3, approximate, &mut walk, &mut kept) + .expect("built"); + assert!(walk.cycle_reads > 0); + assert!(walk.built[&2].across_cycle); + assert_eq!(depths(&walk.built[&2].outputs)[0], 2, "{approximate:?}"); + // From `B`: in full, its first output reads `A` over the leaf + // (depth 3), with kept builds as with none. + let from_b = checker.built(2, approximate, &mut Walk::default(), &mut kept); + let fresh = checker.built( + 2, + approximate, + &mut Walk::default(), + &mut BuiltBefore::default(), + ); + assert_eq!(depths(&from_b), depths(&fresh), "{approximate:?}"); + assert_eq!(depths(&fresh)[0], 3, "{approximate:?}"); + } + } +} diff --git a/crates/plan-selection/src/groups/tests.rs b/crates/plan-selection/src/groups/tests.rs index f000507e1..058fce70f 100644 --- a/crates/plan-selection/src/groups/tests.rs +++ b/crates/plan-selection/src/groups/tests.rs @@ -18,7 +18,7 @@ use asap_types::stages::{ // --------------------------------------------------------------------------- /// A one-row `VALUES` node of `value`. -fn values_node(value: f64) -> Rc { +pub(super) fn values_node(value: f64) -> Rc { OperatorNode::new_shared(Operator::NonASAP(NonASAPOp::Values { rows: vec![vec![ScalarExpr::Literal(ScalarValue::Float64(value))]], schema: Schema::lifted(vec![Field::plain("value", DataType::Float64, false)], None), @@ -37,7 +37,7 @@ fn rows_node(rows: usize) -> Rc { } /// A stateless one-input reader of `child`. -fn reader_of(child: &Rc) -> Rc { +pub(super) fn reader_of(child: &Rc) -> Rc { OperatorNode::new_shared(Operator::NonASAP(NonASAPOp::Dedup { cols: Vec::new(), child: Rc::clone(child), @@ -48,7 +48,7 @@ fn reader_of(child: &Rc) -> Rc { /// A catalog of `nodes`, node `i` in memo group `nodes[i].1` (each node its /// own candidate of that group, all implementation roots), with /// `candidates` and one query root per entry of `roots`. -fn catalog( +pub(super) fn catalog( nodes: Vec<(Rc, u32)>, candidates: Vec, roots: Vec>, @@ -80,7 +80,7 @@ fn catalog( /// One candidate of operator node `node` at query time, not materialized, /// reading `producers` per input, that may produce a query's output when /// `output`. -fn candidate(node: usize, producers: &[&[usize]], output: bool) -> PhysicalCandidate { +pub(super) fn candidate(node: usize, producers: &[&[usize]], output: bool) -> PhysicalCandidate { PhysicalCandidate { operator_node: OperatorNodeOfCandidateForMemoGroupId(node), timing: ExecutionTiming::QueryTime, @@ -99,7 +99,7 @@ fn candidate(node: usize, producers: &[&[usize]], output: bool) -> PhysicalCandi } } -fn root(group: u32) -> QueryRoot { +pub(super) fn root(group: u32) -> QueryRoot { QueryRoot::Operator(MemoGroupId(group)) } @@ -148,7 +148,7 @@ fn data_with(usage: CandidateUsage) -> CandidateData { } } -fn relative(error: f64, failure_probability: f64) -> AccuracyGuarantee { +pub(super) fn relative(error: f64, failure_probability: f64) -> AccuracyGuarantee { AccuracyGuarantee::new( AccuracyMetric::Relative, BoundExpr::Constant { value: error }, @@ -553,21 +553,31 @@ fn through_stages_1_and_2(queries: &[&str]) -> ThroughStages1And2 { /// [`through_stages_1_and_2`] over the pane-width domain `domain`. fn through_stages_1_and_2_over(queries: &[&str], domain: PaneWidthDomain) -> ThroughStages1And2 { + use asap_logical_asap_optimization::test_support::lower_promql; + let roots: Vec = queries + .iter() + .map(|query| QueryRoot::Operator(lower_promql(query, target()))) + .collect(); + through_stages_1_and_2_of(roots, domain) +} + +/// The accuracy target of [`through_stages_1_and_2`]'s queries. +fn target() -> asap_types::ir::properties::AccuracyTarget { + asap_types::ir::properties::AccuracyTarget::max_error(0.01).failure_probability(0.01) +} + +/// [`through_stages_1_and_2_over`] for query roots `roots`. +fn through_stages_1_and_2_of(roots: Vec, domain: PaneWidthDomain) -> ThroughStages1And2 { use asap_logical_asap_optimization::generate_per_computation_logical_memo_candidates; - use asap_logical_asap_optimization::test_support::{entry_with, lower_promql}; + use asap_logical_asap_optimization::test_support::entry_with; use asap_logical_asap_optimization::{generate_logical_asap_memo_candidates, Pass2RoundState}; - use asap_types::ir::properties::AccuracyTarget; use asap_types::workload::{ DurationMs, Predictability, QueryRecurrence, RepeatedDemand, RepetitionInterval, }; - let target = AccuracyTarget::max_error(0.01).failure_probability(0.01); - let roots: Vec = queries - .iter() - .map(|query| QueryRoot::Operator(lower_promql(query, target.clone()))) - .collect(); + let target = target(); let every_minute = QueryRecurrence::Repeated(RepeatedDemand::FixedInterval(RepetitionInterval(60_000))); - let entries: Vec<_> = queries + let entries: Vec<_> = roots .iter() .map(|_| QueryWorkloadEntry { predictability: Predictability::Predictable { known_at: None }, @@ -578,7 +588,7 @@ fn through_stages_1_and_2_over(queries: &[&str], domain: PaneWidthDomain) -> Thr let source = data.sources.get_mut("m").expect("m"); source.series_count = Some(100); source.values_non_negative = true; - let targets = vec![target.clone(); queries.len()]; + let targets = vec![target.clone(); roots.len()]; let pass1 = generate_per_computation_logical_memo_candidates( asap_logical_asap_optimization::generate_dag_of_logical_memo_groups(&roots), &targets, @@ -824,10 +834,11 @@ fn one_pane_chain_serves_a_ratio_and_its_operands() { } } -// The checks keep what they build for later checks: on RangeHeavy T8+T5's -// shape (a quotient and both operands alone) and on a quotient of three -// quantiles (where pairs are excluded), the weights and excluded pairs -// equal those of building every check from scratch. +// The checks keep what they build for later checks and skip repeated +// exact stand-ins: on RangeHeavy T8+T5's shape (a quotient and both +// operands alone), on a quotient of three quantiles (where pairs are +// excluded) and on a ratio with one approximate operand, the weights and +// excluded pairs equal those with no kept builds and no skipped stand-in. #[test] fn kept_builds_give_the_weights_of_fresh_builds() { kept_builds_match_fresh_builds(&[ @@ -839,13 +850,45 @@ fn kept_builds_give_the_weights_of_fresh_builds() { "quantile_over_time(0.9, m[5m]) / quantile_over_time(0.5, m[5m]) / quantile_over_time(0.1, m[5m])", ]); assert!(!three.accuracy_excluded_pairs[0].is_empty()); + // A scalar ratio of two output references whose approximate values sit + // under one operand only: the other reference's output candidates + // reach no member, and its first exact stand-in must still be built, + // or the ratio has no root to check and every approximate candidate + // would weigh nothing. + use asap_logical_asap_optimization::test_support::lower_promql; + use asap_types::ir::scalar::{ArithmeticOpKind, ExprSemantics}; + let operand = |query: &str| { + Box::new(ScalarExpr::PromqlScalarFromVector(lower_promql( + query, + target(), + ))) + }; + let ratio = QueryRoot::Scalar(ScalarExpr::Arithmetic { + op: ArithmeticOpKind::Div, + left: operand("avg_over_time(m[5m])"), + right: operand("quantile_over_time(0.5, m[5m])"), + semantics: ExprSemantics::Promql, + }); + let ratio = kept_builds_match_fresh_builds_of(vec![ratio]); + assert!(!ratio.accuracy_weights[0].is_empty()); } /// Asserts that `queries`' accuracy weights and excluded pairs are the -/// same with builds kept across checks and with fresh builds; returns the -/// former. +/// same with kept builds and with none ([`accuracy_positions::FRESH_BUILDS`]); +/// returns the former. fn kept_builds_match_fresh_builds(queries: &[&str]) -> MilpInputs { - let stages = through_stages_1_and_2_over(queries, PaneWidthDomain::SeedGrids); + use asap_logical_asap_optimization::test_support::lower_promql; + kept_builds_match_fresh_builds_of( + queries + .iter() + .map(|query| QueryRoot::Operator(lower_promql(query, target()))) + .collect(), + ) +} + +/// [`kept_builds_match_fresh_builds`] for query roots `roots`. +fn kept_builds_match_fresh_builds_of(roots: Vec) -> MilpInputs { + let stages = through_stages_1_and_2_of(roots, PaneWidthDomain::SeedGrids); let prepare = |fresh: bool| { accuracy_positions::FRESH_BUILDS.with(|cell| cell.set(fresh)); let inputs = prepare_milp_inputs(