Skip to content

Commit a945497

Browse files
committed
fix(cost): snapshot planner evidence per target
1 parent b8ba76e commit a945497

1 file changed

Lines changed: 144 additions & 50 deletions

File tree

‎crates/asap-aware-mapping/src/analytical_planner.rs‎

Lines changed: 144 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
//! Evidence-backed physical-plan costing at the planner selection boundary.
22
3-
use std::rc::Rc;
3+
use std::{cell::RefCell, rc::Rc};
44

55
use asap_types::post_asap::{SketchAlgorithm, SummaryExpr, SummaryNode};
6-
use asap_types::pre_asap::AggIntent;
6+
use asap_types::pre_asap::{AggIntent, QueryExpr};
77

88
use crate::analytical_cost::{
99
estimate_physical_dag_comparison, AnalyticalCostError, PhysicalDagComparisonEstimate,
@@ -17,6 +17,18 @@ use crate::analytical_statistics::ComparisonScope;
1717
use crate::cost_model::{Cost, CostModel, DefaultCostModel};
1818
use crate::replacement::{Replacement, ReplacementSubDAG, TargetSubDAG};
1919

20+
/// One immutable generation of deployment evidence for a planner target.
21+
///
22+
/// The opaque version lets a provider bind every subsequent query and summary
23+
/// lookup to the same catalog/runtime snapshot. A cost-model instance retains
24+
/// this value for the target, so a caller that needs fresher evidence creates a
25+
/// new model instead of mixing generations in one ranking decision.
26+
#[derive(Debug, Clone, PartialEq)]
27+
pub struct PlannerEvidenceSnapshot {
28+
pub version: String,
29+
pub scope: ComparisonScope,
30+
}
31+
2032
/// Deployment evidence needed to price one planner alternative.
2133
///
2234
/// The planner lowers raw queries and logical rewrites itself. A deployment
@@ -26,21 +38,23 @@ use crate::replacement::{Replacement, ReplacementSubDAG, TargetSubDAG};
2638
/// choices; that binder must return the complete summary DAG, including any
2739
/// embedded `KeepPreAsap` work.
2840
pub trait PlannerPhysicalPlanProvider {
29-
fn comparison_scope(
41+
/// Atomically captures the comparison scope and evidence generation.
42+
fn capture_evidence_snapshot(
3043
&self,
3144
target: &TargetSubDAG<'_>,
32-
) -> Result<ComparisonScope, AnalyticalCostError>;
45+
) -> Result<PlannerEvidenceSnapshot, AnalyticalCostError>;
3346

3447
fn query_node_evidence(
3548
&self,
49+
snapshot: &PlannerEvidenceSnapshot,
3650
request: PhysicalNodeRequest<'_>,
3751
) -> Result<PhysicalNodeEvidence, AnalyticalCostError>;
3852

3953
fn summary_physical_dag(
4054
&self,
55+
snapshot: &PlannerEvidenceSnapshot,
4156
summary: &Rc<SummaryNode>,
4257
target: &TargetSubDAG<'_>,
43-
scope: &ComparisonScope,
4458
) -> Result<PhysicalDag, AnalyticalCostError>;
4559
}
4660

@@ -61,6 +75,14 @@ pub struct PlannerCandidateEstimate {
6175
pub struct AnalyticalPlannerCostModel<'a> {
6276
provider: &'a dyn PlannerPhysicalPlanProvider,
6377
calibration: ResourceCalibration,
78+
target_evidence: RefCell<Vec<CachedTargetEvidence>>,
79+
}
80+
81+
struct CachedTargetEvidence {
82+
root: Rc<QueryExpr>,
83+
consumer_count: usize,
84+
snapshot: PlannerEvidenceSnapshot,
85+
raw: PhysicalDag,
6486
}
6587

6688
impl<'a> AnalyticalPlannerCostModel<'a> {
@@ -72,49 +94,76 @@ impl<'a> AnalyticalPlannerCostModel<'a> {
7294
Ok(Self {
7395
provider,
7496
calibration,
97+
target_evidence: RefCell::new(Vec::new()),
7598
})
7699
}
77100

101+
fn target_evidence(
102+
&self,
103+
target: &TargetSubDAG<'_>,
104+
) -> Result<(PlannerEvidenceSnapshot, PhysicalDag), AnalyticalCostError> {
105+
if let Some(cached) = self.target_evidence.borrow().iter().find(|cached| {
106+
Rc::ptr_eq(&cached.root, target.root) && cached.consumer_count == target.consumer_count
107+
}) {
108+
return Ok((cached.snapshot.clone(), cached.raw.clone()));
109+
}
110+
111+
let snapshot = self.provider.capture_evidence_snapshot(target)?;
112+
if snapshot.version.is_empty() {
113+
return Err(AnalyticalCostError::MissingOrStale(
114+
"planner_evidence_snapshot.version",
115+
));
116+
}
117+
snapshot.scope.validate()?;
118+
let evidence = QueryEvidence {
119+
provider: self.provider,
120+
snapshot: &snapshot,
121+
};
122+
let raw = lower_query_physical_dag(target.root, &snapshot.scope, &evidence)?;
123+
self.target_evidence
124+
.borrow_mut()
125+
.push(CachedTargetEvidence {
126+
root: Rc::clone(target.root),
127+
consumer_count: target.consumer_count,
128+
snapshot: snapshot.clone(),
129+
raw: raw.clone(),
130+
});
131+
Ok((snapshot, raw))
132+
}
133+
78134
pub fn estimate_candidate(
79135
&self,
80136
candidate: &ReplacementSubDAG,
81137
target: &TargetSubDAG<'_>,
82138
) -> Result<PlannerCandidateEstimate, AnalyticalCostError> {
83-
struct QueryEvidence<'a>(&'a dyn PlannerPhysicalPlanProvider);
84-
impl PhysicalNodeEvidenceProvider for QueryEvidence<'_> {
85-
fn evidence(
86-
&self,
87-
request: PhysicalNodeRequest<'_>,
88-
) -> Result<PhysicalNodeEvidence, AnalyticalCostError> {
89-
self.0.query_node_evidence(request)
90-
}
91-
}
92-
93-
let scope = self.provider.comparison_scope(target)?;
94-
let evidence = QueryEvidence(self.provider);
95-
let raw = lower_query_physical_dag(target.root, &scope, &evidence)?;
139+
let (snapshot, raw) = self.target_evidence(target)?;
140+
let scope = &snapshot.scope;
141+
let evidence = QueryEvidence {
142+
provider: self.provider,
143+
snapshot: &snapshot,
144+
};
96145
let replacement = match &candidate.replacement {
97-
Replacement::Rewrite(query) => lower_query_physical_dag(query, &scope, &evidence)?,
146+
Replacement::Rewrite(query) => lower_query_physical_dag(query, scope, &evidence)?,
98147
Replacement::Summary(summary) => match &summary.expr {
99148
SummaryExpr::KeepPreAsap(query) => {
100-
lower_query_physical_dag(query, &scope, &evidence)?
149+
lower_query_physical_dag(query, scope, &evidence)?
101150
}
102151
_ => self
103152
.provider
104-
.summary_physical_dag(summary, target, &scope)?,
153+
.summary_physical_dag(&snapshot, summary, target)?,
105154
},
106155
};
107156
let resources = estimate_physical_dag_comparison(
108157
PhysicalDagEstimateRequest {
109158
nodes: &raw.nodes,
110159
root: &raw.root,
111-
scope: &scope,
160+
scope,
112161
statistics: &raw,
113162
},
114163
PhysicalDagEstimateRequest {
115164
nodes: &replacement.nodes,
116165
root: &replacement.root,
117-
scope: &scope,
166+
scope,
118167
statistics: &replacement,
119168
},
120169
)?;
@@ -128,6 +177,20 @@ impl<'a> AnalyticalPlannerCostModel<'a> {
128177
}
129178
}
130179

180+
struct QueryEvidence<'a> {
181+
provider: &'a dyn PlannerPhysicalPlanProvider,
182+
snapshot: &'a PlannerEvidenceSnapshot,
183+
}
184+
185+
impl PhysicalNodeEvidenceProvider for QueryEvidence<'_> {
186+
fn evidence(
187+
&self,
188+
request: PhysicalNodeRequest<'_>,
189+
) -> Result<PhysicalNodeEvidence, AnalyticalCostError> {
190+
self.provider.query_node_evidence(self.snapshot, request)
191+
}
192+
}
193+
131194
impl CostModel for AnalyticalPlannerCostModel<'_> {
132195
fn candidate_cost_covers_complete_plan(&self) -> bool {
133196
true
@@ -163,6 +226,7 @@ impl CostModel for AnalyticalPlannerCostModel<'_> {
163226
#[cfg(test)]
164227
mod tests {
165228
use super::*;
229+
use std::cell::Cell;
166230
use std::collections::HashMap;
167231

168232
use asap_types::pre_asap::{Column, DataType, QueryExpr, Reduction, Schema, Source};
@@ -244,9 +308,20 @@ mod tests {
244308
struct TestProvider {
245309
summary_available: bool,
246310
candidate_scan_bytes: u64,
311+
snapshot_calls: Cell<u64>,
312+
raw_evidence_calls: Cell<u64>,
247313
}
248314

249315
impl TestProvider {
316+
fn new(summary_available: bool, candidate_scan_bytes: u64) -> Self {
317+
Self {
318+
summary_available,
319+
candidate_scan_bytes,
320+
snapshot_calls: Cell::new(0),
321+
raw_evidence_calls: Cell::new(0),
322+
}
323+
}
324+
250325
fn summary_dag(&self, scope: &ComparisonScope) -> PhysicalDag {
251326
let scan_statistics = statistics(
252327
self.candidate_scan_bytes,
@@ -321,17 +396,25 @@ mod tests {
321396
}
322397

323398
impl PlannerPhysicalPlanProvider for TestProvider {
324-
fn comparison_scope(
399+
fn capture_evidence_snapshot(
325400
&self,
326401
_target: &TargetSubDAG<'_>,
327-
) -> Result<ComparisonScope, AnalyticalCostError> {
328-
Ok(scope())
402+
) -> Result<PlannerEvidenceSnapshot, AnalyticalCostError> {
403+
self.snapshot_calls.set(self.snapshot_calls.get() + 1);
404+
Ok(PlannerEvidenceSnapshot {
405+
version: "test-snapshot-1".into(),
406+
scope: scope(),
407+
})
329408
}
330409

331410
fn query_node_evidence(
332411
&self,
412+
snapshot: &PlannerEvidenceSnapshot,
333413
request: PhysicalNodeRequest<'_>,
334414
) -> Result<PhysicalNodeEvidence, AnalyticalCostError> {
415+
assert_eq!(snapshot.version, "test-snapshot-1");
416+
self.raw_evidence_calls
417+
.set(self.raw_evidence_calls.get() + 1);
335418
let (physical_id, mut statistics) = match request.operator {
336419
PhysicalOperator::Scan => (
337420
"raw-scan",
@@ -357,12 +440,13 @@ mod tests {
357440

358441
fn summary_physical_dag(
359442
&self,
443+
snapshot: &PlannerEvidenceSnapshot,
360444
_summary: &Rc<SummaryNode>,
361445
_target: &TargetSubDAG<'_>,
362-
scope: &ComparisonScope,
363446
) -> Result<PhysicalDag, AnalyticalCostError> {
447+
assert_eq!(snapshot.version, "test-snapshot-1");
364448
self.summary_available
365-
.then(|| self.summary_dag(scope))
449+
.then(|| self.summary_dag(&snapshot.scope))
366450
.ok_or(AnalyticalCostError::MissingOrStale("summary_physical_plan"))
367451
}
368452
}
@@ -384,10 +468,7 @@ mod tests {
384468
&crate::replacement::default_strategies(),
385469
);
386470
let planned_root = Rc::clone(&space.roots[0].1);
387-
let provider = TestProvider {
388-
summary_available: true,
389-
candidate_scan_bytes: 800,
390-
};
471+
let provider = TestProvider::new(true, 800);
391472
let model = AnalyticalPlannerCostModel::new(&provider, calibration()).unwrap();
392473

393474
let selected = space.global_selection(&model);
@@ -405,10 +486,7 @@ mod tests {
405486
&crate::replacement::default_strategies(),
406487
);
407488
let planned_root = Rc::clone(&space.roots[0].1);
408-
let provider = TestProvider {
409-
summary_available: false,
410-
candidate_scan_bytes: 800,
411-
};
489+
let provider = TestProvider::new(false, 800);
412490
let model = AnalyticalPlannerCostModel::new(&provider, calibration()).unwrap();
413491

414492
let selected = space.global_selection(&model);
@@ -422,27 +500,28 @@ mod tests {
422500
fn candidate_with_incomplete_scope_is_unavailable() {
423501
struct WrongScope(TestProvider);
424502
impl PlannerPhysicalPlanProvider for WrongScope {
425-
fn comparison_scope(
503+
fn capture_evidence_snapshot(
426504
&self,
427505
target: &TargetSubDAG<'_>,
428-
) -> Result<ComparisonScope, AnalyticalCostError> {
429-
self.0.comparison_scope(target)
506+
) -> Result<PlannerEvidenceSnapshot, AnalyticalCostError> {
507+
self.0.capture_evidence_snapshot(target)
430508
}
431509

432510
fn query_node_evidence(
433511
&self,
512+
snapshot: &PlannerEvidenceSnapshot,
434513
request: PhysicalNodeRequest<'_>,
435514
) -> Result<PhysicalNodeEvidence, AnalyticalCostError> {
436-
self.0.query_node_evidence(request)
515+
self.0.query_node_evidence(snapshot, request)
437516
}
438517

439518
fn summary_physical_dag(
440519
&self,
520+
snapshot: &PlannerEvidenceSnapshot,
441521
summary: &Rc<SummaryNode>,
442522
target: &TargetSubDAG<'_>,
443-
scope: &ComparisonScope,
444523
) -> Result<PhysicalDag, AnalyticalCostError> {
445-
let mut dag = self.0.summary_physical_dag(summary, target, scope)?;
524+
let mut dag = self.0.summary_physical_dag(snapshot, summary, target)?;
446525
dag.nodes[0].source_coverage.as_mut().unwrap().snapshot_id = "other".into();
447526
Ok(dag)
448527
}
@@ -451,10 +530,7 @@ mod tests {
451530
let root = query();
452531
let candidates = crate::replacement::SketchAlgorithmStrategy::default_cost_model()
453532
.replacements(&TargetSubDAG::new(&root));
454-
let provider = WrongScope(TestProvider {
455-
summary_available: true,
456-
candidate_scan_bytes: 800,
457-
});
533+
let provider = WrongScope(TestProvider::new(true, 800));
458534
let model = AnalyticalPlannerCostModel::new(&provider, calibration()).unwrap();
459535
assert_eq!(
460536
model.candidate_cost(&candidates[0], &TargetSubDAG::new(&root)),
@@ -470,13 +546,31 @@ mod tests {
470546
&crate::replacement::default_strategies(),
471547
);
472548
let planned_root = Rc::clone(&space.roots[0].1);
473-
let provider = TestProvider {
474-
summary_available: true,
475-
candidate_scan_bytes: 100_000,
476-
};
549+
let provider = TestProvider::new(true, 100_000);
477550
let model = AnalyticalPlannerCostModel::new(&provider, calibration()).unwrap();
478551

479552
let selected = space.global_selection(&model);
480553
assert!(selected.for_target(&planned_root).unwrap().chosen.is_none());
481554
}
555+
556+
#[test]
557+
fn sibling_candidates_share_one_scope_and_raw_baseline() {
558+
let root = query();
559+
let candidates = crate::replacement::SketchAlgorithmStrategy::default_cost_model()
560+
.replacements(&TargetSubDAG::new(&root));
561+
assert!(candidates.len() >= 2);
562+
let provider = TestProvider::new(true, 800);
563+
let model = AnalyticalPlannerCostModel::new(&provider, calibration()).unwrap();
564+
let target = TargetSubDAG::new(&root);
565+
566+
model.estimate_candidate(&candidates[0], &target).unwrap();
567+
model.estimate_candidate(&candidates[1], &target).unwrap();
568+
569+
assert_eq!(provider.snapshot_calls.get(), 1);
570+
assert_eq!(
571+
provider.raw_evidence_calls.get(),
572+
2,
573+
"the scan and aggregate evidence for the raw baseline are captured once"
574+
);
575+
}
482576
}

0 commit comments

Comments
 (0)