Skip to content

Commit 673e74a

Browse files
committed
refactor(cost): share physical DAG evidence and split summary cost
1 parent 02a3fac commit 673e74a

3 files changed

Lines changed: 414 additions & 399 deletions

File tree

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
use super::*;
2+
3+
/// Physical evidence that is not represented by [`DataWorkload`] for one
4+
/// incrementally maintained summary deployment. Window counts describe the
5+
/// already-selected physical deployment; this layer does not define another
6+
/// tumbling/sliding policy enum.
7+
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
8+
pub struct StreamingPhysicalInputEvidence {
9+
/// Logical bytes in the snapshot used to bootstrap the state.
10+
pub initial_input_bytes: u64,
11+
/// Source bytes read while bootstrapping. Arriving stream bytes are not a
12+
/// disk scan and are therefore excluded.
13+
pub initial_source_scan_bytes: u64,
14+
/// Simultaneously open windows receiving each arriving item.
15+
pub active_window_count: u64,
16+
/// Window/state partitions receiving each bootstrap row.
17+
pub bootstrap_window_count: u64,
18+
/// Completed windows retained for query coverage.
19+
pub retained_window_count: u64,
20+
/// Independent state instances per window: one for shared
21+
/// multi-subpopulation state, otherwise the resolved group count.
22+
pub physical_summary_count: u64,
23+
/// Resident bytes of one concrete state instance.
24+
pub state_bytes_per_summary: u64,
25+
}
26+
27+
/// Workload-normalized inputs for incremental maintenance over one finite
28+
/// comparison horizon.
29+
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
30+
pub struct StreamingSummaryInputs {
31+
pub initial_input_rows: u64,
32+
pub initial_input_bytes: u64,
33+
pub initial_source_scan_bytes: u64,
34+
pub ingestion_rate_per_second: f64,
35+
pub active_window_count: u64,
36+
pub bootstrap_window_count: u64,
37+
pub retained_window_count: u64,
38+
pub physical_summary_count: u64,
39+
pub state_bytes_per_summary: u64,
40+
}
41+
42+
impl StreamingSummaryInputs {
43+
/// Resolve snapshot size, arriving rows, and reads from the canonical
44+
/// workload. Positive fractional expected work rounds up conservatively.
45+
///
46+
/// `Mixed` fails closed because today's workload schema cannot distinguish
47+
/// its at-rest backlog from its continuing-arrival cardinality.
48+
pub fn from_workload(
49+
physical: StreamingPhysicalInputEvidence,
50+
data: &DataWorkload,
51+
scope: &ComparisonScope,
52+
) -> Result<Self, AnalyticalCostError> {
53+
let _ = scope.validate()?;
54+
if data.arrival != DataArrival::ContinuouslyIngesting || scope.data_arrival != data.arrival
55+
{
56+
return Err(AnalyticalCostError::UnsupportedDataArrival(data.arrival));
57+
}
58+
let initial_input_rows = data
59+
.input_cardinality
60+
.value_at(scope.planning_time.0)
61+
.copied()
62+
.ok_or(AnalyticalCostError::MissingOrStale("input_cardinality"))?;
63+
let ingestion_rate = data
64+
.ingestion_rate
65+
.value_at(scope.planning_time.0)
66+
.copied()
67+
.ok_or(AnalyticalCostError::MissingOrStale("ingestion_rate"))?;
68+
if !ingestion_rate.0.is_finite() || ingestion_rate.0 < 0.0 {
69+
return Err(AnalyticalCostError::InvalidIngestionRate(ingestion_rate.0));
70+
}
71+
Self {
72+
initial_input_rows,
73+
initial_input_bytes: physical.initial_input_bytes,
74+
initial_source_scan_bytes: physical.initial_source_scan_bytes,
75+
ingestion_rate_per_second: ingestion_rate.0,
76+
active_window_count: physical.active_window_count,
77+
bootstrap_window_count: physical.bootstrap_window_count,
78+
retained_window_count: physical.retained_window_count,
79+
physical_summary_count: physical.physical_summary_count,
80+
state_bytes_per_summary: physical.state_bytes_per_summary,
81+
}
82+
.validate()
83+
}
84+
85+
pub fn validate(self) -> Result<Self, AnalyticalCostError> {
86+
for (name, value) in [
87+
("active_window_count", self.active_window_count),
88+
("bootstrap_window_count", self.bootstrap_window_count),
89+
("physical_summary_count", self.physical_summary_count),
90+
("state_bytes_per_summary", self.state_bytes_per_summary),
91+
] {
92+
if value == 0 {
93+
return Err(AnalyticalCostError::MissingOrZero(name));
94+
}
95+
}
96+
if (self.initial_input_rows == 0) != (self.initial_input_bytes == 0)
97+
|| (self.initial_input_rows == 0 && self.initial_source_scan_bytes != 0)
98+
{
99+
return Err(AnalyticalCostError::InconsistentBootstrapEvidence);
100+
}
101+
if !self.ingestion_rate_per_second.is_finite() || self.ingestion_rate_per_second < 0.0 {
102+
return Err(AnalyticalCostError::InvalidIngestionRate(
103+
self.ingestion_rate_per_second,
104+
));
105+
}
106+
Ok(self)
107+
}
108+
}
109+
110+
/// CPU operations for one concrete state operation on one state instance.
111+
/// Missing evidence is legal only when the selected summary DAG does not use
112+
/// that operation.
113+
#[cfg(test)]
114+
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
115+
pub struct SummaryOperationCpuEvidence {
116+
pub insert_cpu_ops: Option<f64>,
117+
pub merge_cpu_ops: Option<f64>,
118+
pub subtract_cpu_ops: Option<f64>,
119+
pub delete_cpu_ops: Option<f64>,
120+
/// Expirations/retractions routed to this DAG per second. Required only
121+
/// when an explicit `SummaryDelete` is present.
122+
pub delete_events_per_second: Option<f64>,
123+
/// Concrete state instances touched by one delete event.
124+
pub delete_routing_fanout: Option<u64>,
125+
pub readout_cpu_ops: Option<f64>,
126+
}
127+
128+
/// Physical evidence for one `SummaryJoin` implementation. Total work,
129+
/// cardinality, and memory cannot be inferred from the logical join key alone.
130+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131+
pub struct SummaryJoinEvidence {
132+
pub physical_id: String,
133+
pub inputs: Vec<EdgeStatistics>,
134+
pub output: EdgeStatistics,
135+
/// Total build, probe, match-production, and output CPU for one complete
136+
/// execution of the selected physical join algorithm.
137+
pub cpu_ops_per_execution: f64,
138+
pub working_memory_bytes: u64,
139+
pub output_buffer_bytes: u64,
140+
pub executions_per_evaluation: u64,
141+
pub io_bytes_per_execution: Option<u64>,
142+
}
143+
144+
#[derive(Debug, Clone, PartialEq)]
145+
pub struct StreamingAggregateEvidence {
146+
pub physical_id: String,
147+
pub input: EdgeStatistics,
148+
pub output: EdgeStatistics,
149+
/// Index into `ComparisonScope.sources` when this state bootstraps directly
150+
/// from storage. `None` means its input is an already-materialized child
151+
/// edge and therefore has no additional source read.
152+
pub source_coverage_index: Option<usize>,
153+
/// Provider-owned identity of the physical bootstrap read. Equal source
154+
/// coverage alone does not prove two independent builds share I/O.
155+
pub bootstrap_read_identity: String,
156+
pub inputs: StreamingSummaryInputs,
157+
/// CPU operations to insert one routed row into one state instance.
158+
pub insert_cpu_ops: f64,
159+
}
160+
161+
#[derive(Debug, Clone, PartialEq)]
162+
pub struct SummaryOperatorResourceEvidence {
163+
pub physical_id: String,
164+
pub inputs: Vec<EdgeStatistics>,
165+
pub output: EdgeStatistics,
166+
pub cpu_ops: f64,
167+
pub working_memory_bytes: u64,
168+
pub output_buffer_bytes: u64,
169+
/// Executions of this physical operator for one query evaluation.
170+
/// This is provider evidence, not inferred from a descendant state.
171+
pub executions_per_evaluation: u64,
172+
pub io_bytes_per_execution: Option<u64>,
173+
}
174+
175+
/// Evidence is structured by logical summary operation so delete-only facts
176+
/// cannot be attached to merge, subtract, or readout nodes.
177+
#[derive(Debug, Clone, PartialEq)]
178+
pub enum StreamingSummaryOperatorEvidence {
179+
Merge(SummaryOperatorResourceEvidence),
180+
Subtract(SummaryOperatorResourceEvidence),
181+
Delete {
182+
resource: SummaryOperatorResourceEvidence,
183+
events_per_second: f64,
184+
routing_fanout: u64,
185+
},
186+
Readout(SummaryOperatorResourceEvidence),
187+
}
188+
189+
impl StreamingSummaryOperatorEvidence {
190+
pub(super) fn resource(&self) -> &SummaryOperatorResourceEvidence {
191+
match self {
192+
Self::Merge(resource)
193+
| Self::Subtract(resource)
194+
| Self::Delete { resource, .. }
195+
| Self::Readout(resource) => resource,
196+
}
197+
}
198+
199+
#[cfg(test)]
200+
pub(super) fn resource_mut(&mut self) -> &mut SummaryOperatorResourceEvidence {
201+
match self {
202+
Self::Merge(resource)
203+
| Self::Subtract(resource)
204+
| Self::Delete { resource, .. }
205+
| Self::Readout(resource) => resource,
206+
}
207+
}
208+
}
209+
210+
/// Non-aggregation work for a retained pre-ASAP subtree over the comparison
211+
/// horizon. Bootstrap/source I/O belongs exclusively to the owning aggregate,
212+
/// and summary insertion belongs exclusively to its insert evidence.
213+
#[derive(Debug, Clone, PartialEq)]
214+
pub struct StreamingRetainedQueryEvidence {
215+
pub physical_id: String,
216+
/// Logical output edge consumed by the parent summary operator.
217+
pub output: EdgeStatistics,
218+
pub preprocessing_cpu_ops_over_horizon: f64,
219+
/// Execution workspace, excluding the separately declared output buffer.
220+
pub working_memory_bytes: u64,
221+
pub output_buffer_bytes: u64,
222+
}
223+
224+
/// Physical evidence bound to the selected DAG's `Rc` identity. A copied,
225+
/// structurally equal node is not silently treated as the same deployment.
226+
#[derive(Debug, Clone, Default)]
227+
pub struct StreamingNodeEvidence {
228+
pub(super) aggregations: HashMap<*const SummaryNode, StreamingAggregateEvidence>,
229+
pub(super) joins: HashMap<*const SummaryNode, SummaryJoinEvidence>,
230+
pub(super) operations: HashMap<*const SummaryNode, StreamingSummaryOperatorEvidence>,
231+
pub(super) operation_state_owners: HashMap<*const SummaryNode, *const SummaryNode>,
232+
pub(super) retained_queries: HashMap<*const SummaryNode, StreamingRetainedQueryEvidence>,
233+
}
234+
235+
impl StreamingNodeEvidence {
236+
pub fn insert_aggregation(
237+
&mut self,
238+
node: &Rc<SummaryNode>,
239+
evidence: StreamingAggregateEvidence,
240+
) {
241+
self.aggregations.insert(Rc::as_ptr(node), evidence);
242+
}
243+
244+
pub fn insert_join(&mut self, node: &Rc<SummaryNode>, evidence: SummaryJoinEvidence) {
245+
self.joins.insert(Rc::as_ptr(node), evidence);
246+
}
247+
248+
pub fn insert_operation(
249+
&mut self,
250+
node: &Rc<SummaryNode>,
251+
evidence: StreamingSummaryOperatorEvidence,
252+
) {
253+
self.operations.insert(Rc::as_ptr(node), evidence);
254+
}
255+
256+
/// Bind a stateful operation (currently `SummaryDelete`) to the exact
257+
/// aggregation deployment whose active interval it follows.
258+
pub fn insert_state_operation(
259+
&mut self,
260+
node: &Rc<SummaryNode>,
261+
state: &Rc<SummaryNode>,
262+
evidence: StreamingSummaryOperatorEvidence,
263+
) {
264+
self.operations.insert(Rc::as_ptr(node), evidence);
265+
self.operation_state_owners
266+
.insert(Rc::as_ptr(node), Rc::as_ptr(state));
267+
}
268+
269+
pub fn insert_retained_query(
270+
&mut self,
271+
node: &Rc<SummaryNode>,
272+
evidence: StreamingRetainedQueryEvidence,
273+
) {
274+
self.retained_queries.insert(Rc::as_ptr(node), evidence);
275+
}
276+
277+
pub(super) fn aggregation(&self, node: &SummaryNode) -> Option<StreamingAggregateEvidence> {
278+
self.aggregations.get(&(node as *const _)).cloned()
279+
}
280+
}
281+
282+
pub(super) fn summary_operation_evidence<'a>(
283+
node: &SummaryNode,
284+
evidence: &'a StreamingNodeEvidence,
285+
) -> Result<&'a StreamingSummaryOperatorEvidence, AnalyticalCostError> {
286+
let operation = evidence
287+
.operations
288+
.get(&(node as *const _))
289+
.ok_or(AnalyticalCostError::MissingOrStale("summary operation"))?;
290+
let matches = matches!(
291+
(&node.expr, operation),
292+
(
293+
SummaryExpr::SummaryMerge { .. },
294+
StreamingSummaryOperatorEvidence::Merge(_)
295+
) | (
296+
SummaryExpr::SummarySubtract { .. },
297+
StreamingSummaryOperatorEvidence::Subtract(_)
298+
) | (
299+
SummaryExpr::SummaryDelete { .. },
300+
StreamingSummaryOperatorEvidence::Delete { .. }
301+
) | (
302+
SummaryExpr::SummaryEstimate { .. },
303+
StreamingSummaryOperatorEvidence::Readout(_)
304+
)
305+
);
306+
if matches {
307+
Ok(operation)
308+
} else {
309+
Err(AnalyticalCostError::InconsistentOperatorStatistics(
310+
"summary operation evidence kind does not match SummaryExpr",
311+
))
312+
}
313+
}
314+
315+
/// Evidence for recomputing the raw target over the full comparison horizon.
316+
/// Planning-time dimensions describe the initial snapshot. Each scheduled
317+
/// evaluation adds arrivals since planning time; `physical_dag` is therefore
318+
/// a once-counted DAG whose edge statistics already aggregate all evaluations.
319+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320+
pub struct StreamingRawInputEvidence {
321+
pub planning_time_input_rows: u64,
322+
pub planning_time_input_bytes: u64,
323+
pub planning_time_source_scan_bytes: u64,
324+
/// Decoded logical bytes added to operator edges by one arriving row.
325+
pub arriving_logical_row_bytes: u64,
326+
/// Physical storage bytes read for one arriving row. Kept separate from
327+
/// logical width so compression and encoding are not silently conflated.
328+
pub arriving_source_row_bytes: u64,
329+
pub ingestion_rate_per_second: f64,
330+
pub physical_dag: EvidenceBackedPhysicalDag,
331+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Analytical resource cost for incrementally maintained summary deployments.
2+
//!
3+
//! The canonical workload and lifecycle types own deployment semantics. This
4+
//! module only adds physical evidence absent from those schemas: state size,
5+
//! window counts, and per-operation CPU measurements or complexity estimates.
6+
7+
use std::collections::{HashMap, HashSet};
8+
use std::rc::Rc;
9+
10+
use asap_types::post_asap::{
11+
SketchAlgorithm, SummaryExpr, SummaryMaintenanceLifecycle,
12+
SummaryMaintenanceLifecycleGuarantee, SummaryMaintenanceMode, SummaryNode,
13+
};
14+
use asap_types::pre_asap::{
15+
agg_intent::AggIntent, CompareOpKind, InfoMatcher, Predicate, QueryExpr, Source,
16+
};
17+
use asap_types::workload::{DataArrival, DataWorkload, QueryRecurrence, RepeatedDemand};
18+
use serde::{Deserialize, Serialize};
19+
20+
use crate::analytical_cost::ExecutionMultiplicity;
21+
use crate::analytical_cost::{
22+
estimate_physical_dag, AnalyticalCostError, EvidenceBackedPhysicalDag, PhysicalOperator,
23+
ResourceCalibration, ResourceEstimate,
24+
};
25+
#[cfg(test)]
26+
use crate::analytical_cost::{PhysicalDagNode, PhysicalNodeEvidence};
27+
use crate::cost_model::CostedSummaryDeployment;
28+
use crate::cost_model::{Cost, CostModel, DefaultCostModel};
29+
#[cfg(test)]
30+
use crate::physical_operator_statistics::UnaryEdgeStatistics;
31+
use crate::physical_operator_statistics::{ComparisonScope, EdgeStatistics, OperatorStatistics};
32+
use crate::recurrence::CostRate;
33+
use crate::replacement::{Replacement, ReplacementSubDAG, TargetSubDAG};
34+
use crate::summary_maintenance_lifecycle::{
35+
evaluation_schedule, maintenance_mode, SummaryMaintenanceCapabilities,
36+
SummaryMaintenanceLifecycleCostInputs,
37+
};
38+
39+
pub const SUMMARY_MAINTENANCE_COST_MODEL_VERSION: &str = "summary-maintenance-resource-v1";
40+
41+
mod evidence;
42+
mod model;
43+
44+
pub use evidence::*;
45+
pub use model::*;

0 commit comments

Comments
 (0)