4444//! resident fence metadata so a probe set faults only the chunk bodies it
4545//! actually touches.
4646
47- use std:: cell:: RefCell ;
47+ use std:: cell:: { Cell , RefCell } ;
4848use std:: collections:: VecDeque ;
4949use std:: rc:: Rc ;
50- use std:: sync:: atomic:: { AtomicBool , Ordering } ;
50+ use std:: sync:: atomic:: { AtomicBool , AtomicU8 , Ordering } ;
5151
5252use columnar:: bytes:: indexed;
5353use columnar:: { Borrow , BorrowedOf , Columnar , Container as _, FromBytes , Index , Len , Push as _} ;
5454use differential_dataflow:: difference:: Semigroup ;
5555use differential_dataflow:: lattice:: Lattice ;
5656use differential_dataflow:: trace:: chunk:: Chunk ;
5757use mz_ore:: cast:: CastFrom ;
58- use mz_ore:: pool:: { ChunkHandle , ChunkHints , ExtentCodec , Pool } ;
58+ use mz_ore:: pool:: { ChunkHandle , ChunkHints , ExtentCodec , IDENTITY_CODEC , Pool } ;
5959use timely:: Accountable ;
6060use timely:: container:: { ContainerBuilder , PushInto } ;
6161use timely:: dataflow:: channels:: ContainerBytes ;
@@ -78,6 +78,11 @@ thread_local! {
7878 /// pool without touching process-global state.
7979 static SPILL_OVERRIDE : RefCell <Option <Pool >> = const { RefCell :: new( None ) } ;
8080
81+ /// A thread-scoped depth-floor override, taking precedence over the
82+ /// global value. Lets tests and benches pin the floor without racing
83+ /// concurrently running tests on the process-global state.
84+ static COMPRESS_MIN_DEPTH_OVERRIDE : Cell <Option <u8 >> = const { Cell :: new( None ) } ;
85+
8186 /// Reusable staging for call-scoped reads of spilled bodies.
8287 static READ_SCRATCH : RefCell <Vec <u64 >> = const { RefCell :: new( Vec :: new( ) ) } ;
8388}
@@ -113,6 +118,51 @@ pub fn set_spill_override(pool: Option<Pool>) {
113118 SPILL_OVERRIDE . with ( |cell| * cell. borrow_mut ( ) = pool) ;
114119}
115120
121+ /// The youngest generational depth whose spilled bodies are compressed. See
122+ /// [`set_compress_min_depth`].
123+ static COMPRESS_MIN_DEPTH : AtomicU8 = AtomicU8 :: new ( DEFAULT_COMPRESS_MIN_DEPTH ) ;
124+
125+ /// Set the youngest generational depth whose spilled bodies are compressed.
126+ ///
127+ /// A chunk at depth `d` is rewritten (merged, extracted, advanced) with
128+ /// frequency proportional to `2^-d` under geometric merging, so compressing
129+ /// a shallow chunk buys a short stay in the pool at the cost of a guaranteed
130+ /// near-term codec round-trip: the body is encoded only to be read back and
131+ /// decoded by the next rewrite. Generations below the floor spill under the
132+ /// identity codec instead: still budgeted and swap-backed like every extent,
133+ /// but encode and decode are copies. The floor never exempts a body from the
134+ /// pool, so it cannot grow unbudgeted resident state.
135+ ///
136+ /// `0` compresses every spilled body. Consulted at every commit, so changes
137+ /// apply to running dataflows.
138+ pub fn set_compress_min_depth ( depth : u8 ) {
139+ COMPRESS_MIN_DEPTH . store ( depth, Ordering :: Relaxed ) ;
140+ }
141+
142+ /// Set or unset a thread-scoped depth-floor override, taking precedence over
143+ /// [`set_compress_min_depth`]. For tests and benches, which run concurrently
144+ /// and must not race on the process-global floor.
145+ pub fn set_compress_min_depth_override ( depth : Option < u8 > ) {
146+ COMPRESS_MIN_DEPTH_OVERRIDE . with ( |cell| cell. set ( depth) ) ;
147+ }
148+
149+ /// The depth floor in effect for this thread's commits.
150+ fn compress_min_depth ( ) -> u8 {
151+ COMPRESS_MIN_DEPTH_OVERRIDE
152+ . with ( |cell| cell. get ( ) )
153+ . unwrap_or_else ( || COMPRESS_MIN_DEPTH . load ( Ordering :: Relaxed ) )
154+ }
155+
156+ /// The codec a body at `depth` stores under: identity below the compression
157+ /// floor, lz4 at and past it.
158+ fn codec_for_depth ( depth : u8 ) -> & ' static dyn ExtentCodec {
159+ if depth < compress_min_depth ( ) {
160+ & IDENTITY_CODEC
161+ } else {
162+ & LZ4_CODEC
163+ }
164+ }
165+
116166/// The pool committed chunks spill to, if any.
117167fn spill_pool ( ) -> Option < Pool > {
118168 if let Some ( pool) = SPILL_OVERRIDE . with ( |cell| cell. borrow ( ) . clone ( ) ) {
@@ -161,6 +211,16 @@ const COMMIT_BYTES: usize = 2 << 20;
161211/// unbudgeted heap, and no accounting here would catch it.
162212const SPILL_MIN_BYTES : usize = 64 << 10 ;
163213
214+ /// The default compression depth floor: fresh (depth 0) bodies spill
215+ /// uncompressed.
216+ ///
217+ /// A fresh chunk is consumed by its first merge with certainty, so
218+ /// compressing it can never save pool bytes for longer than one merge
219+ /// cadence and always costs a full encode plus decode. Depth 1 and beyond
220+ /// have survived a merge and wait geometrically longer for the next, so
221+ /// their compression amortizes.
222+ const DEFAULT_COMPRESS_MIN_DEPTH : u8 = 1 ;
223+
164224/// Whether a column is big enough to commit on its own. A monotone
165225/// threshold, so settle's carry, which grows by whole chunks, cannot step
166226/// over it.
@@ -309,14 +369,20 @@ impl<D: Columnar, T: Columnar, R: Columnar> ColumnChunk<D, T, R> {
309369
310370 /// Spill a non-empty column into `pool` unconditionally, capturing the
311371 /// resident fence metadata.
372+ ///
373+ /// Generations below the compression depth floor store under the
374+ /// identity codec: rewritten too soon for compression to amortize, they
375+ /// stay budgeted and swap-backed while encode and decode reduce to
376+ /// copies.
312377 fn spill_body ( column : Column < ( D , T , R ) > , pool : & Pool , depth : u8 ) -> Self {
378+ let codec = codec_for_depth ( depth) ;
313379 let len_bytes = column. length_in_bytes ( ) ;
314380 let view = column. borrow ( ) ;
315381 let records = view. len ( ) ;
316382 let mut fences = D :: Container :: default ( ) ;
317383 fences. push ( view. 0 . get ( 0 ) ) ;
318384 fences. push ( view. 0 . get ( records - 1 ) ) ;
319- let handle = spill_column ( column, pool, len_bytes, ChunkHints { depth } ) ;
385+ let handle = spill_column ( column, pool, len_bytes, ChunkHints { depth } , codec ) ;
320386 ColumnChunk :: Spilled ( Rc :: new ( SpilledBody {
321387 records,
322388 fences,
@@ -379,13 +445,14 @@ fn spill_column<C: Columnar>(
379445 pool : & Pool ,
380446 len_bytes : usize ,
381447 hints : ChunkHints ,
448+ codec : & ' static dyn ExtentCodec ,
382449) -> ChunkHandle {
383450 mz_ore:: soft_assert_eq_no_log!( len_bytes % 8 , 0 ) ;
384451 match column {
385- Column :: Align ( words) => pool . insert_with ( words . len ( ) , hints , & LZ4_CODEC , |dst| {
386- dst. copy_from_slice ( & words)
387- } ) ,
388- other => pool. insert_with ( len_bytes / 8 , hints, & LZ4_CODEC , |dst| {
452+ Column :: Align ( words) => {
453+ pool . insert_with ( words . len ( ) , hints , codec , | dst| dst . copy_from_slice ( & words) )
454+ }
455+ other => pool. insert_with ( len_bytes / 8 , hints, codec , |dst| {
389456 let bytes: & mut [ u8 ] = bytemuck:: cast_slice_mut ( dst) ;
390457 let mut cursor = std:: io:: Cursor :: new ( bytes) ;
391458 other. into_bytes ( & mut cursor) ;
@@ -1701,6 +1768,39 @@ mod tests {
17011768 set_spill_override ( None ) ;
17021769 }
17031770
1771+ /// The compression depth floor picks the codec, not whether a body
1772+ /// spills: shallow generations store at identity, the floor and deeper
1773+ /// at lz4, and every depth spills and round-trips.
1774+ #[ mz_ore:: test]
1775+ fn spill_codec_depth_floor ( ) {
1776+ set_spill_override ( Some ( test_pool ( ) ) ) ;
1777+ set_compress_min_depth_override ( Some ( 2 ) ) ;
1778+ // Codec identity via Debug: ZST statics and dyn vtables make
1779+ // pointer comparison unreliable.
1780+ let codec_name = |depth : u8 | format ! ( "{:?}" , codec_for_depth( depth) ) ;
1781+ assert_eq ! ( codec_name( 0 ) , "IdentityCodec" ) ;
1782+ assert_eq ! ( codec_name( 1 ) , "IdentityCodec" ) ;
1783+ assert_eq ! ( codec_name( 2 ) , "Lz4Codec" ) ;
1784+ assert_eq ! ( codec_name( u8 :: MAX ) , "Lz4Codec" ) ;
1785+
1786+ let data: Vec < Tuple > = ( 0 ..20_000u64 ) . map ( |i| ( ( i, 0 ) , 0 , 1i64 ) ) . collect ( ) ;
1787+ let data = consolidate ( data) ;
1788+ let column = build_column ( & data) ;
1789+ for depth in [ 0u8 , 1 , 2 , 3 ] {
1790+ let chunk = TestChunk :: commit ( column. clone ( ) , depth) ;
1791+ assert ! ( chunk. is_spilled( ) , "depth {depth} must spill" ) ;
1792+ assert_eq ! ( collect_column( & chunk. into_column( ) ) , data) ;
1793+ }
1794+ set_spill_override ( None ) ;
1795+ set_compress_min_depth_override ( None ) ;
1796+
1797+ // The default floor stores only fresh (depth 0) bodies at identity.
1798+ set_compress_min_depth_override ( Some ( DEFAULT_COMPRESS_MIN_DEPTH ) ) ;
1799+ assert_eq ! ( codec_name( 0 ) , "IdentityCodec" ) ;
1800+ assert_eq ! ( codec_name( 1 ) , "Lz4Codec" ) ;
1801+ set_compress_min_depth_override ( None ) ;
1802+ }
1803+
17041804 /// The compute and storage spill gates compose as an OR: either gate
17051805 /// routes commits to the installed pool, and each setter writes only its
17061806 /// own gate.
@@ -1732,6 +1832,7 @@ mod tests {
17321832 assert ! ( commit( & col) , "the compute gate alone spills" ) ;
17331833 set_compute_spill_enabled ( false ) ;
17341834 assert ! ( !commit( & col) , "both gates off again" ) ;
1835+ set_compress_min_depth_override ( None ) ;
17351836 }
17361837
17371838 /// Re-spilling an already-serialized body exercises the `Column::Align`
0 commit comments