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 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}