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 can internally divert to their best implementations, with amortized overhead.
7//! Each operation is invoked as if "streaming", providing input and output queues.
8//! An implementor is expected to drain as much as possible of the inputs, and any chunk written to the output is "committed" and likely to be shipped onward.
9//!
10//! # Wiring a `Chunk` into an arrangement
11//!
12//! Implementing [`Chunk`] for a type `C` is the only bespoke code needed; three aliases then expand into a full trace:
13//!
14//! * [`ChunkBatcher<Chu, C>`](ChunkBatcher) — the merge batcher.
15//! * [`ChunkBuilder<C>`](ChunkBuilder) — the batch builder.
16//! * [`ChunkSpine<C>`](ChunkSpine) — the trace, a spine of `Rc`-shared batches.
17//!
18//! These are the `Batcher` / `Builder` / `Spine` to hand to [`arrange_core`](crate::operators::arrange::arrangement::arrange_core), along with a chunker that
19//! forms `C` from the input stream — typically [`ContainerChunker<C>`](crate::batcher::merge::chunker::ContainerChunker).
20//! These aliases use [`ChunkBatch`], [`ChunkMerger`], [`ChunkBatchMerger`], and [`ChunkBatchBuilder`] for batch formation and trace maintenance.
21//! The [`vec`](mod@vec) module provides a worked implementation and specializes the aliases for its chunk type.
22//! The `chunks` example builds an arrangement from it.
23//!
24//! Reading chunks through cursors is an optional capability provided by the [`cursor`] module.
25//!
26//! # Bounded footprint
27//!
28//! There is a `TARGET` associated constant that signals the intended chunk size.
29//! The constant should be chosen large enough to amortize overheads, but small enough that per-chunk work does not "stall" the system when invoked.
30//! The implementor is trusted to make a reasonable choice here.
31//!
32//! The [`Chunk::settle`] method "settles" sequences of chunks, and is called as chunks are no longer expected to be needed in the near future.
33//! The implementor should ensure the chunks are "graded", in that the sequence of chunks are all at most `TARGET` in size, any two in order sum to strictly more than `TARGET`.
34//! This is also an opportunity to compress data, or spill to disk or cloud storage.
35//!
36//! Producers settle their committed output as they go, keeping the active, unsettled chunk set small.
37//! Implementors must keep [`len`](Chunk::len) cheap even when a chunk's body is paged out.
38
39use std::collections::VecDeque;
40
41use timely::progress::Antichain;
42use timely::progress::frontier::AntichainRef;
43use crate::lattice::Lattice;
44use crate::trace::implementations::spine_fueled::SpineBatch;
45
46pub mod vec;
47pub mod cursor;
48
49pub use cursor::{ChunkBatchCursor, NavigableChunk};
50
51/// A non-empty, bounded, consolidated, sorted sequence of `(data, time, diff)`.
52///
53/// An implementor gains access to types and trait implementations that provide
54/// batch formation and trace maintenance with no additional effort.
55///
56/// The necessary implementations are either "data" or "metadata" operations.
57/// The "data" operations transform lists of chunks, are expected to do roughly
58/// "one chunk's worth" of work at a time; they can afford to compress and page.
59/// The "metadata" operations provide chunk information, and should be lightweight.
60///
61/// The trait exposes only time, which trace maintenance needs.
62/// Reading a chunk's contents is a separate, optional capability: see the [`cursor`] module.
63pub trait Chunk: Sized + Clone {
64 /// The timestamp type of the chunk's updates.
65 ///
66 /// Trace maintenance uses time to describe intervals and to advance and compact updates.
67 type Time: Lattice + timely::progress::Timestamp;
68
69 /// The intended maximum chunk size.
70 const TARGET: usize;
71
72 /// The number of updates in the chunk.
73 fn len(&self) -> usize;
74
75 /// Merge the fronts of two input deques through their shared horizon.
76 ///
77 /// Both deques are non-empty (the caller guarantees it). The two queues are both
78 /// the heads of lists of chunks, and the implementor should only merge through the
79 /// least last `(key, val, time)` update, or risk emitting an unconsolidated
80 /// output chunk.
81 ///
82 /// When a chunk cannot be completely retired, perhaps it had the larger last update,
83 /// it should be rewritten as a new chunk and pushed back to the front of the queue.
84 /// The invocation is expected to consume at least one of its inputs, and the harness
85 /// may continually re-invoke if this doesn't happen.
86 ///
87 /// A merge concludes when the harness sees that either input is now empty, at which
88 /// point it appends the queue to the output without the method's assistance.
89 fn merge(in1: &mut VecDeque<Self>, in2: &mut VecDeque<Self>, out: &mut VecDeque<Self>);
90
91 /// Partition `input` updates into `keep` (greater or equal `frontier`) or not (`ship`).
92 ///
93 /// An implementation should yield with some frequency to allow the output to "settle".
94 /// The harness may guard against this, but it prefers to provide as much context as it
95 /// can in order to allow broader chunk fusion where needed.
96 fn extract(
97 input: &mut VecDeque<Self>,
98 frontier: AntichainRef<Self::Time>,
99 residual: &mut Antichain<Self::Time>,
100 keep: &mut VecDeque<Self>,
101 ship: &mut VecDeque<Self>,
102 );
103
104 /// Advance times by `frontier` producing consolidated chunks.
105 ///
106 /// An output for `(key, val)` should generally not be produced until a later pair
107 /// is observed, or `done` is set, to ensure the output chunks are consolidated.
108 /// Incomplete work can be pushed back to the front of `input`.
109 ///
110 /// On `done` a single `(key, val)` group may span the whole input; advancing and
111 /// consolidating it should cost time linear in its size, not quadratic.
112 fn advance(
113 input: &mut VecDeque<Self>,
114 frontier: AntichainRef<Self::Time>,
115 done: bool,
116 out: &mut VecDeque<Self>,
117 );
118
119 /// Reshape `input` to a sequence that maintains the "grading" structural invariant.
120 ///
121 /// Specifically, the chunks in `output` should have a maximum size of `TARGET` and
122 /// each adjacent pair should have lengths that sum to more than `TARGET`.
123 /// This is also a good moment to consider compression or paging out the contents.
124 /// When `done` is set the input must be moved to the output.
125 ///
126 /// This method may be called on already settled data, and should be efficient then.
127 ///
128 /// Implementors that want the standard maximal packing can delegate to the
129 /// [`pack`] helper, supplying their layout's coalesce / split / commit closures.
130 fn settle(input: &mut VecDeque<Self>, done: bool, out: &mut VecDeque<Self>);
131
132}
133
134/// Maximal-packing driver an implementor's [`Chunk::settle`] may delegate to.
135///
136/// Holds a `carry` run of chunks, grown until it reaches `TARGET` (then emitted as
137/// one chunk) and emitted early when the next chunk can't be absorbed without
138/// exceeding `TARGET`; over-sized chunks are peeled with `split`. Each committed
139/// chunk is passed through `seal` (the compress / spill hook — use the identity
140/// closure when there's nothing to do). The closures are the only layout-specific
141/// pieces:
142///
143/// * `combine(run)` — one chunk from a run of at least two (caller guarantees their
144/// lengths sum to at most `TARGET`, and that they form one sorted, consolidated
145/// chain). A run is combined once, when emitted, so packing stays linear even
146/// when combining copies its inputs.
147/// * `split(chunk, n)` — the first `n` updates and the remaining `len - n`.
148/// * `seal(chunk)` — commit a chunk (e.g. compress or spill); identity to keep it.
149pub fn pack<C: Chunk>(
150 input: &mut VecDeque<C>,
151 done: bool,
152 out: &mut VecDeque<C>,
153 mut combine: impl FnMut(Vec<C>) -> C,
154 mut split: impl FnMut(C, usize) -> (C, C),
155 mut seal: impl FnMut(C) -> C,
156) {
157 let (mut carry, mut carried) = (Vec::new(), 0);
158 let mut fuse = |carry: &mut Vec<C>| if carry.len() == 1 { carry.pop().unwrap() } else { combine(std::mem::take(carry)) };
159 while let Some(chunk) = input.pop_front() {
160 if carried + chunk.len() <= C::TARGET {
161 // Combines into one legal chunk; absorb it.
162 carried += chunk.len();
163 carry.push(chunk);
164 if carried == C::TARGET { out.push_back(seal(fuse(&mut carry))); carried = 0; }
165 } else if !carry.is_empty() {
166 // The carry is maximal against this neighbour; emit it and absorb afresh.
167 out.push_back(seal(fuse(&mut carry)));
168 carried = 0;
169 input.push_front(chunk);
170 } else {
171 // Peel `TARGET`-sized pieces off an over-sized chunk and absorb the remainder.
172 let mut rest = chunk;
173 while rest.len() > C::TARGET {
174 let (head, tail) = split(rest, C::TARGET);
175 out.push_back(seal(head));
176 rest = tail;
177 }
178 input.push_front(rest);
179 }
180 }
181 if done {
182 if !carry.is_empty() { out.push_back(seal(fuse(&mut carry))); }
183 } else {
184 // Hand the run back uncombined, to be combined once when it is emitted.
185 for chunk in carry.into_iter().rev() { input.push_front(chunk); }
186 }
187}
188
189/// A batch: an ordered [`Chunk`] sequence whose concatenation is its updates.
190pub struct ChunkBatch<C: Chunk> {
191 /// Ordered, consolidated chunks; their concatenation is the batch.
192 pub chunks: Vec<C>,
193}
194
195impl<C: Chunk> ChunkBatch<C> {
196 /// Assemble a batch from ordered chunks.
197 pub fn new(chunks: Vec<C>) -> Self {
198 for chunk in &chunks {
199 assert!(chunk.len() > 0, "ChunkBatch chunks must be non-empty");
200 }
201 ChunkBatch { chunks }
202 }
203}
204
205impl<C: Chunk + 'static> SpineBatch for ChunkBatch<C>
206where
207 C::Time: timely::progress::Timestamp + Lattice + Ord,
208{
209 type Time = C::Time;
210 type Merger = ChunkBatchMerger<C>;
211 fn len(&self) -> usize { self.chunks.iter().map(C::len).sum() }
212}
213
214/// A merge-batcher [`Merger`](crate::batcher::merge::Merger)
215/// over chains of [`Chunk`]s.
216///
217/// `merge` runs the whole-chain binary merger; `extract` splits by the seal frontier
218/// using [`Chunk::extract`]. The batcher consolidates equal `(data, time)` updates
219/// but does *not* advance times — time advancement is advance's job, handled later in
220/// the trace. Both settle their output, since the batcher's chains want to be graded.
221pub type ChunkBatcher<Chu, C> = crate::batcher::merge::MergeBatcher<Chu, ChunkMerger<C>, ChunkBuilder<C>>;
222
223/// A spine of `Rc`-shared [`ChunkBatch`]s of type `C`: the trace type for `arrange`.
224pub type ChunkSpine<C> = crate::trace::implementations::spine_fueled::Spine<std::rc::Rc<ChunkBatch<C>>>;
225
226/// A [`ChunkBatch`] builder over chunks of type `C`, emitting `Rc`-shared updates.
227pub type ChunkBuilder<C> = ChunkBatchBuilder<C>;
228
229/// A merge-batcher [`Merger`](crate::batcher::merge::Merger)
230/// over chains of [`Chunk`]s.
231///
232/// `merge` runs the whole-chain binary merger; `extract` splits by the seal frontier
233/// using [`Chunk::extract`]. The batcher consolidates equal `(data, time)` updates
234/// but does *not* advance times — time advancement is advance's job, handled later in
235/// the trace. Both settle their output, since the batcher's chains want to be graded.
236pub struct ChunkMerger<C> {
237 _marker: std::marker::PhantomData<C>,
238}
239
240impl<C> Default for ChunkMerger<C> {
241 fn default() -> Self { Self { _marker: std::marker::PhantomData } }
242}
243
244impl<C> crate::batcher::merge::Merger for ChunkMerger<C>
245where
246 C: Chunk + Default + 'static,
247 C::Time: Clone + timely::PartialOrder + 'static,
248{
249 type Chunk = C;
250
251 fn merge(
252 &mut self,
253 list1: Vec<C>,
254 list2: Vec<C>,
255 output: &mut Vec<C>,
256 _stash: &mut Vec<C>,
257 ) {
258 // Settle the output after each merge, to maintain bounded active chunks.
259 let mut in1: VecDeque<C> = list1.into();
260 let mut in2: VecDeque<C> = list2.into();
261 let (mut staged, mut settled) = (VecDeque::new(), VecDeque::new());
262 while !in1.is_empty() && !in2.is_empty() {
263 C::merge(&mut in1, &mut in2, &mut staged);
264 C::settle(&mut staged, false, &mut settled);
265 }
266 // Append the non-empty tail from either input, settle as we go.
267 for tail in in1.drain(..).chain(in2.drain(..)) {
268 staged.push_back(tail);
269 C::settle(&mut staged, false, &mut settled);
270 }
271 C::settle(&mut staged, true, &mut settled);
272 output.extend(settled);
273 }
274
275 fn len(chunk: &C) -> usize { chunk.len() }
276}
277
278impl<C> crate::batcher::merge::Extractor for ChunkMerger<C>
279where
280 C: Chunk + Default + 'static,
281 C::Time: Clone + timely::PartialOrder + 'static,
282{
283 type Time = C::Time;
284
285 fn extract(
286 &mut self,
287 merged: Vec<C>,
288 upper: AntichainRef<C::Time>,
289 frontier: &mut Antichain<C::Time>,
290 ship: &mut Vec<C>,
291 kept: &mut Vec<C>,
292 _stash: &mut Vec<C>,
293 ) {
294 // `extract` keeps updates greater-or-equal `upper` and ships the rest, folding
295 // the lower envelope of kept times into `frontier`. Drive it a bounded amount
296 // per call (≈ one input chunk) and `settle` each side as it accumulates, so
297 // neither `keep` (retained across yields) nor `ship` (handed to the builder)
298 // builds up unsettled in core. `settle` may withhold a sub-`TARGET` carry
299 // between calls; the final `settle(done)` flushes it.
300 let mut input: VecDeque<C> = merged.into();
301 let (mut keep, mut shipped) = (VecDeque::new(), VecDeque::new());
302 let (mut kept_q, mut shipped_q) = (VecDeque::new(), VecDeque::new());
303 while !input.is_empty() {
304 C::extract(&mut input, upper, frontier, &mut keep, &mut shipped);
305 C::settle(&mut keep, false, &mut kept_q);
306 C::settle(&mut shipped, false, &mut shipped_q);
307 }
308 C::settle(&mut keep, true, &mut kept_q);
309 C::settle(&mut shipped, true, &mut shipped_q);
310 kept.extend(kept_q);
311 ship.extend(shipped_q);
312 }
313}
314
315/// The resumable [`SpineBatch::Merger`] for [`ChunkBatch`]: merges two updates and advances
316/// their times to the compaction frontier, a fuel-bounded step at a time.
317///
318/// Each step pipelines [`merge`](Chunk::merge) → [`advance`](Chunk::advance) →
319/// [`settle`](Chunk::settle) and settles its output, so a suspended merge holds only
320/// graded chunks. The sources are read by cloning (a cheap refcount bump) and must be
321/// supplied unchanged on every call.
322pub struct ChunkBatchMerger<C: Chunk> {
323 /// Compaction frontier supplied at construction.
324 frontier: Antichain<C::Time>,
325 /// Input deques, refilled from the sources (clones) head-of-list at a time.
326 in1: VecDeque<C>,
327 in2: VecDeque<C>,
328 /// Next source chunk to clone into `in1` / `in2`.
329 idx1: usize,
330 idx2: usize,
331 /// `advance`'s input: the merge output plus advance's withheld carry at the front.
332 merged: VecDeque<C>,
333 /// `advance`'s output and `settle`'s input: merged-and-advanced chunks, with
334 /// settle's withheld sub-`TARGET` carry at the front.
335 advanced: VecDeque<C>,
336 /// `settle`'s output: the committed, graded result, grown by `work`. Graded at
337 /// every yield, so a suspended merge holds well-formed (spillable) chunk state.
338 settled: VecDeque<C>,
339 /// Set once both sources are drained and advance's and settle's final flushes ran.
340 complete: bool,
341}
342
343impl<C> crate::trace::implementations::spine_fueled::Merger<ChunkBatch<C>> for ChunkBatchMerger<C>
344where
345 C: Chunk + 'static,
346 C::Time: timely::progress::Timestamp + Lattice + Ord + 'static,
347{
348 fn new(_source1: &ChunkBatch<C>, _source2: &ChunkBatch<C>, frontier: AntichainRef<C::Time>) -> Self {
349 Self {
350 frontier: frontier.to_owned(),
351 in1: VecDeque::new(),
352 in2: VecDeque::new(),
353 idx1: 0,
354 idx2: 0,
355 merged: VecDeque::new(),
356 advanced: VecDeque::new(),
357 settled: VecDeque::new(),
358 complete: false,
359 }
360 }
361
362 fn work(&mut self, source1: &ChunkBatch<C>, source2: &ChunkBatch<C>, fuel: &mut isize) {
363
364 // TODO: The logic is a bit tortured here, and should be improved.
365
366 if self.complete { return; }
367
368 while *fuel > 0 {
369 // Refill each input deque up to a burst of source chunks (clones).
370 // The constant trades away fuel precision for overhead amortization.
371 const BURST: usize = 8;
372 while self.in1.len() < BURST && self.idx1 < source1.chunks.len() {
373 self.in1.push_back(source1.chunks[self.idx1].clone());
374 self.idx1 += 1;
375 }
376 while self.in2.len() < BURST && self.idx2 < source2.chunks.len() {
377 self.in2.push_back(source2.chunks[self.idx2].clone());
378 self.idx2 += 1;
379 }
380
381 // Merge's per-tick output (a burst's worth, or one tail chunk), measured
382 // for fuel before it joins the carry already in `merged`.
383 let mut produced = VecDeque::new();
384 if !self.in1.is_empty() && !self.in2.is_empty() {
385 // Both sides have data: drain the loaded burst.
386 C::merge(&mut self.in1, &mut self.in2, &mut produced);
387 } else if let Some(chunk) = self.in1.pop_front().or_else(|| self.in2.pop_front()) {
388 // Exactly one side has data: flush its verbatim tail, one chunk a step.
389 produced.push_back(chunk);
390 } else {
391 // Both sources drained: final flush of advance's and settle's carries.
392 C::advance(&mut self.merged, self.frontier.borrow(), true, &mut self.advanced);
393 C::settle(&mut self.advanced, true, &mut self.settled);
394 self.complete = true;
395 break;
396 }
397
398 let work: usize = produced.iter().map(C::len).sum();
399 self.merged.extend(produced);
400 C::advance(&mut self.merged, self.frontier.borrow(), false, &mut self.advanced);
401 // Maintain grading at the yield boundary: this step may exhaust `fuel` and
402 // suspend with `advanced` held, and held chunk state must be graded.
403 C::settle(&mut self.advanced, false, &mut self.settled);
404 *fuel -= work as isize;
405 }
406 }
407
408 fn done(self) -> Option<ChunkBatch<C>> {
409 debug_assert!(self.merged.is_empty() && self.advanced.is_empty());
410 wrap(self.settled.into())
411 }
412}
413
414/// A [`Builder`](crate::trace::Builder) that collects a chunk sequence into a [`ChunkBatch`].
415pub struct ChunkBatchBuilder<C: Chunk> {
416 /// Pushed chunks awaiting settling; holds settle's sub-`TARGET` carry at the front.
417 input: VecDeque<C>,
418 /// The graded chunks emitted so far.
419 output: VecDeque<C>,
420}
421
422impl<C: Chunk> Default for ChunkBatchBuilder<C> {
423 fn default() -> Self {
424 Self { input: VecDeque::new(), output: VecDeque::new() }
425 }
426}
427
428impl<C> crate::trace::Builder for ChunkBatchBuilder<C>
429where
430 C: Chunk + Default + 'static,
431 C::Time: timely::progress::Timestamp,
432{
433 type Input = C;
434 type Time = C::Time;
435 type Output = ChunkBatch<C>;
436
437 fn push(&mut self, chunk: &mut C) {
438 let chunk = std::mem::take(chunk);
439 if chunk.len() > 0 {
440 self.input.push_back(chunk);
441 C::settle(&mut self.input, false, &mut self.output);
442 }
443 }
444
445 fn done(self) -> Option<Self::Output> {
446 let ChunkBatchBuilder { mut input, mut output } = self;
447 C::settle(&mut input, true, &mut output);
448 let chunks: Vec<C> = output.into();
449 wrap(chunks)
450 }
451
452}
453
454impl<C> crate::batcher::merge::Sealer<C> for ChunkBatchBuilder<C>
455where
456 C: Chunk + Default + 'static,
457 C::Time: timely::progress::Timestamp,
458{
459 type Output = ChunkBatch<C>;
460
461 fn seal(chain: &mut Vec<C>) -> Option<Self::Output> {
462 // We settle the chain because we are not guaranteed to received pre-settled data.
463 // This should be efficient on pre-settled data.
464 wrap(settle_all(std::mem::take(chain)))
465 }
466}
467
468/// Wraps settled chunks as a batch, absent when there are no chunks.
469fn wrap<C: Chunk>(chunks: Vec<C>) -> Option<ChunkBatch<C>> {
470 (!chunks.is_empty()).then(|| ChunkBatch::new(chunks))
471}
472
473/// Whether `chunks` satisfy the [`Chunk::TARGET`] grading invariant: every chunk
474/// at most `TARGET`, and every adjacent pair summing to more than `TARGET` (so no
475/// two neighbours could be combined into one legal chunk — a *maximal packing*).
476///
477/// This is the post-[`settle`](Chunk::settle) shape; useful as a test/debug check.
478pub fn is_graded<C: Chunk>(chunks: &[C]) -> bool {
479 chunks.iter().all(|c| c.len() <= C::TARGET)
480 && chunks.windows(2).all(|w| w[0].len() + w[1].len() > C::TARGET)
481}
482
483/// Settle `input` to completion into a fresh graded `Vec` (see [`Chunk::settle`]).
484///
485/// A convenience for the one-shot callers (batch sealing, the batcher's merge and
486/// extract) that have a whole sequence in hand and want it graded; the streaming
487/// callers drive [`Chunk::settle`] directly across ticks.
488pub fn settle_all<C: Chunk>(input: impl IntoIterator<Item = C>) -> Vec<C> {
489 let mut input: VecDeque<C> = input.into_iter().collect();
490 let mut out = VecDeque::new();
491 C::settle(&mut input, true, &mut out);
492 debug_assert!(input.is_empty());
493 out.into()
494}
495
496/// Merge two full chains of chunks into one, to completion, appending to `out`.
497///
498/// The plain whole-chain driver: ticks [`Chunk::merge`] until one deque empties, then
499/// appends the other's remainder (the verbatim tail). Output is near-graded, not
500/// settled. The batcher's `merge` runs the same loop but settles after each push (the
501/// bounded-footprint discipline) and so does not use this; it stays as the simplest way
502/// to drive [`Chunk::merge`] to completion.
503pub fn merge_chains<C: Chunk>(
504 chain1: Vec<C>,
505 chain2: Vec<C>,
506 out: &mut VecDeque<C>,
507) {
508 let mut in1: VecDeque<C> = chain1.into();
509 let mut in2: VecDeque<C> = chain2.into();
510 while !in1.is_empty() && !in2.is_empty() {
511 C::merge(&mut in1, &mut in2, out);
512 }
513 // One deque is empty; the other's remainder is all greater than everything merged.
514 out.extend(in1.drain(..));
515 out.extend(in2.drain(..));
516}