@@ -34,11 +34,11 @@ pub struct StreamingPhysicalInputEvidence {
3434 pub active_window_count : u64 ,
3535 /// Completed windows retained for query coverage.
3636 pub retained_window_count : u64 ,
37- /// Independent state instances per window: one for shared
37+ /// Independent summary- state instances per window: one for shared
3838 /// multi-subpopulation state, otherwise the resolved group count.
39- pub physical_sketch_count : u64 ,
39+ pub physical_summary_count : u64 ,
4040 /// Resident bytes of one concrete state instance.
41- pub state_bytes_per_sketch : u64 ,
41+ pub state_bytes_per_summary : u64 ,
4242}
4343
4444/// Workload-normalized inputs for incremental maintenance over one finite
@@ -54,8 +54,8 @@ pub struct StreamingSummaryInputs {
5454 pub horizon_ms : u64 ,
5555 pub active_window_count : u64 ,
5656 pub retained_window_count : u64 ,
57- pub physical_sketch_count : u64 ,
58- pub state_bytes_per_sketch : u64 ,
57+ pub physical_summary_count : u64 ,
58+ pub state_bytes_per_summary : u64 ,
5959 pub evaluation_count : u64 ,
6060}
6161
@@ -101,8 +101,8 @@ impl StreamingSummaryInputs {
101101 horizon_ms,
102102 active_window_count : physical. active_window_count ,
103103 retained_window_count : physical. retained_window_count ,
104- physical_sketch_count : physical. physical_sketch_count ,
105- state_bytes_per_sketch : physical. state_bytes_per_sketch ,
104+ physical_summary_count : physical. physical_summary_count ,
105+ state_bytes_per_summary : physical. state_bytes_per_summary ,
106106 evaluation_count : evaluations_in_horizon (
107107 & query. recurrence ,
108108 planning_time_ms,
@@ -119,19 +119,26 @@ impl StreamingSummaryInputs {
119119 ) ) ;
120120 }
121121 for ( name, value) in [
122- ( "initial_input_rows" , self . initial_input_rows ) ,
123- ( "initial_input_bytes" , self . initial_input_bytes ) ,
124122 ( "horizon_ms" , self . horizon_ms ) ,
125123 ( "active_window_count" , self . active_window_count ) ,
126- ( "retained_window_count" , self . retained_window_count ) ,
127- ( "physical_sketch_count" , self . physical_sketch_count ) ,
128- ( "state_bytes_per_sketch" , self . state_bytes_per_sketch ) ,
124+ ( "physical_summary_count" , self . physical_summary_count ) ,
125+ ( "state_bytes_per_summary" , self . state_bytes_per_summary ) ,
129126 ( "evaluation_count" , self . evaluation_count ) ,
130127 ] {
131128 if value == 0 {
132129 return Err ( AnalyticalCostError :: MissingOrZero ( name) ) ;
133130 }
134131 }
132+ let bootstrap_is_consistent = if self . initial_input_rows == 0 {
133+ self . initial_input_bytes == 0 && self . initial_source_scan_bytes == 0
134+ } else {
135+ self . initial_input_bytes > 0 && self . initial_source_scan_bytes > 0
136+ } ;
137+ if !bootstrap_is_consistent {
138+ return Err ( AnalyticalCostError :: InconsistentOperatorStatistics (
139+ "streaming bootstrap rows, logical bytes, and source bytes disagree" ,
140+ ) ) ;
141+ }
135142 if !self . ingestion_rate_per_second . is_finite ( ) || self . ingestion_rate_per_second < 0.0 {
136143 return Err ( AnalyticalCostError :: InvalidIngestionRate (
137144 self . ingestion_rate_per_second ,
@@ -176,7 +183,10 @@ pub fn estimate_incremental_summary_maintenance(
176183 validate_guarantee ( guarantee, inputs. data_arrival ) ?;
177184 let arriving_input_rows = arriving_rows_for_lifecycle ( inputs, guarantee) ?;
178185 let counts = count_operations ( root) ?;
179- if counts. state_builds == 0 {
186+ if counts. state_builds != 1 {
187+ // One flat evidence record cannot safely describe several summary
188+ // nodes with different inputs, state sizes, or algorithms. Complete
189+ // multi-node streaming DAGs use per-node evidence instead.
180190 return Err ( AnalyticalCostError :: UnsupportedCandidate ) ;
181191 }
182192
@@ -200,19 +210,19 @@ pub fn estimate_incremental_summary_maintenance(
200210
201211 let build_inserts = inputs
202212 . initial_input_rows
203- . checked_mul ( counts. state_builds )
213+ . checked_mul ( inputs. active_window_count )
214+ . and_then ( |n| n. checked_mul ( counts. state_builds ) )
204215 . ok_or ( AnalyticalCostError :: Overflow ) ?;
205216 let update_inserts = arriving_input_rows
206217 . checked_mul ( inputs. active_window_count )
207218 . and_then ( |n| n. checked_mul ( counts. state_builds ) )
208219 . ok_or ( AnalyticalCostError :: Overflow ) ?;
209- let instances = inputs. physical_sketch_count as f64 ;
220+ let instances = inputs. physical_summary_count as f64 ;
210221 let evaluations = inputs. evaluation_count as f64 ;
211- let updates = arriving_input_rows as f64 ;
212222 let cpu_ops = ( build_inserts as f64 + update_inserts as f64 ) * insert
213223 + evaluations * counts. merges_per_read as f64 * instances * merge
214224 + evaluations * counts. subtracts_per_read as f64 * instances * subtract
215- + updates * counts. deletes_per_update as f64 * instances * delete
225+ + update_inserts as f64 * counts. deletes_per_update as f64 * delete
216226 + evaluations * counts. readouts_per_read as f64 * instances * readout;
217227 if !cpu_ops. is_finite ( ) {
218228 return Err ( AnalyticalCostError :: Overflow ) ;
@@ -221,26 +231,30 @@ pub fn estimate_incremental_summary_maintenance(
221231 let state_instances = inputs
222232 . active_window_count
223233 . checked_add ( inputs. retained_window_count )
224- . and_then ( |n| n. checked_mul ( inputs. physical_sketch_count ) )
234+ . and_then ( |n| n. checked_mul ( inputs. physical_summary_count ) )
225235 . and_then ( |n| n. checked_mul ( counts. state_builds ) )
226236 . ok_or ( AnalyticalCostError :: Overflow ) ?;
227237 let retained_bytes = state_instances
228- . checked_mul ( inputs. state_bytes_per_sketch )
238+ . checked_mul ( inputs. state_bytes_per_summary )
229239 . ok_or ( AnalyticalCostError :: Overflow ) ?;
230240 // Merge/subtract may stream over persistent inputs but still needs one
231241 // result state per physical instance. Persistent retained windows are
232242 // already included above and are not loaded a second time.
233243 let transient_bytes = if counts. merges_per_read > 0 || counts. subtracts_per_read > 0 {
234244 inputs
235- . physical_sketch_count
236- . checked_mul ( inputs. state_bytes_per_sketch )
245+ . physical_summary_count
246+ . checked_mul ( inputs. state_bytes_per_summary )
237247 . ok_or ( AnalyticalCostError :: Overflow ) ?
238248 } else {
239249 0
240250 } ;
241- let bootstrap_row_buffer = inputs
242- . initial_input_bytes
243- . div_ceil ( inputs. initial_input_rows ) ;
251+ let bootstrap_row_buffer = if inputs. initial_input_rows == 0 {
252+ 0
253+ } else {
254+ inputs
255+ . initial_input_bytes
256+ . div_ceil ( inputs. initial_input_rows )
257+ } ;
244258 Ok ( ResourceEstimate {
245259 cpu_ops,
246260 peak_memory_bytes : retained_bytes
@@ -269,14 +283,13 @@ fn arriving_rows_for_lifecycle(
269283 }
270284 let start = activate_at. 0 . max ( inputs. planning_time_ms ) ;
271285 let end = retire_at. 0 . min ( horizon_end) ;
272- end. saturating_sub ( start)
273- }
274- SummaryMaintenanceLifecycle :: Shared { retention } => {
275- if retention. 0 < inputs. horizon_ms {
286+ let active_ms = end. saturating_sub ( start) ;
287+ if active_ms == 0 {
276288 return Err ( AnalyticalCostError :: IncompatibleLifecycleGuarantee ) ;
277289 }
278- inputs . horizon_ms
290+ active_ms
279291 }
292+ SummaryMaintenanceLifecycle :: Shared { .. } => inputs. horizon_ms ,
280293 SummaryMaintenanceLifecycle :: ContinuouslyMaintained => inputs. horizon_ms ,
281294 SummaryMaintenanceLifecycle :: Ephemeral => {
282295 return Err ( AnalyticalCostError :: IncompatibleLifecycleGuarantee )
@@ -306,7 +319,7 @@ fn validate_guarantee(
306319
307320fn required_cpu ( name : & ' static str , value : Option < f64 > ) -> Result < f64 , AnalyticalCostError > {
308321 let value = value. ok_or ( AnalyticalCostError :: MissingOrStale ( name) ) ?;
309- if !value. is_finite ( ) || value < 0.0 {
322+ if !value. is_finite ( ) || value <= 0.0 {
310323 return Err ( AnalyticalCostError :: InvalidOperationCost ( name, value) ) ;
311324 }
312325 Ok ( value)
@@ -412,8 +425,8 @@ mod tests {
412425 initial_source_scan_bytes : 640 ,
413426 active_window_count : 2 ,
414427 retained_window_count : 3 ,
415- physical_sketch_count : 2 ,
416- state_bytes_per_sketch : 100 ,
428+ physical_summary_count : 2 ,
429+ state_bytes_per_summary : 100 ,
417430 }
418431 }
419432
@@ -500,8 +513,8 @@ mod tests {
500513 horizon_ms : 5_000 ,
501514 active_window_count : 2 ,
502515 retained_window_count : 3 ,
503- physical_sketch_count : 2 ,
504- state_bytes_per_sketch : 100 ,
516+ physical_summary_count : 2 ,
517+ state_bytes_per_summary : 100 ,
505518 evaluation_count : 5 ,
506519 } ,
507520 SummaryOperationCpuEvidence {
@@ -511,8 +524,9 @@ mod tests {
511524 } ,
512525 )
513526 . unwrap ( ) ;
514- // 10 bootstrap + 10 arrivals into two active windows; two states read 5 times.
515- assert_eq ! ( estimate. cpu_ops, 90.0 ) ;
527+ // 10 bootstrap + 10 arrivals, each routed to two active windows;
528+ // two physical summary instances are read 5 times.
529+ assert_eq ! ( estimate. cpu_ops, 110.0 ) ;
516530 assert_eq ! ( estimate. peak_memory_bytes, 1_000 ) ;
517531 assert_eq ! ( estimate. scan_bytes, 640 ) ;
518532 }
@@ -532,8 +546,8 @@ mod tests {
532546 horizon_ms : 1_000 ,
533547 active_window_count : 1 ,
534548 retained_window_count : 2 ,
535- physical_sketch_count : 2 ,
536- state_bytes_per_sketch : 10 ,
549+ physical_summary_count : 2 ,
550+ state_bytes_per_summary : 10 ,
537551 evaluation_count : 3 ,
538552 } ,
539553 SummaryOperationCpuEvidence {
@@ -545,7 +559,7 @@ mod tests {
545559 } ,
546560 )
547561 . unwrap ( ) ;
548- assert_eq ! ( estimate. cpu_ops, 5.0 + 40 .0 + 12 .0 + 18 .0 + 42.0 ) ;
562+ assert_eq ! ( estimate. cpu_ops, 5.0 + 12 .0 + 18 .0 + 20 .0 + 42.0 ) ;
549563 // Three persistent windows plus one transient result, for two instances.
550564 assert_eq ! ( estimate. peak_memory_bytes, 80 ) ;
551565 }
@@ -568,8 +582,8 @@ mod tests {
568582 horizon_ms: 1_000 ,
569583 active_window_count: 1 ,
570584 retained_window_count: 1 ,
571- physical_sketch_count : 1 ,
572- state_bytes_per_sketch : 8 ,
585+ physical_summary_count : 1 ,
586+ state_bytes_per_summary : 8 ,
573587 evaluation_count: 1 ,
574588 } ,
575589 SummaryOperationCpuEvidence {
@@ -598,8 +612,8 @@ mod tests {
598612 horizon_ms: 1_000 ,
599613 active_window_count: 1 ,
600614 retained_window_count: 1 ,
601- physical_sketch_count : 1 ,
602- state_bytes_per_sketch : 8 ,
615+ physical_summary_count : 1 ,
616+ state_bytes_per_summary : 8 ,
603617 evaluation_count: 1 ,
604618 } ,
605619 SummaryOperationCpuEvidence {
@@ -632,8 +646,8 @@ mod tests {
632646 horizon_ms: 1_000 ,
633647 active_window_count: 1 ,
634648 retained_window_count: 1 ,
635- physical_sketch_count : 1 ,
636- state_bytes_per_sketch : 8 ,
649+ physical_summary_count : 1 ,
650+ state_bytes_per_summary : 8 ,
637651 evaluation_count: 1 ,
638652 } ,
639653 SummaryOperationCpuEvidence {
@@ -670,22 +684,22 @@ mod tests {
670684 horizon_ms : 5_000 ,
671685 active_window_count : 1 ,
672686 retained_window_count : 1 ,
673- physical_sketch_count : 1 ,
674- state_bytes_per_sketch : 8 ,
687+ physical_summary_count : 1 ,
688+ state_bytes_per_summary : 8 ,
675689 evaluation_count : 1 ,
676690 } ,
677691 SummaryOperationCpuEvidence {
678692 insert_cpu_ops : Some ( 1.0 ) ,
679- readout_cpu_ops : Some ( 0 .0) ,
693+ readout_cpu_ops : Some ( 1 .0) ,
680694 ..SummaryOperationCpuEvidence :: default ( )
681695 } ,
682696 )
683697 . unwrap ( ) ;
684- assert_eq ! ( estimate. cpu_ops, 12 .0) ; // 10 bootstrap + 2 updates in [1s, 2s] .
698+ assert_eq ! ( estimate. cpu_ops, 13 .0) ; // 10 bootstrap + 2 updates + one read .
685699 }
686700
687701 #[ test]
688- fn shared_retention_must_cover_the_comparison_horizon ( ) {
702+ fn shared_retention_is_not_confused_with_the_planning_horizon ( ) {
689703 let guarantee = SummaryMaintenanceLifecycleGuarantee {
690704 summary_maintenance_lifecycle : SummaryMaintenanceLifecycle :: Shared {
691705 retention : asap_types:: workload:: DurationMs ( 999 ) ,
@@ -694,32 +708,62 @@ mod tests {
694708 evaluation_schedule : EvaluationSchedule :: PerUpdate ,
695709 output_representation : OutputRepresentation :: SummaryState ,
696710 } ;
697- assert_eq ! (
698- estimate_incremental_summary_maintenance(
699- & summary_with_operations( false , false , false ) ,
700- & guarantee,
701- StreamingSummaryInputs {
702- data_arrival: DataArrival :: ContinuouslyIngesting ,
703- initial_input_rows: 1 ,
704- initial_input_bytes: 8 ,
705- initial_source_scan_bytes: 8 ,
706- ingestion_rate_per_second: 1.0 ,
707- planning_time_ms: 0 ,
708- horizon_ms: 1_000 ,
709- active_window_count: 1 ,
710- retained_window_count: 1 ,
711- physical_sketch_count: 1 ,
712- state_bytes_per_sketch: 8 ,
713- evaluation_count: 1 ,
714- } ,
715- SummaryOperationCpuEvidence {
716- insert_cpu_ops: Some ( 1.0 ) ,
717- readout_cpu_ops: Some ( 1.0 ) ,
718- ..SummaryOperationCpuEvidence :: default ( )
719- } ,
720- ) ,
721- Err ( AnalyticalCostError :: IncompatibleLifecycleGuarantee )
722- ) ;
711+ assert ! ( estimate_incremental_summary_maintenance(
712+ & summary_with_operations( false , false , false ) ,
713+ & guarantee,
714+ StreamingSummaryInputs {
715+ data_arrival: DataArrival :: ContinuouslyIngesting ,
716+ initial_input_rows: 1 ,
717+ initial_input_bytes: 8 ,
718+ initial_source_scan_bytes: 8 ,
719+ ingestion_rate_per_second: 1.0 ,
720+ planning_time_ms: 0 ,
721+ horizon_ms: 1_000 ,
722+ active_window_count: 1 ,
723+ retained_window_count: 1 ,
724+ physical_summary_count: 1 ,
725+ state_bytes_per_summary: 8 ,
726+ evaluation_count: 1 ,
727+ } ,
728+ SummaryOperationCpuEvidence {
729+ insert_cpu_ops: Some ( 1.0 ) ,
730+ readout_cpu_ops: Some ( 1.0 ) ,
731+ ..SummaryOperationCpuEvidence :: default ( )
732+ } ,
733+ )
734+ . is_ok( ) ) ;
735+ }
736+
737+ #[ test]
738+ fn an_empty_bootstrap_is_valid_for_a_new_stream ( ) {
739+ let estimate = estimate_incremental_summary_maintenance (
740+ & summary_with_operations ( false , false , false ) ,
741+ & continuous_guarantee ( ) ,
742+ StreamingSummaryInputs {
743+ data_arrival : DataArrival :: ContinuouslyIngesting ,
744+ initial_input_rows : 0 ,
745+ initial_input_bytes : 0 ,
746+ initial_source_scan_bytes : 0 ,
747+ ingestion_rate_per_second : 1.0 ,
748+ planning_time_ms : 0 ,
749+ horizon_ms : 1_000 ,
750+ active_window_count : 1 ,
751+ retained_window_count : 0 ,
752+ physical_summary_count : 1 ,
753+ state_bytes_per_summary : 8 ,
754+ evaluation_count : 1 ,
755+ } ,
756+ SummaryOperationCpuEvidence {
757+ insert_cpu_ops : Some ( 1.0 ) ,
758+ readout_cpu_ops : Some ( 1.0 ) ,
759+ ..SummaryOperationCpuEvidence :: default ( )
760+ } ,
761+ )
762+ . unwrap ( ) ;
763+
764+ assert_eq ! ( estimate. cpu_ops, 2.0 ) ;
765+ assert_eq ! ( estimate. peak_memory_bytes, 8 ) ;
766+ assert_eq ! ( estimate. scan_bytes, 0 ) ;
723767 }
724768
725769 fn summary_with_operations ( merge : bool , subtract : bool , delete : bool ) -> Rc < SummaryNode > {
0 commit comments