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}