1use std::cell::RefCell;
48use std::collections::VecDeque;
49use std::rc::Rc;
50use std::sync::atomic::{AtomicBool, Ordering};
51
52use columnar::bytes::indexed;
53use columnar::{Borrow, BorrowedOf, Columnar, Container as _, FromBytes, Index, Len, Push as _};
54use differential_dataflow::difference::Semigroup;
55use differential_dataflow::lattice::Lattice;
56use differential_dataflow::trace::chunk::Chunk;
57use mz_ore::cast::CastFrom;
58use mz_ore::pool::{ChunkHandle, ChunkHints, ExtentCodec, Pool};
59use timely::Accountable;
60use timely::container::{ContainerBuilder, PushInto};
61use timely::dataflow::channels::ContainerBytes;
62use timely::progress::Timestamp;
63use timely::progress::frontier::AntichainRef;
64
65use crate::columnar::batcher::{ColumnChunker, gallop};
66use crate::columnar::unload::UnloadChunk;
67use crate::columnar::{Column, at_serialized_capacity};
68
69static COMPUTE_SPILL_ENABLED: AtomicBool = AtomicBool::new(false);
71
72static STORAGE_SPILL_ENABLED: AtomicBool = AtomicBool::new(false);
74
75thread_local! {
76 static SPILL_OVERRIDE: RefCell<Option<Pool>> = const { RefCell::new(None) };
80
81 static READ_SCRATCH: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
83}
84
85pub fn set_compute_spill_enabled(enabled: bool) {
99 COMPUTE_SPILL_ENABLED.store(enabled, Ordering::Relaxed);
100}
101
102pub fn set_storage_spill_enabled(enabled: bool) {
106 STORAGE_SPILL_ENABLED.store(enabled, Ordering::Relaxed);
107}
108
109pub fn set_spill_override(pool: Option<Pool>) {
113 SPILL_OVERRIDE.with(|cell| *cell.borrow_mut() = pool);
114}
115
116fn spill_pool() -> Option<Pool> {
118 if let Some(pool) = SPILL_OVERRIDE.with(|cell| cell.borrow().clone()) {
119 return Some(pool);
120 }
121 let enabled = COMPUTE_SPILL_ENABLED.load(Ordering::Relaxed)
122 || STORAGE_SPILL_ENABLED.load(Ordering::Relaxed);
123 if enabled {
124 crate::pool_config::active_pool()
125 } else {
126 None
127 }
128}
129
130const SCRATCH_RETAIN_WORDS: usize = 1 << 18;
134
135fn with_scratch<Out>(f: impl FnOnce(&mut Vec<u64>) -> Out) -> Out {
137 READ_SCRATCH.with(|cell| {
138 let mut scratch = cell.take();
139 scratch.clear();
140 let out = f(&mut scratch);
141 if scratch.capacity() > SCRATCH_RETAIN_WORDS {
142 scratch.clear();
143 scratch.shrink_to_fit();
144 }
145 cell.replace(scratch);
146 out
147 })
148}
149
150const COMMIT_BYTES: usize = 2 << 20;
153
154const SPILL_MIN_BYTES: usize = 64 << 10;
163
164fn at_commit_size<C: Columnar>(column: &Column<C>) -> bool {
168 column.length_in_bytes() >= COMMIT_BYTES - COMMIT_BYTES / 10
169}
170
171fn borrow_words<C: Columnar>(words: &[u64]) -> BorrowedOf<'_, C> {
174 <BorrowedOf<'_, C>>::from_bytes(&mut indexed::decode(words))
175}
176
177#[inline(always)]
181fn rr<'b, 'a: 'b, C: Columnar>(item: columnar::Ref<'a, C>) -> columnar::Ref<'b, C> {
182 columnar::ContainerOf::<C>::reborrow_ref(item)
183}
184
185pub struct SpilledBody<D: Columnar> {
190 records: usize,
192 fences: D::Container,
196 depth: u8,
199 handle: ChunkHandle,
201}
202
203pub enum ColumnChunk<D: Columnar, T: Columnar, R: Columnar> {
212 Resident(Rc<Column<(D, T, R)>>, u8),
214 Spilled(Rc<SpilledBody<D>>),
216}
217
218impl<D: Columnar, T: Columnar, R: Columnar> Clone for ColumnChunk<D, T, R> {
219 fn clone(&self) -> Self {
220 match self {
221 ColumnChunk::Resident(col, depth) => ColumnChunk::Resident(Rc::clone(col), *depth),
222 ColumnChunk::Spilled(body) => ColumnChunk::Spilled(Rc::clone(body)),
223 }
224 }
225}
226
227impl<D: Columnar, T: Columnar, R: Columnar> Default for ColumnChunk<D, T, R> {
228 fn default() -> Self {
229 ColumnChunk::Resident(Rc::new(Column::default()), 0)
230 }
231}
232
233impl<D: Columnar, T: Columnar, R: Columnar> Accountable for ColumnChunk<D, T, R> {
234 fn record_count(&self) -> i64 {
235 i64::try_from(self.records()).expect("record count fits i64")
236 }
237}
238
239impl<D: Columnar, T: Columnar, R: Columnar> ColumnChunk<D, T, R> {
240 pub fn from_column(column: Column<(D, T, R)>) -> Self {
243 mz_ore::soft_assert_no_log!(!column.is_empty(), "chunks must be non-empty");
244 ColumnChunk::Resident(Rc::new(column), 0)
245 }
246
247 pub fn into_column(self) -> Column<(D, T, R)> {
250 match self {
251 ColumnChunk::Resident(col, _) => {
252 Rc::try_unwrap(col).unwrap_or_else(|shared| copy_column(&shared))
253 }
254 ColumnChunk::Spilled(body) => {
255 let mut words = Vec::new();
256 body.handle.read_into(&mut words);
257 Column::Align(words)
258 }
259 }
260 }
261
262 pub fn is_spilled(&self) -> bool {
264 matches!(self, ColumnChunk::Spilled(_))
265 }
266
267 fn records(&self) -> usize {
269 match self {
270 ColumnChunk::Resident(col, _) => col.borrow().len(),
271 ColumnChunk::Spilled(body) => body.records,
272 }
273 }
274
275 fn depth(&self) -> u8 {
277 match self {
278 ColumnChunk::Resident(_, depth) => *depth,
279 ColumnChunk::Spilled(body) => body.depth,
280 }
281 }
282
283 fn data_span(&self) -> (columnar::Ref<'_, D>, columnar::Ref<'_, D>) {
285 match self {
286 ColumnChunk::Resident(col, _) => {
287 let data = col.borrow().0;
288 (data.get(0), data.get(data.len() - 1))
289 }
290 ColumnChunk::Spilled(body) => {
291 let fences = body.fences.borrow();
292 (fences.get(0), fences.get(1))
293 }
294 }
295 }
296
297 fn commit(column: Column<(D, T, R)>, depth: u8) -> Self {
301 mz_ore::soft_assert_no_log!(!column.is_empty(), "chunks must be non-empty");
302 if let Some(pool) = spill_pool() {
303 if column.length_in_bytes() >= SPILL_MIN_BYTES {
304 return Self::spill_body(column, &pool, depth);
305 }
306 }
307 ColumnChunk::Resident(Rc::new(column), depth)
308 }
309
310 fn spill_body(column: Column<(D, T, R)>, pool: &Pool, depth: u8) -> Self {
313 let len_bytes = column.length_in_bytes();
314 let view = column.borrow();
315 let records = view.len();
316 let mut fences = D::Container::default();
317 fences.push(view.0.get(0));
318 fences.push(view.0.get(records - 1));
319 let handle = spill_column(column, pool, len_bytes, ChunkHints { depth });
320 ColumnChunk::Spilled(Rc::new(SpilledBody {
321 records,
322 fences,
323 depth,
324 handle,
325 }))
326 }
327}
328
329fn copy_column<C: Columnar>(column: &Column<C>) -> Column<C> {
331 let view = column.borrow();
332 let mut fresh = C::Container::default();
333 fresh.extend_from_self(view, 0..view.len());
334 Column::Typed(fresh)
335}
336
337#[derive(Debug)]
342pub struct Lz4Codec;
343
344pub static LZ4_CODEC: Lz4Codec = Lz4Codec;
347
348impl ExtentCodec for Lz4Codec {
349 fn encode(&self, body: &[u8], out: &mut Vec<u8>) {
350 let max_out = lz4_flex::block::get_maximum_output_size(body.len());
351 out.resize(4 + max_out, 0);
352 let len = u32::try_from(body.len()).expect("chunk bodies are bounded by the size classes");
353 out[..4].copy_from_slice(&len.to_le_bytes());
354 let compressed = lz4_flex::block::compress_into(body, &mut out[4..])
355 .expect("output sized to the maximum");
356 out.truncate(4 + compressed);
357 }
358
359 fn decode(&self, stored: &[u8], body: &mut [u8]) {
360 let prefix: [u8; 4] = stored[..4].try_into().expect("prefix length");
361 let len = usize::try_from(u32::from_le_bytes(prefix)).expect("length fits usize");
362 assert_eq!(
363 len,
364 body.len(),
365 "destination must match the encoded body length"
366 );
367 let written = lz4_flex::block::decompress_into(&stored[4..], body)
368 .expect("stored bytes hold a valid lz4 block");
369 assert_eq!(written, body.len(), "decoded length mismatch");
370 }
371}
372
373fn spill_column<C: Columnar>(
378 column: Column<C>,
379 pool: &Pool,
380 len_bytes: usize,
381 hints: ChunkHints,
382) -> ChunkHandle {
383 mz_ore::soft_assert_eq_no_log!(len_bytes % 8, 0);
384 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| {
389 let bytes: &mut [u8] = bytemuck::cast_slice_mut(dst);
390 let mut cursor = std::io::Cursor::new(bytes);
391 other.into_bytes(&mut cursor);
392 assert_eq!(
393 usize::try_from(cursor.position()).expect("usize position"),
394 len_bytes,
395 "serialized body must fill the chunk exactly",
396 );
397 }),
398 }
399}
400
401fn to_typed<C: Columnar>(column: Column<C>) -> Column<C> {
405 match column {
406 typed @ Column::Typed(_) => typed,
407 other => copy_column(&other),
408 }
409}
410
411impl<D, T, R> Chunk for ColumnChunk<D, T, R>
412where
413 D: Columnar,
414 for<'a> columnar::Ref<'a, D>: Copy + Ord,
415 T: Columnar + Default + Timestamp + Lattice + Ord,
416 for<'a> columnar::Ref<'a, T>: Copy + Ord,
417 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
418{
419 type Time = T;
420
421 const TARGET: usize = 65536;
427
428 fn len(&self) -> usize {
429 self.records()
430 }
431
432 fn merge(in1: &mut VecDeque<Self>, in2: &mut VecDeque<Self>, out: &mut VecDeque<Self>) {
441 let (a_first, a_last) = in1
446 .front()
447 .expect("caller guarantees non-empty input")
448 .data_span();
449 let (b_first, b_last) = in2
450 .front()
451 .expect("caller guarantees non-empty input")
452 .data_span();
453 let a_low = rr::<D>(a_last) < rr::<D>(b_first);
454 let b_low = rr::<D>(b_last) < rr::<D>(a_first);
455 if a_low {
456 out.push_back(in1.pop_front().expect("front observed above"));
457 return;
458 }
459 if b_low {
460 out.push_back(in2.pop_front().expect("front observed above"));
461 return;
462 }
463
464 let a = in1.pop_front().expect("caller guarantees non-empty input");
465 let b = in2.pop_front().expect("caller guarantees non-empty input");
466 let depths = [a.depth(), b.depth()];
469 let out_depth = depths[0].max(depths[1]).saturating_add(1);
470 let mut spill_a = match &a {
471 ColumnChunk::Spilled(body) => Some(Rc::clone(body)),
472 ColumnChunk::Resident(_, _) => None,
473 };
474 let mut spill_b = match &b {
475 ColumnChunk::Spilled(body) => Some(Rc::clone(body)),
476 ColumnChunk::Resident(_, _) => None,
477 };
478 let mut cols = [a.into_column(), b.into_column()];
479 let mut positions = [0usize, 0usize];
480 loop {
481 let mut result: Column<(D, T, R)> = Column::default();
482 let yielded = result.merge_from(&mut cols, &mut positions);
483 if !result.is_empty() {
484 out.push_back(ColumnChunk::Resident(Rc::new(result), out_depth));
485 }
486 if !yielded {
487 break;
488 }
489 }
490 let [col_a, col_b] = &mut cols;
491 for (col, pos, depth, spilled, queue) in [
495 (col_a, positions[0], depths[0], &mut spill_a, in1),
496 (col_b, positions[1], depths[1], &mut spill_b, in2),
497 ] {
498 let len = col.borrow().len();
499 if pos == 0 && len > 0 {
500 let chunk = match spilled.take() {
503 Some(body) => ColumnChunk::Spilled(body),
504 None => ColumnChunk::Resident(Rc::new(std::mem::take(col)), depth),
505 };
506 queue.push_front(chunk);
507 } else if pos < len {
508 let view = col.borrow();
509 let mut rest = <(D, T, R) as Columnar>::Container::default();
510 rest.extend_from_self(view, pos..len);
511 queue.push_front(ColumnChunk::Resident(Rc::new(Column::Typed(rest)), depth));
512 }
513 }
514 }
515
516 fn extract(
520 input: &mut VecDeque<Self>,
521 frontier: AntichainRef<T>,
522 residual: &mut timely::progress::Antichain<T>,
523 keep: &mut VecDeque<Self>,
524 ship: &mut VecDeque<Self>,
525 ) {
526 let Some(chunk) = input.pop_front() else {
527 return;
528 };
529 let depth = chunk.depth();
532 let mut col = chunk.into_column();
533 let len = col.borrow().len();
534 let mut pos = 0;
535 let mut keep_col: Column<(D, T, R)> = Column::default();
536 let mut ship_col: Column<(D, T, R)> = Column::default();
537 let cut = |col: &mut Column<(D, T, R)>, queue: &mut VecDeque<Self>, force: bool| {
543 if !col.is_empty() && (force || at_serialized_capacity(&col.borrow())) {
544 queue.push_back(ColumnChunk::Resident(Rc::new(std::mem::take(col)), depth));
545 }
546 };
547 while pos < len {
548 col.extract(&mut pos, frontier, residual, &mut keep_col, &mut ship_col);
549 if pos < len {
550 cut(&mut keep_col, keep, false);
551 cut(&mut ship_col, ship, false);
552 }
553 }
554 cut(&mut keep_col, keep, true);
555 cut(&mut ship_col, ship, true);
556 }
557
558 fn advance(
568 input: &mut VecDeque<Self>,
569 frontier: AntichainRef<T>,
570 done: bool,
571 out: &mut VecDeque<Self>,
572 ) {
573 let Some(front) = input.pop_front() else {
574 return;
575 };
576 let mut depth = front.depth();
579 let mut base = to_typed(front.into_column());
583 {
584 let Column::Typed(base_c) = &mut base else {
585 unreachable!("to_typed returns Typed");
586 };
587 for chunk in input.drain(..) {
588 depth = depth.max(chunk.depth());
589 let col = chunk.into_column();
590 let view = col.borrow();
591 base_c.extend_from_self(view, 0..view.len());
592 }
593 }
594 let view = base.borrow();
595 let total = view.len();
596 if total == 0 {
597 return;
598 }
599 let data = view.0;
600
601 if !done && data.get(0) == data.get(total - 1) {
604 input.push_front(ColumnChunk::Resident(Rc::new(base), depth));
605 return;
606 }
607
608 let end = if done {
611 total
612 } else {
613 let last = data.get(total - 1);
614 let mut end = total - 1;
615 while end > 0 && data.get(end - 1) == last {
616 end -= 1;
617 }
618 end
619 };
620
621 let mut result = <(D, T, R) as Columnar>::Container::default();
622 let mut scratch: Vec<(T, R)> = Vec::new();
624 let mut index = 0;
625 const CUT_CHECK_RECORDS: usize = 1024;
634 let mut records_since_check = 0usize;
635 while index < end {
642 let group_d = data.get(index);
643 scratch.clear();
644 while index < end && data.get(index) == group_d {
645 let (_, t, r) = view.get(index);
646 let mut owned_t = T::into_owned(t);
647 owned_t.advance_by(frontier);
648 scratch.push((owned_t, R::into_owned(r)));
649 index += 1;
650 }
651 scratch.sort_by(|a, b| a.0.cmp(&b.0));
652 let mut run = scratch.drain(..).peekable();
653 while let Some((t, mut r)) = run.next() {
654 while run.peek().is_some_and(|(t2, _)| *t2 == t) {
655 let (_, r2) = run.next().expect("peeked");
656 r.plus_equals(&r2);
657 }
658 if !r.is_zero() {
659 result.0.push(group_d);
660 result.1.push(&t);
661 result.2.push(&r);
662 records_since_check += 1;
663 if records_since_check >= CUT_CHECK_RECORDS {
664 records_since_check = 0;
665 if u64::cast_from(indexed::length_in_words(&result.borrow()))
666 >= u64::cast_from(COMMIT_BYTES / 8)
667 {
668 out.push_back(ColumnChunk::Resident(
669 Rc::new(Column::Typed(std::mem::take(&mut result))),
670 depth,
671 ));
672 }
673 }
674 }
675 }
676 }
677 if !result.is_empty() {
678 out.push_back(ColumnChunk::Resident(Rc::new(Column::Typed(result)), depth));
679 }
680
681 if end < total {
683 let mut carry = <(D, T, R) as Columnar>::Container::default();
684 carry.extend_from_self(view, end..total);
685 input.push_front(ColumnChunk::Resident(Rc::new(Column::Typed(carry)), depth));
686 }
687 }
688
689 fn settle(input: &mut VecDeque<Self>, done: bool, out: &mut VecDeque<Self>) {
695 let mut carry: Option<(Column<(D, T, R)>, u8)> = None;
698 while let Some(chunk) = input.pop_front() {
699 let (rc, depth) = match chunk {
700 spilled @ ColumnChunk::Spilled(_) => {
701 if let Some((col, depth)) = carry.take() {
702 out.push_back(ColumnChunk::commit(col, depth));
703 }
704 out.push_back(spilled);
705 continue;
706 }
707 ColumnChunk::Resident(rc, depth) => (rc, depth),
708 };
709 let full = at_commit_size(&rc);
710 if !full && let Some((mut acc, acc_depth)) = carry.take() {
713 let Column::Typed(acc_c) = &mut acc else {
714 unreachable!("carry is always Typed");
715 };
716 let view = rc.borrow();
717 acc_c.extend_from_self(view, 0..view.len());
718 let acc_depth = acc_depth.max(depth);
719 if at_commit_size(&acc) {
720 out.push_back(ColumnChunk::commit(acc, acc_depth));
721 } else {
722 carry = Some((acc, acc_depth));
723 }
724 continue;
725 }
726 if let Some((acc, acc_depth)) = carry.take() {
729 out.push_back(ColumnChunk::commit(acc, acc_depth));
730 }
731 let col = Rc::try_unwrap(rc).unwrap_or_else(|rc| copy_column(&rc));
732 if full {
733 out.push_back(ColumnChunk::commit(col, depth));
734 } else {
735 carry = Some((to_typed(col), depth));
736 }
737 }
738 if let Some((col, depth)) = carry {
739 if done {
740 out.push_back(ColumnChunk::commit(col, depth));
741 } else {
742 input.push_front(ColumnChunk::Resident(Rc::new(col), depth));
743 }
744 }
745 }
746}
747
748fn extract_view_into<'v, 'p, K, V, T, R>(
753 view: BorrowedOf<'v, ((K, V), T, R)>,
754 probes: BorrowedOf<'p, K>,
755 probe_index: &mut usize,
756 staging: &mut <((K, V), T, R) as Columnar>::Container,
757) where
758 K: Columnar,
759 V: Columnar,
760 T: Columnar,
761 R: Columnar,
762 for<'b> columnar::Ref<'b, K>: Copy + Ord,
763{
764 let keys = view.0.0;
765 let len = keys.len();
766 let last = keys.get(len - 1);
767 let count = probes.len();
768 let mut pos = 0;
769 while *probe_index < count {
770 let probe = probes.get(*probe_index);
771 mz_ore::soft_assert_no_log!(
772 *probe_index == 0 || rr::<K>(probes.get(*probe_index - 1)) < rr::<K>(probe),
773 "probe keys must be sorted and deduplicated"
774 );
775 if rr::<K>(probe) > rr::<K>(last) {
776 return;
777 }
778 gallop(len, &mut pos, |i| rr::<K>(keys.get(i)) < rr::<K>(probe));
779 let start = pos;
780 while pos < len && rr::<K>(keys.get(pos)) == rr::<K>(probe) {
781 pos += 1;
782 }
783 staging.extend_from_self(view, start..pos);
784 if rr::<K>(probe) == rr::<K>(last) {
785 return;
786 }
787 *probe_index += 1;
788 }
789}
790
791impl<K, V, T, R> UnloadChunk for ColumnChunk<(K, V), T, R>
792where
793 K: Columnar,
794 for<'a> columnar::Ref<'a, K>: Copy + Ord,
795 V: Columnar,
796 for<'a> columnar::Ref<'a, V>: Copy + Ord,
797 T: Columnar + Default + Timestamp + Lattice + Ord,
798 for<'a> columnar::Ref<'a, T>: Copy + Ord,
799 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
800{
801 type Staging = <((K, V), T, R) as Columnar>::Container;
804
805 type Probes<'a> = BorrowedOf<'a, K>;
808
809 fn probe_count(probes: Self::Probes<'_>) -> usize {
810 probes.len()
811 }
812
813 fn locate(&self, probes: Self::Probes<'_>, probe_index: usize) -> std::cmp::Ordering {
814 let probe = probes.get(probe_index);
815 let (first, last) = self.data_span();
818 let (first, last) = (first.0, last.0);
819 if rr::<K>(probe) < rr::<K>(first) {
820 std::cmp::Ordering::Less
821 } else if rr::<K>(probe) > rr::<K>(last) {
822 std::cmp::Ordering::Greater
823 } else {
824 std::cmp::Ordering::Equal
825 }
826 }
827
828 fn extract_into(
829 &self,
830 probes: Self::Probes<'_>,
831 probe_index: &mut usize,
832 staging: &mut Self::Staging,
833 ) {
834 match self {
835 ColumnChunk::Resident(col, _) => {
836 extract_view_into::<K, V, T, R>(col.borrow(), probes, probe_index, staging);
837 }
838 ColumnChunk::Spilled(body) => with_scratch(|scratch| {
839 body.handle.read_into(scratch);
845 let view = borrow_words::<((K, V), T, R)>(scratch);
846 extract_view_into::<K, V, T, R>(view, probes, probe_index, staging);
847 }),
848 }
849 }
850
851 fn fetch_into(&self, staging: &mut Self::Staging) {
852 match self {
853 ColumnChunk::Resident(col, _) => {
854 let view = col.borrow();
855 staging.extend_from_self(view, 0..view.len());
856 }
857 ColumnChunk::Spilled(body) => with_scratch(|scratch| {
858 body.handle.read_into(scratch);
859 let view = borrow_words::<((K, V), T, R)>(scratch);
860 staging.extend_from_self(view, 0..view.len());
861 }),
862 }
863 }
864}
865
866pub struct UnchunkBuilder<Bu, D: Columnar, T: Columnar, R: Columnar> {
876 inner: Bu,
877 _marker: std::marker::PhantomData<(D, T, R)>,
878}
879
880impl<Bu, D, T, R> differential_dataflow::trace::Builder for UnchunkBuilder<Bu, D, T, R>
881where
882 Bu: differential_dataflow::trace::Builder<Input = Column<(D, T, R)>>,
883 D: Columnar + 'static,
884 T: Columnar + 'static,
885 R: Columnar + 'static,
886{
887 type Input = ColumnChunk<D, T, R>;
888 type Time = Bu::Time;
889 type Output = Bu::Output;
890
891 fn with_capacity(keys: usize, vals: usize, upds: usize) -> Self {
892 Self {
893 inner: Bu::with_capacity(keys, vals, upds),
894 _marker: std::marker::PhantomData,
895 }
896 }
897
898 fn push(&mut self, chunk: &mut Self::Input) {
899 let mut column = std::mem::take(chunk).into_column();
900 self.inner.push(&mut column);
901 }
902
903 fn done(
904 self,
905 description: differential_dataflow::trace::Description<Self::Time>,
906 ) -> Self::Output {
907 self.inner.done(description)
908 }
909
910 fn seal(
911 chain: &mut Vec<Self::Input>,
912 description: differential_dataflow::trace::Description<Self::Time>,
913 ) -> Self::Output {
914 let mut builder = Self::new();
917 for chunk in chain.iter_mut() {
918 builder.push(chunk);
919 }
920 chain.clear();
921 builder.done(description)
922 }
923}
924
925pub struct ChunkChunker<D: Columnar, T: Columnar, R: Columnar> {
928 inner: ColumnChunker<(D, T, R)>,
929 staged: ColumnChunk<D, T, R>,
930}
931
932impl<D, T, R> Default for ChunkChunker<D, T, R>
933where
934 D: Columnar,
935 T: Columnar,
936 R: Columnar,
937 ColumnChunker<(D, T, R)>: Default,
938{
939 fn default() -> Self {
940 Self {
941 inner: Default::default(),
942 staged: Default::default(),
943 }
944 }
945}
946
947impl<'a, D, T, R> PushInto<&'a mut Column<(D, T, R)>> for ChunkChunker<D, T, R>
948where
949 D: Columnar,
950 T: Columnar,
951 R: Columnar,
952 ColumnChunker<(D, T, R)>: PushInto<&'a mut Column<(D, T, R)>>,
953{
954 fn push_into(&mut self, item: &'a mut Column<(D, T, R)>) {
955 self.inner.push_into(item);
956 }
957}
958
959impl<D, T, R> ContainerBuilder for ChunkChunker<D, T, R>
960where
961 D: Columnar + 'static,
962 T: Columnar + 'static,
963 R: Columnar + 'static,
964 ColumnChunker<(D, T, R)>: ContainerBuilder<Container = Column<(D, T, R)>>,
965{
966 type Container = ColumnChunk<D, T, R>;
967
968 fn extract(&mut self) -> Option<&mut Self::Container> {
969 let col = self.inner.extract()?;
970 self.staged = ColumnChunk::from_column(std::mem::take(col));
971 Some(&mut self.staged)
972 }
973
974 fn finish(&mut self) -> Option<&mut Self::Container> {
975 let col = self.inner.finish()?;
976 self.staged = ColumnChunk::from_column(std::mem::take(col));
977 Some(&mut self.staged)
978 }
979}
980
981#[cfg(test)]
982mod tests {
983 use differential_dataflow::trace::chunk::{ChunkBatch, ChunkBatcher};
993 use differential_dataflow::trace::{Batcher, Description};
994 use mz_ore::pool::Pool;
995 use proptest::prelude::*;
996 use timely::container::PushInto;
997 use timely::progress::Antichain;
998
999 use crate::columnar::unload::UnloadBatch;
1000
1001 use super::*;
1002
1003 type Tuple = ((u64, u64), u64, i64);
1004 type TestChunk = ColumnChunk<(u64, u64), u64, i64>;
1005
1006 #[mz_ore::test]
1011 fn lz4_codec_matches_the_previous_extent_framing() {
1012 let body: Vec<u8> = (0..100_000u32).flat_map(|i| i.to_le_bytes()).collect();
1013 let mut stored = Vec::new();
1014 LZ4_CODEC.encode(&body, &mut stored);
1015 assert_eq!(stored, lz4_flex::block::compress_prepend_size(&body));
1016 let mut round = vec![0u8; body.len()];
1017 LZ4_CODEC.decode(&stored, &mut round);
1018 assert_eq!(round, body);
1019 }
1020
1021 #[mz_ore::test]
1022 #[should_panic(expected = "destination must match")]
1023 fn lz4_codec_decode_length_mismatch_panics() {
1024 let mut stored = Vec::new();
1025 LZ4_CODEC.encode(&[7u8; 64], &mut stored);
1026 let mut short = vec![0u8; 32];
1027 LZ4_CODEC.decode(&stored, &mut short);
1028 }
1029
1030 fn consolidate(mut v: Vec<Tuple>) -> Vec<Tuple> {
1033 v.sort();
1034 let mut out: Vec<Tuple> = Vec::new();
1035 for (d, t, r) in v {
1036 if let Some(last) = out.last_mut() {
1037 if last.0 == d && last.1 == t {
1038 last.2 += r;
1039 continue;
1040 }
1041 }
1042 out.push((d, t, r));
1043 }
1044 out.retain(|x| x.2 != 0);
1045 out
1046 }
1047
1048 fn arb_consolidated() -> impl Strategy<Value = Vec<Tuple>> {
1049 prop::collection::vec(((0u64..5, 0u64..5), 0u64..4, -3i64..=3i64), 0..40)
1050 .prop_map(consolidate)
1051 }
1052
1053 fn build_column(v: &[Tuple]) -> Column<Tuple> {
1054 let mut col: Column<Tuple> = Default::default();
1055 for tup in v {
1056 col.push_into(*tup);
1057 }
1058 col
1059 }
1060
1061 fn collect_column(col: &Column<Tuple>) -> Vec<Tuple> {
1062 col.borrow()
1063 .into_index_iter()
1064 .map(|((k, v), t, r)| {
1065 (
1066 (u64::into_owned(k), u64::into_owned(v)),
1067 u64::into_owned(t),
1068 i64::into_owned(r),
1069 )
1070 })
1071 .collect()
1072 }
1073
1074 fn collect_chunks(chunks: impl IntoIterator<Item = TestChunk>) -> Vec<Tuple> {
1075 chunks
1076 .into_iter()
1077 .flat_map(|chunk| collect_column(&chunk.into_column()))
1078 .collect()
1079 }
1080
1081 fn collect_staging(staging: &<Tuple as Columnar>::Container) -> Vec<Tuple> {
1082 staging
1083 .borrow()
1084 .into_index_iter()
1085 .map(|((k, v), t, r)| {
1086 (
1087 (u64::into_owned(k), u64::into_owned(v)),
1088 u64::into_owned(t),
1089 i64::into_owned(r),
1090 )
1091 })
1092 .collect()
1093 }
1094
1095 fn chunked(data: &[Tuple], cuts: &[usize]) -> VecDeque<TestChunk> {
1097 let mut chunks = VecDeque::new();
1098 let mut start = 0;
1099 for cut in cuts {
1100 let end = (start + 1 + cut % 7).min(data.len());
1101 if end > start {
1102 chunks.push_back(ColumnChunk::from_column(build_column(&data[start..end])));
1103 start = end;
1104 }
1105 }
1106 if start < data.len() {
1107 chunks.push_back(ColumnChunk::from_column(build_column(&data[start..])));
1108 }
1109 chunks
1110 }
1111
1112 fn chunked_spilled(data: &[Tuple], cuts: &[usize], pool: &Pool) -> VecDeque<TestChunk> {
1115 chunked(data, cuts)
1116 .into_iter()
1117 .map(|chunk| force_spill(chunk, pool))
1118 .collect()
1119 }
1120
1121 fn force_spill(chunk: TestChunk, pool: &Pool) -> TestChunk {
1124 let depth = chunk.depth();
1125 TestChunk::spill_body(chunk.into_column(), pool, depth)
1126 }
1127
1128 fn test_pool() -> Pool {
1132 static POOL: std::sync::OnceLock<Pool> = std::sync::OnceLock::new();
1133 POOL.get_or_init(|| Pool::new().expect("pool creation"))
1134 .clone()
1135 }
1136
1137 proptest! {
1138 #[mz_ore::test]
1141 #[cfg_attr(miri, ignore)]
1142 fn batcher_round_trip(
1143 inputs in prop::collection::vec(arb_consolidated(), 1..6),
1144 cuts in prop::collection::vec(0usize..7, 0..8),
1145 ) {
1146 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1147 let mut union = Vec::new();
1148 for input in &inputs {
1149 Extend::extend(&mut union, input.iter().copied());
1150 for chunk in chunked(input, &cuts) {
1151 batcher.push_into(chunk);
1152 }
1153 }
1154 let (sealed, _description) = batcher.seal(Antichain::new());
1156 prop_assert_eq!(collect_chunks(sealed), consolidate(union));
1157 }
1158
1159 #[mz_ore::test]
1162 #[cfg_attr(miri, ignore)]
1163 fn batcher_round_trip_spilled(
1164 inputs in prop::collection::vec(arb_consolidated(), 1..4),
1165 cuts in prop::collection::vec(0usize..7, 0..6),
1166 ) {
1167 let pool = test_pool();
1168 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1169 let mut union = Vec::new();
1170 for input in &inputs {
1171 Extend::extend(&mut union, input.iter().copied());
1172 for chunk in chunked_spilled(input, &cuts, &pool) {
1173 batcher.push_into(chunk);
1174 }
1175 }
1176 let (sealed, _description) = batcher.seal(Antichain::new());
1177 prop_assert_eq!(collect_chunks(sealed), consolidate(union));
1178 }
1179
1180 #[mz_ore::test]
1183 #[cfg_attr(miri, ignore)]
1184 fn seal_partitions_by_time(
1185 input in arb_consolidated(),
1186 cuts in prop::collection::vec(0usize..7, 0..8),
1187 upper in 0u64..5,
1188 ) {
1189 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1190 for chunk in chunked(&input, &cuts) {
1191 batcher.push_into(chunk);
1192 }
1193 let (shipped, _) = batcher.seal(Antichain::from_elem(upper));
1194 let expected_shipped: Vec<Tuple> =
1195 input.iter().copied().filter(|(_, t, _)| *t < upper).collect();
1196 prop_assert_eq!(collect_chunks(shipped), consolidate(expected_shipped));
1197
1198 let kept_min = input.iter().filter(|(_, t, _)| *t >= upper).map(|(_, t, _)| *t).min();
1199 let frontier = batcher.frontier().to_owned();
1200 prop_assert_eq!(frontier.elements().first().copied(), kept_min);
1201
1202 let (rest, _) = batcher.seal(Antichain::new());
1203 let expected_rest: Vec<Tuple> =
1204 input.iter().copied().filter(|(_, t, _)| *t >= upper).collect();
1205 prop_assert_eq!(collect_chunks(rest), consolidate(expected_rest));
1206 }
1207
1208 #[mz_ore::test]
1212 #[cfg_attr(miri, ignore)]
1213 fn seal_partitions_by_time_spilled(
1214 input in arb_consolidated(),
1215 cuts in prop::collection::vec(0usize..7, 0..8),
1216 upper in 0u64..5,
1217 ) {
1218 let pool = test_pool();
1219 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1220 for chunk in chunked_spilled(&input, &cuts, &pool) {
1221 batcher.push_into(chunk);
1222 }
1223 let (shipped, _) = batcher.seal(Antichain::from_elem(upper));
1224 let expected_shipped: Vec<Tuple> =
1225 input.iter().copied().filter(|(_, t, _)| *t < upper).collect();
1226 prop_assert_eq!(collect_chunks(shipped), consolidate(expected_shipped));
1227
1228 let kept_min = input.iter().filter(|(_, t, _)| *t >= upper).map(|(_, t, _)| *t).min();
1229 let frontier = batcher.frontier().to_owned();
1230 prop_assert_eq!(frontier.elements().first().copied(), kept_min);
1231
1232 let (rest, _) = batcher.seal(Antichain::new());
1233 let expected_rest: Vec<Tuple> =
1234 input.iter().copied().filter(|(_, t, _)| *t >= upper).collect();
1235 prop_assert_eq!(collect_chunks(rest), consolidate(expected_rest));
1236 }
1237
1238 #[mz_ore::test]
1241 #[cfg_attr(miri, ignore)]
1242 fn advance_matches_reference(
1243 input in arb_consolidated(),
1244 cuts in prop::collection::vec(0usize..7, 0..8),
1245 frontier_elem in 0u64..5,
1246 ) {
1247 let frontier = Antichain::from_elem(frontier_elem);
1248 let mut chunks = chunked(&input, &cuts);
1249 let mut out = VecDeque::new();
1250 TestChunk::advance(&mut chunks, frontier.borrow(), false, &mut out);
1251 TestChunk::advance(&mut chunks, frontier.borrow(), true, &mut out);
1252 prop_assert!(chunks.is_empty());
1253
1254 let expected = consolidate(
1255 input
1256 .iter()
1257 .map(|&(d, mut t, r)| {
1258 t.advance_by(frontier.borrow());
1259 (d, t, r)
1260 })
1261 .collect(),
1262 );
1263 prop_assert_eq!(collect_chunks(out), expected);
1264 }
1265
1266 #[mz_ore::test]
1269 #[cfg_attr(miri, ignore)]
1270 fn settle_preserves_and_packs(
1271 input in arb_consolidated(),
1272 cuts in prop::collection::vec(0usize..7, 1..8),
1273 ) {
1274 let mut chunks = chunked(&input, &cuts);
1275 let mut out = VecDeque::new();
1276 TestChunk::settle(&mut chunks, true, &mut out);
1277 prop_assert!(chunks.is_empty());
1278 prop_assert!(out.len() <= 1);
1281 prop_assert_eq!(collect_chunks(out), input);
1282 }
1283
1284 #[mz_ore::test]
1288 #[cfg_attr(miri, ignore)]
1289 fn unload_extract_matches_filter(
1290 input in arb_consolidated(),
1291 cuts in prop::collection::vec(0usize..7, 0..8),
1292 probe_keys in prop::collection::btree_set(0u64..6, 0..6),
1293 spill in any::<bool>(),
1294 ) {
1295 prop_assume!(!input.is_empty());
1296 let pool = test_pool();
1297 let chunks: Vec<TestChunk> = if spill {
1298 chunked_spilled(&input, &cuts, &pool).into()
1299 } else {
1300 chunked(&input, &cuts).into()
1301 };
1302 let description = Description::new(
1303 Antichain::from_elem(0u64),
1304 Antichain::new(),
1305 Antichain::from_elem(0u64),
1306 );
1307 let batch = ChunkBatch::new(chunks, description);
1308
1309 let mut probe_col = <u64 as Columnar>::Container::default();
1310 for key in &probe_keys {
1311 probe_col.push(*key);
1312 }
1313 let mut staging = <Tuple as Columnar>::Container::default();
1314 batch.extract_into(probe_col.borrow(), &mut staging);
1315
1316 let expected: Vec<Tuple> = input
1317 .iter()
1318 .copied()
1319 .filter(|((k, _), _, _)| probe_keys.contains(k))
1320 .collect();
1321 prop_assert_eq!(collect_staging(&staging), expected);
1322
1323 let mut staging = <Tuple as Columnar>::Container::default();
1326 batch.fetch_into(&mut staging);
1327 prop_assert_eq!(collect_staging(&staging), input);
1328 }
1329 }
1330
1331 #[mz_ore::test]
1334 fn locate_spans_keys() {
1335 let chunk = ColumnChunk::from_column(build_column(&[
1336 ((2, 0), 0, 1),
1337 ((4, 0), 0, 1),
1338 ((6, 0), 0, 1),
1339 ]));
1340 let mut probe_col = <u64 as Columnar>::Container::default();
1341 for key in [0u64, 2, 3, 6, 9] {
1342 probe_col.push(key);
1343 }
1344 let probes = probe_col.borrow();
1345 use std::cmp::Ordering::*;
1346 let expected = [Less, Equal, Equal, Equal, Greater];
1347 for (index, expected) in expected.iter().enumerate() {
1348 assert_eq!(chunk.locate(probes, index), *expected, "probe {index}");
1349 }
1350 }
1351
1352 fn collect_bounded(chunks: impl IntoIterator<Item = TestChunk>, bound: usize) -> Vec<Tuple> {
1355 let mut collected = Vec::new();
1356 for chunk in chunks {
1357 let col = chunk.into_column();
1358 let bytes = col.length_in_bytes();
1359 assert!(bytes <= bound, "chunk of {bytes} bytes exceeds {bound}");
1360 Extend::extend(&mut collected, collect_column(&col));
1361 }
1362 collected
1363 }
1364
1365 #[mz_ore::test]
1368 #[cfg_attr(miri, ignore)]
1369 fn advance_cuts_large_output() {
1370 let records: Vec<Tuple> = (0..300_000u64).map(|k| ((k, 0), 0, 1)).collect();
1371 let mut input = VecDeque::from([ColumnChunk::from_column(build_column(&records))]);
1372 let frontier = Antichain::from_elem(0u64);
1373 let mut out = VecDeque::new();
1374 TestChunk::advance(&mut input, frontier.borrow(), true, &mut out);
1375 assert!(input.is_empty());
1376 assert!(
1377 out.len() >= 2,
1378 "expected a cut output, got {} chunk(s)",
1379 out.len()
1380 );
1381 assert_eq!(collect_bounded(out, 2 * COMMIT_BYTES), records);
1382 }
1383
1384 #[mz_ore::test]
1387 #[cfg_attr(miri, ignore)]
1388 fn advance_withholds_giant_group() {
1389 let records: Vec<Tuple> = (0..100u64).map(|t| ((7, 7), t, 1)).collect();
1390 let mut input: VecDeque<TestChunk> = VecDeque::new();
1391 for piece in records.chunks(30) {
1392 input.push_back(ColumnChunk::from_column(build_column(piece)));
1393 }
1394 let frontier = Antichain::from_elem(50u64);
1395 let mut out = VecDeque::new();
1396 TestChunk::advance(&mut input, frontier.borrow(), false, &mut out);
1397 assert!(out.is_empty(), "nothing may ship from a single open group");
1398 assert_eq!(input.len(), 1, "the whole input becomes one carry chunk");
1399 TestChunk::advance(&mut input, frontier.borrow(), true, &mut out);
1401 assert!(input.is_empty());
1402 let advanced = records.iter().map(|&(d, t, r)| (d, t.max(50), r)).collect();
1403 assert_eq!(collect_chunks(out), consolidate(advanced));
1404 }
1405
1406 #[mz_ore::test]
1409 #[cfg_attr(miri, ignore)]
1410 fn extract_cuts_large_output() {
1411 let records: Vec<Tuple> = (0..300_000u64).map(|k| ((k, 0), k % 2, 1)).collect();
1412 let mut input = VecDeque::from([ColumnChunk::from_column(build_column(&records))]);
1413 let frontier = Antichain::from_elem(1u64);
1414 let mut residual = Antichain::new();
1415 let (mut keep, mut ship) = (VecDeque::new(), VecDeque::new());
1416 while !input.is_empty() {
1417 TestChunk::extract(
1418 &mut input,
1419 frontier.borrow(),
1420 &mut residual,
1421 &mut keep,
1422 &mut ship,
1423 );
1424 }
1425 assert!(
1426 keep.len() >= 2,
1427 "expected a cut keep side, got {} chunk(s)",
1428 keep.len()
1429 );
1430 assert!(
1431 ship.len() >= 2,
1432 "expected a cut ship side, got {} chunk(s)",
1433 ship.len()
1434 );
1435 let kept: Vec<Tuple> = records.iter().copied().filter(|r| r.1 >= 1).collect();
1436 let shipped: Vec<Tuple> = records.iter().copied().filter(|r| r.1 < 1).collect();
1437 assert_eq!(collect_bounded(keep, 2 * COMMIT_BYTES), kept);
1438 assert_eq!(collect_bounded(ship, 2 * COMMIT_BYTES), shipped);
1439 assert_eq!(residual, Antichain::from_elem(1));
1440 }
1441
1442 #[mz_ore::test]
1445 fn locate_uses_resident_bounds() {
1446 let pool = test_pool();
1447 let data: Vec<Tuple> = vec![((2, 0), 0, 1), ((4, 0), 0, 1)];
1448 let chunk = force_spill(ColumnChunk::from_column(build_column(&data)), &pool);
1449
1450 let mut probe_col = <u64 as Columnar>::Container::default();
1451 for key in [1u64, 3, 5] {
1452 probe_col.push(key);
1453 }
1454 let probes = probe_col.borrow();
1455 assert_eq!(chunk.locate(probes, 0), std::cmp::Ordering::Less);
1456 assert_eq!(chunk.locate(probes, 1), std::cmp::Ordering::Equal);
1457 assert_eq!(chunk.locate(probes, 2), std::cmp::Ordering::Greater);
1458 }
1459
1460 #[mz_ore::test]
1463 #[cfg_attr(miri, ignore)] fn spill_round_trip() {
1465 set_spill_override(Some(test_pool()));
1466
1467 let data: Vec<Tuple> = (0..40_000u64)
1468 .map(|i| ((i / 4, i % 4), i % 8, 1i64))
1469 .collect();
1470 let data = consolidate(data);
1471
1472 let column = build_column(&data);
1473 let committed = TestChunk::commit(column, 0);
1474 assert!(committed.is_spilled(), "large body must spill");
1475 assert_eq!(committed.len(), data.len());
1476 assert_eq!(collect_column(&committed.clone().into_column()), data);
1477
1478 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1479 for piece in data.chunks(10_000) {
1480 batcher.push_into(ColumnChunk::from_column(build_column(piece)));
1481 }
1482 let (sealed, _) = batcher.seal(Antichain::new());
1483 assert!(
1484 sealed.iter().any(ColumnChunk::is_spilled),
1485 "sealed output should contain spilled chunks",
1486 );
1487 assert_eq!(collect_chunks(sealed), data);
1488
1489 set_spill_override(None);
1490 }
1491
1492 #[mz_ore::test]
1495 #[cfg_attr(miri, ignore)] fn merge_spilled_chains() {
1497 set_spill_override(Some(test_pool()));
1498
1499 let a: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), 0, 1i64)).collect();
1500 let b: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), 0, 2i64)).collect();
1501
1502 let mut in1 = VecDeque::from([TestChunk::commit(build_column(&a), 0)]);
1503 let mut in2 = VecDeque::from([TestChunk::commit(build_column(&b), 0)]);
1504 assert!(in1[0].is_spilled() && in2[0].is_spilled());
1505
1506 let mut out = VecDeque::new();
1507 while !in1.is_empty() && !in2.is_empty() {
1508 TestChunk::merge(&mut in1, &mut in2, &mut out);
1509 }
1510 for tail in in1.drain(..).chain(in2.drain(..)) {
1511 out.push_back(tail);
1512 }
1513
1514 let expected: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), 0, 3i64)).collect();
1515 assert_eq!(collect_chunks(out), expected);
1516
1517 set_spill_override(None);
1518 }
1519
1520 #[mz_ore::test]
1523 fn merge_untouched_survivor_stays_spilled() {
1524 let pool = test_pool();
1525 let low: Vec<Tuple> = (0..100u64).map(|i| ((i, 0), 0, 1i64)).collect();
1526 let high: Vec<Tuple> = (1000..1100u64).map(|i| ((i, 0), 0, 1i64)).collect();
1527
1528 let mut in1 = VecDeque::from([force_spill(
1529 ColumnChunk::from_column(build_column(&low)),
1530 &pool,
1531 )]);
1532 let mut in2 = VecDeque::from([force_spill(
1533 ColumnChunk::from_column(build_column(&high)),
1534 &pool,
1535 )]);
1536 let mut out = VecDeque::new();
1537 TestChunk::merge(&mut in1, &mut in2, &mut out);
1538
1539 assert!(in1.is_empty());
1542 assert_eq!(in2.len(), 1);
1543 assert!(in2[0].is_spilled(), "untouched survivor must stay spilled");
1544 let mut all = collect_chunks(out);
1545 Extend::extend(&mut all, collect_chunks(in2.drain(..)));
1546 let mut expected = low;
1547 Extend::extend(&mut expected, high);
1548 assert_eq!(all, expected);
1549 }
1550
1551 #[mz_ore::test]
1555 fn merge_derives_generational_depth() {
1556 let low: Vec<Tuple> = (0..100u64).map(|i| ((i, 0), 0, 1i64)).collect();
1557 let high: Vec<Tuple> = (50..150u64).map(|i| ((i, 0), 0, 1i64)).collect();
1558 let mut in1 = VecDeque::from([ColumnChunk::from_column(build_column(&low))]);
1559 let mut in2 = VecDeque::from([ColumnChunk::from_column(build_column(&high))]);
1560 assert_eq!(in1[0].depth(), 0, "fresh chunks start at depth 0");
1561 let mut out = VecDeque::new();
1562 TestChunk::merge(&mut in1, &mut in2, &mut out);
1563 assert!(!out.is_empty());
1564 for chunk in &out {
1565 assert_eq!(chunk.depth(), 1, "merge output is one past its inputs");
1566 }
1567 assert!(in1.is_empty());
1570 assert_eq!(in2.len(), 1);
1571 assert_eq!(in2[0].depth(), 0, "rewritten survivor keeps its depth");
1572
1573 let mut in1 = VecDeque::from([ColumnChunk::Resident(Rc::new(build_column(&low)), 3)]);
1575 let far: Vec<Tuple> = (1000..1100u64).map(|i| ((i, 0), 0, 1i64)).collect();
1576 let mut in2 = VecDeque::from([ColumnChunk::from_column(build_column(&far))]);
1577 let mut out = VecDeque::new();
1578 TestChunk::merge(&mut in1, &mut in2, &mut out);
1579 assert_eq!(out.len(), 1);
1580 assert_eq!(out[0].depth(), 3, "pass-through keeps its depth");
1581 }
1582
1583 #[mz_ore::test]
1586 fn advance_preserves_depth() {
1587 let data: Vec<Tuple> = (0..100u64).map(|i| ((i, 0), 1, 1i64)).collect();
1588 let mut input = VecDeque::from([
1589 ColumnChunk::Resident(Rc::new(build_column(&data[..50])), 2),
1590 ColumnChunk::Resident(Rc::new(build_column(&data[50..])), 1),
1591 ]);
1592 let frontier = Antichain::from_elem(5u64);
1593 let mut out = VecDeque::new();
1594 TestChunk::advance(&mut input, frontier.borrow(), false, &mut out);
1595 for chunk in out.iter().chain(input.iter()) {
1596 assert_eq!(chunk.depth(), 2);
1597 }
1598 TestChunk::advance(&mut input, frontier.borrow(), true, &mut out);
1599 assert!(input.is_empty());
1600 assert!(!out.is_empty());
1601 for chunk in &out {
1602 assert_eq!(chunk.depth(), 2);
1603 }
1604 }
1605
1606 #[mz_ore::test]
1610 #[cfg_attr(miri, ignore)] fn settle_commits_at_accumulated_depth() {
1612 set_spill_override(Some(test_pool()));
1613 let big: Vec<Tuple> = (0..100_000u64).map(|i| ((i, 0), 0, 1i64)).collect();
1614 let mut input = VecDeque::from([
1615 ColumnChunk::Resident(Rc::new(build_column(&big)), 1),
1616 ColumnChunk::Resident(Rc::new(build_column(&[((0, 0), 0, 1)])), 0),
1617 ColumnChunk::Resident(Rc::new(build_column(&[((1, 0), 0, 1)])), 2),
1618 ]);
1619 let mut out = VecDeque::new();
1620 TestChunk::settle(&mut input, true, &mut out);
1621 assert!(input.is_empty());
1622 assert_eq!(out.len(), 2);
1623 assert!(out[0].is_spilled(), "large commit must spill");
1624 assert_eq!(out[0].depth(), 1, "sole commit keeps its depth");
1625 assert!(!out[1].is_spilled(), "small commit stays resident");
1626 assert_eq!(out[1].depth(), 2, "coalesced commit takes the max depth");
1627 set_spill_override(None);
1628 }
1629
1630 #[mz_ore::test]
1634 #[cfg_attr(miri, ignore)] fn settle_carry_commits_at_target() {
1636 let chunk_rows = u64::cast_from(1_500_000usize / 32);
1640 let mut input: VecDeque<TestChunk> = (0..4u64)
1641 .map(|c| {
1642 let data: Vec<Tuple> = (0..chunk_rows)
1643 .map(|i| ((c * chunk_rows + i, 0), 0, 1i64))
1644 .collect();
1645 ColumnChunk::from_column(build_column(&data))
1646 })
1647 .collect();
1648 let mut out = VecDeque::new();
1649 TestChunk::settle(&mut input, true, &mut out);
1650 assert!(out.len() < 4, "nothing coalesced");
1653 for chunk in &out {
1654 let col = chunk.clone().into_column();
1655 assert!(
1656 col.length_in_bytes() < 2 * COMMIT_BYTES,
1657 "settled chunk of {} bytes exceeds twice the commit target",
1658 col.length_in_bytes(),
1659 );
1660 }
1661 assert_eq!(
1662 collect_chunks(out).len(),
1663 usize::try_from(4 * chunk_rows).unwrap(),
1664 );
1665 }
1666
1667 #[mz_ore::test]
1669 fn small_chunks_stay_resident() {
1670 set_spill_override(Some(test_pool()));
1671 let committed = TestChunk::commit(build_column(&[((1, 1), 0, 1)]), 0);
1672 assert!(!committed.is_spilled());
1673 set_spill_override(None);
1674 }
1675
1676 fn column_at_spill_floor() -> (Column<Tuple>, u64) {
1679 let mut col: Column<Tuple> = Column::default();
1680 let mut n = 0u64;
1681 while col.length_in_bytes() < SPILL_MIN_BYTES {
1682 col.push_into(((n, n), 0, 1));
1683 n += 1;
1684 }
1685 (col, n)
1686 }
1687
1688 #[mz_ore::test]
1691 fn spill_floor_boundary() {
1692 set_spill_override(Some(test_pool()));
1693 let (col, n) = column_at_spill_floor();
1694 let mut under: Column<Tuple> = Column::default();
1695 for m in 0..n - 1 {
1696 under.push_into(((m, m), 0, 1));
1697 }
1698 assert!(under.length_in_bytes() < SPILL_MIN_BYTES);
1699 assert!(!TestChunk::commit(under, 0).is_spilled());
1700 assert!(TestChunk::commit(col, 0).is_spilled());
1701 set_spill_override(None);
1702 }
1703
1704 #[mz_ore::test]
1708 #[cfg_attr(miri, ignore)]
1709 fn spill_gates_compose_as_or() {
1710 let installed =
1711 crate::pool_config::apply_pool_config(crate::pool_config::PoolPagerConfig {
1712 budget_bytes: 32 << 20,
1713 spill_threads: 1,
1714 eager_backing: false,
1715 rss_target_bytes: 16 << 20,
1716 });
1717 assert!(installed, "pool reservation failed");
1718 let (col, _) = column_at_spill_floor();
1720 let commit = |col: &Column<Tuple>| TestChunk::commit(col.clone(), 0).is_spilled();
1721
1722 assert!(!commit(&col), "both gates off");
1723 set_storage_spill_enabled(true);
1724 assert!(commit(&col), "the storage gate alone spills");
1725 set_compute_spill_enabled(false);
1726 assert!(
1727 commit(&col),
1728 "the compute setter must not clobber the storage gate"
1729 );
1730 set_compute_spill_enabled(true);
1731 set_storage_spill_enabled(false);
1732 assert!(commit(&col), "the compute gate alone spills");
1733 set_compute_spill_enabled(false);
1734 assert!(!commit(&col), "both gates off again");
1735 }
1736
1737 #[mz_ore::test]
1740 fn spill_align_round_trip() {
1741 let pool = test_pool();
1742 let data: Vec<Tuple> = (0..64u64).map(|k| ((k, k), 0, 1)).collect();
1743 let spilled = force_spill(ColumnChunk::from_column(build_column(&data)), &pool);
1744 let column = spilled.into_column();
1745 let Column::Align(words) = &column else {
1746 panic!("a spilled body reads back as Column::Align");
1747 };
1748 let words = words.clone();
1749 let respilled = force_spill(ColumnChunk::from_column(column), &pool);
1750 let reread = respilled.into_column();
1751 let Column::Align(words2) = &reread else {
1752 panic!("a spilled body reads back as Column::Align");
1753 };
1754 assert_eq!(&words, words2, "byte-identical round trip");
1755 assert_eq!(collect_column(&reread), data);
1756 }
1757
1758 #[mz_ore::test]
1760 fn merge_depth_saturates() {
1761 let a = ColumnChunk::Resident(
1762 Rc::new(build_column(&[((1, 0), 0, 1), ((3, 0), 0, 1)])),
1763 u8::MAX,
1764 );
1765 let b = ColumnChunk::Resident(
1766 Rc::new(build_column(&[((2, 0), 0, 1), ((4, 0), 0, 1)])),
1767 u8::MAX,
1768 );
1769 let mut in1 = VecDeque::from([a]);
1770 let mut in2 = VecDeque::from([b]);
1771 let mut out = VecDeque::new();
1772 TestChunk::merge(&mut in1, &mut in2, &mut out);
1773 for chunk in out.iter().chain(in1.iter()).chain(in2.iter()) {
1774 assert_eq!(chunk.depth(), u8::MAX, "depth saturates");
1775 }
1776 }
1777
1778 #[mz_ore::test]
1781 fn into_column_copies_shared_resident() {
1782 let data: Vec<Tuple> = vec![((1, 1), 0, 1), ((2, 2), 0, 1)];
1783 let a = ColumnChunk::from_column(build_column(&data));
1784 let b = a.clone();
1785 assert_eq!(collect_column(&a.into_column()), data);
1786 assert_eq!(collect_column(&b.into_column()), data);
1787 }
1788}