1use std::collections::VecDeque;
31
32use columnar::{Clear, Columnar, Index, Len};
33use differential_dataflow::batcher::{Batcher, Extract};
34use differential_dataflow::difference::Semigroup;
35use differential_dataflow::logging::{BatcherEvent, Logger};
36use differential_dataflow::trace::implementations::merge_batcher::Sealer;
37use timely::Accountable;
38use timely::PartialOrder;
39use timely::container::{ContainerBuilder, PushInto, SizableContainer};
40use timely::dataflow::channels::ContainerBytes;
41use timely::progress::Timestamp;
42use timely::progress::frontier::{Antichain, AntichainRef};
43
44use crate::column_pager::{self, ColumnPager, PagedColumn};
45use crate::columnar::Column;
46use crate::columnar::batcher::ColumnChunker;
47use crate::columnar::body::ColumnBody;
48
49const STASH_CAP: usize = 2;
64
65const MAX_RECYCLE_BYTES: usize = 1 << 22;
72
73fn recycle_capped<C: Columnar>(chunk: Column<C>, stash: &mut Vec<Column<C>>) {
78 if stash.len() < STASH_CAP && chunk.length_in_bytes() <= MAX_RECYCLE_BYTES {
79 recycle_chunk(chunk, stash);
80 }
81}
82
83#[inline]
89fn empty_chunk<C: Columnar>(stash: &mut Vec<Column<C>>) -> Column<C> {
90 stash.pop().unwrap_or_default()
91}
92
93#[inline]
97fn recycle_chunk<C: Columnar>(mut chunk: Column<C>, stash: &mut Vec<Column<C>>) {
98 if let Column::Typed(c) = &mut chunk {
99 c.clear();
100 stash.push(chunk);
101 }
102}
103
104pub struct PagedChunker<U: Columnar> {
107 inner: ColumnChunker<U>,
108 staged: Column<U>,
109}
110
111impl<U: Columnar> Default for PagedChunker<U> {
112 fn default() -> Self {
113 Self {
114 inner: Default::default(),
115 staged: Default::default(),
116 }
117 }
118}
119
120impl<'a, D, T, R> PushInto<&'a mut Column<(D, T, R)>> for PagedChunker<(D, T, R)>
121where
122 D: Columnar,
123 T: Columnar,
124 R: Columnar,
125 ColumnChunker<(D, T, R)>: PushInto<&'a mut Column<(D, T, R)>>,
126{
127 fn push_into(&mut self, item: &'a mut Column<(D, T, R)>) {
128 self.inner.push_into(item);
129 }
130}
131
132impl<U: Columnar + 'static> ContainerBuilder for PagedChunker<U>
133where
134 U::Container: Clone + 'static,
135 ColumnChunker<U>: ContainerBuilder<Container = ColumnBody<U>>,
136{
137 type Container = Column<U>;
138
139 fn extract(&mut self) -> Option<&mut Self::Container> {
140 let body = self.inner.extract()?;
141 self.staged = Column::from(std::mem::take(body));
142 Some(&mut self.staged)
143 }
144
145 fn finish(&mut self) -> Option<&mut Self::Container> {
146 let body = self.inner.finish()?;
147 self.staged = Column::from(std::mem::take(body));
148 Some(&mut self.staged)
149 }
150}
151
152pub struct ColumnMergeBatcher<Chu, D, T, R, S>
165where
166 D: Columnar,
167 T: Columnar,
168 R: Columnar,
169{
170 chunker: Chu,
172 chains: Vec<VecDeque<PagedColumn<(D, T, R)>>>,
173 frontier: Antichain<T>,
174 stash: Vec<Column<(D, T, R)>>,
182 pager_override: Option<ColumnPager>,
187 logger: Option<Logger>,
188 operator_id: usize,
189 sealer: std::marker::PhantomData<S>,
191}
192
193impl<Chu: Default, D, T, R, S> ColumnMergeBatcher<Chu, D, T, R, S>
194where
195 D: Columnar,
196 T: Columnar + Timestamp,
197 R: Columnar,
198{
199 pub fn new(logger: Option<Logger>, operator_id: usize) -> Self {
204 Self {
205 chunker: Chu::default(),
206 chains: Vec::new(),
207 frontier: Antichain::new(),
208 stash: Vec::new(),
209 pager_override: None,
210 logger,
211 operator_id,
212 sealer: std::marker::PhantomData,
213 }
214 }
215}
216
217impl<Chu, D, T, R, S> ColumnMergeBatcher<Chu, D, T, R, S>
218where
219 D: Columnar,
220 T: Columnar,
221 R: Columnar,
222{
223 pub fn set_pager(&mut self, pager: ColumnPager) {
227 self.pager_override = Some(pager);
228 }
229
230 fn pager(&self) -> ColumnPager {
234 self.pager_override
235 .clone()
236 .unwrap_or_else(column_pager::global_pager)
237 }
238
239 fn chain_push(&mut self, chain: VecDeque<PagedColumn<(D, T, R)>>) {
242 self.emit_account(&chain, 1);
243 self.chains.push(chain);
244 }
245
246 fn chain_pop(&mut self) -> Option<VecDeque<PagedColumn<(D, T, R)>>> {
257 let chain = self.chains.pop()?;
258 self.emit_account(&chain, -1);
259 Some(chain)
260 }
261
262 fn emit_account(&self, chain: &VecDeque<PagedColumn<(D, T, R)>>, diff: isize) {
265 let Some(logger) = &self.logger else {
266 return;
267 };
268 let (mut records, mut size, mut capacity, mut allocations) =
269 (0isize, 0isize, 0isize, 0isize);
270 for entry in chain {
271 let (r, s, c, a) = account_chunk(entry);
272 records = records.saturating_add_unsigned(r);
273 size = size.saturating_add_unsigned(s);
274 capacity = capacity.saturating_add_unsigned(c);
275 allocations = allocations.saturating_add_unsigned(a);
276 }
277 logger.log(BatcherEvent {
278 operator: self.operator_id,
279 records_diff: records.saturating_mul(diff),
280 size_diff: size.saturating_mul(diff),
281 capacity_diff: capacity.saturating_mul(diff),
282 allocations_diff: allocations.saturating_mul(diff),
283 });
284 }
285}
286
287impl<Chu, D, T, R, S> Drop for ColumnMergeBatcher<Chu, D, T, R, S>
288where
289 D: Columnar,
290 T: Columnar,
291 R: Columnar,
292{
293 fn drop(&mut self) {
294 while self.chain_pop().is_some() {}
297 }
298}
299
300fn account_chunk<C: Columnar>(entry: &PagedColumn<C>) -> (usize, usize, usize, usize) {
309 match entry {
310 PagedColumn::Resident(col, _) => {
311 let records = usize::try_from(col.record_count()).expect("non-negative");
312 let bytes = col.length_in_bytes();
313 (records, bytes, bytes, 1)
314 }
315 PagedColumn::Paged { .. } | PagedColumn::Compressed { .. } => (0, 0, 0, 0),
316 }
317}
318
319impl<C, Chu, D, T, R, S> Batcher<C> for ColumnMergeBatcher<Chu, D, T, R, S>
320where
321 D: Columnar,
322 for<'a> columnar::Ref<'a, D>: Copy + Ord,
323 T: Columnar + Default + Timestamp + PartialOrder,
324 for<'a> columnar::Ref<'a, T>: Copy + Ord,
325 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
326 for<'a> columnar::Ref<'a, R>: Ord,
327 Chu: ContainerBuilder<Container = Column<(D, T, R)>> + for<'a> PushInto<&'a mut C>,
328 S: Sealer<Column<(D, T, R)>>,
329{
330 fn insert(&mut self, container: &mut C) {
331 self.chunker.push_into(container);
332 while let Some(chunk) = self.chunker.extract().map(std::mem::take) {
333 self.push_chunk(chunk);
334 }
335 }
336}
337
338impl<Chu, D, T, R, S> Extract for ColumnMergeBatcher<Chu, D, T, R, S>
339where
340 D: Columnar,
341 for<'a> columnar::Ref<'a, D>: Copy + Ord,
342 T: Columnar + Default + Timestamp + PartialOrder,
343 for<'a> columnar::Ref<'a, T>: Copy + Ord,
344 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
345 for<'a> columnar::Ref<'a, R>: Ord,
346 Chu: ContainerBuilder<Container = Column<(D, T, R)>>,
347 S: Sealer<Column<(D, T, R)>>,
348{
349 type Output = S::Output;
350 type Time = T;
351
352 fn extract<'a>(
353 &'a mut self,
354 upper: AntichainRef<'_, Self::Time>,
355 ) -> (Option<Self::Output>, AntichainRef<'a, Self::Time>) {
356 let (mut readied, frontier) = self.extract_chain(upper);
357 (S::seal(&mut readied), frontier)
358 }
359}
360
361impl<Chu, D, T, R, S> ColumnMergeBatcher<Chu, D, T, R, S>
362where
363 D: Columnar,
364 for<'a> columnar::Ref<'a, D>: Copy + Ord,
365 T: Columnar + Default + Timestamp + PartialOrder,
366 for<'a> columnar::Ref<'a, T>: Copy + Ord,
367 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
368 for<'a> columnar::Ref<'a, R>: Ord,
369 Chu: ContainerBuilder<Container = Column<(D, T, R)>>,
370{
371 pub fn extract_chain<'a>(
376 &'a mut self,
377 upper: AntichainRef<'_, T>,
378 ) -> (Vec<Column<(D, T, R)>>, AntichainRef<'a, T>) {
379 while let Some(chunk) = self.chunker.finish().map(std::mem::take) {
382 self.push_chunk(chunk);
383 }
384
385 let pager = self.pager();
386 while self.chains.len() > 1 {
388 let a = self.chain_pop().unwrap();
389 let b = self.chain_pop().unwrap();
390 let merged = self.merge_by(a, b);
391 self.chain_push(merged);
392 }
393 let merged = self.chain_pop().unwrap_or_default();
394
395 let mut readied: Vec<Column<(D, T, R)>> = Vec::new();
398 let mut kept_chain: VecDeque<PagedColumn<(D, T, R)>> = VecDeque::new();
399 self.frontier.clear();
400 {
401 let pager = &pager;
402 let frontier = &mut self.frontier;
403 let stash = &mut self.stash;
404 extract_chain(
405 FetchIter::new(merged, pager),
406 upper,
407 frontier,
408 |paged| readied.push(pager.take(paged)),
409 |paged| kept_chain.push_back(paged),
410 stash,
411 );
412 }
413
414 if !kept_chain.is_empty() {
415 self.chain_push(kept_chain);
416 }
417
418 self.stash.clear();
422
423 (readied, self.frontier.borrow())
424 }
425}
426
427impl<Chu, D, T, R, S> PushInto<Column<(D, T, R)>> for ColumnMergeBatcher<Chu, D, T, R, S>
428where
429 D: Columnar,
430 for<'a> columnar::Ref<'a, D>: Copy + Ord,
431 T: Columnar + Default + Clone + PartialOrder,
432 for<'a> columnar::Ref<'a, T>: Copy + Ord,
433 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
434{
435 fn push_into(&mut self, chunk: Column<(D, T, R)>) {
438 self.push_chunk(chunk);
439 }
440}
441
442impl<Chu, D, T, R, S> ColumnMergeBatcher<Chu, D, T, R, S>
443where
444 D: Columnar,
445 for<'a> columnar::Ref<'a, D>: Copy + Ord,
446 T: Columnar + Default + Clone + PartialOrder,
447 for<'a> columnar::Ref<'a, T>: Copy + Ord,
448 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
449{
450 pub fn push_chunk(&mut self, mut chunk: Column<(D, T, R)>) {
453 let pager = self.pager();
454 let paged = pager.page(&mut chunk);
455 self.insert_chain(VecDeque::from([paged]));
456 }
457
458 fn insert_chain(&mut self, chain: VecDeque<PagedColumn<(D, T, R)>>) {
461 if chain.is_empty() {
462 return;
463 }
464 self.chain_push(chain);
465 while self.chains.len() > 1
466 && self.chains[self.chains.len() - 1].len()
467 >= self.chains[self.chains.len() - 2].len() / 2
468 {
469 let a = self.chain_pop().unwrap();
470 let b = self.chain_pop().unwrap();
471 let merged = self.merge_by(a, b);
472 self.chain_push(merged);
473 }
474 }
475
476 fn merge_by(
480 &mut self,
481 a: VecDeque<PagedColumn<(D, T, R)>>,
482 b: VecDeque<PagedColumn<(D, T, R)>>,
483 ) -> VecDeque<PagedColumn<(D, T, R)>> {
484 let mut output: VecDeque<PagedColumn<(D, T, R)>> = VecDeque::new();
485 let pager = self.pager();
486 let pager = &pager;
487 let stash = &mut self.stash;
488 merge_chains(
489 FetchIter::new(a, pager),
490 FetchIter::new(b, pager),
491 |paged| output.push_back(paged),
492 stash,
493 );
494 output
495 }
496}
497
498pub struct FetchIter<'a, D, T, R>
504where
505 (D, T, R): Columnar,
506{
507 queue: VecDeque<PagedColumn<(D, T, R)>>,
508 pager: &'a ColumnPager,
509}
510
511impl<'a, D, T, R> FetchIter<'a, D, T, R>
512where
513 (D, T, R): Columnar,
514{
515 pub fn new(queue: VecDeque<PagedColumn<(D, T, R)>>, pager: &'a ColumnPager) -> Self {
517 Self { queue, pager }
518 }
519
520 pub fn pager(&self) -> &'a ColumnPager {
525 self.pager
526 }
527
528 pub fn into_paged(self) -> std::collections::vec_deque::IntoIter<PagedColumn<(D, T, R)>> {
533 self.queue.into_iter()
534 }
535}
536
537impl<D, T, R> Iterator for FetchIter<'_, D, T, R>
538where
539 (D, T, R): Columnar,
540{
541 type Item = Column<(D, T, R)>;
542
543 fn next(&mut self) -> Option<Self::Item> {
544 self.queue.pop_front().map(|p| self.pager.take(p))
545 }
546}
547
548pub fn merge_chains<D, T, R, Sink>(
563 list1: FetchIter<'_, D, T, R>,
564 list2: FetchIter<'_, D, T, R>,
565 mut sink: Sink,
566 stash: &mut Vec<Column<(D, T, R)>>,
567) where
568 D: Columnar,
569 for<'a> columnar::Ref<'a, D>: Copy + Ord,
570 T: Columnar + Default + Clone + PartialOrder,
571 for<'a> columnar::Ref<'a, T>: Copy + Ord,
572 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
573 Sink: FnMut(PagedColumn<(D, T, R)>),
574{
575 let pager = list1.pager();
576 let mut list1 = list1;
577 let mut list2 = list2;
578
579 let mut heads = [
580 list1.next().unwrap_or_default(),
581 list2.next().unwrap_or_default(),
582 ];
583 let mut positions = [0usize, 0usize];
584 let mut result: Column<(D, T, R)> = empty_chunk(stash);
585
586 loop {
587 let upper_l = heads[0].borrow().len();
588 let upper_r = heads[1].borrow().len();
589 if positions[0] >= upper_l || positions[1] >= upper_r {
590 break;
591 }
592
593 let lhs_passthrough = positions[0] == 0 && upper_l > 0 && {
595 let lhs = heads[0].borrow();
596 let rhs = heads[1].borrow();
597 let last_l = (lhs.0.get(upper_l - 1), lhs.1.get(upper_l - 1));
598 let cur_r = (rhs.0.get(positions[1]), rhs.1.get(positions[1]));
599 last_l < cur_r
600 };
601 if lhs_passthrough {
602 if !result.is_empty() {
603 sink(pager.page(&mut result));
604 if let Some(reuse) = stash.pop() {
605 result = reuse;
606 }
607 }
608 let mut head = std::mem::replace(&mut heads[0], list1.next().unwrap_or_default());
609 sink(pager.page(&mut head));
610 positions[0] = 0;
611 continue;
612 }
613
614 let rhs_passthrough = positions[1] == 0 && upper_r > 0 && {
615 let lhs = heads[0].borrow();
616 let rhs = heads[1].borrow();
617 let last_r = (rhs.0.get(upper_r - 1), rhs.1.get(upper_r - 1));
618 let cur_l = (lhs.0.get(positions[0]), lhs.1.get(positions[0]));
619 last_r < cur_l
620 };
621 if rhs_passthrough {
622 if !result.is_empty() {
623 sink(pager.page(&mut result));
624 if let Some(reuse) = stash.pop() {
625 result = reuse;
626 }
627 }
628 let mut head = std::mem::replace(&mut heads[1], list2.next().unwrap_or_default());
629 sink(pager.page(&mut head));
630 positions[1] = 0;
631 continue;
632 }
633
634 let yielded = result.merge_from(&mut heads, &mut positions);
635
636 if positions[0] >= heads[0].borrow().len() {
637 let old = std::mem::replace(&mut heads[0], list1.next().unwrap_or_default());
638 recycle_capped(old, stash);
639 positions[0] = 0;
640 }
641 if positions[1] >= heads[1].borrow().len() {
642 let old = std::mem::replace(&mut heads[1], list2.next().unwrap_or_default());
643 recycle_capped(old, stash);
644 positions[1] = 0;
645 }
646 if yielded || result.at_capacity() {
647 sink(pager.page(&mut result));
648 if let Some(reuse) = stash.pop() {
654 result = reuse;
655 }
656 }
657 }
658
659 drain_side(
663 &mut heads[0],
664 &mut positions[0],
665 list1,
666 &mut result,
667 &mut sink,
668 pager,
669 stash,
670 );
671 drain_side(
672 &mut heads[1],
673 &mut positions[1],
674 list2,
675 &mut result,
676 &mut sink,
677 pager,
678 stash,
679 );
680
681 if !result.is_empty() {
682 sink(pager.page(&mut result));
683 } else {
684 recycle_capped(result, stash);
688 }
689 let [h0, h1] = heads;
693 recycle_capped(h0, stash);
694 recycle_capped(h1, stash);
695}
696
697fn drain_side<D, T, R, Sink>(
701 head: &mut Column<(D, T, R)>,
702 pos: &mut usize,
703 rest: FetchIter<'_, D, T, R>,
704 result: &mut Column<(D, T, R)>,
705 sink: &mut Sink,
706 pager: &ColumnPager,
707 stash: &mut Vec<Column<(D, T, R)>>,
708) where
709 D: Columnar,
710 for<'a> columnar::Ref<'a, D>: Copy + Ord,
711 T: Columnar + Default + Clone + PartialOrder,
712 for<'a> columnar::Ref<'a, T>: Copy + Ord,
713 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
714 Sink: FnMut(PagedColumn<(D, T, R)>),
715{
716 if *pos < head.borrow().len() {
717 let _ = result.merge_from(std::slice::from_mut(head), std::slice::from_mut(pos));
719 }
720 if !result.is_empty() {
721 sink(pager.page(result));
722 if let Some(reuse) = stash.pop() {
723 *result = reuse;
724 }
725 }
726 for paged in rest.into_paged() {
727 sink(paged);
728 }
729}
730
731pub fn extract_chain<D, T, R, SinkShip, SinkKeep>(
740 merged: FetchIter<'_, D, T, R>,
741 upper: AntichainRef<T>,
742 frontier: &mut Antichain<T>,
743 mut ship: SinkShip,
744 mut keep: SinkKeep,
745 stash: &mut Vec<Column<(D, T, R)>>,
746) where
747 D: Columnar,
748 for<'a> columnar::Ref<'a, D>: Copy + Ord,
749 T: Columnar + Default + Clone + PartialOrder,
750 for<'a> columnar::Ref<'a, T>: Copy + Ord,
751 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
752 SinkShip: FnMut(PagedColumn<(D, T, R)>),
753 SinkKeep: FnMut(PagedColumn<(D, T, R)>),
754{
755 let pager = merged.pager();
756 let mut keep_buf: Column<(D, T, R)> = empty_chunk(stash);
757 let mut ship_buf: Column<(D, T, R)> = empty_chunk(stash);
758
759 for mut buffer in merged {
760 let mut position = 0;
761 let len = buffer.borrow().len();
762 while position < len {
763 buffer.extract(&mut position, upper, frontier, &mut keep_buf, &mut ship_buf);
764 if keep_buf.at_capacity() {
765 keep(pager.page(&mut keep_buf));
766 if let Some(reuse) = stash.pop() {
767 keep_buf = reuse;
768 }
769 }
770 if ship_buf.at_capacity() {
771 ship(pager.page(&mut ship_buf));
772 if let Some(reuse) = stash.pop() {
773 ship_buf = reuse;
774 }
775 }
776 }
777 recycle_capped(buffer, stash);
779 }
780 if !keep_buf.is_empty() {
781 keep(pager.page(&mut keep_buf));
782 } else {
783 recycle_capped(keep_buf, stash);
784 }
785 if !ship_buf.is_empty() {
786 ship(pager.page(&mut ship_buf));
787 } else {
788 recycle_capped(ship_buf, stash);
789 }
790}
791
792#[cfg(test)]
793#[allow(clippy::clone_on_ref_ptr)]
794mod tests {
795 use std::sync::Arc;
796
797 use columnar::Index;
798
799 use super::*;
800 use crate::column_pager::{PageDecision, PageEvent, PageHint, PagingPolicy};
801
802 type TestPagedBatcher =
804 ColumnMergeBatcher<PagedChunker<((u64, u64), u64, i64)>, (u64, u64), u64, i64, ()>;
805
806 type KvUpdate = ((u64, u64), u64, i64);
807
808 fn col(rows: &[KvUpdate]) -> Column<KvUpdate> {
809 let mut c: Column<KvUpdate> = Default::default();
810 for &t in rows {
811 c.push_into(t);
812 }
813 c
814 }
815
816 fn collect_pc(chunks: &[PagedColumn<KvUpdate>], pager: &ColumnPager) -> Vec<KvUpdate> {
817 chunks
820 .iter()
821 .flat_map(|p| {
822 let view: Column<KvUpdate> = match p {
823 PagedColumn::Resident(c, _) => clone_column(c),
824 _ => pager.take(clone_paged(p)),
825 };
826 collect_column(&view).into_iter()
827 })
828 .collect()
829 }
830
831 fn collect_column(c: &Column<KvUpdate>) -> Vec<KvUpdate> {
832 c.borrow()
833 .into_index_iter()
834 .map(|((k, v), t, r)| {
835 (
836 (u64::into_owned(k), u64::into_owned(v)),
837 u64::into_owned(t),
838 i64::into_owned(r),
839 )
840 })
841 .collect()
842 }
843
844 fn clone_column(c: &Column<KvUpdate>) -> Column<KvUpdate> {
845 c.clone()
849 }
850
851 fn clone_paged(p: &PagedColumn<KvUpdate>) -> PagedColumn<KvUpdate> {
855 match p {
856 PagedColumn::Resident(c, _) => {
857 let mut c = c.clone();
859 ColumnPager::disabled().page(&mut c)
860 }
861 _ => panic!("clone_paged only supports Resident"),
864 }
865 }
866
867 struct ForcePagePolicy {
870 out: std::sync::atomic::AtomicUsize,
871 r#in: std::sync::atomic::AtomicUsize,
872 }
873 impl ForcePagePolicy {
874 fn new() -> Arc<Self> {
875 Arc::new(Self {
876 out: std::sync::atomic::AtomicUsize::new(0),
877 r#in: std::sync::atomic::AtomicUsize::new(0),
878 })
879 }
880 }
881 impl PagingPolicy for ForcePagePolicy {
882 fn decide(&self, _hint: PageHint) -> PageDecision {
883 PageDecision::Page {
884 backend: mz_ore::pager::Backend::Swap,
885 codec: None,
886 }
887 }
888 fn record(&self, event: PageEvent) {
889 use std::sync::atomic::Ordering;
890 match event {
891 PageEvent::PagedOut { .. } => {
892 self.out.fetch_add(1, Ordering::Relaxed);
893 }
894 PageEvent::PagedIn { .. } => {
895 self.r#in.fetch_add(1, Ordering::Relaxed);
896 }
897 _ => {}
898 }
899 }
900 }
901
902 fn to_chain(
904 cols: Vec<Column<KvUpdate>>,
905 pager: &ColumnPager,
906 ) -> VecDeque<PagedColumn<KvUpdate>> {
907 cols.into_iter().map(|mut c| pager.page(&mut c)).collect()
908 }
909
910 fn drive_merge(chain1: Vec<Column<KvUpdate>>, chain2: Vec<Column<KvUpdate>>) -> Vec<KvUpdate> {
912 let pager = ColumnPager::disabled();
913 let q1 = to_chain(chain1, &pager);
914 let q2 = to_chain(chain2, &pager);
915 let mut output: Vec<PagedColumn<KvUpdate>> = Vec::new();
916 let mut stash: Vec<Column<KvUpdate>> = Vec::new();
917 merge_chains(
918 FetchIter::new(q1, &pager),
919 FetchIter::new(q2, &pager),
920 |paged| output.push(paged),
921 &mut stash,
922 );
923 collect_pc(&output, &pager)
924 }
925
926 #[mz_ore::test]
930 fn merge_chains_disjoint_ranges() {
931 let out = drive_merge(
932 vec![
933 col(&[((0, 0), 0, 1), ((1, 0), 0, 1)]),
934 col(&[((2, 0), 0, 1), ((3, 0), 0, 1)]),
935 ],
936 vec![
937 col(&[((10, 0), 0, 1), ((11, 0), 0, 1)]),
938 col(&[((12, 0), 0, 1), ((13, 0), 0, 1)]),
939 ],
940 );
941 let expected: Vec<_> = (0..4u64)
942 .map(|d| ((d, 0u64), 0u64, 1i64))
943 .chain((10..14u64).map(|d| ((d, 0u64), 0u64, 1i64)))
944 .collect();
945 assert_eq!(out, expected);
946 }
947
948 #[mz_ore::test]
949 fn merge_chains_interleaved() {
950 let out = drive_merge(
951 vec![
952 col(&[((0, 0), 0, 1), ((2, 0), 0, 1)]),
953 col(&[((4, 0), 0, 1), ((6, 0), 0, 1)]),
954 ],
955 vec![
956 col(&[((1, 0), 0, 1), ((3, 0), 0, 1)]),
957 col(&[((5, 0), 0, 1), ((7, 0), 0, 1)]),
958 ],
959 );
960 let expected: Vec<_> = (0..8u64).map(|d| ((d, 0u64), 0u64, 1i64)).collect();
961 assert_eq!(out, expected);
962 }
963
964 #[mz_ore::test]
968 fn merge_chains_equal_boundary() {
969 let out = drive_merge(
970 vec![col(&[((0, 0), 0, 1), ((5, 0), 0, 1)])],
971 vec![col(&[((5, 0), 0, 1), ((10, 0), 0, 1)])],
972 );
973 assert_eq!(out, vec![((0, 0), 0, 1), ((5, 0), 0, 2), ((10, 0), 0, 1)]);
974 }
975
976 #[mz_ore::test]
984 fn merge_chains_ships_fitting_align() {
985 let pager = ColumnPager::disabled();
986 let q1 = to_chain(vec![col(&[((0, 0), 0, 1), ((2, 0), 0, 1)])], &pager);
990 let q2 = to_chain(vec![col(&[((1, 0), 0, 1), ((3, 0), 0, 1)])], &pager);
991
992 let mut output: Vec<PagedColumn<KvUpdate>> = Vec::new();
993 let mut stash: Vec<Column<KvUpdate>> = Vec::new();
994 merge_chains(
995 FetchIter::new(q1, &pager),
996 FetchIter::new(q2, &pager),
997 |paged| output.push(paged),
998 &mut stash,
999 );
1000
1001 assert!(!output.is_empty(), "merge produced no chunks");
1002 for entry in &output {
1003 match entry {
1004 PagedColumn::Resident(col, _) => assert!(
1005 matches!(col, Column::Align(_)),
1006 "shipped chunk parked as non-Align resident: {:?}",
1007 std::mem::discriminant(col),
1008 ),
1009 other => panic!(
1010 "disabled pager should ship Resident, got a paged variant: {:?}",
1011 std::mem::discriminant(other)
1012 ),
1013 }
1014 }
1015
1016 assert_eq!(
1018 collect_pc(&output, &pager),
1019 vec![
1020 ((0, 0), 0, 1),
1021 ((1, 0), 0, 1),
1022 ((2, 0), 0, 1),
1023 ((3, 0), 0, 1)
1024 ],
1025 );
1026 }
1027
1028 #[mz_ore::test]
1031 fn merge_chains_force_paged_round_trip() {
1032 let policy = ForcePagePolicy::new();
1033 let pager = ColumnPager::new(policy.clone());
1034 let q1 = to_chain(vec![col(&[((0, 0), 0, 1), ((2, 0), 0, 1)])], &pager);
1035 let q2 = to_chain(vec![col(&[((1, 0), 0, 1), ((3, 0), 0, 1)])], &pager);
1036
1037 assert!(matches!(q1.front().unwrap(), PagedColumn::Paged { .. }));
1039 assert!(matches!(q2.front().unwrap(), PagedColumn::Paged { .. }));
1040
1041 let mut output: Vec<PagedColumn<KvUpdate>> = Vec::new();
1042 let mut stash: Vec<Column<KvUpdate>> = Vec::new();
1043 merge_chains(
1044 FetchIter::new(q1, &pager),
1045 FetchIter::new(q2, &pager),
1046 |paged| output.push(paged),
1047 &mut stash,
1048 );
1049
1050 for p in &output {
1052 assert!(matches!(p, PagedColumn::Paged { .. }));
1053 }
1054
1055 let mut collected = Vec::new();
1057 for p in output {
1058 let c = pager.take(p);
1059 collected.extend(collect_column(&c));
1060 }
1061 let expected: Vec<_> = (0..4u64).map(|d| ((d, 0u64), 0u64, 1i64)).collect();
1062 assert_eq!(collected, expected);
1063 }
1064
1065 #[mz_ore::test]
1066 fn extract_chain_partitions_by_frontier() {
1067 let pager = ColumnPager::disabled();
1068 let data = vec![
1069 ((0, 0), 0u64, 1i64),
1070 ((1, 0), 1, 1),
1071 ((2, 0), 2, 1),
1072 ((3, 0), 3, 1),
1073 ];
1074 let chain = to_chain(vec![col(&data)], &pager);
1075 let upper = Antichain::from_elem(2u64);
1076 let mut frontier: Antichain<u64> = Antichain::new();
1077 let mut ship: Vec<PagedColumn<KvUpdate>> = Vec::new();
1078 let mut keep: Vec<PagedColumn<KvUpdate>> = Vec::new();
1079 let mut stash: Vec<Column<KvUpdate>> = Vec::new();
1080
1081 extract_chain(
1082 FetchIter::new(chain, &pager),
1083 upper.borrow(),
1084 &mut frontier,
1085 |p| ship.push(p),
1086 |p| keep.push(p),
1087 &mut stash,
1088 );
1089
1090 let shipped = collect_pc(&ship, &pager);
1091 let kept = collect_pc(&keep, &pager);
1092 for (_, t, _) in &shipped {
1093 assert!(*t < 2, "shipped time {t} should be < upper");
1094 }
1095 for (_, t, _) in &kept {
1096 assert!(*t >= 2, "kept time {t} should be >= upper");
1097 }
1098 assert_eq!(shipped.len() + kept.len(), data.len());
1099 }
1100
1101 #[mz_ore::test]
1102 fn batcher_seal_round_trip() {
1103 let mut b = TestPagedBatcher::new(None, 0);
1104 let input1 = col(&[((1, 1), 0, 1), ((2, 0), 0, 1), ((3, 0), 0, 1)]);
1108 let input2 = col(&[((2, 0), 0, 2), ((4, 0), 0, 1)]);
1109 b.push_into(input1);
1110 b.push_into(input2);
1111
1112 let upper = Antichain::from_elem(u64::MAX);
1114 let (chain, _frontier) = b.extract_chain(upper.borrow());
1115 let out: Vec<KvUpdate> = chain.iter().flat_map(collect_column).collect();
1116
1117 let mut expected = vec![
1119 ((1u64, 1u64), 0u64, 1i64),
1120 ((2, 0), 0, 3),
1121 ((3, 0), 0, 1),
1122 ((4, 0), 0, 1),
1123 ];
1124 expected.sort();
1125 let mut out_sorted = out.clone();
1126 out_sorted.sort();
1127 assert_eq!(out_sorted, expected);
1128 }
1129
1130 #[mz_ore::test]
1131 fn account_chunk_resident_vs_paged() {
1132 let policy = ForcePagePolicy::new();
1133 let pager_paged = ColumnPager::new(policy.clone());
1134 let pager_res = ColumnPager::disabled();
1135
1136 let mut c1 = col(&[((1, 1), 0, 1), ((2, 0), 0, 1), ((3, 0), 0, 1)]);
1137 let resident = pager_res.page(&mut c1);
1138 let (records, size, capacity, allocations) = account_chunk(&resident);
1139 assert_eq!(records, 3);
1140 assert!(size > 0);
1141 assert_eq!(size, capacity);
1142 assert_eq!(allocations, 1);
1143
1144 let mut c2 = col(&[((1, 1), 0, 1), ((2, 0), 0, 1)]);
1145 let paged = pager_paged.page(&mut c2);
1146 assert!(matches!(paged, PagedColumn::Paged { .. }));
1147 assert_eq!(account_chunk(&paged), (0, 0, 0, 0));
1149 }
1150
1151 #[mz_ore::test]
1152 fn batcher_seal_keeps_kept_chain_paged() {
1153 let policy = ForcePagePolicy::new();
1156 let pager = ColumnPager::new(policy.clone());
1157
1158 let mut b = TestPagedBatcher::new(None, 0);
1159 b.set_pager(pager);
1160
1161 let n: u64 = 200;
1164 for i in 0..n {
1165 let input = col(&[((i, 0), i % 10, 1)]);
1166 b.push_into(input);
1167 }
1168 let upper = Antichain::from_elem(5u64);
1169 let _ = b.extract_chain(upper.borrow());
1170
1171 let kept_records: usize = b
1173 .chains
1174 .iter()
1175 .flat_map(|c| c.iter())
1176 .map(|p| match p {
1177 PagedColumn::Paged { meta, .. } => {
1178 let _ = meta;
1181 1
1182 }
1183 PagedColumn::Compressed { meta, .. } => {
1184 let _ = meta;
1185 1
1186 }
1187 PagedColumn::Resident(_, _) => {
1188 panic!("kept chain entry was Resident under ForcePagePolicy");
1189 }
1190 })
1191 .sum();
1192 assert!(kept_records > 0, "expected at least one kept paged entry");
1194 assert!(policy.out.load(std::sync::atomic::Ordering::Relaxed) > 0);
1195 let _ = n;
1196 }
1197}