Skip to main content

differential_dataflow/trace/chunk/
mod.rs

1//! Sorted, consolidated runs of updates, and operators over sequences of them.
2//!
3//! A [`Chunk`] is a consolidated, sorted run of `(data, time, diff)` updates.
4//! A sequence of chunks is also expected to be consolidated and sorted.
5//!
6//! The [`Chunk`] trait exposes whole-chunk operations, so that the implementor
7//! can internally divert to their best implementations, with amortized overhead.
8//! Each operation is invoked as if "streaming", providing input and output queues.
9//! An implementor is expected to drain as much as possible of the inputs, and any
10//! chunk written to the output is "committed" and likely to be shipped onward.
11//!
12//! # Wiring a `Chunk` into an arrangement
13//!
14//! Implementing [`Chunk`] for a type `C` is the only bespoke code needed; three
15//! aliases then expand into a full trace:
16//!
17//! * [`ChunkBatcher<C>`](ChunkBatcher) — the merge batcher.
18//! * [`ChunkBuilder<C>`](ChunkBuilder) — the batch builder.
19//! * [`ChunkSpine<C>`](ChunkSpine) — the trace, a spine of `Rc`-shared batches.
20//!
21//! These are the `Batcher` / `Builder` / `Spine` to hand to
22//! [`arrange_core`](crate::operators::arrange::arrangement::arrange_core), along with a
23//! chunker that forms `C` from the input stream — typically
24//! [`ContainerChunker<C>`](crate::trace::implementations::chunker::ContainerChunker).
25//! Trace *maintenance* needs only [`Chunk`]; cursor-driven *consumption* of the
26//! arrangement additionally asks `C` for the [`NavigableChunk`] capability.
27//! Everything else here ([`ChunkBatch`], [`ChunkMerger`], [`ChunkBatchMerger`],
28//! [`ChunkBatchCursor`], [`ChunkBatchBuilder`]) is machinery those aliases expand to and is
29//! not named directly. The [`vec`](mod@vec) module is a worked `Chunk`
30//! that re-exports the three aliases specialized to its layout, and the `chunks` example
31//! stands one up.
32//!
33//! # Bounded footprint
34//!
35//! There is a `TARGET` associated constant that signals the intended chunk size.
36//! The constant should be chosen large enough to amortize overheads, but small
37//! enough that per-chunk work does not "stall" the system when invoked.
38//! The implementor is trusted to make a reasonable choice here.
39//!
40//! The [`Chunk::settle`] method "settles" sequences of chunks, and is called as
41//! chunks are no longer expected to be needed in the near future. The implementor
42//! should ensure the chunks are "graded", in that the sequence of chunks are all
43//! at most `TARGET` in size, any two in order sum to strictly more than `TARGET`.
44//! This is also an opportunity to compress data, or spill to disk or cloud storage.
45//!
46//! The active (un-settled) chunk set is kept small from both sides. Every producer
47//! settles its committed output as it goes (see [`Chunk::settle`]), rather than
48//! building a whole sequence and settling at the end. And every walk over a whole
49//! chunk sequence reads only resident metadata — [`len`](Chunk::len) and
50//! [`bounds`](NavigableChunk::bounds) — never a chunk body: the straddle cursor
51//! seeks by galloping the chunks' bounds from a hint and opens only the chunk(s) a
52//! query touches. Implementors must therefore keep `len` and `bounds` cheap even
53//! when a chunk's body is paged out.
54
55use std::collections::VecDeque;
56
57use timely::progress::Antichain;
58use timely::progress::frontier::AntichainRef;
59use crate::lattice::Lattice;
60use crate::trace::{Batch, BatchReader, Description, Navigable};
61use crate::trace::cursor::Cursor;
62use crate::trace::implementations::BatchContainer;
63
64pub mod vec;
65
66/// A non-empty, bounded, consolidated, sorted sequence of `(data, time, diff)`.
67///
68/// An implementor gains access to types and trait implementations that provide
69/// batch formation and trace maintenance with no additional effort.
70///
71/// The necessary implementations are either "data" or "metadata" operations.
72/// The "data" operations transform lists of chunks, are expected to do roughly
73/// "one chunk's worth" of work at a time; they can afford to compress and page.
74/// The "metadata" operations provide chunk information, and should be lightweight.
75///
76/// The trait has no opinion about keys, vals, or diffs — only time, which trace
77/// maintenance needs. Reading a chunk's contents is a separate, optional
78/// capability: see [`NavigableChunk`].
79pub trait Chunk: Sized + Clone {
80    /// The timestamp type of the chunk's updates.
81    ///
82    /// Key/val/diff opinions live on the optional [`NavigableChunk`] capability; the chunk itself
83    /// only needs time, to bound its interval and participate in advancement and compaction.
84    type Time: Lattice + timely::progress::Timestamp;
85
86    /// The intended maximum chunk size.
87    const TARGET: usize;
88
89    /// The number of updates in the chunk.
90    fn len(&self) -> usize;
91
92    /// Merge the fronts of two input deques through their shared horizon.
93    ///
94    /// Both deques are non-empty (the caller guarantees it). The two queues are both
95    /// the heads of lists of chunks, and the implementor should only merge through the
96    /// least last `(key, val, time)` update, or risk emitting an unconsolidated
97    /// output chunk.
98    ///
99    /// When a chunk cannot be completely retired, perhaps it had the larger last update,
100    /// it should be rewritten as a new chunk and pushed back to the front of the queue.
101    /// The invocation is expected to consume at least one of its inputs, and the harness
102    /// may continually re-invoke if this doesn't happen.
103    ///
104    /// A merge concludes when the harness sees that either input is now empty, at which
105    /// point it appends the queue to the output without the method's assistance.
106    fn merge(in1: &mut VecDeque<Self>, in2: &mut VecDeque<Self>, out: &mut VecDeque<Self>);
107
108    /// Partition `input` updates into `keep` (greater or equal `frontier`) or not (`ship`).
109    ///
110    /// An implementation should yield with some frequency to allow the output to "settle".
111    /// The harness may guard against this, but it prefers to provide as much context as it
112    /// can in order to allow broader chunk fusion where needed.
113    fn extract(
114        input: &mut VecDeque<Self>,
115        frontier: AntichainRef<Self::Time>,
116        residual: &mut Antichain<Self::Time>,
117        keep: &mut VecDeque<Self>,
118        ship: &mut VecDeque<Self>,
119    );
120
121    /// Advance times by `frontier` producing consolidated chunks.
122    ///
123    /// An output for `(key, val)` should generally not be produced until a later pair
124    /// is observed, or `done` is set, to ensure the output chunks are consolidated.
125    /// Incomplete work can be pushed back to the front of `input`.
126    ///
127    /// On `done` a single `(key, val)` group may span the whole input; advancing and
128    /// consolidating it should cost time linear in its size, not quadratic.
129    fn advance(
130        input: &mut VecDeque<Self>,
131        frontier: AntichainRef<Self::Time>,
132        done: bool,
133        out: &mut VecDeque<Self>,
134    );
135
136    /// Reshape `input` to a sequence that maintains the "grading" structural invariant.
137    ///
138    /// Specifically, the chunks in `output` should have a maximum size of `TARGET` and
139    /// each adjacent pair should have lengths that sum to more than `TARGET`.
140    /// This is also a good moment to consider compression or paging out the contents.
141    /// When `done` is set the input must be moved to the output.
142    ///
143    /// This method may be called on already settled data, and should be efficient then.
144    ///
145    /// Implementors that want the standard maximal packing can delegate to the
146    /// [`pack`] helper, supplying their layout's coalesce / split / commit closures.
147    fn settle(input: &mut VecDeque<Self>, done: bool, out: &mut VecDeque<Self>);
148
149}
150
151/// The navigation capability: a [`Chunk`] whose contents can be read by cursor.
152///
153/// This is optional. Batch formation and trace maintenance need only [`Chunk`];
154/// implementing this trait additionally lets [`ChunkBatch`] offer the straddle
155/// cursor ([`ChunkBatchCursor`]), which is how cursor-driven operator paths read
156/// an arrangement. Chunks consumed only by whole-chunk logic (tactics) can skip it.
157///
158/// `bounds` must stay cheap even when a chunk's body is paged out: the straddle
159/// cursor consults chunk bounds throughout navigation — seeks binary-search them,
160/// boundary crossings compare against them — and opens a chunk's body only when a
161/// query touches it.
162pub trait NavigableChunk: Chunk + Navigable<Cursor: Cursor<Time = <Self as Chunk>::Time>> {
163    /// The first and last `(key, val, time)` triples in the chunk.
164    fn bounds(&self) -> (
165        (<Self::Cursor as Cursor>::Key<'_>, <Self::Cursor as Cursor>::Val<'_>, <Self::Cursor as Cursor>::TimeGat<'_>),
166        (<Self::Cursor as Cursor>::Key<'_>, <Self::Cursor as Cursor>::Val<'_>, <Self::Cursor as Cursor>::TimeGat<'_>),
167    );
168}
169
170/// Maximal-packing driver an implementor's [`Chunk::settle`] may delegate to.
171///
172/// Holds a `carry` chunk under construction, grown by `combine` until it reaches
173/// `TARGET` (then emitted) and emitted early when the next chunk can't be absorbed
174/// without exceeding `TARGET`; over-sized chunks are peeled with `split`. Each
175/// committed chunk is passed through `seal` (the compress / spill hook — use the
176/// identity closure when there's nothing to do). The closures are the only
177/// layout-specific pieces:
178///
179/// * `combine(&mut acc, next)` — append `next` onto `acc` (caller guarantees their
180///   lengths sum to at most `TARGET`, and `next` follows `acc` in one sorted,
181///   consolidated chain), so packing a run of small chunks stays linear.
182/// * `split(chunk, n)` — the first `n` updates and the remaining `len - n`.
183/// * `seal(chunk)` — commit a chunk (e.g. compress or spill); identity to keep it.
184pub fn pack<C: Chunk>(
185    input: &mut VecDeque<C>,
186    done: bool,
187    out: &mut VecDeque<C>,
188    mut combine: impl FnMut(&mut C, C),
189    mut split: impl FnMut(C, usize) -> (C, C),
190    mut seal: impl FnMut(C) -> C,
191) {
192    let mut carry: Option<C> = None;
193    while let Some(chunk) = input.pop_front() {
194        match carry.take() {
195            None => pack_absorb(chunk, &mut carry, out, &mut split, &mut seal),
196            Some(mut c) if c.len() + chunk.len() <= C::TARGET => {
197                // Combines into one legal chunk; coalesce in place.
198                combine(&mut c, chunk);
199                if c.len() == C::TARGET { out.push_back(seal(c)); } else { carry = Some(c); }
200            }
201            Some(c) => {
202                // `c` is maximal against this neighbour; emit it and absorb afresh.
203                out.push_back(seal(c));
204                pack_absorb(chunk, &mut carry, out, &mut split, &mut seal);
205            }
206        }
207    }
208    if let Some(c) = carry {
209        if done { out.push_back(seal(c)); } else { input.push_front(c); }
210    }
211}
212
213/// Absorb `chunk` into an empty `carry` (a [`pack`] helper): pass a `TARGET` chunk
214/// straight through (sealed), hold a smaller one as the new carry, or peel
215/// `TARGET`-sized pieces off a larger one and carry the remainder.
216fn pack_absorb<C, S, L>(chunk: C, carry: &mut Option<C>, out: &mut VecDeque<C>, split: &mut S, seal: &mut L)
217where
218    C: Chunk,
219    S: FnMut(C, usize) -> (C, C),
220    L: FnMut(C) -> C,
221{
222    match chunk.len().cmp(&C::TARGET) {
223        std::cmp::Ordering::Equal => out.push_back(seal(chunk)),
224        std::cmp::Ordering::Less => *carry = Some(chunk),
225        std::cmp::Ordering::Greater => {
226            let mut rest = chunk;
227            loop {
228                let (head, tail) = split(rest, C::TARGET);
229                out.push_back(seal(head));
230                if tail.len() >= C::TARGET { rest = tail; }
231                else { if tail.len() > 0 { *carry = Some(tail); } break; }
232            }
233        }
234    }
235}
236
237type KeyCon<C> = <<C as Navigable>::Cursor as Cursor>::KeyContainer;
238type ValCon<C> = <<C as Navigable>::Cursor as Cursor>::ValContainer;
239
240/// A batch is a [`Chunk`] sequence plus a [`Description`].
241pub struct ChunkBatch<C: Chunk> {
242    /// Ordered, consolidated chunks; their concatenation is the batch.
243    pub chunks: Vec<C>,
244    /// The lower, upper, and since frontiers of the batch.
245    pub description: Description<C::Time>,
246}
247
248impl<C: Chunk> ChunkBatch<C> {
249    /// Assemble a batch from ordered chunks.
250    pub fn new(chunks: Vec<C>, description: Description<C::Time>) -> Self {
251        for chunk in &chunks {
252            assert!(chunk.len() > 0, "ChunkBatch chunks must be non-empty");
253        }
254        ChunkBatch { chunks, description }
255    }
256}
257
258impl<C: NavigableChunk> crate::trace::Navigable for ChunkBatch<C> {
259    type Cursor = ChunkBatchCursor<C>;
260    fn cursor(&self) -> Self::Cursor {
261        ChunkBatchCursor { key_chunk: 0, chunk: 0, inner: self.chunks.first().map(C::cursor) }
262    }
263}
264
265impl<C: Chunk> BatchReader for ChunkBatch<C> {
266    type Time = C::Time;
267    fn len(&self) -> usize { self.chunks.iter().map(C::len).sum() }
268    fn description(&self) -> &Description<Self::Time> { &self.description }
269}
270
271impl<C: Chunk + Default + 'static> Batch for ChunkBatch<C>
272where
273    C::Time: timely::progress::Timestamp + Lattice + Ord,
274{
275    type Merger = ChunkBatchMerger<C>;
276
277    fn empty(lower: Antichain<Self::Time>, upper: Antichain<Self::Time>) -> Self {
278        use timely::progress::Timestamp;
279        let since = Antichain::from_elem(Self::Time::minimum());
280        ChunkBatch::new(Vec::new(), Description::new(lower, upper, since))
281    }
282}
283
284/// A merge-batcher [`Merger`](crate::trace::implementations::merge_batcher::Merger)
285/// over chains of [`Chunk`]s.
286///
287/// `merge` runs the whole-chain binary merger; `extract` splits by the seal frontier
288/// using [`Chunk::extract`]. The batcher consolidates equal `(data, time)` updates
289/// but does *not* advance times — time advancement is advance's job, handled later in
290/// the trace. Both settle their output, since the batcher's chains want to be graded.
291pub type ChunkBatcher<C> = crate::trace::implementations::merge_batcher::MergeBatcher<ChunkMerger<C>>;
292
293/// A spine of `Rc`-shared [`ChunkBatch`]es of type `C`: the trace type for `arrange`.
294pub type ChunkSpine<C> = crate::trace::implementations::spine_fueled::Spine<std::rc::Rc<ChunkBatch<C>>>;
295
296/// A reference-counted [`ChunkBatch`] builder over chunks of type `C`.
297pub type ChunkBuilder<C> = crate::trace::rc_blanket_impls::RcBuilder<ChunkBatchBuilder<C>>;
298
299/// A cursor over a [`ChunkBatch`], merging the per-chunk cursors.
300///
301/// Chunk breakpoints are unconstrained, so a single key — or `(key, val)` — may
302/// straddle consecutive chunks. But the chunks are one globally-sorted sequence
303/// merely cut at arbitrary points, so the operation is *concatenation*, never a
304/// merge: across a boundary a key's vals concatenate and a `(key, val)`'s times
305/// concatenate. The cursor exploits this. It holds the chunk currently being read
306/// and a cursor into it; it seeks by galloping the chunks' resident
307/// [`bounds`](NavigableChunk::bounds) from a remembered hint (the current key's first
308/// chunk), and at boundaries it *continues* into the next chunk rather than merging —
309/// consulting the two neighbouring chunks' bounds to detect when a key or `(key, val)`
310/// spills forward, without touching chunk contents. No state is materialized up front:
311/// a monotone seek sweep costs `O(log Δ)` bounds reads per seek and a sequential pass two
312/// per boundary, so cursor construction is free.
313pub struct ChunkBatchCursor<C: NavigableChunk> {
314    /// First chunk of the current key's run; where `rewind_vals` returns to.
315    key_chunk: usize,
316    /// Chunk currently being read; `>= key_chunk`, within the current key's span.
317    chunk: usize,
318    /// Cursor into `chunk`; `None` once `chunk` is past the last chunk.
319    inner: Option<C::Cursor>,
320}
321
322impl<C: NavigableChunk> ChunkBatchCursor<C> {
323    /// Move the active chunk to `c`, opening a fresh inner cursor at its start.
324    fn goto(&mut self, c: usize, storage: &ChunkBatch<C>) {
325        self.chunk = c;
326        self.inner = storage.chunks.get(c).map(C::cursor);
327    }
328
329    /// Does key `k` span the boundary between chunks `c` and `c + 1` — chunk `c`
330    /// ends with it and chunk `c + 1` begins with it?
331    ///
332    /// Two resident [`bounds`](NavigableChunk::bounds) reads; the `reborrow`s
333    /// unify the (invariant) item lifetimes with `k`'s.
334    fn key_spills(s: &ChunkBatch<C>, c: usize, k: <C::Cursor as Cursor>::Key<'_>) -> bool {
335        <KeyCon<C> as BatchContainer>::reborrow(s.chunks[c].bounds().1.0) == <KeyCon<C> as BatchContainer>::reborrow(k)
336            && <KeyCon<C> as BatchContainer>::reborrow(s.chunks[c + 1].bounds().0.0) == <KeyCon<C> as BatchContainer>::reborrow(k)
337    }
338
339    /// Does `(k, v)` span the boundary between chunks `c` and `c + 1`?
340    fn val_spills(s: &ChunkBatch<C>, c: usize, k: <C::Cursor as Cursor>::Key<'_>, v: <C::Cursor as Cursor>::Val<'_>) -> bool {
341        Self::key_spills(s, c, k)
342            && <ValCon<C> as BatchContainer>::reborrow(s.chunks[c].bounds().1.1) == <ValCon<C> as BatchContainer>::reborrow(v)
343            && <ValCon<C> as BatchContainer>::reborrow(s.chunks[c + 1].bounds().0.1) == <ValCon<C> as BatchContainer>::reborrow(v)
344    }
345
346    /// The first chunk, at or after `hint`, whose last key is `>= key`: where `key`'s run
347    /// begins. `hint` — the current key's first chunk (`key_chunk`) — is a valid lower bound
348    /// for a forward seek; a backward seek is detected and served by a full search from the
349    /// front. Galloping keeps a monotone seek sweep at `O(log Δ)` bounds reads per seek rather
350    /// than `O(log chunks)`; only resident [`bounds`](NavigableChunk::bounds) are read.
351    fn locate_key(s: &ChunkBatch<C>, hint: usize, key: <C::Cursor as Cursor>::Key<'_>) -> usize {
352        let n = s.chunks.len();
353        // `last_key(i) < key`, from chunk `i`'s resident bounds.
354        let lt = |i: usize| <KeyCon<C> as BatchContainer>::reborrow(s.chunks[i].bounds().1.0)
355            .lt(&<KeyCon<C> as BatchContainer>::reborrow(key));
356        let hint = hint.min(n);
357        // The hint can skip the answer only on a backward seek; then search from the front.
358        let lo = if hint == 0 || lt(hint - 1) { hint } else { 0 };
359        if lo >= n || !lt(lo) { return lo; }
360        // Exponential search from `lo`, then binary within the final bracket.
361        let (mut prev, mut step) = (lo, 1usize);
362        while prev + step < n && lt(prev + step) { prev += step; step <<= 1; }
363        let (mut a, mut b) = (prev + 1, (prev + step).min(n));
364        while a < b { let m = a + (b - a) / 2; if lt(m) { a = m + 1; } else { b = m; } }
365        a
366    }
367}
368
369impl<C: NavigableChunk> Cursor for ChunkBatchCursor<C> {
370    type Storage = ChunkBatch<C>;
371
372    type KeyContainer = <C::Cursor as Cursor>::KeyContainer;
373    type Key<'a> = <C::Cursor as Cursor>::Key<'a>;
374    type ValContainer = <C::Cursor as Cursor>::ValContainer;
375    type Val<'a> = <C::Cursor as Cursor>::Val<'a>;
376    type ValOwn = <C::Cursor as Cursor>::ValOwn;
377    type TimeContainer = <C::Cursor as Cursor>::TimeContainer;
378    type TimeGat<'a> = <C::Cursor as Cursor>::TimeGat<'a>;
379    type Time = <C::Cursor as Cursor>::Time;
380    type DiffContainer = <C::Cursor as Cursor>::DiffContainer;
381    type DiffGat<'a> = <C::Cursor as Cursor>::DiffGat<'a>;
382    type Diff = <C::Cursor as Cursor>::Diff;
383
384    fn key_valid(&self, s: &Self::Storage) -> bool { self.chunk < s.chunks.len() && self.inner.as_ref().is_some_and(|i| i.key_valid(&s.chunks[self.chunk])) }
385    fn val_valid(&self, s: &Self::Storage) -> bool { self.chunk < s.chunks.len() && self.inner.as_ref().is_some_and(|i| i.val_valid(&s.chunks[self.chunk])) }
386    fn key<'a>(&self, s: &'a Self::Storage) -> Self::Key<'a> { self.inner.as_ref().unwrap().key(&s.chunks[self.chunk]) }
387    fn val<'a>(&self, s: &'a Self::Storage) -> Self::Val<'a> { self.inner.as_ref().unwrap().val(&s.chunks[self.chunk]) }
388    fn get_key<'a>(&self, s: &'a Self::Storage) -> Option<Self::Key<'a>> { if self.key_valid(s) { Some(self.key(s)) } else { None } }
389    fn get_val<'a>(&self, s: &'a Self::Storage) -> Option<Self::Val<'a>> { if self.val_valid(s) { Some(self.val(s)) } else { None } }
390
391    fn map_times<L: FnMut(Self::TimeGat<'_>, Self::DiffGat<'_>)>(&mut self, s: &Self::Storage, mut logic: L) {
392        if !self.val_valid(s) { return; }
393        let (k, v) = (self.key(s), self.val(s));
394        self.inner.as_mut().unwrap().map_times(&s.chunks[self.chunk], &mut logic);
395        // Follow the (key, val) forward across boundaries while it spills.
396        let mut c = self.chunk;
397        while c + 1 < s.chunks.len() && Self::val_spills(s, c, k, v) {
398            c += 1;
399            s.chunks[c].cursor().map_times(&s.chunks[c], &mut logic);
400        }
401    }
402
403    fn step_key(&mut self, s: &Self::Storage) {
404        if !self.key_valid(s) { return; }
405        let n = s.chunks.len();
406        let k = self.key(s);
407        // Advance to the last chunk the key spans.
408        while self.chunk + 1 < n && Self::key_spills(s, self.chunk, k) {
409            self.goto(self.chunk + 1, s);
410        }
411        // Step past the key within its last chunk.
412        {
413            let inner = self.inner.as_mut().unwrap();
414            inner.seek_key(&s.chunks[self.chunk], k);
415            inner.step_key(&s.chunks[self.chunk]);
416        }
417        // If that exhausted the chunk, the next key (if any) starts the next chunk.
418        if !self.inner.as_ref().unwrap().key_valid(&s.chunks[self.chunk]) && self.chunk + 1 < n {
419            self.goto(self.chunk + 1, s);
420        }
421        self.key_chunk = self.chunk;
422    }
423
424    fn seek_key(&mut self, s: &Self::Storage, key: Self::Key<'_>) {
425        let n = s.chunks.len();
426        // First chunk whose last key is `>= key`: where `key`'s run begins. Gallop from the
427        // current key's first chunk (`key_chunk`), a lower bound for forward seeks.
428        let lo = Self::locate_key(s, self.key_chunk, key);
429        self.goto(lo, s);
430        self.key_chunk = lo;
431        if lo < n { self.inner.as_mut().unwrap().seek_key(&s.chunks[lo], key); }
432    }
433
434    fn step_val(&mut self, s: &Self::Storage) {
435        if !self.val_valid(s) { return; }
436        let n = s.chunks.len();
437        let (k, v) = (self.key(s), self.val(s));
438        // Advance to the last chunk the (key, val) spans.
439        while self.chunk + 1 < n && Self::val_spills(s, self.chunk, k, v) {
440            self.goto(self.chunk + 1, s);
441        }
442        // Step past the (key, val) within that chunk.
443        self.inner.as_mut().unwrap().step_val(&s.chunks[self.chunk]);
444        // If the key's vals are exhausted here but the key spills, roll forward.
445        if !self.inner.as_ref().unwrap().val_valid(&s.chunks[self.chunk])
446            && self.chunk + 1 < n && Self::key_spills(s, self.chunk, k)
447        {
448            self.goto(self.chunk + 1, s);
449            self.inner.as_mut().unwrap().seek_key(&s.chunks[self.chunk], k);
450        }
451    }
452
453    fn seek_val(&mut self, s: &Self::Storage, val: Self::Val<'_>) {
454        if !self.key_valid(s) { return; }
455        let n = s.chunks.len();
456        let k = self.key(s);
457        loop {
458            self.inner.as_mut().unwrap().seek_val(&s.chunks[self.chunk], val);
459            if self.inner.as_ref().unwrap().val_valid(&s.chunks[self.chunk]) { return; }
460            // Key's vals exhausted in this chunk; if the key spills, retry in the next.
461            if self.chunk + 1 < n && Self::key_spills(s, self.chunk, k) {
462                self.goto(self.chunk + 1, s);
463                self.inner.as_mut().unwrap().seek_key(&s.chunks[self.chunk], k);
464            } else {
465                return;
466            }
467        }
468    }
469
470    fn rewind_keys(&mut self, s: &Self::Storage) {
471        self.key_chunk = 0;
472        self.goto(0, s);
473    }
474
475    fn rewind_vals(&mut self, s: &Self::Storage) {
476        if !self.key_valid(s) { return; }
477        let k = self.key(s);
478        let kc = self.key_chunk;
479        self.goto(kc, s);
480        self.inner.as_mut().unwrap().seek_key(&s.chunks[kc], k);
481    }
482}
483
484/// A merge-batcher [`Merger`](crate::trace::implementations::merge_batcher::Merger)
485/// over chains of [`Chunk`]s.
486///
487/// `merge` runs the whole-chain binary merger; `extract` splits by the seal frontier
488/// using [`Chunk::extract`]. The batcher consolidates equal `(data, time)` updates
489/// but does *not* advance times — time advancement is advance's job, handled later in
490/// the trace. Both settle their output, since the batcher's chains want to be graded.
491pub struct ChunkMerger<C> {
492    _marker: std::marker::PhantomData<C>,
493}
494
495impl<C> Default for ChunkMerger<C> {
496    fn default() -> Self { Self { _marker: std::marker::PhantomData } }
497}
498
499impl<C> crate::trace::implementations::merge_batcher::Merger for ChunkMerger<C>
500where
501    C: Chunk + Default + 'static,
502    C::Time: Clone + timely::PartialOrder + 'static,
503{
504    type Chunk = C;
505    type Time = C::Time;
506
507    fn merge(
508        &mut self,
509        list1: Vec<C>,
510        list2: Vec<C>,
511        output: &mut Vec<C>,
512        _stash: &mut Vec<C>,
513    ) {
514        // Settle the output after each merge, to maintain bounded active chunks.
515        let mut in1: VecDeque<C> = list1.into();
516        let mut in2: VecDeque<C> = list2.into();
517        let (mut staged, mut settled) = (VecDeque::new(), VecDeque::new());
518        while !in1.is_empty() && !in2.is_empty() {
519            C::merge(&mut in1, &mut in2, &mut staged);
520            C::settle(&mut staged, false, &mut settled);
521        }
522        // Append the non-empty tail from either input, settle as we go.
523        for tail in in1.drain(..).chain(in2.drain(..)) {
524            staged.push_back(tail);
525            C::settle(&mut staged, false, &mut settled);
526        }
527        C::settle(&mut staged, true, &mut settled);
528        output.extend(settled);
529    }
530
531    fn extract(
532        &mut self,
533        merged: Vec<C>,
534        upper: AntichainRef<C::Time>,
535        frontier: &mut Antichain<C::Time>,
536        ship: &mut Vec<C>,
537        kept: &mut Vec<C>,
538        _stash: &mut Vec<C>,
539    ) {
540        // `extract` keeps updates greater-or-equal `upper` and ships the rest, folding
541        // the lower envelope of kept times into `frontier`. Drive it a bounded amount
542        // per call (≈ one input chunk) and `settle` each side as it accumulates, so
543        // neither `keep` (retained across yields) nor `ship` (handed to the builder)
544        // builds up unsettled in core. `settle` may withhold a sub-`TARGET` carry
545        // between calls; the final `settle(done)` flushes it.
546        let mut input: VecDeque<C> = merged.into();
547        let (mut keep, mut shipped) = (VecDeque::new(), VecDeque::new());
548        let (mut kept_q, mut shipped_q) = (VecDeque::new(), VecDeque::new());
549        while !input.is_empty() {
550            C::extract(&mut input, upper, frontier, &mut keep, &mut shipped);
551            C::settle(&mut keep, false, &mut kept_q);
552            C::settle(&mut shipped, false, &mut shipped_q);
553        }
554        C::settle(&mut keep, true, &mut kept_q);
555        C::settle(&mut shipped, true, &mut shipped_q);
556        kept.extend(kept_q);
557        ship.extend(shipped_q);
558    }
559
560    fn len(chunk: &C) -> usize { chunk.len() }
561}
562
563/// The resumable [`Batch::Merger`] for [`ChunkBatch`]: merges two batches and advances
564/// their times to the compaction frontier, a fuel-bounded step at a time.
565///
566/// Each step pipelines [`merge`](Chunk::merge) → [`advance`](Chunk::advance) →
567/// [`settle`](Chunk::settle) and settles its output, so a suspended merge holds only
568/// graded chunks. The sources are read by cloning (a cheap refcount bump) and must be
569/// supplied unchanged on every call.
570pub struct ChunkBatchMerger<C: Chunk> {
571    /// Compaction frontier supplied at construction.
572    frontier: Antichain<C::Time>,
573    /// Result frontiers, retained for the output description.
574    lower: Antichain<C::Time>,
575    upper: Antichain<C::Time>,
576    /// Input deques, refilled from the sources (clones) head-of-list at a time.
577    in1: VecDeque<C>,
578    in2: VecDeque<C>,
579    /// Next source chunk to clone into `in1` / `in2`.
580    idx1: usize,
581    idx2: usize,
582    /// `advance`'s input: the merge output plus advance's withheld carry at the front.
583    merged: VecDeque<C>,
584    /// `advance`'s output and `settle`'s input: merged-and-advanced chunks, with
585    /// settle's withheld sub-`TARGET` carry at the front.
586    advanced: VecDeque<C>,
587    /// `settle`'s output: the committed, graded result, grown by `work`. Graded at
588    /// every yield, so a suspended merge holds well-formed (spillable) chunk state.
589    settled: VecDeque<C>,
590    /// Set once both sources are drained and advance's and settle's final flushes ran.
591    complete: bool,
592}
593
594impl<C> crate::trace::Merger<ChunkBatch<C>> for ChunkBatchMerger<C>
595where
596    C: Chunk + Default + 'static,
597    C::Time: timely::progress::Timestamp + Lattice + Ord + 'static,
598{
599    fn new(source1: &ChunkBatch<C>, source2: &ChunkBatch<C>, frontier: AntichainRef<C::Time>) -> Self {
600        let lower = source1.description.lower().meet(source2.description.lower());
601        let upper = source1.description.upper().join(source2.description.upper());
602        Self {
603            frontier: frontier.to_owned(),
604            lower,
605            upper,
606            in1: VecDeque::new(),
607            in2: VecDeque::new(),
608            idx1: 0,
609            idx2: 0,
610            merged: VecDeque::new(),
611            advanced: VecDeque::new(),
612            settled: VecDeque::new(),
613            complete: false,
614        }
615    }
616
617    fn work(&mut self, source1: &ChunkBatch<C>, source2: &ChunkBatch<C>, fuel: &mut isize) {
618
619        // TODO: The logic is a bit tortured here, and should be improved.
620
621        if self.complete { return; }
622
623        while *fuel > 0 {
624            // Refill each input deque up to a burst of source chunks (clones).
625            // The constant trades away fuel precision for overhead amortization.
626            const BURST: usize = 8;
627            while self.in1.len() < BURST && self.idx1 < source1.chunks.len() {
628                self.in1.push_back(source1.chunks[self.idx1].clone());
629                self.idx1 += 1;
630            }
631            while self.in2.len() < BURST && self.idx2 < source2.chunks.len() {
632                self.in2.push_back(source2.chunks[self.idx2].clone());
633                self.idx2 += 1;
634            }
635
636            // Merge's per-tick output (a burst's worth, or one tail chunk), measured
637            // for fuel before it joins the carry already in `merged`.
638            let mut produced = VecDeque::new();
639            if !self.in1.is_empty() && !self.in2.is_empty() {
640                // Both sides have data: drain the loaded burst.
641                C::merge(&mut self.in1, &mut self.in2, &mut produced);
642            } else if let Some(chunk) = self.in1.pop_front().or_else(|| self.in2.pop_front()) {
643                // Exactly one side has data: flush its verbatim tail, one chunk a step.
644                produced.push_back(chunk);
645            } else {
646                // Both sources drained: final flush of advance's and settle's carries.
647                C::advance(&mut self.merged, self.frontier.borrow(), true, &mut self.advanced);
648                C::settle(&mut self.advanced, true, &mut self.settled);
649                self.complete = true;
650                break;
651            }
652
653            let work: usize = produced.iter().map(C::len).sum();
654            self.merged.extend(produced);
655            C::advance(&mut self.merged, self.frontier.borrow(), false, &mut self.advanced);
656            // Maintain grading at the yield boundary: this step may exhaust `fuel` and
657            // suspend with `advanced` held, and held chunk state must be graded.
658            C::settle(&mut self.advanced, false, &mut self.settled);
659            *fuel -= work as isize;
660        }
661    }
662
663    fn done(self) -> ChunkBatch<C> {
664        debug_assert!(self.merged.is_empty() && self.advanced.is_empty());
665        let description = Description::new(self.lower, self.upper, self.frontier);
666        ChunkBatch::new(self.settled.into(), description)
667    }
668}
669
670/// A [`Builder`](crate::trace::Builder) that collects a chunk sequence into a [`ChunkBatch`].
671pub struct ChunkBatchBuilder<C: Chunk> {
672    /// Pushed chunks awaiting settling; holds settle's sub-`TARGET` carry at the front.
673    input: VecDeque<C>,
674    /// The graded chunks emitted so far.
675    output: VecDeque<C>,
676}
677
678impl<C> crate::trace::Builder for ChunkBatchBuilder<C>
679where
680    C: Chunk + Default + 'static,
681    C::Time: timely::progress::Timestamp,
682{
683    type Input = C;
684    type Time = C::Time;
685    type Output = ChunkBatch<C>;
686
687    fn with_capacity(_keys: usize, _vals: usize, _upds: usize) -> Self {
688        Self { input: VecDeque::new(), output: VecDeque::new() }
689    }
690
691    fn push(&mut self, chunk: &mut C) {
692        let chunk = std::mem::take(chunk);
693        if chunk.len() > 0 {
694            self.input.push_back(chunk);
695            C::settle(&mut self.input, false, &mut self.output);
696        }
697    }
698
699    fn done(self, description: Description<C::Time>) -> ChunkBatch<C> {
700        let ChunkBatchBuilder { mut input, mut output } = self;
701        C::settle(&mut input, true, &mut output);
702        ChunkBatch::new(output.into(), description)
703    }
704
705    fn seal(chain: &mut Vec<C>, description: Description<C::Time>) -> ChunkBatch<C> {
706        // We settle the chain because we are not guaranteed to received pre-settled data.
707        // This should be efficient on pre-settled data.
708        ChunkBatch::new(settle_all(std::mem::take(chain)), description)
709    }
710}
711
712/// Whether `chunks` satisfy the [`Chunk::TARGET`] grading invariant: every chunk
713/// at most `TARGET`, and every adjacent pair summing to more than `TARGET` (so no
714/// two neighbours could be combined into one legal chunk — a *maximal packing*).
715///
716/// This is the post-[`settle`](Chunk::settle) shape; useful as a test/debug check.
717pub fn is_graded<C: Chunk>(chunks: &[C]) -> bool {
718    chunks.iter().all(|c| c.len() <= C::TARGET)
719        && chunks.windows(2).all(|w| w[0].len() + w[1].len() > C::TARGET)
720}
721
722/// Settle `input` to completion into a fresh graded `Vec` (see [`Chunk::settle`]).
723///
724/// A convenience for the one-shot callers (batch sealing, the batcher's merge and
725/// extract) that have a whole sequence in hand and want it graded; the streaming
726/// callers drive [`Chunk::settle`] directly across ticks.
727pub fn settle_all<C: Chunk>(input: impl IntoIterator<Item = C>) -> Vec<C> {
728    let mut input: VecDeque<C> = input.into_iter().collect();
729    let mut out = VecDeque::new();
730    C::settle(&mut input, true, &mut out);
731    debug_assert!(input.is_empty());
732    out.into()
733}
734
735/// Merge two full chains of chunks into one, to completion, appending to `out`.
736///
737/// The plain whole-chain driver: ticks [`Chunk::merge`] until one deque empties, then
738/// appends the other's remainder (the verbatim tail). Output is near-graded, not
739/// settled. The batcher's `merge` runs the same loop but settles after each push (the
740/// bounded-footprint discipline) and so does not use this; it stays as the simplest way
741/// to drive [`Chunk::merge`] to completion.
742pub fn merge_chains<C: Chunk>(
743    chain1: Vec<C>,
744    chain2: Vec<C>,
745    out: &mut VecDeque<C>,
746) {
747    let mut in1: VecDeque<C> = chain1.into();
748    let mut in2: VecDeque<C> = chain2.into();
749    while !in1.is_empty() && !in2.is_empty() {
750        C::merge(&mut in1, &mut in2, out);
751    }
752    // One deque is empty; the other's remainder is all greater than everything merged.
753    out.extend(in1.drain(..));
754    out.extend(in2.drain(..));
755}