1use std::borrow::Cow;
41#[cfg(test)]
42use std::cell::Cell;
43use std::cell::RefCell;
44use std::collections::VecDeque;
45use std::marker::PhantomData;
46use std::rc::Rc;
47use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
48
49use columnar::bytes::indexed;
50use columnar::{Borrow, BorrowedOf, Columnar, Container as _, Index, Len, Push as _};
51use differential_dataflow::difference::Semigroup;
52use differential_dataflow::lattice::Lattice;
53use differential_dataflow::trace::chunk::Chunk;
54use mz_ore::cast::CastFrom;
55use mz_ore::pool::{ChunkHandle, ChunkHints, ExtentCodec, IDENTITY_CODEC, Pool};
56use smallvec::SmallVec;
57use timely::Accountable;
58use timely::PartialOrder;
59use timely::container::{ContainerBuilder, PushInto};
60use timely::progress::Timestamp;
61use timely::progress::frontier::{Antichain, AntichainRef};
62
63use crate::columnar::batcher::{ColumnChunker, gallop};
64use crate::columnar::body::{ColumnBody, borrow_words};
65use crate::columnar::unload::UnloadChunk;
66use crate::columnar::{Column, at_serialized_capacity};
67
68pub mod metrics;
69
70static COMPUTE_SPILL_ENABLED: AtomicBool = AtomicBool::new(false);
72
73static STORAGE_SPILL_ENABLED: AtomicBool = AtomicBool::new(false);
75
76static SINK_SPILL_ENABLED: AtomicBool = AtomicBool::new(false);
79
80thread_local! {
81 static SPILL_OVERRIDE: RefCell<Option<Pool>> = const { RefCell::new(None) };
85
86 #[cfg(test)]
90 static COMPRESS_MIN_DEPTH_OVERRIDE: Cell<Option<u8>> = const { Cell::new(None) };
91
92 static READ_SCRATCH: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
94}
95
96pub trait SpillGate: 'static {
105 fn enabled() -> bool;
107}
108
109pub enum ComputeSpill {}
111
112impl SpillGate for ComputeSpill {
113 fn enabled() -> bool {
114 COMPUTE_SPILL_ENABLED.load(Ordering::Relaxed)
115 }
116}
117
118pub enum StorageSpill {}
121
122impl SpillGate for StorageSpill {
123 fn enabled() -> bool {
124 STORAGE_SPILL_ENABLED.load(Ordering::Relaxed)
125 }
126}
127
128pub fn set_compute_spill_enabled(enabled: bool) {
131 COMPUTE_SPILL_ENABLED.store(enabled, Ordering::Relaxed);
132}
133
134pub fn set_storage_spill_enabled(enabled: bool) {
137 STORAGE_SPILL_ENABLED.store(enabled, Ordering::Relaxed);
138}
139
140pub fn set_sink_spill_enabled(enabled: bool) {
145 SINK_SPILL_ENABLED.store(enabled, Ordering::Relaxed);
146}
147
148pub fn set_spill_override(pool: Option<Pool>) {
152 SPILL_OVERRIDE.with(|cell| *cell.borrow_mut() = pool);
153}
154
155static COMPRESS_MIN_DEPTH: AtomicU8 = AtomicU8::new(DEFAULT_COMPRESS_MIN_DEPTH);
158
159pub fn set_compress_min_depth(depth: u8) {
178 COMPRESS_MIN_DEPTH.store(depth, Ordering::Relaxed);
179}
180
181#[cfg(test)]
185pub fn set_compress_min_depth_override(depth: Option<u8>) {
186 COMPRESS_MIN_DEPTH_OVERRIDE.with(|cell| cell.set(depth));
187}
188
189fn compress_min_depth() -> u8 {
191 #[cfg(test)]
192 if let Some(depth) = COMPRESS_MIN_DEPTH_OVERRIDE.with(|cell| cell.get()) {
193 return depth;
194 }
195 COMPRESS_MIN_DEPTH.load(Ordering::Relaxed)
196}
197
198fn codec_for_depth(depth: u8) -> (&'static dyn ExtentCodec, bool) {
203 if depth < compress_min_depth() {
204 (&IDENTITY_CODEC, false)
205 } else {
206 (&LZ4_CODEC, true)
207 }
208}
209
210fn spill_target(enabled: bool, len_bytes: usize) -> Option<Pool> {
216 resolve_pool(enabled).filter(|_| len_bytes >= SPILL_MIN_BYTES)
217}
218
219fn resolve_pool(enabled: bool) -> Option<Pool> {
221 if let Some(pool) = SPILL_OVERRIDE.with(|cell| cell.borrow().clone()) {
222 return Some(pool);
223 }
224 if enabled {
225 crate::pool_config::active_pool()
226 } else {
227 None
228 }
229}
230
231const SCRATCH_RETAIN_WORDS: usize = 1 << 18;
235
236fn with_scratch<Out>(f: impl FnOnce(&mut Vec<u64>) -> Out) -> Out {
238 READ_SCRATCH.with(|cell| {
239 let mut scratch = cell.take();
240 scratch.clear();
241 let out = f(&mut scratch);
242 if scratch.capacity() > SCRATCH_RETAIN_WORDS {
243 scratch.clear();
244 scratch.shrink_to_fit();
245 }
246 cell.replace(scratch);
247 out
248 })
249}
250
251const COMMIT_BYTES: usize = 2 << 20;
254
255const SPILL_MIN_BYTES: usize = 64 << 10;
264
265const DEFAULT_COMPRESS_MIN_DEPTH: u8 = 1;
274
275fn cut_records(len: usize, bytes: usize, space: usize) -> usize {
281 let space = space.saturating_sub(COMMIT_BYTES / 20);
282 (len.saturating_mul(space) / bytes.max(1)).min(len)
283}
284
285fn at_commit_size<C: Columnar>(body: &ColumnBody<C>) -> bool {
287 body.length_in_bytes() >= COMMIT_BYTES - COMMIT_BYTES / 10
288}
289
290#[inline(always)]
294fn rr<'b, 'a: 'b, C: Columnar>(item: columnar::Ref<'a, C>) -> columnar::Ref<'b, C> {
295 columnar::ContainerOf::<C>::reborrow_ref(item)
296}
297
298pub struct SpilledBody<D: Columnar, T, G> {
304 records: usize,
306 fences: D::Container,
310 time_lower: Antichain<T>,
314 time_upper: SmallVec<[T; 1]>,
319 compressed: bool,
326 len_bytes: usize,
331 handle: ChunkHandle,
333 gate: PhantomData<G>,
336}
337
338pub enum ColumnChunk<D: Columnar, T: Columnar, R: Columnar, G: SpillGate> {
357 Resident(Rc<ColumnBody<(D, T, R)>>, u8),
359 Spilled(Rc<SpilledBody<D, T, G>>, u8),
361}
362
363impl<D: Columnar, T: Columnar, R: Columnar, G: SpillGate> Clone for ColumnChunk<D, T, R, G> {
364 fn clone(&self) -> Self {
365 match self {
366 ColumnChunk::Resident(col, depth) => ColumnChunk::Resident(Rc::clone(col), *depth),
367 ColumnChunk::Spilled(body, depth) => ColumnChunk::Spilled(Rc::clone(body), *depth),
368 }
369 }
370}
371
372impl<D: Columnar, T: Columnar, R: Columnar, G: SpillGate> Default for ColumnChunk<D, T, R, G> {
373 fn default() -> Self {
374 ColumnChunk::Resident(Rc::new(ColumnBody::default()), 0)
375 }
376}
377
378impl<D: Columnar, T: Columnar, R: Columnar, G: SpillGate> Accountable for ColumnChunk<D, T, R, G> {
379 fn record_count(&self) -> i64 {
380 i64::try_from(self.records()).expect("record count fits i64")
381 }
382}
383
384impl<D: Columnar, T: Columnar, R: Columnar, G: SpillGate> ColumnChunk<D, T, R, G> {
385 pub fn from_body(body: ColumnBody<(D, T, R)>) -> Self {
388 mz_ore::soft_assert_no_log!(!body.is_empty(), "chunks must be non-empty");
389 ColumnChunk::Resident(Rc::new(body), 0)
390 }
391
392 fn push_bounded(body: ColumnBody<(D, T, R)>, depth: u8, out: &mut VecDeque<Self>) {
396 let len = body.len();
397 let bytes = body.length_in_bytes();
398 if len <= 1 || bytes <= COMMIT_BYTES {
399 if len > 0 {
400 out.push_back(Self::Resident(Rc::new(body), depth));
401 }
402 return;
403 }
404 Self::push_cuts(
405 &body,
406 cut_records(len, bytes, COMMIT_BYTES).max(1),
407 depth,
408 out,
409 );
410 }
411
412 fn push_cuts(
419 body: &ColumnBody<(D, T, R)>,
420 records: usize,
421 depth: u8,
422 out: &mut VecDeque<Self>,
423 ) {
424 let view = body.borrow();
425 Self::push_range(view, 0..view.len(), records, depth, out);
426 }
427
428 fn push_range(
433 view: BorrowedOf<'_, (D, T, R)>,
434 range: std::ops::Range<usize>,
435 records: usize,
436 depth: u8,
437 out: &mut VecDeque<Self>,
438 ) {
439 let mut start = range.start;
440 while start < range.end {
441 let end = range.end.min(start + records);
442 let mut part = <(D, T, R) as Columnar>::Container::default();
443 part.extend_from_self(view, start..end);
444 let part = ColumnBody::Typed(part);
445 if end - start > 1 && part.length_in_bytes() > COMMIT_BYTES {
446 drop(part);
447 Self::push_range(view, start..end, (end - start).div_ceil(2), depth, out);
448 } else {
449 out.push_back(Self::Resident(Rc::new(part), depth));
450 }
451 start = end;
452 }
453 }
454
455 pub fn into_body(self) -> ColumnBody<(D, T, R)> {
458 match self {
459 ColumnChunk::Resident(col, _) => {
460 Rc::try_unwrap(col).unwrap_or_else(|shared| shared.duplicate())
461 }
462 ColumnChunk::Spilled(body, _) => {
463 let mut words = Vec::new();
464 body.handle.read_into(&mut words);
465 ColumnBody::Words(words)
466 }
467 }
468 }
469
470 pub fn with_body<F, X>(&self, f: F) -> X
473 where
474 F: FnOnce(&ColumnBody<(D, T, R)>) -> X,
475 {
476 match self {
477 ColumnChunk::Resident(col, _) => f(col),
478 ColumnChunk::Spilled(body, _) => {
479 let mut words = Vec::new();
480 body.handle.read_into(&mut words);
481 f(&ColumnBody::Words(words))
482 }
483 }
484 }
485
486 pub fn is_spilled(&self) -> bool {
488 matches!(self, ColumnChunk::Spilled(_, _))
489 }
490
491 fn records(&self) -> usize {
493 match self {
494 ColumnChunk::Resident(col, _) => col.len(),
495 ColumnChunk::Spilled(body, _) => body.records,
496 }
497 }
498
499 fn depth(&self) -> u8 {
501 match self {
502 ColumnChunk::Resident(_, depth) | ColumnChunk::Spilled(_, depth) => *depth,
503 }
504 }
505
506 fn data_span(&self) -> (columnar::Ref<'_, D>, columnar::Ref<'_, D>) {
508 match self {
509 ColumnChunk::Resident(col, _) => {
510 let data = col.borrow().0;
511 (data.get(0), data.get(data.len() - 1))
512 }
513 ColumnChunk::Spilled(body, _) => {
514 let fences = body.fences.borrow();
515 (fences.get(0), fences.get(1))
516 }
517 }
518 }
519
520 fn commit(body: ColumnBody<(D, T, R)>, depth: u8) -> Self
524 where
525 T: Timestamp,
526 {
527 let len_bytes = body.length_in_bytes();
528 metrics::record(metrics::Stage::Commit, body.len(), len_bytes);
529 mz_ore::soft_assert_no_log!(!body.is_empty(), "chunks must be non-empty");
530 match spill_target(G::enabled(), len_bytes) {
531 Some(pool) => Self::spill_body(body, &pool, depth),
532 None => ColumnChunk::Resident(Rc::new(body), depth),
533 }
534 }
535
536 fn spill_body(body: ColumnBody<(D, T, R)>, pool: &Pool, depth: u8) -> Self
544 where
545 T: Timestamp,
546 {
547 let (codec, compressed) = codec_for_depth(depth);
548 let len_bytes = body.length_in_bytes();
549 let (time_lower, time_upper) = Self::time_bounds(&body);
550 let view = body.borrow();
551 let records = view.len();
552 let mut fences = D::Container::default();
553 fences.push(view.0.get(0));
554 fences.push(view.0.get(records - 1));
555 let handle = spill_serialized(&body, pool, len_bytes, ChunkHints { depth }, codec);
556 ColumnChunk::Spilled(
557 Rc::new(SpilledBody {
558 records,
559 fences,
560 time_lower,
561 time_upper: time_upper.into(),
562 compressed,
563 len_bytes,
564 handle,
565 gate: PhantomData,
566 }),
567 depth,
568 )
569 }
570
571 fn survive_merge(self) -> Self
587 where
588 T: Timestamp,
589 {
590 let depth = self.depth().saturating_add(1);
591 match self {
592 ColumnChunk::Resident(col, _) => ColumnChunk::Resident(col, depth),
593 ColumnChunk::Spilled(body, was) => {
594 let migrate = !body.compressed && depth >= compress_min_depth();
595 if !migrate || Rc::strong_count(&body) > 1 {
596 return ColumnChunk::Spilled(body, depth);
597 }
598 match resolve_pool(G::enabled()) {
599 Some(pool) => {
600 let body = ColumnChunk::Spilled(body, was).into_body();
601 Self::spill_body(body, &pool, depth)
602 }
603 None => ColumnChunk::Spilled(body, depth),
604 }
605 }
606 }
607 }
608
609 fn chunk_time_bounds(&self) -> (Cow<'_, Antichain<T>>, Cow<'_, [T]>)
614 where
615 T: Timestamp,
616 {
617 match self {
618 ColumnChunk::Resident(col, _) => {
619 let (lower, upper) = Self::time_bounds(col);
620 (Cow::Owned(lower), Cow::Owned(upper))
621 }
622 ColumnChunk::Spilled(body, _) => (
623 Cow::Borrowed(&body.time_lower),
624 Cow::Borrowed(&body.time_upper[..]),
625 ),
626 }
627 }
628
629 fn time_bounds(body: &ColumnBody<(D, T, R)>) -> (Antichain<T>, Vec<T>)
634 where
635 T: Timestamp,
636 {
637 let (_, times, _) = body.borrow();
638 let mut lower = Antichain::new();
639 let mut upper: Vec<T> = Vec::new();
640 let mut time = T::minimum();
644 for i in 0..times.len() {
645 time.copy_from(rr::<T>(times.get(i)));
646 if !upper.iter().any(|u| PartialOrder::less_equal(&time, u)) {
647 upper.retain(|u| !PartialOrder::less_equal(u, &time));
648 upper.push(time.clone());
649 }
650 lower.insert_ref(&time);
651 }
652 (lower, upper)
653 }
654}
655
656#[derive(Debug)]
661pub struct Lz4Codec;
662
663pub static LZ4_CODEC: Lz4Codec = Lz4Codec;
666
667impl ExtentCodec for Lz4Codec {
668 fn encode(&self, body: &[u8], out: &mut Vec<u8>) {
669 let max_out = lz4_flex::block::get_maximum_output_size(body.len());
670 out.resize(4 + max_out, 0);
671 let len = u32::try_from(body.len()).expect("chunk bodies are bounded by the size classes");
672 out[..4].copy_from_slice(&len.to_le_bytes());
673 let compressed = lz4_flex::block::compress_into(body, &mut out[4..])
674 .expect("output sized to the maximum");
675 out.truncate(4 + compressed);
676 }
677
678 fn decode(&self, stored: &[u8], body: &mut [u8]) {
679 let prefix: [u8; 4] = stored[..4].try_into().expect("prefix length");
680 let len = usize::try_from(u32::from_le_bytes(prefix)).expect("length fits usize");
681 assert_eq!(
682 len,
683 body.len(),
684 "destination must match the encoded body length"
685 );
686 let written = lz4_flex::block::decompress_into(&stored[4..], body)
687 .expect("stored bytes hold a valid lz4 block");
688 assert_eq!(written, body.len(), "decoded length mismatch");
689 }
690}
691
692fn spill_serialized<C: Columnar>(
696 body: &ColumnBody<C>,
697 pool: &Pool,
698 len_bytes: usize,
699 hints: ChunkHints,
700 codec: &'static dyn ExtentCodec,
701) -> ChunkHandle {
702 mz_ore::soft_assert_eq_no_log!(len_bytes % 8, 0);
703 pool.insert_with(len_bytes / 8, hints, codec, |dst| {
704 let bytes: &mut [u8] = bytemuck::cast_slice_mut(dst);
705 let mut cursor = std::io::Cursor::new(bytes);
706 body.write_into(&mut cursor)
707 .expect("the slot is sized to the serialized body");
708 assert_eq!(
709 usize::try_from(cursor.position()).expect("usize position"),
710 len_bytes,
711 "serialized body must fill the chunk exactly",
712 );
713 })
714}
715
716pub fn try_spill_ref<C: Columnar>(body: &ColumnBody<C>, depth: u8) -> Option<ChunkHandle> {
729 let len_bytes = body.length_in_bytes();
730 let pool = spill_target(SINK_SPILL_ENABLED.load(Ordering::Relaxed), len_bytes)?;
731 let (codec, _) = codec_for_depth(depth);
732 Some(spill_serialized(
733 body,
734 &pool,
735 len_bytes,
736 ChunkHints { depth },
737 codec,
738 ))
739}
740
741impl<D, T, R, G> Chunk for ColumnChunk<D, T, R, G>
742where
743 G: SpillGate,
744 D: Columnar,
745 for<'a> columnar::Ref<'a, D>: Copy + Ord,
746 T: Columnar + Default + Timestamp + Lattice + Ord,
747 for<'a> columnar::Ref<'a, T>: Copy + Ord,
748 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
749{
750 type Time = T;
751
752 const TARGET: usize = 65536;
758
759 fn len(&self) -> usize {
760 self.records()
761 }
762
763 fn merge(in1: &mut VecDeque<Self>, in2: &mut VecDeque<Self>, out: &mut VecDeque<Self>) {
770 let (a_first, a_last) = in1
775 .front()
776 .expect("caller guarantees non-empty input")
777 .data_span();
778 let (b_first, b_last) = in2
779 .front()
780 .expect("caller guarantees non-empty input")
781 .data_span();
782 let a_low = rr::<D>(a_last) < rr::<D>(b_first);
783 let b_low = rr::<D>(b_last) < rr::<D>(a_first);
784 if a_low {
785 let chunk = in1.pop_front().expect("front observed above");
786 out.push_back(chunk.survive_merge());
787 return;
788 }
789 if b_low {
790 let chunk = in2.pop_front().expect("front observed above");
791 out.push_back(chunk.survive_merge());
792 return;
793 }
794
795 let a = in1.pop_front().expect("caller guarantees non-empty input");
796 let b = in2.pop_front().expect("caller guarantees non-empty input");
797 let depths = [a.depth(), b.depth()];
800 let out_depth = depths[0].max(depths[1]).saturating_add(1);
801 let mut spill_a = match &a {
802 ColumnChunk::Spilled(body, _) => Some(Rc::clone(body)),
803 ColumnChunk::Resident(_, _) => None,
804 };
805 let mut spill_b = match &b {
806 ColumnChunk::Spilled(body, _) => Some(Rc::clone(body)),
807 ColumnChunk::Resident(_, _) => None,
808 };
809 let mut cols = [a.into_body(), b.into_body()];
810 let mut positions = [0usize, 0usize];
811 loop {
812 let mut result: ColumnBody<(D, T, R)> = ColumnBody::default();
813 let yielded = result.merge_from(&mut cols, &mut positions);
814 if !result.is_empty() {
815 out.push_back(ColumnChunk::Resident(Rc::new(result), out_depth));
816 }
817 if !yielded {
818 break;
819 }
820 }
821 metrics::record(metrics::Stage::Merge, positions[0] + positions[1], 0);
824 let [col_a, col_b] = &mut cols;
825 for (col, pos, depth, spilled, queue) in [
829 (col_a, positions[0], depths[0], &mut spill_a, in1),
830 (col_b, positions[1], depths[1], &mut spill_b, in2),
831 ] {
832 let len = col.len();
833 if pos == 0 && len > 0 {
834 let chunk = match spilled.take() {
837 Some(body) => ColumnChunk::Spilled(body, depth),
838 None => ColumnChunk::Resident(Rc::new(std::mem::take(col)), depth),
839 };
840 queue.push_front(chunk.survive_merge());
841 } else if pos < len {
842 let view = col.borrow();
843 let mut rest = <(D, T, R) as Columnar>::Container::default();
844 rest.extend_from_self(view, pos..len);
845 queue.push_front(ColumnChunk::Resident(
846 Rc::new(ColumnBody::Typed(rest)),
847 depth,
848 ));
849 }
850 }
851 }
852
853 fn extract(
857 input: &mut VecDeque<Self>,
858 frontier: AntichainRef<T>,
859 residual: &mut timely::progress::Antichain<T>,
860 keep: &mut VecDeque<Self>,
861 ship: &mut VecDeque<Self>,
862 ) {
863 let Some(chunk) = input.pop_front() else {
864 return;
865 };
866 let (time_lower, time_upper) = chunk.chunk_time_bounds();
872 if time_upper.iter().all(|t| !frontier.less_equal(t)) {
873 ship.push_back(chunk);
874 return;
875 }
876 if time_lower.elements().iter().all(|m| frontier.less_equal(m)) {
877 for m in time_lower.elements() {
880 residual.insert_ref(m);
881 }
882 keep.push_back(chunk);
883 return;
884 }
885 let depth = chunk.depth();
888 let mut col = chunk.into_body();
889 let len = col.len();
890 let mut pos = 0;
891 let mut keep_col: ColumnBody<(D, T, R)> = ColumnBody::default();
892 let mut ship_col: ColumnBody<(D, T, R)> = ColumnBody::default();
893 let cut = |col: &mut ColumnBody<(D, T, R)>, queue: &mut VecDeque<Self>, force: bool| {
899 if !col.is_empty() && (force || at_serialized_capacity(&col.borrow())) {
900 queue.push_back(ColumnChunk::Resident(Rc::new(std::mem::take(col)), depth));
901 }
902 };
903 while pos < len {
904 col.extract(&mut pos, frontier, residual, &mut keep_col, &mut ship_col);
905 if pos < len {
906 cut(&mut keep_col, keep, false);
907 cut(&mut ship_col, ship, false);
908 }
909 }
910 cut(&mut keep_col, keep, true);
911 cut(&mut ship_col, ship, true);
912 }
913
914 fn advance(
924 input: &mut VecDeque<Self>,
925 frontier: AntichainRef<T>,
926 done: bool,
927 out: &mut VecDeque<Self>,
928 ) {
929 let Some(front) = input.pop_front() else {
930 return;
931 };
932 let mut depth = front.depth();
935 let mut base = front.into_body();
939 {
940 let base_c = base.typed_mut();
941 for chunk in input.drain(..) {
942 depth = depth.max(chunk.depth());
943 let col = chunk.into_body();
944 let view = col.borrow();
945 base_c.extend_from_self(view, 0..view.len());
946 }
947 }
948 let view = base.borrow();
949 let total = view.len();
950 if total == 0 {
951 return;
952 }
953 let data = view.0;
954
955 if !done && data.get(0) == data.get(total - 1) {
958 input.push_front(ColumnChunk::Resident(Rc::new(base), depth));
959 return;
960 }
961
962 let end = if done {
965 total
966 } else {
967 let last = data.get(total - 1);
968 let mut end = total - 1;
969 while end > 0 && data.get(end - 1) == last {
970 end -= 1;
971 }
972 end
973 };
974 metrics::record(metrics::Stage::Advance, end, 0);
978
979 let mut result = <(D, T, R) as Columnar>::Container::default();
980 let mut scratch: Vec<(T, R)> = Vec::new();
982 let mut index = 0;
983 const CUT_CHECK_RECORDS: usize = 1024;
992 let mut records_since_check = 0usize;
993 while index < end {
1000 let group_d = data.get(index);
1001 scratch.clear();
1002 while index < end && data.get(index) == group_d {
1003 let (_, t, r) = view.get(index);
1004 let mut owned_t = T::into_owned(t);
1005 owned_t.advance_by(frontier);
1006 scratch.push((owned_t, R::into_owned(r)));
1007 index += 1;
1008 }
1009 scratch.sort_by(|a, b| a.0.cmp(&b.0));
1010 let mut run = scratch.drain(..).peekable();
1011 while let Some((t, mut r)) = run.next() {
1012 while run.peek().is_some_and(|(t2, _)| *t2 == t) {
1013 let (_, r2) = run.next().expect("peeked");
1014 r.plus_equals(&r2);
1015 }
1016 if !r.is_zero() {
1017 result.0.push(group_d);
1018 result.1.push(&t);
1019 result.2.push(&r);
1020 records_since_check += 1;
1021 if records_since_check >= CUT_CHECK_RECORDS {
1022 records_since_check = 0;
1023 if u64::cast_from(indexed::length_in_words(&result.borrow()))
1024 >= u64::cast_from(COMMIT_BYTES / 8)
1025 {
1026 out.push_back(ColumnChunk::Resident(
1027 Rc::new(ColumnBody::Typed(std::mem::take(&mut result))),
1028 depth,
1029 ));
1030 }
1031 }
1032 }
1033 }
1034 }
1035 if !result.is_empty() {
1036 out.push_back(ColumnChunk::Resident(
1037 Rc::new(ColumnBody::Typed(result)),
1038 depth,
1039 ));
1040 }
1041
1042 if end < total {
1044 let mut carry = <(D, T, R) as Columnar>::Container::default();
1045 carry.extend_from_self(view, end..total);
1046 input.push_front(ColumnChunk::Resident(
1047 Rc::new(ColumnBody::Typed(carry)),
1048 depth,
1049 ));
1050 }
1051 }
1052
1053 fn settle(input: &mut VecDeque<Self>, done: bool, out: &mut VecDeque<Self>) {
1059 Self::settle_graded(input, done, out, true)
1060 }
1061}
1062
1063impl<D, T, R, G> ColumnChunk<D, T, R, G>
1064where
1065 G: SpillGate,
1066 D: Columnar,
1067 T: Columnar + Timestamp,
1068 R: Columnar,
1069 for<'a> columnar::Ref<'a, D>: Ord,
1070 for<'a> columnar::Ref<'a, T>: Ord,
1071{
1072 fn settle_graded(
1079 input: &mut VecDeque<Self>,
1080 done: bool,
1081 out: &mut VecDeque<Self>,
1082 commit: bool,
1083 ) {
1084 let mut acc: Option<(ColumnBody<(D, T, R)>, u8)> = None;
1088 while let Some(chunk) = input.pop_front() {
1089 let (rc, depth) = match chunk {
1090 spilled @ ColumnChunk::Spilled(_, _) => {
1091 if let Some((body, depth)) = acc.take() {
1092 out.push_back(Self::grade(body, depth, commit));
1093 }
1094 out.push_back(spilled);
1095 continue;
1096 }
1097 ColumnChunk::Resident(rc, depth) => (rc, depth),
1098 };
1099 if rc.length_in_bytes() > COMMIT_BYTES && rc.len() > 1 {
1100 let records = cut_records(rc.len(), rc.length_in_bytes(), COMMIT_BYTES).max(1);
1103 let mut pieces = VecDeque::new();
1104 Self::push_cuts(&rc, records, depth, &mut pieces);
1105 for piece in pieces.into_iter().rev() {
1106 input.push_front(piece);
1107 }
1108 continue;
1109 }
1110 let full = at_commit_size(&rc);
1111 let fits = acc.as_ref().is_some_and(|(body, _)| {
1114 body.length_in_bytes().saturating_add(rc.length_in_bytes()) <= COMMIT_BYTES
1115 });
1116 if !full
1117 && fits
1118 && let Some((mut body, acc_depth)) = acc.take()
1119 {
1120 let view = rc.borrow();
1121 body.typed_mut().extend_from_self(view, 0..view.len());
1122 let acc_depth = acc_depth.max(depth);
1123 if at_commit_size(&body) {
1124 out.push_back(Self::grade(body, acc_depth, commit));
1125 } else {
1126 acc = Some((body, acc_depth));
1127 }
1128 continue;
1129 }
1130 if let Some((mut body, acc_depth)) = acc.take() {
1136 let len = rc.len();
1137 let prefix = if full {
1138 0
1139 } else {
1140 let space = COMMIT_BYTES.saturating_sub(body.length_in_bytes());
1141 cut_records(len, rc.length_in_bytes(), space)
1142 };
1143 if prefix == 0 || prefix >= len {
1144 out.push_back(Self::grade(body, acc_depth, commit));
1145 } else {
1146 let view = rc.borrow();
1147 body.typed_mut().extend_from_self(view, 0..prefix);
1148 let mut rest = <(D, T, R) as Columnar>::Container::default();
1149 rest.extend_from_self(view, prefix..len);
1150 input.push_front(ColumnChunk::Resident(
1151 Rc::new(ColumnBody::Typed(rest)),
1152 depth,
1153 ));
1154 let acc_depth = acc_depth.max(depth);
1155 if body.length_in_bytes() > COMMIT_BYTES {
1156 let mut pieces = VecDeque::new();
1158 Self::push_bounded(body, acc_depth, &mut pieces);
1159 for piece in pieces {
1160 let ColumnChunk::Resident(piece, piece_depth) = piece else {
1161 unreachable!("push_bounded emits resident pieces");
1162 };
1163 let piece =
1164 Rc::try_unwrap(piece).unwrap_or_else(|piece| piece.duplicate());
1165 out.push_back(Self::grade(piece, piece_depth, commit));
1166 }
1167 } else {
1168 out.push_back(Self::grade(body, acc_depth, commit));
1169 }
1170 continue;
1171 }
1172 }
1173 let body = Rc::try_unwrap(rc).unwrap_or_else(|rc| rc.duplicate());
1174 if full {
1175 out.push_back(Self::grade(body, depth, commit));
1176 } else {
1177 acc = Some((body, depth));
1178 }
1179 }
1180 if let Some((body, depth)) = acc {
1181 if done {
1182 out.push_back(Self::grade(body, depth, commit));
1183 } else {
1184 input.push_front(ColumnChunk::Resident(Rc::new(body), depth));
1185 }
1186 }
1187 }
1188
1189 fn grade(body: ColumnBody<(D, T, R)>, depth: u8, commit: bool) -> Self {
1192 if commit {
1193 Self::commit(body, depth)
1194 } else {
1195 ColumnChunk::Resident(Rc::new(body), depth)
1196 }
1197 }
1198}
1199
1200fn extract_view_into<'v, 'p, K, V, T, R>(
1205 view: BorrowedOf<'v, ((K, V), T, R)>,
1206 probes: BorrowedOf<'p, K>,
1207 probe_index: &mut usize,
1208 staging: &mut <((K, V), T, R) as Columnar>::Container,
1209) where
1210 K: Columnar,
1211 V: Columnar,
1212 T: Columnar,
1213 R: Columnar,
1214 for<'b> columnar::Ref<'b, K>: Copy + Ord,
1215{
1216 let keys = view.0.0;
1217 let len = keys.len();
1218 let last = keys.get(len - 1);
1219 let count = probes.len();
1220 let mut pos = 0;
1221 while *probe_index < count {
1222 let probe = probes.get(*probe_index);
1223 mz_ore::soft_assert_no_log!(
1224 *probe_index == 0 || rr::<K>(probes.get(*probe_index - 1)) < rr::<K>(probe),
1225 "probe keys must be sorted and deduplicated"
1226 );
1227 if rr::<K>(probe) > rr::<K>(last) {
1228 return;
1229 }
1230 gallop(len, &mut pos, |i| rr::<K>(keys.get(i)) < rr::<K>(probe));
1231 let start = pos;
1232 while pos < len && rr::<K>(keys.get(pos)) == rr::<K>(probe) {
1233 pos += 1;
1234 }
1235 staging.extend_from_self(view, start..pos);
1236 if rr::<K>(probe) == rr::<K>(last) {
1237 return;
1238 }
1239 *probe_index += 1;
1240 }
1241}
1242
1243impl<K, V, T, R, G> UnloadChunk for ColumnChunk<(K, V), T, R, G>
1244where
1245 G: SpillGate,
1246 K: Columnar,
1247 for<'a> columnar::Ref<'a, K>: Copy + Ord,
1248 V: Columnar,
1249 for<'a> columnar::Ref<'a, V>: Copy + Ord,
1250 T: Columnar + Default + Timestamp + Lattice + Ord,
1251 for<'a> columnar::Ref<'a, T>: Copy + Ord,
1252 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
1253{
1254 type Staging = <((K, V), T, R) as Columnar>::Container;
1257
1258 type Probes<'a> = BorrowedOf<'a, K>;
1261
1262 fn probe_count(probes: Self::Probes<'_>) -> usize {
1263 probes.len()
1264 }
1265
1266 fn locate(&self, probes: Self::Probes<'_>, probe_index: usize) -> std::cmp::Ordering {
1267 let probe = probes.get(probe_index);
1268 let (first, last) = self.data_span();
1271 let (first, last) = (first.0, last.0);
1272 if rr::<K>(probe) < rr::<K>(first) {
1273 std::cmp::Ordering::Less
1274 } else if rr::<K>(probe) > rr::<K>(last) {
1275 std::cmp::Ordering::Greater
1276 } else {
1277 std::cmp::Ordering::Equal
1278 }
1279 }
1280
1281 fn extract_into(
1282 &self,
1283 probes: Self::Probes<'_>,
1284 probe_index: &mut usize,
1285 staging: &mut Self::Staging,
1286 ) {
1287 match self {
1288 ColumnChunk::Resident(col, _) => {
1289 extract_view_into::<K, V, T, R>(col.borrow(), probes, probe_index, staging);
1290 }
1291 ColumnChunk::Spilled(body, _) => with_scratch(|scratch| {
1292 body.handle.read_into(scratch);
1298 let view = borrow_words::<((K, V), T, R)>(scratch);
1299 extract_view_into::<K, V, T, R>(view, probes, probe_index, staging);
1300 }),
1301 }
1302 }
1303
1304 fn fetch_into(&self, staging: &mut Self::Staging) {
1305 match self {
1306 ColumnChunk::Resident(col, _) => {
1307 let view = col.borrow();
1308 staging.extend_from_self(view, 0..view.len());
1309 }
1310 ColumnChunk::Spilled(body, _) => with_scratch(|scratch| {
1311 body.handle.read_into(scratch);
1312 let view = borrow_words::<((K, V), T, R)>(scratch);
1313 staging.extend_from_self(view, 0..view.len());
1314 }),
1315 }
1316 }
1317}
1318
1319pub struct UnchunkBuilder<Bu, D: Columnar, T: Columnar, R: Columnar, G: SpillGate> {
1329 inner: Bu,
1330 _marker: std::marker::PhantomData<(D, T, R, G)>,
1331}
1332
1333impl<Bu, D, T, R, G> differential_dataflow::trace::Builder for UnchunkBuilder<Bu, D, T, R, G>
1334where
1335 Bu: differential_dataflow::trace::Builder<Input = ColumnBody<(D, T, R)>> + ChainState,
1336 D: Columnar + 'static,
1337 T: Columnar + 'static,
1338 R: Columnar + 'static,
1339 G: SpillGate,
1340{
1341 type Input = ColumnChunk<D, T, R, G>;
1342 type Time = Bu::Time;
1343 type Output = Bu::Output;
1344
1345 fn with_capacity(keys: usize, vals: usize, upds: usize) -> Self {
1346 Self {
1347 inner: Bu::with_capacity(keys, vals, upds),
1348 _marker: std::marker::PhantomData,
1349 }
1350 }
1351
1352 fn push(&mut self, chunk: &mut Self::Input) {
1353 let mut body = std::mem::take(chunk).into_body();
1354 self.inner.push(&mut body);
1355 }
1356
1357 fn done(
1358 self,
1359 description: differential_dataflow::trace::Description<Self::Time>,
1360 ) -> Self::Output {
1361 self.inner.done(description)
1362 }
1363
1364 fn seal(
1365 chain: &mut Vec<Self::Input>,
1366 description: differential_dataflow::trace::Description<Self::Time>,
1367 ) -> Self::Output {
1368 let mut state = Bu::State::default();
1369 for chunk in chain.iter() {
1370 if !chunk.is_spilled() || Bu::wants_bodies() {
1375 chunk.with_body(|body| Bu::observe(&mut state, body));
1376 } else {
1377 Bu::observe_records(&mut state, chunk.records());
1378 }
1379 }
1380 let mut builder = Self {
1381 inner: Bu::from_state(state),
1382 _marker: std::marker::PhantomData,
1383 };
1384 for chunk in chain.iter_mut() {
1387 builder.push(chunk);
1388 }
1389 chain.clear();
1390 builder.done(description)
1391 }
1392}
1393
1394pub trait ChainState: differential_dataflow::trace::Builder {
1411 type State: Default;
1413
1414 fn wants_bodies() -> bool;
1418
1419 fn observe(state: &mut Self::State, input: &Self::Input);
1421
1422 fn observe_records(state: &mut Self::State, records: usize);
1425
1426 fn from_state(state: Self::State) -> Self;
1428}
1429
1430pub struct ChunkChunker<D: Columnar, T: Columnar, R: Columnar, G: SpillGate> {
1433 inner: ColumnChunker<(D, T, R)>,
1434 ready: VecDeque<ColumnChunk<D, T, R, G>>,
1435 staged: ColumnChunk<D, T, R, G>,
1436}
1437
1438impl<D, T, R, G> Default for ChunkChunker<D, T, R, G>
1439where
1440 D: Columnar,
1441 T: Columnar,
1442 R: Columnar,
1443 G: SpillGate,
1444 ColumnChunker<(D, T, R)>: Default,
1445{
1446 fn default() -> Self {
1447 Self {
1448 inner: Default::default(),
1449 ready: VecDeque::new(),
1450 staged: Default::default(),
1451 }
1452 }
1453}
1454
1455impl<'a, D, T, R, G> PushInto<&'a mut Column<(D, T, R)>> for ChunkChunker<D, T, R, G>
1456where
1457 D: Columnar,
1458 T: Columnar,
1459 R: Columnar,
1460 G: SpillGate,
1461 ColumnChunker<(D, T, R)>: PushInto<&'a mut Column<(D, T, R)>>,
1462{
1463 fn push_into(&mut self, item: &'a mut Column<(D, T, R)>) {
1464 self.inner.push_into(item);
1465 }
1466}
1467
1468impl<D, T, R, G> ContainerBuilder for ChunkChunker<D, T, R, G>
1469where
1470 D: Columnar + 'static,
1471 T: Columnar + 'static,
1472 R: Columnar + 'static,
1473 G: SpillGate,
1474 ColumnChunker<(D, T, R)>: ContainerBuilder<Container = ColumnBody<(D, T, R)>>,
1475{
1476 type Container = ColumnChunk<D, T, R, G>;
1477
1478 fn extract(&mut self) -> Option<&mut Self::Container> {
1479 if self.ready.is_empty() {
1480 let body = self.inner.extract()?;
1481 ColumnChunk::push_bounded(std::mem::take(body), 0, &mut self.ready);
1482 }
1483 self.staged = self.ready.pop_front()?;
1484 Some(&mut self.staged)
1485 }
1486
1487 fn finish(&mut self) -> Option<&mut Self::Container> {
1488 if self.ready.is_empty() {
1489 let body = self.inner.finish()?;
1490 ColumnChunk::push_bounded(std::mem::take(body), 0, &mut self.ready);
1491 }
1492 self.staged = self.ready.pop_front()?;
1493 Some(&mut self.staged)
1494 }
1495}
1496
1497pub type AccountedChunkBatcher<D, T, R, G> =
1502 differential_dataflow::trace::implementations::merge_batcher::MergeBatcher<
1503 AccountedChunkMerger<D, T, R, G>,
1504 >;
1505
1506pub struct AccountedChunkMerger<D: Columnar, T: Columnar, R: Columnar, G: SpillGate> {
1519 inner: differential_dataflow::trace::chunk::ChunkMerger<ColumnChunk<D, T, R, G>>,
1520}
1521
1522impl<D: Columnar, T: Columnar, R: Columnar, G: SpillGate> Default
1523 for AccountedChunkMerger<D, T, R, G>
1524{
1525 fn default() -> Self {
1526 Self {
1527 inner: Default::default(),
1528 }
1529 }
1530}
1531
1532impl<D, T, R, G> differential_dataflow::trace::implementations::merge_batcher::Merger
1533 for AccountedChunkMerger<D, T, R, G>
1534where
1535 D: Columnar + 'static,
1536 T: Columnar + Timestamp + 'static,
1537 R: Columnar + 'static,
1538 G: SpillGate,
1539 for<'a> columnar::Ref<'a, D>: Ord,
1540 for<'a> columnar::Ref<'a, T>: Ord,
1541 ColumnChunk<D, T, R, G>: Chunk,
1542 <ColumnChunk<D, T, R, G> as Chunk>::Time: Clone + PartialOrder + 'static,
1543{
1544 type Chunk = ColumnChunk<D, T, R, G>;
1545 type Time = <ColumnChunk<D, T, R, G> as Chunk>::Time;
1546
1547 fn merge(
1548 &mut self,
1549 list1: Vec<Self::Chunk>,
1550 list2: Vec<Self::Chunk>,
1551 output: &mut Vec<Self::Chunk>,
1552 stash: &mut Vec<Self::Chunk>,
1553 ) {
1554 self.inner.merge(list1, list2, output, stash)
1555 }
1556
1557 fn extract(
1558 &mut self,
1559 merged: Vec<Self::Chunk>,
1560 upper: AntichainRef<Self::Time>,
1561 frontier: &mut Antichain<Self::Time>,
1562 readied: &mut Vec<Self::Chunk>,
1563 kept: &mut Vec<Self::Chunk>,
1564 _stash: &mut Vec<Self::Chunk>,
1565 ) {
1566 let mut input: VecDeque<Self::Chunk> = merged.into();
1574 let (mut keep, mut shipped) = (VecDeque::new(), VecDeque::new());
1575 let (mut kept_q, mut shipped_q) = (VecDeque::new(), VecDeque::new());
1576 while !input.is_empty() {
1577 Chunk::extract(&mut input, upper, frontier, &mut keep, &mut shipped);
1578 Chunk::settle(&mut keep, false, &mut kept_q);
1579 ColumnChunk::settle_graded(&mut shipped, false, &mut shipped_q, false);
1580 }
1581 Chunk::settle(&mut keep, true, &mut kept_q);
1582 ColumnChunk::settle_graded(&mut shipped, true, &mut shipped_q, false);
1583 Extend::extend(kept, kept_q);
1584 Extend::extend(readied, shipped_q);
1585 }
1586
1587 fn len(chunk: &Self::Chunk) -> usize {
1588 Chunk::len(chunk)
1589 }
1590
1591 fn allocation(chunk: &Self::Chunk) -> (usize, usize, usize) {
1592 let bytes = match chunk {
1593 ColumnChunk::Resident(col, _) => col.length_in_bytes(),
1594 ColumnChunk::Spilled(body, _) => body.len_bytes,
1595 };
1596 (bytes, bytes, 1)
1597 }
1598}
1599
1600#[cfg(test)]
1601mod tests {
1602 use differential_dataflow::trace::chunk::{ChunkBatch, ChunkBatcher};
1612 use differential_dataflow::trace::{Batcher, Description};
1613 use mz_ore::pool::Pool;
1614 use proptest::prelude::*;
1615 use timely::container::PushInto;
1616 use timely::progress::Antichain;
1617
1618 use crate::columnar::unload::UnloadBatch;
1619
1620 use super::*;
1621
1622 type Tuple = ((u64, u64), u64, i64);
1623 type TestChunk = ColumnChunk<(u64, u64), u64, i64, ComputeSpill>;
1624
1625 #[mz_ore::test]
1630 fn lz4_codec_matches_the_previous_extent_framing() {
1631 let body: Vec<u8> = (0..100_000u32).flat_map(|i| i.to_le_bytes()).collect();
1632 let mut stored = Vec::new();
1633 LZ4_CODEC.encode(&body, &mut stored);
1634 assert_eq!(stored, lz4_flex::block::compress_prepend_size(&body));
1635 let mut round = vec![0u8; body.len()];
1636 LZ4_CODEC.decode(&stored, &mut round);
1637 assert_eq!(round, body);
1638 }
1639
1640 #[mz_ore::test]
1641 #[should_panic(expected = "destination must match")]
1642 fn lz4_codec_decode_length_mismatch_panics() {
1643 let mut stored = Vec::new();
1644 LZ4_CODEC.encode(&[7u8; 64], &mut stored);
1645 let mut short = vec![0u8; 32];
1646 LZ4_CODEC.decode(&stored, &mut short);
1647 }
1648
1649 fn consolidate(mut v: Vec<Tuple>) -> Vec<Tuple> {
1652 v.sort();
1653 let mut out: Vec<Tuple> = Vec::new();
1654 for (d, t, r) in v {
1655 if let Some(last) = out.last_mut() {
1656 if last.0 == d && last.1 == t {
1657 last.2 += r;
1658 continue;
1659 }
1660 }
1661 out.push((d, t, r));
1662 }
1663 out.retain(|x| x.2 != 0);
1664 out
1665 }
1666
1667 fn arb_consolidated() -> impl Strategy<Value = Vec<Tuple>> {
1668 prop::collection::vec(((0u64..5, 0u64..5), 0u64..4, -3i64..=3i64), 0..40)
1669 .prop_map(consolidate)
1670 }
1671
1672 fn build_column(v: &[Tuple]) -> ColumnBody<Tuple> {
1673 let mut col: ColumnBody<Tuple> = Default::default();
1674 for tup in v {
1675 col.push_into(*tup);
1676 }
1677 col
1678 }
1679
1680 fn collect_column(col: &ColumnBody<Tuple>) -> Vec<Tuple> {
1681 col.borrow()
1682 .into_index_iter()
1683 .map(|((k, v), t, r)| {
1684 (
1685 (u64::into_owned(k), u64::into_owned(v)),
1686 u64::into_owned(t),
1687 i64::into_owned(r),
1688 )
1689 })
1690 .collect()
1691 }
1692
1693 fn collect_chunks(chunks: impl IntoIterator<Item = TestChunk>) -> Vec<Tuple> {
1694 chunks
1695 .into_iter()
1696 .flat_map(|chunk| collect_column(&chunk.into_body()))
1697 .collect()
1698 }
1699
1700 fn collect_staging(staging: &<Tuple as Columnar>::Container) -> Vec<Tuple> {
1701 staging
1702 .borrow()
1703 .into_index_iter()
1704 .map(|((k, v), t, r)| {
1705 (
1706 (u64::into_owned(k), u64::into_owned(v)),
1707 u64::into_owned(t),
1708 i64::into_owned(r),
1709 )
1710 })
1711 .collect()
1712 }
1713
1714 fn chunked(data: &[Tuple], cuts: &[usize]) -> VecDeque<TestChunk> {
1716 let mut chunks = VecDeque::new();
1717 let mut start = 0;
1718 for cut in cuts {
1719 let end = (start + 1 + cut % 7).min(data.len());
1720 if end > start {
1721 chunks.push_back(ColumnChunk::from_body(build_column(&data[start..end])));
1722 start = end;
1723 }
1724 }
1725 if start < data.len() {
1726 chunks.push_back(ColumnChunk::from_body(build_column(&data[start..])));
1727 }
1728 chunks
1729 }
1730
1731 fn chunked_spilled(data: &[Tuple], cuts: &[usize], pool: &Pool) -> VecDeque<TestChunk> {
1734 chunked(data, cuts)
1735 .into_iter()
1736 .map(|chunk| force_spill(chunk, pool))
1737 .collect()
1738 }
1739
1740 fn body_compressed(chunk: &TestChunk) -> bool {
1744 match chunk {
1745 ColumnChunk::Spilled(body, _) => body.compressed,
1746 ColumnChunk::Resident(_, _) => panic!("chunk must be spilled"),
1747 }
1748 }
1749
1750 fn force_spill(chunk: TestChunk, pool: &Pool) -> TestChunk {
1753 let depth = chunk.depth();
1754 TestChunk::spill_body(chunk.into_body(), pool, depth)
1755 }
1756
1757 #[mz_ore::test]
1760 #[cfg_attr(miri, ignore)]
1761 fn allocation_reports_a_body_wherever_it_lives() {
1762 use differential_dataflow::trace::implementations::merge_batcher::Merger;
1763
1764 let data: Vec<Tuple> = (0..64u64).map(|i| ((i, i), 0, 1)).collect();
1765 let resident = TestChunk::from_body(build_column(&data));
1766 let (size, capacity, allocations) =
1767 AccountedChunkMerger::<(u64, u64), u64, i64, ComputeSpill>::allocation(&resident);
1768 assert!(size > 0, "a resident body reports its bytes");
1769 assert_eq!((capacity, allocations), (size, 1));
1770
1771 let spilled = force_spill(resident, &test_pool());
1772 assert_eq!(
1773 AccountedChunkMerger::<(u64, u64), u64, i64, ComputeSpill>::allocation(&spilled),
1774 (size, capacity, allocations),
1775 "spilling a body does not change what its owner holds"
1776 );
1777 }
1778
1779 #[mz_ore::test]
1788 #[cfg_attr(miri, ignore)]
1789 fn seal_readies_resident_chunks() {
1790 use differential_dataflow::trace::Batcher;
1791
1792 let data: Vec<Tuple> = (0..20_000u64).map(|i| ((i, i), 0, 1)).collect();
1796
1797 set_spill_override(Some(test_pool()));
1798 let mut batcher = AccountedChunkBatcher::<(u64, u64), u64, i64, ComputeSpill>::new(None, 0);
1799 batcher.push_into(TestChunk::from_body(build_column(&data)));
1800 let (chain, _description) = batcher.seal(Antichain::new());
1801 set_spill_override(None);
1802
1803 assert!(!chain.is_empty(), "the seal ships what was pushed");
1804 assert!(
1805 chain.iter().all(|chunk| !chunk.is_spilled()),
1806 "a chunk that arrived resident is readied resident"
1807 );
1808 }
1809
1810 fn test_pool() -> Pool {
1814 static POOL: std::sync::OnceLock<Pool> = std::sync::OnceLock::new();
1815 POOL.get_or_init(|| Pool::new().expect("pool creation"))
1816 .clone()
1817 }
1818
1819 proptest! {
1820 #[mz_ore::test]
1823 #[cfg_attr(miri, ignore)]
1824 fn batcher_round_trip(
1825 inputs in prop::collection::vec(arb_consolidated(), 1..6),
1826 cuts in prop::collection::vec(0usize..7, 0..8),
1827 ) {
1828 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1829 let mut union = Vec::new();
1830 for input in &inputs {
1831 Extend::extend(&mut union, input.iter().copied());
1832 for chunk in chunked(input, &cuts) {
1833 batcher.push_into(chunk);
1834 }
1835 }
1836 let (sealed, _description) = batcher.seal(Antichain::new());
1838 prop_assert_eq!(collect_chunks(sealed), consolidate(union));
1839 }
1840
1841 #[mz_ore::test]
1844 #[cfg_attr(miri, ignore)]
1845 fn batcher_round_trip_spilled(
1846 inputs in prop::collection::vec(arb_consolidated(), 1..4),
1847 cuts in prop::collection::vec(0usize..7, 0..6),
1848 ) {
1849 let pool = test_pool();
1850 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1851 let mut union = Vec::new();
1852 for input in &inputs {
1853 Extend::extend(&mut union, input.iter().copied());
1854 for chunk in chunked_spilled(input, &cuts, &pool) {
1855 batcher.push_into(chunk);
1856 }
1857 }
1858 let (sealed, _description) = batcher.seal(Antichain::new());
1859 prop_assert_eq!(collect_chunks(sealed), consolidate(union));
1860 }
1861
1862 #[mz_ore::test]
1865 #[cfg_attr(miri, ignore)]
1866 fn seal_partitions_by_time(
1867 input in arb_consolidated(),
1868 cuts in prop::collection::vec(0usize..7, 0..8),
1869 upper in 0u64..5,
1870 ) {
1871 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1872 for chunk in chunked(&input, &cuts) {
1873 batcher.push_into(chunk);
1874 }
1875 let (shipped, _) = batcher.seal(Antichain::from_elem(upper));
1876 let expected_shipped: Vec<Tuple> =
1877 input.iter().copied().filter(|(_, t, _)| *t < upper).collect();
1878 prop_assert_eq!(collect_chunks(shipped), consolidate(expected_shipped));
1879
1880 let kept_min = input.iter().filter(|(_, t, _)| *t >= upper).map(|(_, t, _)| *t).min();
1881 let frontier = batcher.frontier().to_owned();
1882 prop_assert_eq!(frontier.elements().first().copied(), kept_min);
1883
1884 let (rest, _) = batcher.seal(Antichain::new());
1885 let expected_rest: Vec<Tuple> =
1886 input.iter().copied().filter(|(_, t, _)| *t >= upper).collect();
1887 prop_assert_eq!(collect_chunks(rest), consolidate(expected_rest));
1888 }
1889
1890 #[mz_ore::test]
1894 #[cfg_attr(miri, ignore)]
1895 fn seal_partitions_by_time_spilled(
1896 input in arb_consolidated(),
1897 cuts in prop::collection::vec(0usize..7, 0..8),
1898 upper in 0u64..5,
1899 ) {
1900 let pool = test_pool();
1901 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
1902 for chunk in chunked_spilled(&input, &cuts, &pool) {
1903 batcher.push_into(chunk);
1904 }
1905 let (shipped, _) = batcher.seal(Antichain::from_elem(upper));
1906 let expected_shipped: Vec<Tuple> =
1907 input.iter().copied().filter(|(_, t, _)| *t < upper).collect();
1908 prop_assert_eq!(collect_chunks(shipped), consolidate(expected_shipped));
1909
1910 let kept_min = input.iter().filter(|(_, t, _)| *t >= upper).map(|(_, t, _)| *t).min();
1911 let frontier = batcher.frontier().to_owned();
1912 prop_assert_eq!(frontier.elements().first().copied(), kept_min);
1913
1914 let (rest, _) = batcher.seal(Antichain::new());
1915 let expected_rest: Vec<Tuple> =
1916 input.iter().copied().filter(|(_, t, _)| *t >= upper).collect();
1917 prop_assert_eq!(collect_chunks(rest), consolidate(expected_rest));
1918 }
1919
1920 #[mz_ore::test]
1923 #[cfg_attr(miri, ignore)]
1924 fn advance_matches_reference(
1925 input in arb_consolidated(),
1926 cuts in prop::collection::vec(0usize..7, 0..8),
1927 frontier_elem in 0u64..5,
1928 ) {
1929 let frontier = Antichain::from_elem(frontier_elem);
1930 let mut chunks = chunked(&input, &cuts);
1931 let mut out = VecDeque::new();
1932 TestChunk::advance(&mut chunks, frontier.borrow(), false, &mut out);
1933 TestChunk::advance(&mut chunks, frontier.borrow(), true, &mut out);
1934 prop_assert!(chunks.is_empty());
1935
1936 let expected = consolidate(
1937 input
1938 .iter()
1939 .map(|&(d, mut t, r)| {
1940 t.advance_by(frontier.borrow());
1941 (d, t, r)
1942 })
1943 .collect(),
1944 );
1945 prop_assert_eq!(collect_chunks(out), expected);
1946 }
1947
1948 #[mz_ore::test]
1951 #[cfg_attr(miri, ignore)]
1952 fn settle_preserves_and_packs(
1953 input in arb_consolidated(),
1954 cuts in prop::collection::vec(0usize..7, 1..8),
1955 ) {
1956 let mut chunks = chunked(&input, &cuts);
1957 let mut out = VecDeque::new();
1958 TestChunk::settle(&mut chunks, true, &mut out);
1959 prop_assert!(chunks.is_empty());
1960 prop_assert!(out.len() <= 1);
1963 prop_assert_eq!(collect_chunks(out), input);
1964 }
1965
1966 #[mz_ore::test]
1970 #[cfg_attr(miri, ignore)]
1971 fn unload_extract_matches_filter(
1972 input in arb_consolidated(),
1973 cuts in prop::collection::vec(0usize..7, 0..8),
1974 probe_keys in prop::collection::btree_set(0u64..6, 0..6),
1975 spill in any::<bool>(),
1976 ) {
1977 prop_assume!(!input.is_empty());
1978 let pool = test_pool();
1979 let chunks: Vec<TestChunk> = if spill {
1980 chunked_spilled(&input, &cuts, &pool).into()
1981 } else {
1982 chunked(&input, &cuts).into()
1983 };
1984 let description = Description::new(
1985 Antichain::from_elem(0u64),
1986 Antichain::new(),
1987 Antichain::from_elem(0u64),
1988 );
1989 let batch = ChunkBatch::new(chunks, description);
1990
1991 let mut probe_col = <u64 as Columnar>::Container::default();
1992 for key in &probe_keys {
1993 probe_col.push(*key);
1994 }
1995 let mut staging = <Tuple as Columnar>::Container::default();
1996 batch.extract_into(probe_col.borrow(), &mut staging);
1997
1998 let expected: Vec<Tuple> = input
1999 .iter()
2000 .copied()
2001 .filter(|((k, _), _, _)| probe_keys.contains(k))
2002 .collect();
2003 prop_assert_eq!(collect_staging(&staging), expected);
2004
2005 let mut staging = <Tuple as Columnar>::Container::default();
2008 batch.fetch_into(&mut staging);
2009 prop_assert_eq!(collect_staging(&staging), input);
2010 }
2011 }
2012
2013 #[mz_ore::test]
2016 fn locate_spans_keys() {
2017 let chunk = TestChunk::from_body(build_column(&[
2018 ((2, 0), 0, 1),
2019 ((4, 0), 0, 1),
2020 ((6, 0), 0, 1),
2021 ]));
2022 let mut probe_col = <u64 as Columnar>::Container::default();
2023 for key in [0u64, 2, 3, 6, 9] {
2024 probe_col.push(key);
2025 }
2026 let probes = probe_col.borrow();
2027 use std::cmp::Ordering::*;
2028 let expected = [Less, Equal, Equal, Equal, Greater];
2029 for (index, expected) in expected.iter().enumerate() {
2030 assert_eq!(chunk.locate(probes, index), *expected, "probe {index}");
2031 }
2032 }
2033
2034 fn collect_bounded(chunks: impl IntoIterator<Item = TestChunk>, bound: usize) -> Vec<Tuple> {
2037 let mut collected = Vec::new();
2038 for chunk in chunks {
2039 let col = chunk.into_body();
2040 let bytes = col.length_in_bytes();
2041 assert!(bytes <= bound, "chunk of {bytes} bytes exceeds {bound}");
2042 Extend::extend(&mut collected, collect_column(&col));
2043 }
2044 collected
2045 }
2046
2047 type WideUpdate = ((u64, String), u64, i64);
2048 type WideChunk = ColumnChunk<(u64, String), u64, i64, ComputeSpill>;
2049
2050 fn wide_column(keys: impl Iterator<Item = u64>, bytes: usize) -> ColumnBody<WideUpdate> {
2051 let mut column = ColumnBody::default();
2052 for key in keys {
2053 column.push_into(&((key, "x".repeat(bytes)), 0, 1));
2054 }
2055 column
2056 }
2057
2058 fn assert_wide_byte_bound(chunks: VecDeque<WideChunk>, expected: usize, payload_bytes: usize) {
2059 let mut keys = Vec::new();
2060 for chunk in chunks {
2061 let column = chunk.into_body();
2062 assert!(
2063 column.length_in_bytes() <= COMMIT_BYTES || column.len() == 1,
2064 "{} bytes in a {}-record chunk",
2065 column.length_in_bytes(),
2066 column.len(),
2067 );
2068 let view = column.borrow();
2069 for index in 0..view.len() {
2070 let ((key, payload), time, diff) = view.get(index);
2071 assert_eq!(payload.len(), payload_bytes);
2072 assert!(payload.iter().all(|byte| *byte == b'x'));
2073 assert_eq!((*time, *diff), (0, 1));
2074 keys.push(*key);
2075 }
2076 }
2077 assert_eq!(keys, (0..u64::cast_from(expected)).collect::<Vec<_>>());
2078 }
2079
2080 #[mz_ore::test]
2081 fn chunker_enforces_byte_bound() {
2082 let mut chunker = ChunkChunker::default();
2083 let mut input: Column<WideUpdate> = wide_column((0..4000).rev(), 3000).into();
2084 chunker.push_into(&mut input);
2085 let mut chunks = VecDeque::new();
2086 if let Some(chunk) = chunker.extract() {
2087 chunks.push_back(std::mem::take(chunk));
2088 }
2089 while let Some(chunk) = chunker.finish() {
2090 chunks.push_back(std::mem::take(chunk));
2091 }
2092 assert_wide_byte_bound(chunks, 4000, 3000);
2093 }
2094
2095 #[mz_ore::test]
2096 fn merge_settle_enforces_byte_bound() {
2097 let mut left = VecDeque::from([WideChunk::from_body(wide_column(
2098 (0..2000).step_by(2),
2099 2100,
2100 ))]);
2101 let mut right = VecDeque::from([WideChunk::from_body(wide_column(
2102 (1..2000).step_by(2),
2103 2100,
2104 ))]);
2105 let mut merged = VecDeque::new();
2106 while !left.is_empty() && !right.is_empty() {
2107 WideChunk::merge(&mut left, &mut right, &mut merged);
2108 }
2109 merged.append(&mut left);
2110 merged.append(&mut right);
2111 let mut settled = VecDeque::new();
2112 WideChunk::settle(&mut merged, true, &mut settled);
2113 assert_wide_byte_bound(settled, 2000, 2100);
2114 }
2115
2116 #[mz_ore::test]
2117 fn settle_enforces_byte_bound_after_coalescing() {
2118 let mut input = VecDeque::from([
2119 WideChunk::from_body(wide_column(0..400, 3000)),
2120 WideChunk::from_body(wide_column(400..800, 3000)),
2121 ]);
2122 let mut settled = VecDeque::new();
2123 WideChunk::settle(&mut input, true, &mut settled);
2124 assert_wide_byte_bound(settled, 800, 3000);
2125 }
2126
2127 #[mz_ore::test]
2128 fn settle_byte_bound_allows_indivisible_update() {
2129 let mut input = VecDeque::from([WideChunk::from_body(wide_column(0..1, 2 * COMMIT_BYTES))]);
2130 let mut settled = VecDeque::new();
2131 WideChunk::settle(&mut input, true, &mut settled);
2132 assert_wide_byte_bound(settled, 1, 2 * COMMIT_BYTES);
2133 }
2134
2135 #[mz_ore::test]
2139 fn push_bounded_splits_uneven_widths() {
2140 let mut column: ColumnBody<WideUpdate> = ColumnBody::default();
2141 column.push_into(&((0, "x".repeat(COMMIT_BYTES - 4096)), 0, 1));
2142 for key in 1..20_000u64 {
2143 column.push_into(&((key, "x".to_string()), 0, 1));
2144 }
2145 assert!(column.length_in_bytes() > COMMIT_BYTES);
2146 let mut out = VecDeque::new();
2147 WideChunk::push_bounded(column, 3, &mut out);
2148 let mut keys = Vec::new();
2149 for chunk in out {
2150 let ColumnChunk::Resident(_, depth) = &chunk else {
2151 panic!("pieces are resident");
2152 };
2153 assert_eq!(*depth, 3, "pieces keep the input's depth");
2154 let column = chunk.into_body();
2155 assert!(
2156 column.length_in_bytes() <= COMMIT_BYTES || column.len() == 1,
2157 "{} bytes in a {}-record piece",
2158 column.length_in_bytes(),
2159 column.len(),
2160 );
2161 let view = column.borrow();
2162 for index in 0..view.len() {
2163 let ((key, _), _, _) = view.get(index);
2164 keys.push(*key);
2165 }
2166 }
2167 assert_eq!(keys, (0..20_000u64).collect::<Vec<_>>());
2168 }
2169
2170 #[mz_ore::test]
2174 fn merge_output_within_byte_bound_before_settle() {
2175 let interleaved = (
2176 wide_column((0..1600).step_by(2), 2100),
2177 wide_column((1..1600).step_by(2), 2100),
2178 );
2179 let runs = (
2180 wide_column((0..500).chain(1000..1100), 2100),
2181 wide_column(500..1000, 2100),
2182 );
2183 for (left, right) in [interleaved, runs] {
2184 let expected = left.len() + right.len();
2185 let mut left = VecDeque::from([WideChunk::from_body(left)]);
2186 let mut right = VecDeque::from([WideChunk::from_body(right)]);
2187 let mut merged = VecDeque::new();
2188 while !left.is_empty() && !right.is_empty() {
2189 WideChunk::merge(&mut left, &mut right, &mut merged);
2190 }
2191 assert!(merged.len() > 1, "merge never cut its output");
2192 merged.append(&mut left);
2193 merged.append(&mut right);
2194 assert_wide_byte_bound(merged, expected, 2100);
2195 }
2196 }
2197
2198 #[mz_ore::test]
2199 fn settle_split_preserves_depth() {
2200 let column = Rc::new(wide_column(0..2000, 2100));
2201 let shared = Rc::clone(&column);
2202 let mut input = VecDeque::from([WideChunk::Resident(column, 3)]);
2203 let mut settled = VecDeque::new();
2204 WideChunk::settle(&mut input, true, &mut settled);
2205 assert!(settled.len() > 1, "oversized chunk was not split");
2206 for chunk in &settled {
2207 assert_eq!(chunk.depth(), 3, "split piece changed generation");
2208 }
2209 assert_eq!(shared.len(), 2000, "shared body was mutated");
2210 assert_wide_byte_bound(settled, 2000, 2100);
2211 }
2212
2213 #[mz_ore::test]
2216 fn settle_split_then_coalesce_keeps_depth() {
2217 let mut input = VecDeque::from([
2218 WideChunk::Resident(Rc::new(wide_column(0..800, 3000)), 2),
2219 WideChunk::Resident(Rc::new(wide_column(800..810, 3000)), 0),
2220 ]);
2221 let mut settled = VecDeque::new();
2222 WideChunk::settle(&mut input, true, &mut settled);
2223 assert!(settled.len() > 1, "oversized chunk was not split");
2224 for chunk in &settled {
2225 assert_eq!(chunk.depth(), 2, "coalescing lost the deeper generation");
2226 }
2227 assert_wide_byte_bound(settled, 810, 3000);
2228 }
2229
2230 #[mz_ore::test]
2234 fn settle_fills_accumulated_column_from_next_chunk() {
2235 let per_chunk = 370;
2236 let mut input: VecDeque<WideChunk> = (0..8u64)
2237 .map(|i| {
2238 let start = i * per_chunk;
2239 WideChunk::from_body(wide_column(start..start + per_chunk, 3000))
2240 })
2241 .collect();
2242 let chunk_bytes = input[0].clone().into_body().length_in_bytes();
2243 assert!(2 * chunk_bytes > COMMIT_BYTES && chunk_bytes < COMMIT_BYTES);
2244 let mut settled = VecDeque::new();
2245 WideChunk::settle(&mut input, true, &mut settled);
2246 let sizes: Vec<usize> = settled
2247 .iter()
2248 .map(|chunk| chunk.clone().into_body().length_in_bytes())
2249 .collect();
2250 let (last, rest) = sizes.split_last().expect("settled output");
2251 assert!(*last <= COMMIT_BYTES);
2252 for size in rest {
2253 assert!(
2254 *size >= COMMIT_BYTES - COMMIT_BYTES / 10 && *size <= COMMIT_BYTES,
2255 "committed {size} bytes, sizes {sizes:?}",
2256 );
2257 }
2258 assert_wide_byte_bound(settled, 8 * 370, 3000);
2259 }
2260
2261 #[mz_ore::test]
2264 #[cfg_attr(miri, ignore)]
2265 fn advance_cuts_large_output() {
2266 let records: Vec<Tuple> = (0..300_000u64).map(|k| ((k, 0), 0, 1)).collect();
2267 let mut input = VecDeque::from([ColumnChunk::from_body(build_column(&records))]);
2268 let frontier = Antichain::from_elem(0u64);
2269 let mut out = VecDeque::new();
2270 TestChunk::advance(&mut input, frontier.borrow(), true, &mut out);
2271 assert!(input.is_empty());
2272 assert!(
2273 out.len() >= 2,
2274 "expected a cut output, got {} chunk(s)",
2275 out.len()
2276 );
2277 assert_eq!(collect_bounded(out, 2 * COMMIT_BYTES), records);
2278 }
2279
2280 #[mz_ore::test]
2283 #[cfg_attr(miri, ignore)]
2284 fn advance_withholds_giant_group() {
2285 let records: Vec<Tuple> = (0..100u64).map(|t| ((7, 7), t, 1)).collect();
2286 let mut input: VecDeque<TestChunk> = VecDeque::new();
2287 for piece in records.chunks(30) {
2288 input.push_back(ColumnChunk::from_body(build_column(piece)));
2289 }
2290 let frontier = Antichain::from_elem(50u64);
2291 let mut out = VecDeque::new();
2292 TestChunk::advance(&mut input, frontier.borrow(), false, &mut out);
2293 assert!(out.is_empty(), "nothing may ship from a single open group");
2294 assert_eq!(input.len(), 1, "the whole input becomes one carry chunk");
2295 TestChunk::advance(&mut input, frontier.borrow(), true, &mut out);
2297 assert!(input.is_empty());
2298 let advanced = records.iter().map(|&(d, t, r)| (d, t.max(50), r)).collect();
2299 assert_eq!(collect_chunks(out), consolidate(advanced));
2300 }
2301
2302 #[mz_ore::test]
2307 #[cfg_attr(miri, ignore)]
2308 fn extract_passes_frontier_disjoint_chunks_through() {
2309 set_spill_override(Some(test_pool()));
2310 let low: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), i % 4, 1)).collect();
2311 let high: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), 6 + i % 4, 1)).collect();
2312 let spilled_chunk = |data: &[Tuple]| {
2313 let chunk = TestChunk::commit(build_column(&consolidate(data.to_vec())), 1);
2314 assert!(chunk.is_spilled());
2315 chunk
2316 };
2317
2318 let mut input = VecDeque::from([spilled_chunk(&low), spilled_chunk(&high)]);
2323 let frontier = Antichain::from_elem(5u64);
2324 let mut residual = Antichain::new();
2325 let (mut keep, mut ship) = (VecDeque::new(), VecDeque::new());
2326 while !input.is_empty() {
2327 TestChunk::extract(
2328 &mut input,
2329 frontier.borrow(),
2330 &mut residual,
2331 &mut keep,
2332 &mut ship,
2333 );
2334 }
2335 assert_eq!(ship.len(), 1);
2336 assert!(ship[0].is_spilled(), "shipped whole: body untouched");
2337 assert_eq!(keep.len(), 1);
2338 assert!(keep[0].is_spilled(), "kept whole: body untouched");
2339 assert_eq!(residual, Antichain::from_elem(6));
2340 let shipped = ship.pop_front().unwrap().into_body();
2341 assert_eq!(collect_column(&shipped), consolidate(low));
2342 let kept = keep.pop_front().unwrap().into_body();
2343 assert_eq!(collect_column(&kept), consolidate(high));
2344 set_spill_override(None);
2345 }
2346
2347 #[mz_ore::test]
2350 #[cfg_attr(miri, ignore)]
2351 fn extract_cuts_large_output() {
2352 let records: Vec<Tuple> = (0..300_000u64).map(|k| ((k, 0), k % 2, 1)).collect();
2353 let mut input = VecDeque::from([ColumnChunk::from_body(build_column(&records))]);
2354 let frontier = Antichain::from_elem(1u64);
2355 let mut residual = Antichain::new();
2356 let (mut keep, mut ship) = (VecDeque::new(), VecDeque::new());
2357 while !input.is_empty() {
2358 TestChunk::extract(
2359 &mut input,
2360 frontier.borrow(),
2361 &mut residual,
2362 &mut keep,
2363 &mut ship,
2364 );
2365 }
2366 assert!(
2367 keep.len() >= 2,
2368 "expected a cut keep side, got {} chunk(s)",
2369 keep.len()
2370 );
2371 assert!(
2372 ship.len() >= 2,
2373 "expected a cut ship side, got {} chunk(s)",
2374 ship.len()
2375 );
2376 let kept: Vec<Tuple> = records.iter().copied().filter(|r| r.1 >= 1).collect();
2377 let shipped: Vec<Tuple> = records.iter().copied().filter(|r| r.1 < 1).collect();
2378 assert_eq!(collect_bounded(keep, 2 * COMMIT_BYTES), kept);
2379 assert_eq!(collect_bounded(ship, 2 * COMMIT_BYTES), shipped);
2380 assert_eq!(residual, Antichain::from_elem(1));
2381 }
2382
2383 #[mz_ore::test]
2386 fn locate_uses_resident_bounds() {
2387 let pool = test_pool();
2388 let data: Vec<Tuple> = vec![((2, 0), 0, 1), ((4, 0), 0, 1)];
2389 let chunk = force_spill(ColumnChunk::from_body(build_column(&data)), &pool);
2390
2391 let mut probe_col = <u64 as Columnar>::Container::default();
2392 for key in [1u64, 3, 5] {
2393 probe_col.push(key);
2394 }
2395 let probes = probe_col.borrow();
2396 assert_eq!(chunk.locate(probes, 0), std::cmp::Ordering::Less);
2397 assert_eq!(chunk.locate(probes, 1), std::cmp::Ordering::Equal);
2398 assert_eq!(chunk.locate(probes, 2), std::cmp::Ordering::Greater);
2399 }
2400
2401 #[mz_ore::test]
2404 #[cfg_attr(miri, ignore)] fn spill_round_trip() {
2406 set_spill_override(Some(test_pool()));
2407
2408 let data: Vec<Tuple> = (0..40_000u64)
2409 .map(|i| ((i / 4, i % 4), i % 8, 1i64))
2410 .collect();
2411 let data = consolidate(data);
2412
2413 let column = build_column(&data);
2414 let committed = TestChunk::commit(column, 0);
2415 assert!(committed.is_spilled(), "large body must spill");
2416 assert_eq!(committed.len(), data.len());
2417 assert_eq!(collect_column(&committed.clone().into_body()), data);
2418
2419 let mut batcher: ChunkBatcher<TestChunk> = Batcher::new(None, 0);
2420 for piece in data.chunks(10_000) {
2421 batcher.push_into(ColumnChunk::from_body(build_column(piece)));
2422 }
2423 let (sealed, _) = batcher.seal(Antichain::new());
2424 assert!(
2425 sealed.iter().any(ColumnChunk::is_spilled),
2426 "sealed output should contain spilled chunks",
2427 );
2428 assert_eq!(collect_chunks(sealed), data);
2429
2430 set_spill_override(None);
2431 }
2432
2433 #[mz_ore::test]
2436 #[cfg_attr(miri, ignore)] fn merge_spilled_chains() {
2438 set_spill_override(Some(test_pool()));
2439
2440 let a: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), 0, 1i64)).collect();
2441 let b: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), 0, 2i64)).collect();
2442
2443 let mut in1 = VecDeque::from([TestChunk::commit(build_column(&a), 0)]);
2444 let mut in2 = VecDeque::from([TestChunk::commit(build_column(&b), 0)]);
2445 assert!(in1[0].is_spilled() && in2[0].is_spilled());
2446
2447 let mut out = VecDeque::new();
2448 while !in1.is_empty() && !in2.is_empty() {
2449 TestChunk::merge(&mut in1, &mut in2, &mut out);
2450 }
2451 for tail in in1.drain(..).chain(in2.drain(..)) {
2452 out.push_back(tail);
2453 }
2454
2455 let expected: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), 0, 3i64)).collect();
2456 assert_eq!(collect_chunks(out), expected);
2457
2458 set_spill_override(None);
2459 }
2460
2461 #[mz_ore::test]
2464 fn merge_untouched_survivor_stays_spilled() {
2465 let pool = test_pool();
2466 let low: Vec<Tuple> = (0..100u64).map(|i| ((i, 0), 0, 1i64)).collect();
2467 let high: Vec<Tuple> = (1000..1100u64).map(|i| ((i, 0), 0, 1i64)).collect();
2468
2469 let mut in1 = VecDeque::from([force_spill(
2470 ColumnChunk::from_body(build_column(&low)),
2471 &pool,
2472 )]);
2473 let mut in2 = VecDeque::from([force_spill(
2474 ColumnChunk::from_body(build_column(&high)),
2475 &pool,
2476 )]);
2477 let mut out = VecDeque::new();
2478 TestChunk::merge(&mut in1, &mut in2, &mut out);
2479
2480 assert!(in1.is_empty());
2483 assert_eq!(in2.len(), 1);
2484 assert!(in2[0].is_spilled(), "untouched survivor must stay spilled");
2485 let mut all = collect_chunks(out);
2486 Extend::extend(&mut all, collect_chunks(in2.drain(..)));
2487 let mut expected = low;
2488 Extend::extend(&mut expected, high);
2489 assert_eq!(all, expected);
2490 }
2491
2492 #[mz_ore::test]
2496 fn merge_derives_generational_depth() {
2497 let low: Vec<Tuple> = (0..100u64).map(|i| ((i, 0), 0, 1i64)).collect();
2498 let high: Vec<Tuple> = (50..150u64).map(|i| ((i, 0), 0, 1i64)).collect();
2499 let mut in1 = VecDeque::from([ColumnChunk::from_body(build_column(&low))]);
2500 let mut in2 = VecDeque::from([ColumnChunk::from_body(build_column(&high))]);
2501 assert_eq!(in1[0].depth(), 0, "fresh chunks start at depth 0");
2502 let mut out = VecDeque::new();
2503 TestChunk::merge(&mut in1, &mut in2, &mut out);
2504 assert!(!out.is_empty());
2505 for chunk in &out {
2506 assert_eq!(chunk.depth(), 1, "merge output is one past its inputs");
2507 }
2508 assert!(in1.is_empty());
2511 assert_eq!(in2.len(), 1);
2512 assert_eq!(in2[0].depth(), 0, "rewritten survivor keeps its depth");
2513
2514 let mut in1 = VecDeque::from([ColumnChunk::Resident(Rc::new(build_column(&low)), 3)]);
2517 let far: Vec<Tuple> = (1000..1100u64).map(|i| ((i, 0), 0, 1i64)).collect();
2518 let mut in2 = VecDeque::from([ColumnChunk::from_body(build_column(&far))]);
2519 let mut out = VecDeque::new();
2520 TestChunk::merge(&mut in1, &mut in2, &mut out);
2521 assert_eq!(out.len(), 1);
2522 assert_eq!(out[0].depth(), 4, "pass-through ages a generation");
2523 assert_eq!(collect_chunks(out), low);
2524 }
2525
2526 #[mz_ore::test]
2529 fn advance_preserves_depth() {
2530 let data: Vec<Tuple> = (0..100u64).map(|i| ((i, 0), 1, 1i64)).collect();
2531 let mut input = VecDeque::from([
2532 ColumnChunk::Resident(Rc::new(build_column(&data[..50])), 2),
2533 ColumnChunk::Resident(Rc::new(build_column(&data[50..])), 1),
2534 ]);
2535 let frontier = Antichain::from_elem(5u64);
2536 let mut out = VecDeque::new();
2537 TestChunk::advance(&mut input, frontier.borrow(), false, &mut out);
2538 for chunk in out.iter().chain(input.iter()) {
2539 assert_eq!(chunk.depth(), 2);
2540 }
2541 TestChunk::advance(&mut input, frontier.borrow(), true, &mut out);
2542 assert!(input.is_empty());
2543 assert!(!out.is_empty());
2544 for chunk in &out {
2545 assert_eq!(chunk.depth(), 2);
2546 }
2547 }
2548
2549 #[mz_ore::test]
2553 #[cfg_attr(miri, ignore)] fn settle_commits_at_accumulated_depth() {
2555 set_spill_override(Some(test_pool()));
2556 let big: Vec<Tuple> = (0..60_000u64).map(|i| ((i, 0), 0, 1i64)).collect();
2557 let mut input = VecDeque::from([
2558 ColumnChunk::Resident(Rc::new(build_column(&big)), 1),
2559 ColumnChunk::Resident(Rc::new(build_column(&[((0, 0), 0, 1)])), 0),
2560 ColumnChunk::Resident(Rc::new(build_column(&[((1, 0), 0, 1)])), 2),
2561 ]);
2562 let mut out = VecDeque::new();
2563 TestChunk::settle(&mut input, true, &mut out);
2564 assert!(input.is_empty());
2565 assert_eq!(out.len(), 2);
2566 assert!(out[0].is_spilled(), "large commit must spill");
2567 assert_eq!(out[0].depth(), 1, "sole commit keeps its depth");
2568 assert!(!out[1].is_spilled(), "small commit stays resident");
2569 assert_eq!(out[1].depth(), 2, "coalesced commit takes the max depth");
2570 set_spill_override(None);
2571 }
2572
2573 #[mz_ore::test]
2577 #[cfg_attr(miri, ignore)] fn settle_carry_commits_at_target() {
2579 let chunk_rows = u64::cast_from(800_000usize / 32);
2581 let mut input: VecDeque<TestChunk> = (0..4u64)
2582 .map(|c| {
2583 let data: Vec<Tuple> = (0..chunk_rows)
2584 .map(|i| ((c * chunk_rows + i, 0), 0, 1i64))
2585 .collect();
2586 ColumnChunk::from_body(build_column(&data))
2587 })
2588 .collect();
2589 let mut out = VecDeque::new();
2590 TestChunk::settle(&mut input, true, &mut out);
2591 assert!(out.len() < 4, "nothing coalesced");
2594 for chunk in &out {
2595 let col = chunk.clone().into_body();
2596 assert!(
2597 col.length_in_bytes() <= COMMIT_BYTES,
2598 "settled chunk of {} bytes exceeds the commit target",
2599 col.length_in_bytes(),
2600 );
2601 }
2602 assert_eq!(
2603 collect_chunks(out).len(),
2604 usize::try_from(4 * chunk_rows).unwrap(),
2605 );
2606 }
2607
2608 #[mz_ore::test]
2609 fn small_chunks_stay_resident() {
2610 set_spill_override(Some(test_pool()));
2611 let committed = TestChunk::commit(build_column(&[((1, 1), 0, 1)]), 0);
2612 assert!(!committed.is_spilled());
2613 set_spill_override(None);
2614 }
2615
2616 fn column_at_spill_floor() -> (ColumnBody<Tuple>, u64) {
2619 let mut col: ColumnBody<Tuple> = ColumnBody::default();
2620 let mut n = 0u64;
2621 while col.length_in_bytes() < SPILL_MIN_BYTES {
2622 col.push_into(((n, n), 0, 1));
2623 n += 1;
2624 }
2625 (col, n)
2626 }
2627
2628 #[mz_ore::test]
2631 fn spill_floor_boundary() {
2632 set_spill_override(Some(test_pool()));
2633 let (col, n) = column_at_spill_floor();
2634 let mut under: ColumnBody<Tuple> = ColumnBody::default();
2635 for m in 0..n - 1 {
2636 under.push_into(((m, m), 0, 1));
2637 }
2638 assert!(under.length_in_bytes() < SPILL_MIN_BYTES);
2639 assert!(!TestChunk::commit(under, 0).is_spilled());
2640 assert!(TestChunk::commit(col, 0).is_spilled());
2641 set_spill_override(None);
2642 }
2643
2644 #[mz_ore::test]
2648 fn spill_codec_depth_floor() {
2649 set_spill_override(Some(test_pool()));
2650 set_compress_min_depth_override(Some(2));
2651 let codec_name = |depth: u8| {
2655 let (codec, compressed) = codec_for_depth(depth);
2656 let name = format!("{:?}", codec);
2657 assert_eq!(compressed, name == "Lz4Codec", "flag tracks the codec");
2658 name
2659 };
2660 assert_eq!(codec_name(0), "IdentityCodec");
2661 assert_eq!(codec_name(1), "IdentityCodec");
2662 assert_eq!(codec_name(2), "Lz4Codec");
2663 assert_eq!(codec_name(u8::MAX), "Lz4Codec");
2664
2665 let data: Vec<Tuple> = (0..20_000u64).map(|i| ((i, 0), 0, 1i64)).collect();
2666 let data = consolidate(data);
2667 let column = build_column(&data);
2668 for depth in [0u8, 1, 2, 3] {
2669 let chunk = TestChunk::commit(column.clone(), depth);
2670 assert!(chunk.is_spilled(), "depth {depth} must spill");
2671 assert_eq!(collect_column(&chunk.into_body()), data);
2672 }
2673 set_spill_override(None);
2674 set_compress_min_depth_override(None);
2675
2676 set_compress_min_depth_override(Some(DEFAULT_COMPRESS_MIN_DEPTH));
2678 assert_eq!(codec_name(0), "IdentityCodec");
2679 assert_eq!(codec_name(1), "Lz4Codec");
2680 set_compress_min_depth_override(None);
2681 }
2682
2683 #[mz_ore::test]
2689 #[cfg_attr(miri, ignore)] fn merge_survivor_crosses_compression_floor() {
2691 set_spill_override(Some(test_pool()));
2692 set_compress_min_depth_override(Some(1));
2693
2694 let low = consolidate((0..20_000u64).map(|i| ((i, 0), 0, 1i64)).collect());
2695 let far = consolidate((100_000..120_000u64).map(|i| ((i, 0), 0, 1i64)).collect());
2696 let fresh_far = || VecDeque::from([TestChunk::commit(build_column(&far), 0)]);
2697
2698 let mut in1 = VecDeque::from([TestChunk::commit(build_column(&low), 0)]);
2701 let mut in2 = fresh_far();
2702 assert!(in1[0].is_spilled() && in2[0].is_spilled());
2703 assert!(
2704 !body_compressed(&in1[0]),
2705 "a fresh body below the floor is identity coded"
2706 );
2707
2708 let mut out = VecDeque::new();
2709 TestChunk::merge(&mut in1, &mut in2, &mut out);
2710 assert_eq!(out.len(), 1);
2711 let survived = out.pop_front().expect("the lower front passes through");
2712 assert_eq!(survived.depth(), 1, "survival ages across the floor");
2713 assert!(
2714 survived.is_spilled(),
2715 "the crossing re-spills, it does not evict"
2716 );
2717 assert!(
2718 body_compressed(&survived),
2719 "the survivor is re-spilled under the compressing codec"
2720 );
2721
2722 let mut in1 = VecDeque::from([survived]);
2725 let mut in2 = fresh_far();
2726 let mut out = VecDeque::new();
2727 TestChunk::merge(&mut in1, &mut in2, &mut out);
2728 assert_eq!(out.len(), 1);
2729 assert_eq!(out[0].depth(), 2, "an aged survivor keeps aging");
2730 assert!(out[0].is_spilled());
2731 assert_eq!(
2732 collect_chunks(out),
2733 low,
2734 "the body reads back intact across both survivals"
2735 );
2736
2737 set_spill_override(None);
2738 set_compress_min_depth_override(None);
2739 }
2740
2741 #[mz_ore::test]
2748 #[cfg_attr(miri, ignore)] fn merge_survivor_ages_while_shared() {
2750 set_spill_override(Some(test_pool()));
2751 set_compress_min_depth_override(Some(1));
2752
2753 let low = consolidate((0..20_000u64).map(|i| ((i, 0), 0, 1i64)).collect());
2754 let far = consolidate((100_000..120_000u64).map(|i| ((i, 0), 0, 1i64)).collect());
2755
2756 let source = TestChunk::commit(build_column(&low), 0);
2759 let ColumnChunk::Spilled(source_body, 0) = &source else {
2760 panic!("a fresh commit above the spill floor is spilled at depth 0");
2761 };
2762 let source_body = Rc::clone(source_body);
2763
2764 let mut in1 = VecDeque::from([source.clone()]);
2765 let mut in2 = VecDeque::from([TestChunk::commit(build_column(&far), 0)]);
2766 let mut out = VecDeque::new();
2767 TestChunk::merge(&mut in1, &mut in2, &mut out);
2768
2769 assert_eq!(out.len(), 1);
2770 assert_eq!(out[0].depth(), 1, "a shared body ages all the same");
2771 let ColumnChunk::Spilled(survived_body, _) = &out[0] else {
2772 panic!("the survivor stays spilled");
2773 };
2774 assert!(
2775 Rc::ptr_eq(&source_body, survived_body),
2776 "a shared body is aged in place, not re-spilled"
2777 );
2778 assert_eq!(source.depth(), 0, "the other holder is left as it was");
2779
2780 let mut in1 = VecDeque::from([out.pop_front().expect("survivor observed above")]);
2783 let mut in2 = VecDeque::from([TestChunk::commit(build_column(&far), 0)]);
2784 let mut out = VecDeque::new();
2785 TestChunk::merge(&mut in1, &mut in2, &mut out);
2786 assert_eq!(out.len(), 1);
2787 assert_eq!(out[0].depth(), 2, "aging past the floor is not pinned");
2788 assert_eq!(collect_chunks(out), low);
2789
2790 set_spill_override(None);
2791 set_compress_min_depth_override(None);
2792 }
2793
2794 #[mz_ore::test]
2800 #[cfg_attr(miri, ignore)] fn survive_merge_retries_missed_migrations() {
2802 let low = consolidate((0..20_000u64).map(|i| ((i, 0), 0, 1i64)).collect());
2803 let far = consolidate((100_000..120_000u64).map(|i| ((i, 0), 0, 1i64)).collect());
2804
2805 let survive = |chunk: TestChunk| {
2808 let mut in1 = VecDeque::from([chunk]);
2809 let mut in2 = VecDeque::from([TestChunk::commit(build_column(&far), 0)]);
2810 let mut out = VecDeque::new();
2811 TestChunk::merge(&mut in1, &mut in2, &mut out);
2812 out.pop_front().expect("the lower front passes through")
2813 };
2814
2815 set_spill_override(Some(test_pool()));
2818 set_compress_min_depth_override(Some(1));
2819 let chunk = TestChunk::commit(build_column(&low), 0);
2820 assert!(!body_compressed(&chunk));
2821 set_spill_override(None);
2822 let chunk = survive(chunk);
2823 assert_eq!(chunk.depth(), 1, "aging does not need a pool");
2824 assert!(!body_compressed(&chunk), "no pool, no migration");
2825 set_spill_override(Some(test_pool()));
2826 let chunk = survive(chunk);
2827 assert!(
2828 body_compressed(&chunk),
2829 "the migration retries once a pool is back"
2830 );
2831
2832 let chunk = TestChunk::commit(build_column(&low), 0);
2835 let held = chunk.clone();
2836 let chunk = survive(chunk);
2837 assert!(!body_compressed(&chunk), "shared, so not migrated");
2838 drop(held);
2839 let chunk = survive(chunk);
2840 assert!(
2841 body_compressed(&chunk),
2842 "the migration retries once the body is unshared"
2843 );
2844
2845 set_compress_min_depth_override(Some(8));
2850 let chunk = TestChunk::commit(build_column(&low), 3);
2851 assert!(!body_compressed(&chunk));
2852 set_compress_min_depth_override(Some(1));
2853 let chunk = survive(chunk);
2854 assert_eq!(chunk.depth(), 4);
2855 assert!(
2856 body_compressed(&chunk),
2857 "lowering the floor migrates bodies already past it"
2858 );
2859
2860 set_spill_override(None);
2861 set_compress_min_depth_override(None);
2862 }
2863
2864 #[mz_ore::test]
2866 #[cfg_attr(miri, ignore)]
2867 fn spill_gates_are_independent() {
2868 let installed =
2869 crate::pool_config::apply_pool_config(crate::pool_config::PoolPagerConfig {
2870 budget_bytes: 32 << 20,
2871 spill_threads: 1,
2872 eager_backing: false,
2873 rss_target_bytes: 16 << 20,
2874 });
2875 assert!(installed, "pool reservation failed");
2876 let (col, _) = column_at_spill_floor();
2878 let compute = |col: &ColumnBody<Tuple>| TestChunk::commit(col.clone(), 0).is_spilled();
2879 let storage = |col: &ColumnBody<Tuple>| {
2880 ColumnChunk::<(u64, u64), u64, i64, StorageSpill>::commit(col.clone(), 0).is_spilled()
2881 };
2882 let sink = |col: &ColumnBody<Tuple>| try_spill_ref(col, 0).is_some();
2883
2884 assert!(!compute(&col), "all gates off");
2885 assert!(!storage(&col), "all gates off");
2886 assert!(!sink(&col), "all gates off");
2887
2888 set_storage_spill_enabled(true);
2889 assert!(storage(&col), "the storage gate spills storage chunks");
2890 assert!(
2891 !compute(&col),
2892 "the storage gate must not spill compute chunks"
2893 );
2894 assert!(!sink(&col), "the storage gate must not spill sink bodies");
2895 set_storage_spill_enabled(false);
2896
2897 set_compute_spill_enabled(true);
2898 assert!(compute(&col), "the compute gate spills compute chunks");
2899 assert!(
2900 !storage(&col),
2901 "the compute gate must not spill storage chunks"
2902 );
2903 assert!(!sink(&col), "the compute gate must not spill sink bodies");
2904 set_compute_spill_enabled(false);
2905
2906 set_sink_spill_enabled(true);
2907 assert!(sink(&col), "the sink gate spills sink bodies");
2908 assert!(
2909 !compute(&col),
2910 "the sink gate must not spill compute chunks"
2911 );
2912 assert!(
2913 !storage(&col),
2914 "the sink gate must not spill storage chunks"
2915 );
2916 set_sink_spill_enabled(false);
2917 set_compress_min_depth_override(None);
2918 }
2919
2920 #[mz_ore::test]
2923 fn spill_align_round_trip() {
2924 let pool = test_pool();
2925 let data: Vec<Tuple> = (0..64u64).map(|k| ((k, k), 0, 1)).collect();
2926 let spilled = force_spill(ColumnChunk::from_body(build_column(&data)), &pool);
2927 let column = spilled.into_body();
2928 let ColumnBody::Words(words) = &column else {
2929 panic!("a spilled body reads back as ColumnBody::Words");
2930 };
2931 let words = words.clone();
2932 let respilled = force_spill(ColumnChunk::from_body(column), &pool);
2933 let reread = respilled.into_body();
2934 let ColumnBody::Words(words2) = &reread else {
2935 panic!("a spilled body reads back as ColumnBody::Words");
2936 };
2937 assert_eq!(&words, words2, "byte-identical round trip");
2938 assert_eq!(collect_column(&reread), data);
2939 }
2940
2941 #[mz_ore::test]
2942 fn merge_depth_saturates() {
2943 let a = ColumnChunk::Resident(
2944 Rc::new(build_column(&[((1, 0), 0, 1), ((3, 0), 0, 1)])),
2945 u8::MAX,
2946 );
2947 let b = ColumnChunk::Resident(
2948 Rc::new(build_column(&[((2, 0), 0, 1), ((4, 0), 0, 1)])),
2949 u8::MAX,
2950 );
2951 let mut in1 = VecDeque::from([a]);
2952 let mut in2 = VecDeque::from([b]);
2953 let mut out = VecDeque::new();
2954 TestChunk::merge(&mut in1, &mut in2, &mut out);
2955 for chunk in out.iter().chain(in1.iter()).chain(in2.iter()) {
2956 assert_eq!(chunk.depth(), u8::MAX, "depth saturates");
2957 }
2958 }
2959
2960 #[mz_ore::test]
2961 fn into_body_copies_shared_resident() {
2962 let data: Vec<Tuple> = vec![((1, 1), 0, 1), ((2, 2), 0, 1)];
2963 let a = TestChunk::from_body(build_column(&data));
2964 let b = a.clone();
2965 assert_eq!(collect_column(&a.into_body()), data);
2966 assert_eq!(collect_column(&b.into_body()), data);
2967 }
2968}