@@ -57,10 +57,11 @@ use differential_dataflow::trace::chunk::Chunk;
5757use mz_ore:: cast:: CastFrom ;
5858use mz_ore:: pool:: { ChunkHandle , ChunkHints , ExtentCodec , IDENTITY_CODEC , Pool } ;
5959use timely:: Accountable ;
60+ use timely:: PartialOrder ;
6061use timely:: container:: { ContainerBuilder , PushInto } ;
6162use timely:: dataflow:: channels:: ContainerBytes ;
6263use timely:: progress:: Timestamp ;
63- use timely:: progress:: frontier:: AntichainRef ;
64+ use timely:: progress:: frontier:: { Antichain , AntichainRef } ;
6465
6566use crate :: columnar:: batcher:: { ColumnChunker , gallop} ;
6667use crate :: columnar:: unload:: UnloadChunk ;
@@ -243,15 +244,24 @@ fn rr<'b, 'a: 'b, C: Columnar>(item: columnar::Ref<'a, C>) -> columnar::Ref<'b,
243244
244245/// A spilled chunk body: the serialized column in the pool, plus the resident
245246/// metadata every [`Chunk`] must answer without fetching. That metadata is
246- /// the record count and the first and last data items (the fence entries
247- /// [`UnloadChunk::locate`] consults).
248- pub struct SpilledBody < D : Columnar > {
247+ /// the record count, the first and last data items (the fence entries
248+ /// [`UnloadChunk::locate`] consults), and the time bounds `extract` consults
249+ /// to pass frontier-disjoint chunks through without loading them.
250+ pub struct SpilledBody < D : Columnar , T > {
249251 /// Number of updates in the body.
250252 records : usize ,
251253 /// The first and last data items, as a two-element container. One
252254 /// container rather than two singletons, so the leaf allocations are not
253255 /// duplicated per fence.
254256 fences : D :: Container ,
257+ /// The minimal times in the body: a lower bound antichain every
258+ /// contained time is greater-or-equal to. Folded into `extract`'s
259+ /// residual frontier when the chunk is kept whole.
260+ time_lower : Antichain < T > ,
261+ /// The maximal times in the body. Some contained time is
262+ /// greater-or-equal to a frontier exactly when some maximal time is,
263+ /// which is `extract`'s ship-whole test.
264+ time_upper : Vec < T > ,
255265 /// The chunk's generational depth, mirrored into the pool's
256266 /// [`ChunkHints`] at spill time.
257267 depth : u8 ,
@@ -271,7 +281,7 @@ pub enum ColumnChunk<D: Columnar, T: Columnar, R: Columnar> {
271281 /// Body on the heap, shared via `Rc`, with its generational depth.
272282 Resident ( Rc < Column < ( D , T , R ) > > , u8 ) ,
273283 /// Body in the pool. See [`SpilledBody`].
274- Spilled ( Rc < SpilledBody < D > > ) ,
284+ Spilled ( Rc < SpilledBody < D , T > > ) ,
275285}
276286
277287impl < D : Columnar , T : Columnar , R : Columnar > Clone for ColumnChunk < D , T , R > {
@@ -356,7 +366,10 @@ impl<D: Columnar, T: Columnar, R: Columnar> ColumnChunk<D, T, R> {
356366 /// Commit a non-empty column at the given generational depth: spill it to
357367 /// the pool when spilling is on and the body is worth a slot, else keep it
358368 /// resident.
359- fn commit ( column : Column < ( D , T , R ) > , depth : u8 ) -> Self {
369+ fn commit ( column : Column < ( D , T , R ) > , depth : u8 ) -> Self
370+ where
371+ T : Timestamp ,
372+ {
360373 debug_assert ! ( column. borrow( ) . len( ) > 0 , "chunks must be non-empty" ) ;
361374 if let Some ( pool) = spill_pool ( ) {
362375 if column. length_in_bytes ( ) >= SPILL_MIN_BYTES {
@@ -373,9 +386,13 @@ impl<D: Columnar, T: Columnar, R: Columnar> ColumnChunk<D, T, R> {
373386 /// identity codec: rewritten too soon for compression to amortize, they
374387 /// stay budgeted and swap-backed while encode and decode reduce to
375388 /// copies.
376- fn spill_body ( column : Column < ( D , T , R ) > , pool : & Pool , depth : u8 ) -> Self {
389+ fn spill_body ( column : Column < ( D , T , R ) > , pool : & Pool , depth : u8 ) -> Self
390+ where
391+ T : Timestamp ,
392+ {
377393 let codec = codec_for_depth ( depth) ;
378394 let len_bytes = column. length_in_bytes ( ) ;
395+ let ( time_lower, time_upper) = Self :: time_bounds ( & column) ;
379396 let view = column. borrow ( ) ;
380397 let records = view. len ( ) ;
381398 let mut fences = D :: Container :: default ( ) ;
@@ -385,10 +402,48 @@ impl<D: Columnar, T: Columnar, R: Columnar> ColumnChunk<D, T, R> {
385402 ColumnChunk :: Spilled ( Rc :: new ( SpilledBody {
386403 records,
387404 fences,
405+ time_lower,
406+ time_upper,
388407 depth,
389408 handle,
390409 } ) )
391410 }
411+
412+ /// The chunk's time bounds: stored metadata for spilled bodies, a scan
413+ /// of the time column for resident ones. The scan costs less than the
414+ /// copy it lets `extract` avoid when the chunk passes through whole.
415+ fn chunk_time_bounds ( & self ) -> ( Antichain < T > , Vec < T > )
416+ where
417+ T : Timestamp ,
418+ {
419+ match self {
420+ ColumnChunk :: Resident ( col, _) => Self :: time_bounds ( col) ,
421+ ColumnChunk :: Spilled ( body) => ( body. time_lower . clone ( ) , body. time_upper . clone ( ) ) ,
422+ }
423+ }
424+
425+ /// The time bounds of a non-empty column: the antichain of minimal times
426+ /// (every contained time is greater-or-equal to some element) and the
427+ /// set of maximal times (some contained time is greater-or-equal to a
428+ /// frontier exactly when some maximal one is).
429+ fn time_bounds ( column : & Column < ( D , T , R ) > ) -> ( Antichain < T > , Vec < T > )
430+ where
431+ T : Timestamp ,
432+ {
433+ let view = column. borrow ( ) ;
434+ let times = view. 1 ;
435+ let mut lower = Antichain :: new ( ) ;
436+ let mut upper: Vec < T > = Vec :: new ( ) ;
437+ for i in 0 ..times. len ( ) {
438+ let t = T :: into_owned ( rr :: < T > ( times. get ( i) ) ) ;
439+ if !upper. iter ( ) . any ( |u| PartialOrder :: less_equal ( & t, u) ) {
440+ upper. retain ( |u| !PartialOrder :: less_equal ( u, & t) ) ;
441+ upper. push ( t. clone ( ) ) ;
442+ }
443+ lower. insert ( t) ;
444+ }
445+ ( lower, upper)
446+ }
392447}
393448
394449/// Copy a column into a fresh `Typed` column via bulk per-leaf extension.
@@ -592,6 +647,25 @@ where
592647 let Some ( chunk) = input. pop_front ( ) else {
593648 return ;
594649 } ;
650+ // Whole-chunk pass-through from the resident time bounds: a chunk
651+ // the frontier is entirely past ships unchanged, one entirely at or
652+ // past the frontier keeps unchanged. Spilled bodies pass through
653+ // without a load, a re-commit, or any codec work; only chunks the
654+ // frontier actually splits are loaded below.
655+ let ( time_lower, time_upper) = chunk. chunk_time_bounds ( ) ;
656+ if time_upper. iter ( ) . all ( |t| !frontier. less_equal ( t) ) {
657+ ship. push_back ( chunk) ;
658+ return ;
659+ }
660+ if time_lower. elements ( ) . iter ( ) . all ( |m| frontier. less_equal ( m) ) {
661+ // The residual must lower-bound every kept time, which is the
662+ // chunk's lower bound antichain by construction.
663+ for m in time_lower. elements ( ) {
664+ residual. insert ( m. clone ( ) ) ;
665+ }
666+ keep. push_back ( chunk) ;
667+ return ;
668+ }
595669 // Partitioning rewrites within a generation, so both sides keep the
596670 // input chunk's depth.
597671 let depth = chunk. depth ( ) ;
@@ -1469,10 +1543,54 @@ mod tests {
14691543 assert_eq ! ( collect_chunks( out) , consolidate( advanced) ) ;
14701544 }
14711545
1546+ /// Chunks the extract frontier does not split pass through whole from
1547+ /// their resident time bounds: spilled bodies land on their side still
1548+ /// spilled, with no load or re-commit, and a kept chunk's minimal times
1549+ /// feed the residual frontier.
1550+ #[ mz_ore:: test]
1551+ #[ cfg_attr( miri, ignore) ]
1552+ fn extract_passes_frontier_disjoint_chunks_through ( ) {
1553+ set_spill_override ( Some ( test_pool ( ) ) ) ;
1554+ let low: Vec < Tuple > = ( 0 ..20_000u64 ) . map ( |i| ( ( i, 0 ) , i % 4 , 1 ) ) . collect ( ) ;
1555+ let high: Vec < Tuple > = ( 0 ..20_000u64 ) . map ( |i| ( ( i, 0 ) , 6 + i % 4 , 1 ) ) . collect ( ) ;
1556+ let spilled_chunk = |data : & [ Tuple ] | {
1557+ let chunk = TestChunk :: commit ( build_column ( & consolidate ( data. to_vec ( ) ) ) , 1 ) ;
1558+ assert ! ( chunk. is_spilled( ) ) ;
1559+ chunk
1560+ } ;
1561+
1562+ // A frontier between the two chunks' time ranges: the low chunk
1563+ // ships whole and the high chunk keeps whole, both still spilled
1564+ // (no load, no re-commit), and the residual is the kept chunk's
1565+ // minimal time.
1566+ let mut input = VecDeque :: from ( [ spilled_chunk ( & low) , spilled_chunk ( & high) ] ) ;
1567+ let frontier = Antichain :: from_elem ( 5u64 ) ;
1568+ let mut residual = Antichain :: new ( ) ;
1569+ let ( mut keep, mut ship) = ( VecDeque :: new ( ) , VecDeque :: new ( ) ) ;
1570+ while !input. is_empty ( ) {
1571+ TestChunk :: extract (
1572+ & mut input,
1573+ frontier. borrow ( ) ,
1574+ & mut residual,
1575+ & mut keep,
1576+ & mut ship,
1577+ ) ;
1578+ }
1579+ assert_eq ! ( ship. len( ) , 1 ) ;
1580+ assert ! ( ship[ 0 ] . is_spilled( ) , "shipped whole: body untouched" ) ;
1581+ assert_eq ! ( keep. len( ) , 1 ) ;
1582+ assert ! ( keep[ 0 ] . is_spilled( ) , "kept whole: body untouched" ) ;
1583+ assert_eq ! ( residual, Antichain :: from_elem( 6 ) ) ;
1584+ let shipped = ship. pop_front ( ) . unwrap ( ) . into_column ( ) ;
1585+ assert_eq ! ( collect_column( & shipped) , consolidate( low) ) ;
1586+ let kept = keep. pop_front ( ) . unwrap ( ) . into_column ( ) ;
1587+ assert_eq ! ( collect_column( & kept) , consolidate( high) ) ;
1588+ set_spill_override ( None ) ;
1589+ }
1590+
14721591 /// Extracting a large chunk at an intermediate frontier cuts both sides
14731592 /// into several chunks and partitions exactly by time.
14741593 #[ mz_ore:: test]
1475- #[ cfg_attr( miri, ignore) ]
14761594 fn extract_cuts_large_output ( ) {
14771595 let records: Vec < Tuple > = ( 0 ..300_000u64 ) . map ( |k| ( ( k, 0 ) , k % 2 , 1 ) ) . collect ( ) ;
14781596 let mut input = VecDeque :: from ( [ ColumnChunk :: from_column ( build_column ( & records) ) ] ) ;
0 commit comments