Skip to main content

differential_dataflow/trace/chunk/
vec.rs

1//! A worked [`Chunk`]: `Vec<((K, V), T, R)>` behind an `Rc`.
2//!
3//! The reference implementation. It shows the two integration points any `Chunk`
4//! satisfies; another layout copies this *shape*, not the `Vec`:
5//!
6//! * **Batcher side.** The chunker builds chunks through timely's container traits
7//!   (`Accountable`, `SizableContainer`, `Consolidate`, `PushInto`), which here
8//!   delegate to the inner `Vec` via `Rc::make_mut` (free while building, never
9//!   copying a shared batch).
10//! * **Trace side.** [`Chunk`] plus a cursor: key lookups gallop (logarithmic in
11//!   chunk size), stepping is linear.
12//!
13//! `Clone` is a refcount bump, so the trace merger shares source chunks rather than
14//! copying them.
15
16use std::collections::VecDeque;
17use std::marker::PhantomData;
18use std::rc::Rc;
19
20use timely::Accountable;
21use timely::container::{PushInto, SizableContainer};
22use timely::progress::{Antichain, Timestamp};
23use timely::progress::frontier::AntichainRef;
24
25use crate::consolidation::Consolidate;
26use crate::difference::Semigroup;
27use crate::lattice::Lattice;
28use crate::trace::Navigable;
29use crate::trace::cursor::Cursor;
30use crate::trace::implementations::{BatchContainer, Layout, Vector, WithLayout};
31
32use super::Chunk;
33
34/// The chunk size: the [`Chunk::TARGET`] value, also used for buffer sizing.
35const TARGET: usize = 8192;
36
37/// A sorted, consolidated run of `((key, val), time, diff)`, shared via `Rc`.
38pub struct VecChunk<K, V, T, R>(Rc<Vec<((K, V), T, R)>>);
39
40impl<K, V, T, R> Clone for VecChunk<K, V, T, R> {
41    fn clone(&self) -> Self { VecChunk(Rc::clone(&self.0)) }
42}
43impl<K, V, T, R> Default for VecChunk<K, V, T, R> {
44    fn default() -> Self { VecChunk(Rc::new(Vec::new())) }
45}
46
47/// The trace type for `arrange`: a spine of `Rc`-shared chunk batches.
48pub type ChunkSpine<K, V, T, R> = super::ChunkSpine<VecChunk<K, V, T, R>>;
49/// Merge batcher over `VecChunk`s; a `ContainerChunker<VecChunk>` at the
50/// `arrange_core` callsite forms the chunks it merges (via the container traits below).
51pub type ChunkBatcher<K, V, T, R> = super::ChunkBatcher<VecChunk<K, V, T, R>>;
52/// Batch builder.
53pub type ChunkBuilder<K, V, T, R> = super::ChunkBuilder<VecChunk<K, V, T, R>>;
54
55impl<K: 'static, V: 'static, T: 'static, R: 'static> Accountable for VecChunk<K, V, T, R> {
56    fn record_count(&self) -> i64 { self.0.len() as i64 }
57}
58
59impl<K, V, T, R> SizableContainer for VecChunk<K, V, T, R>
60where K: Clone+'static, V: Clone+'static, T: Clone+'static, R: Clone+'static {
61    // Absorb at `TARGET`, the grading size, so the chunker emits pre-graded chunks
62    // rather than timely's byte-derived ones.
63    fn at_capacity(&self) -> bool { self.0.len() >= TARGET }
64    fn ensure_capacity(&mut self, _stash: &mut Option<Self>) {
65        let inner = Rc::make_mut(&mut self.0);
66        inner.reserve(TARGET.saturating_sub(inner.len()));
67    }
68}
69
70impl<K, V, T, R> Consolidate for VecChunk<K, V, T, R>
71where K: Ord+Clone+'static, V: Ord+Clone+'static, T: Ord+Clone+'static, R: Semigroup+'static {
72    fn len(&self) -> usize { self.0.len() }
73    fn clear(&mut self) { Rc::make_mut(&mut self.0).clear() }
74    fn consolidate_into(&mut self, target: &mut Self) {
75        Rc::make_mut(&mut self.0).consolidate_into(Rc::make_mut(&mut target.0));
76    }
77}
78
79impl<K, V, T, R> PushInto<((K, V), T, R)> for VecChunk<K, V, T, R>
80where K: Clone+'static, V: Clone+'static, T: Clone+'static, R: Clone+'static {
81    fn push_into(&mut self, item: ((K, V), T, R)) { Rc::make_mut(&mut self.0).push(item); }
82}
83
84/// First index `>= start` at which `pred` turns false, by galloping (exponential)
85/// search. `pred` must hold for a prefix then not — i.e. `|u| u < target`.
86/// O(log distance), so O(1) for short hops and logarithmic for long ones.
87fn gallop<U>(s: &[U], start: usize, pred: impl Fn(&U) -> bool) -> usize {
88    let mut pos = start;
89    if pos < s.len() && pred(&s[pos]) {
90        let mut step = 1;
91        while pos + step < s.len() && pred(&s[pos + step]) { pos += step; step <<= 1; }
92        step >>= 1;
93        while step > 0 {
94            if pos + step < s.len() && pred(&s[pos + step]) { pos += step; }
95            step >>= 1;
96        }
97        pos += 1;
98    }
99    pos
100}
101
102/// A cursor over a [`VecChunk`], tracking the current key and `(key, val)`
103/// group starts as indices into the flat vector.
104pub struct VecChunkCursor<K, V, T, R> {
105    key_pos: usize,
106    val_pos: usize,
107    phantom: PhantomData<(K, V, T, R)>,
108}
109
110impl<K, V, T, R> WithLayout for VecChunk<K, V, T, R>
111where K: Ord+Clone+'static, V: Ord+Clone+'static, T: Lattice+Timestamp, R: Ord+Semigroup+'static {
112    type Layout = Vector<((K, V), T, R)>;
113}
114
115impl<K, V, T, R> WithLayout for VecChunkCursor<K, V, T, R>
116where K: Ord+Clone+'static, V: Ord+Clone+'static, T: Lattice+Timestamp, R: Ord+Semigroup+'static {
117    type Layout = Vector<((K, V), T, R)>;
118}
119
120impl<K, V, T, R> Cursor for VecChunkCursor<K, V, T, R>
121where K: Ord+Clone+'static, V: Ord+Clone+'static, T: Lattice+Timestamp, R: Ord+Semigroup+'static {
122    type Storage = VecChunk<K, V, T, R>;
123
124    type KeyContainer = <Vector<((K, V), T, R)> as Layout>::KeyContainer;
125    type Key<'a> = <<Vector<((K, V), T, R)> as Layout>::KeyContainer as BatchContainer>::ReadItem<'a>;
126    type ValContainer = <Vector<((K, V), T, R)> as Layout>::ValContainer;
127    type Val<'a> = <<Vector<((K, V), T, R)> as Layout>::ValContainer as BatchContainer>::ReadItem<'a>;
128    type ValOwn = <<Vector<((K, V), T, R)> as Layout>::ValContainer as BatchContainer>::Owned;
129    type TimeContainer = <Vector<((K, V), T, R)> as Layout>::TimeContainer;
130    type TimeGat<'a> = <<Vector<((K, V), T, R)> as Layout>::TimeContainer as BatchContainer>::ReadItem<'a>;
131    type Time = <<Vector<((K, V), T, R)> as Layout>::TimeContainer as BatchContainer>::Owned;
132    type DiffContainer = <Vector<((K, V), T, R)> as Layout>::DiffContainer;
133    type DiffGat<'a> = <<Vector<((K, V), T, R)> as Layout>::DiffContainer as BatchContainer>::ReadItem<'a>;
134    type Diff = <<Vector<((K, V), T, R)> as Layout>::DiffContainer as BatchContainer>::Owned;
135
136    fn key_valid(&self, s: &Self::Storage) -> bool { self.key_pos < s.0.len() }
137    fn val_valid(&self, s: &Self::Storage) -> bool {
138        self.key_pos < s.0.len() && self.val_pos < s.0.len() && s.0[self.val_pos].0.0 == s.0[self.key_pos].0.0
139    }
140    fn key<'a>(&self, s: &'a Self::Storage) -> &'a K { &s.0[self.key_pos].0.0 }
141    fn val<'a>(&self, s: &'a Self::Storage) -> &'a V { &s.0[self.val_pos].0.1 }
142    fn get_key<'a>(&self, s: &'a Self::Storage) -> Option<&'a K> {
143        if self.key_valid(s) { Some(self.key(s)) } else { None }
144    }
145    fn get_val<'a>(&self, s: &'a Self::Storage) -> Option<&'a V> {
146        if self.val_valid(s) { Some(self.val(s)) } else { None }
147    }
148    fn map_times<L: FnMut(&T, &R)>(&mut self, s: &Self::Storage, mut logic: L) {
149        if !self.val_valid(s) { return; }
150        let kv = &s.0[self.val_pos].0;
151        let mut i = self.val_pos;
152        while i < s.0.len() && &s.0[i].0 == kv {
153            logic(&s.0[i].1, &s.0[i].2);
154            i += 1;
155        }
156    }
157    fn step_key(&mut self, s: &Self::Storage) {
158        // Linear: stepping is a short hop to the next group; an inlined scan
159        // beats a gallop call for the common small-group case.
160        if self.key_pos >= s.0.len() { return; }
161        let key = s.0[self.key_pos].0.0.clone();
162        let mut i = self.key_pos;
163        while i < s.0.len() && s.0[i].0.0 == key { i += 1; }
164        self.key_pos = i;
165        self.val_pos = i;
166    }
167    fn seek_key(&mut self, s: &Self::Storage, key: &K) {
168        // Logarithmic: O(log distance), independent of chunk size.
169        self.key_pos = gallop(&s.0, self.key_pos, |u| &u.0.0 < key);
170        self.val_pos = self.key_pos;
171    }
172    fn step_val(&mut self, s: &Self::Storage) {
173        if !self.val_valid(s) { return; }
174        let kv = s.0[self.val_pos].0.clone();
175        let mut i = self.val_pos;
176        while i < s.0.len() && s.0[i].0 == kv { i += 1; }
177        self.val_pos = i;
178    }
179    fn seek_val(&mut self, s: &Self::Storage, val: &V) {
180        if !self.key_valid(s) { return; }
181        let key = s.0[self.key_pos].0.0.clone();
182        self.val_pos = gallop(&s.0, self.val_pos, |u| (&u.0.0, &u.0.1) < (&key, val));
183    }
184    fn rewind_keys(&mut self, _s: &Self::Storage) { self.key_pos = 0; self.val_pos = 0; }
185    fn rewind_vals(&mut self, _s: &Self::Storage) { self.val_pos = self.key_pos; }
186}
187
188/// Take the `Vec` out of a chunk, copying only if the `Rc` is shared.
189fn take<K: Clone, V: Clone, T: Clone, R: Clone>(chunk: VecChunk<K, V, T, R>) -> Vec<((K, V), T, R)> {
190    Rc::try_unwrap(chunk.0).unwrap_or_else(|rc| (*rc).clone())
191}
192
193impl<K, V, T, R> Navigable for VecChunk<K, V, T, R>
194where K: Ord+Clone+'static, V: Ord+Clone+'static, T: Lattice+Timestamp, R: Ord+Semigroup+'static {
195    type Cursor = VecChunkCursor<K, V, T, R>;
196
197    fn cursor(&self) -> Self::Cursor {
198        VecChunkCursor { key_pos: 0, val_pos: 0, phantom: PhantomData }
199    }
200}
201
202impl<K, V, T, R> super::NavigableChunk for VecChunk<K, V, T, R>
203where K: Ord+Clone+'static, V: Ord+Clone+'static, T: Lattice+Timestamp, R: Ord+Semigroup+'static {
204    fn bounds(&self) -> ((&K, &V, &T), (&K, &V, &T)) {
205        let s = &self.0[..];
206        let (f, l) = (&s[0], &s[s.len() - 1]);
207        ((&f.0.0, &f.0.1, &f.1), (&l.0.0, &l.0.1, &l.1))
208    }
209}
210
211impl<K, V, T, R> Chunk for VecChunk<K, V, T, R>
212where K: Ord+Clone+'static, V: Ord+Clone+'static, T: Lattice+Timestamp, R: Ord+Semigroup+'static {
213    type Time = <<Vector<((K, V), T, R)> as Layout>::TimeContainer as BatchContainer>::Owned;
214
215    const TARGET: usize = TARGET;
216
217    fn len(&self) -> usize { self.0.len() }
218
219    /// A two-pointer binary merge of the two deques' loaded content, up to their shared
220    /// horizon — the lesser of the two last `(key, val, time)`s. Consolidates equal
221    /// triples and bulk-copies disjoint runs as slices, walking chunk boundaries with
222    /// local indices. The horizon's owner drains fully; the other's partial front is
223    /// pruned and pushed back once, at the yield.
224    fn merge(in1: &mut VecDeque<Self>, in2: &mut VecDeque<Self>, out: &mut VecDeque<Self>) {
225        fn kv<K, V, T, R>(u: &((K, V), T, R)) -> (&K, &V) { (&u.0.0, &u.0.1) }
226
227        let mut result: Vec<((K, V), T, R)> = Vec::with_capacity(TARGET);
228        let mut flush = |result: &mut Vec<((K, V), T, R)>, force: bool| {
229            if result.len() >= TARGET || (force && !result.is_empty()) {
230                out.push_back(VecChunk(Rc::new(std::mem::replace(result, Vec::with_capacity(TARGET)))));
231            }
232        };
233
234        // Read working chunks by index (never `take`n, so a source clone stays shared).
235        // Both deques are non-empty on entry; the loop stops when one runs dry — its
236        // last triple is the horizon, and the rest waits for the next call.
237        let mut c1 = in1.pop_front().unwrap();
238        let mut c2 = in2.pop_front().unwrap();
239        let (mut p1, mut p2) = (0usize, 0usize);
240        while p1 < c1.0.len() && p2 < c2.0.len() {
241            let a = &c1.0[p1];
242            let b = &c2.0[p2];
243            match (kv(a), &a.1).cmp(&(kv(b), &b.1)) {
244                // Copy the run strictly below the other's head — no collisions there —
245                // as `TARGET`-sized slices.
246                std::cmp::Ordering::Less => {
247                    let run = gallop(&c1.0[..], p1 + 1, |u| (kv(u), &u.1) < (kv(b), &b.1));
248                    for piece in c1.0[p1..run].chunks(TARGET) {
249                        result.extend_from_slice(piece);
250                        flush(&mut result, false);
251                    }
252                    p1 = run;
253                }
254                std::cmp::Ordering::Greater => {
255                    let run = gallop(&c2.0[..], p2 + 1, |u| (kv(u), &u.1) < (kv(a), &a.1));
256                    for piece in c2.0[p2..run].chunks(TARGET) {
257                        result.extend_from_slice(piece);
258                        flush(&mut result, false);
259                    }
260                    p2 = run;
261                }
262                std::cmp::Ordering::Equal => {
263                    let mut diff = a.2.clone();
264                    diff.plus_equals(&b.2);
265                    if !diff.is_zero() {
266                        result.push((a.0.clone(), a.1.clone(), diff));
267                    }
268                    p1 += 1;
269                    p2 += 1;
270                    flush(&mut result, false);
271                }
272            }
273            // Refill a working chunk consumed above; if its deque is empty, stop.
274            if p1 == c1.0.len() {
275                match in1.pop_front() { Some(c) => { c1 = c; p1 = 0; } None => break }
276            }
277            if p2 == c2.0.len() {
278                match in2.pop_front() { Some(c) => { c2 = c; p2 = 0; } None => break }
279            }
280        }
281        flush(&mut result, true);
282        // Push back the survivor's unconsumed suffix (one copy), ahead of its
283        // remaining loaded chunks.
284        if p1 < c1.0.len() { in1.push_front(VecChunk(Rc::new(c1.0[p1..].to_vec()))); }
285        if p2 < c2.0.len() { in2.push_front(VecChunk(Rc::new(c2.0[p2..].to_vec()))); }
286    }
287
288    fn extract(
289        input: &mut VecDeque<Self>,
290        frontier: AntichainRef<T>,
291        residual: &mut Antichain<T>,
292        keep: &mut VecDeque<Self>,
293        ship: &mut VecDeque<Self>,
294    ) {
295        // One input chunk per call: partition it into a keep piece and a ship piece and
296        // return, so the harness settles each side before the next chunk is read. The
297        // pieces may be small; `settle` grades them.
298        let Some(chunk) = input.pop_front() else { return };
299        let (mut k, mut s) = (Vec::new(), Vec::new());
300        for u in take(chunk) {
301            if frontier.less_equal(&u.1) { residual.insert_ref(&u.1); k.push(u); }
302            else { s.push(u); }
303        }
304        if !k.is_empty() { keep.push_back(VecChunk(Rc::new(k))); }
305        if !s.is_empty() { ship.push_back(VecChunk(Rc::new(s))); }
306    }
307
308    fn advance(
309        input: &mut VecDeque<Self>,
310        frontier: AntichainRef<T>,
311        done: bool,
312        out: &mut VecDeque<Self>,
313    ) {
314        // Advance and consolidate each *complete* `(key, val)` group eagerly. Only the
315        // last group might still grow; unless `done`, withhold it (push it back as the
316        // carry) and emit the rest.
317        let mut stash: Vec<Vec<((K, V), T, R)>> = Vec::new();
318        // Reuse the front chunk's storage (last call's carry) as the working buffer and
319        // append the rest, so a withheld group accumulates in place: O(total) over the
320        // run, not O(total²).
321        let mut buf = match input.pop_front() { Some(chunk) => take(chunk), None => return };
322        while let Some(chunk) = input.pop_front() {
323            let mut v = take(chunk);
324            buf.append(&mut v);
325            stash.push(v);
326        }
327        if buf.is_empty() { return; }
328
329        // Giant-key case: if the whole buffer is one `(key, val)`, no group is provably
330        // complete, so unless `done` withhold it all and return. First-vs-last detects
331        // this without a scan.
332        if !done && buf[0].0 == buf[buf.len() - 1].0 {
333            input.push_front(VecChunk(Rc::new(buf)));
334            return;
335        }
336
337        // At least the first group is complete; withhold the last as the carry unless `done`.
338        let end = if done { buf.len() } else {
339            let last_kv = buf[buf.len() - 1].0.clone();
340            let mut start = buf.len();
341            while start > 0 && buf[start - 1].0 == last_kv { start -= 1; }
342            start
343        };
344        if end < buf.len() {
345            input.push_front(VecChunk(Rc::new(buf.split_off(end))));
346        }
347        // Advance and consolidate each group into `TARGET`-sized output chunks, filling
348        // buffers reclaimed from the recycled `Vec`s.
349        let mut result = stash.pop().unwrap_or_default();
350        let mut i = 0;
351        while i < buf.len() {
352            let mut j = i;
353            while j < buf.len() && buf[j].0 == buf[i].0 { j += 1; }
354            for u in &mut buf[i..j] { u.1.advance_by(frontier); }
355            // Advancing is lattice-monotone but not total-order-monotone; re-sort by time.
356            buf[i..j].sort_by(|a, b| a.1.cmp(&b.1));
357            let mut k = i;
358            while k < j {
359                let kv = buf[k].0.clone();
360                let t = buf[k].1.clone();
361                let mut diff = buf[k].2.clone();
362                k += 1;
363                while k < j && buf[k].1 == t { diff.plus_equals(&buf[k].2); k += 1; }
364                if !diff.is_zero() {
365                    result.push((kv, t, diff));
366                    if result.len() >= TARGET { out.push_back(VecChunk(Rc::new(std::mem::replace(&mut result, stash.pop().unwrap_or_default())))); }
367                }
368            }
369            i = j;
370        }
371        if !result.is_empty() { out.push_back(VecChunk(Rc::new(result))); }
372    }
373
374    /// Maximal packing via the harness [`pack`](super::pack): coalesce by
375    /// extending the inner `Vec` in place (`make_mut` is free while the carry's
376    /// `Rc` is unique, so packing a run of small chunks stays linear), split with
377    /// `split_off`, and seal as a no-op (`Vec` chunks are never paged).
378    fn settle(input: &mut VecDeque<Self>, done: bool, out: &mut VecDeque<Self>) {
379        super::pack(
380            input, done, out,
381            |acc, next| Rc::make_mut(&mut acc.0).extend(take(next)),
382            |chunk, n| { let mut rows = take(chunk); let rest = rows.split_off(n); (VecChunk(Rc::new(rows)), VecChunk(Rc::new(rest))) },
383            |chunk| chunk,
384        );
385    }
386}
387
388#[cfg(test)]
389mod test {
390    use std::collections::VecDeque;
391    use super::{Chunk, VecChunk};
392    use crate::trace::chunk::merge_chains;
393    use crate::trace::Navigable;
394    use std::rc::Rc;
395
396    fn chunk(updates: Vec<((u64, u64), u64, i64)>) -> VecChunk<u64, u64, u64, i64> {
397        VecChunk(Rc::new(updates))
398    }
399
400    // Flatten a chunk sequence back to its update stream.
401    fn flat<I: IntoIterator<Item = VecChunk<u64, u64, u64, i64>>>(chunks: I) -> Vec<((u64, u64), u64, i64)> {
402        chunks.into_iter().flat_map(|c| (*c.0).clone()).collect()
403    }
404
405    // `extract` partitions by frontier, a bounded amount per call, folding the kept
406    // frontier into `residual`; `settle` then fuses each side's pieces into a graded run.
407    #[test]
408    fn extract_partitions_and_grades() {
409        use super::TARGET;
410        use crate::trace::chunk::{is_graded, settle_all};
411        use timely::progress::Antichain;
412
413        // 4·TARGET updates spread over many input chunks; even times ship
414        // (< frontier), odd times keep (>= frontier), so both sides straddle.
415        let n = 4 * TARGET as u64;
416        let mut input: VecDeque<_> = (0..n).map(|i| chunk(vec![((i, 0), i % 2, 1)])).collect();
417        let frontier = Antichain::from_elem(1u64);
418        let mut residual = Antichain::new();
419        let (mut keep, mut ship) = (VecDeque::new(), VecDeque::new());
420        // Drive to completion, as the harness does (one input chunk per call).
421        while !input.is_empty() {
422            VecChunk::extract(&mut input, frontier.borrow(), &mut residual, &mut keep, &mut ship);
423        }
424        let (keep, ship) = (settle_all(keep), settle_all(ship));
425
426        // Kept times are exactly {1}; that is the residual frontier.
427        assert_eq!(residual, Antichain::from_elem(1u64));
428        // Both sides are graded after the settle.
429        assert!(is_graded(&keep), "ungraded keep: {:?}", keep.iter().map(Chunk::len).collect::<Vec<_>>());
430        assert!(is_graded(&ship), "ungraded ship: {:?}", ship.iter().map(Chunk::len).collect::<Vec<_>>());
431        // Nothing lost: half the updates each way.
432        assert_eq!(keep.iter().map(Chunk::len).sum::<usize>(), n as usize / 2);
433        assert_eq!(ship.iter().map(Chunk::len).sum::<usize>(), n as usize / 2);
434    }
435
436    // `advance` advances and consolidates complete `(key, val)` groups eagerly,
437    // pushing the (possibly-growing) last group back as the carry when not `done`.
438    #[test]
439    fn advance_emits_complete_groups_eagerly() {
440        use timely::progress::Antichain;
441
442        let frontier = Antichain::from_elem(5u64);
443        // Group (0,0) is complete within this chunk; group (1,0) might still grow.
444        let c0 = chunk(vec![((0, 0), 0, 1), ((0, 0), 1, 1), ((1, 0), 0, 1)]);
445        let mut input: VecDeque<_> = VecDeque::from([c0]);
446        let mut out = VecDeque::new();
447        VecChunk::advance(&mut input, frontier.borrow(), false, &mut out);
448
449        // The trailing group (1,0) is withheld as the carry at the front of `input`.
450        assert_eq!(input.len(), 1);
451        assert_eq!(Chunk::len(&input[0]), 1);
452        // Group (0,0)'s times {0,1} advanced to 5 and consolidated, emitted now.
453        assert_eq!(flat(out), vec![((0, 0), 5, 2)]);
454    }
455
456    // Streaming the input one chunk at a time must yield exactly what a single
457    // all-at-once flush does — the resumable path is just the one-shot path cut
458    // at group boundaries.
459    #[test]
460    fn advance_resumable_matches_oneshot() {
461        use timely::progress::Antichain;
462
463        let frontier = Antichain::from_elem(3u64);
464        // Groups span chunk boundaries and carry several times each.
465        let input = || vec![
466            chunk(vec![((0, 0), 0, 1), ((0, 0), 1, 1), ((1, 0), 0, 1)]),
467            chunk(vec![((1, 0), 5, 1), ((1, 1), 0, 1), ((2, 0), 0, 1)]),
468            chunk(vec![((2, 0), 2, 1), ((2, 0), 9, 1)]),
469        ];
470
471        let oneshot = {
472            let mut q: VecDeque<_> = input().into();
473            let mut out = VecDeque::new();
474            VecChunk::advance(&mut q, frontier.borrow(), false, &mut out);
475            VecChunk::advance(&mut q, frontier.borrow(), true, &mut out);
476            flat(out)
477        };
478        let incremental = {
479            let mut q = VecDeque::new();
480            let mut out = VecDeque::new();
481            for c in input() { q.push_back(c); VecChunk::advance(&mut q, frontier.borrow(), false, &mut out); }
482            VecChunk::advance(&mut q, frontier.borrow(), true, &mut out);
483            flat(out)
484        };
485        assert_eq!(oneshot, incremental);
486        // Times are advanced: nothing below the frontier survives.
487        for u in &oneshot { assert!(u.1 >= 3); }
488    }
489
490    // A single `(key, val)` whose updates span every pushed chunk: `advance`
491    // can make no progress until `done`, accumulating in the carry in place.
492    // It must still produce the right advanced+consolidated result at the end.
493    #[test]
494    fn advance_single_key_spanning_pushes() {
495        use timely::progress::Antichain;
496
497        let frontier = Antichain::from_elem(100u64);
498        let n = 50u64;
499        let make = || (0..n).map(|t| chunk(vec![((7u64, 0u64), t, 1i64)])).collect::<Vec<_>>();
500
501        let mut q = VecDeque::new();
502        let mut out = VecDeque::new();
503        for c in make() { q.push_back(c); VecChunk::advance(&mut q, frontier.borrow(), false, &mut out); }
504        VecChunk::advance(&mut q, frontier.borrow(), true, &mut out);
505        // All times advance to 100 and consolidate to one update of diff `n`.
506        assert_eq!(flat(out), vec![((7u64, 0u64), 100u64, n as i64)]);
507    }
508
509    #[test]
510    fn merge_chains_consolidates() {
511        let a = chunk(vec![((0, 0), 0, 1), ((1, 0), 0, 1)]);
512        let b = chunk(vec![((0, 0), 0, 1), ((2, 0), 0, 1)]);
513        let mut out = VecDeque::new();
514        merge_chains(vec![a], vec![b], &mut out);
515        assert_eq!(flat(out), vec![((0, 0), 0, 2), ((1, 0), 0, 1), ((2, 0), 0, 1)]);
516    }
517
518    // Merging runs larger than `TARGET`, then settling, yields a *graded* sequence
519    // (each chunk `<= TARGET`, adjacent pairs summing past `TARGET`) reproducing the
520    // consolidated sorted contents.
521    #[test]
522    fn merge_emits_graded_chunks() {
523        use super::TARGET;
524        use crate::trace::chunk::{is_graded, merge_chains, settle_all};
525
526        // Two interleaving single-chunk chains: evens and odds over `0..4·TARGET`.
527        let n = 4 * TARGET as u64;
528        let evens = chunk((0..n).step_by(2).map(|k| ((k, 0), 0, 1)).collect());
529        let odds = chunk((0..n).step_by(2).map(|k| ((k + 1, 0), 0, 1)).collect());
530
531        let mut out = VecDeque::new();
532        merge_chains(vec![evens], vec![odds], &mut out);
533        let chunks = settle_all(out);
534
535        assert!(is_graded(&chunks), "merge output not graded: {:?}",
536            chunks.iter().map(Chunk::len).collect::<Vec<_>>());
537        // Contents are exactly the sorted keys `0..4·TARGET`, each once.
538        let want: Vec<_> = (0..n).map(|k| ((k, 0u64), 0u64, 1i64)).collect();
539        assert_eq!(flat(chunks), want);
540    }
541
542    // Property test: merging two *multi-chunk* chains (driven through `merge` by
543    // `merge_chains`) reproduces the union of all updates, consolidated. Tiny
544    // chunks force `(key, val)` groups — which can span several times — to
545    // straddle chunk boundaries on both sides, exercising the refill path the
546    // single-chunk merge tests never reach. The independent oracle is
547    // `consolidate_updates` over the concatenation.
548    #[test]
549    fn merge_matches_reference() {
550        use crate::trace::chunk::merge_chains;
551        use crate::consolidation::consolidate_updates;
552
553        // Deterministic xorshift PRNG — no dev-dependency on `rand`.
554        let mut seed = 0x2545F4914F6CDD1Du64;
555        let mut rng = move || { seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17; seed };
556
557        // A sorted, consolidated update set over a small (key, val, time) space,
558        // so the two chains collide and a `(key, val)` carries several times.
559        fn gen(rng: &mut impl FnMut() -> u64, n: usize) -> Vec<((u64, u64), u64, i64)> {
560            let mut v: Vec<((u64, u64), u64, i64)> = (0..n).map(|_| {
561                let k = rng() % 20; let val = rng() % 3; let t = rng() % 8;
562                let d = if rng() % 4 == 0 { -1 } else { 1 };
563                ((k, val), t, d)
564            }).collect();
565            consolidate_updates(&mut v);
566            v
567        }
568        // Split a consolidated set into a chain of small chunks (each sorted and
569        // consolidated; together globally sorted), so groups straddle boundaries.
570        fn chain(updates: &[((u64, u64), u64, i64)], sz: usize) -> Vec<VecChunk<u64, u64, u64, i64>> {
571            updates.chunks(sz).map(|c| VecChunk(Rc::new(c.to_vec()))).collect()
572        }
573
574        for _ in 0..300 {
575            let n1 = (rng() as usize % 60) + 1;
576            let u1 = gen(&mut rng, n1);
577            let n2 = (rng() as usize % 60) + 1;
578            let u2 = gen(&mut rng, n2);
579            if u1.is_empty() || u2.is_empty() { continue; }
580            let sz = (rng() as usize % 5) + 1; // tiny chunks → heavy straddling
581
582            let mut out = VecDeque::new();
583            merge_chains(chain(&u1, sz), chain(&u2, sz), &mut out);
584            let merged = flat(out);
585
586            let mut reference: Vec<_> = u1.iter().chain(u2.iter()).cloned().collect();
587            consolidate_updates(&mut reference);
588
589            assert_eq!(merged, reference, "chunk size {sz}\n  u1={u1:?}\n  u2={u2:?}");
590        }
591    }
592
593    // Driving `ChunkBatchMerger` to completion with tiny `fuel` — so it suspends and
594    // settles on nearly every tick — must yield the same advanced-and-consolidated
595    // batch as a one-shot reference, and that batch must be graded. Exercises the
596    // resumable merge→advance→settle pipeline and the grade-at-yield invariant.
597    #[test]
598    fn batch_merger_resumable_matches_reference() {
599        use crate::trace::{Description, Merger};
600        use crate::trace::chunk::{ChunkBatch, ChunkBatchMerger, is_graded};
601        use crate::trace::cursor::Cursor;
602        use crate::consolidation::consolidate_updates;
603        use timely::progress::Antichain;
604
605        let mut seed = 0x9E3779B97F4A7C15u64;
606        let mut rng = move || { seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17; seed };
607
608        // A sorted, consolidated set over a small space, so the two sources collide
609        // and a `(key, val)` carries several times.
610        fn gen(rng: &mut impl FnMut() -> u64) -> Vec<((u64, u64), u64, i64)> {
611            let n = rng() as usize % 40 + 1;
612            let mut v: Vec<((u64, u64), u64, i64)> = (0..n).map(|_| {
613                let k = rng() % 10; let val = rng() % 3; let t = rng() % 6;
614                let d = if rng() % 4 == 0 { -1 } else { 1 };
615                ((k, val), t, d)
616            }).collect();
617            consolidate_updates(&mut v);
618            v
619        }
620        // Cut a consolidated set into a batch of small chunks, so groups straddle.
621        fn batch(updates: &[((u64, u64), u64, i64)], sz: usize) -> ChunkBatch<VecChunk<u64, u64, u64, i64>> {
622            let chunks: Vec<_> = updates.chunks(sz).map(|c| VecChunk(Rc::new(c.to_vec()))).collect();
623            let desc = Description::new(
624                Antichain::from_elem(0u64), Antichain::from_elem(10u64), Antichain::from_elem(0u64));
625            ChunkBatch::new(chunks, desc)
626        }
627        // Flatten a batch through its straddle-aware cursor, then consolidate.
628        fn read(b: &ChunkBatch<VecChunk<u64, u64, u64, i64>>) -> Vec<((u64, u64), u64, i64)> {
629            let mut out = Vec::new();
630            let mut c = b.cursor();
631            while c.key_valid(b) {
632                let k = *c.key(b);
633                while c.val_valid(b) {
634                    let v = *c.val(b);
635                    c.map_times(b, |t, d| out.push(((k, v), *t, *d)));
636                    c.step_val(b);
637                }
638                c.step_key(b);
639            }
640            consolidate_updates(&mut out);
641            out
642        }
643
644        for _ in 0..200 {
645            let u1 = gen(&mut rng);
646            let u2 = gen(&mut rng);
647            if u1.is_empty() || u2.is_empty() { continue; }
648            let sz = (rng() as usize % 4) + 1;
649            let f = rng() % 6;
650            let (s1, s2) = (batch(&u1, sz), batch(&u2, sz));
651            let frontier = Antichain::from_elem(f);
652
653            let mut merger = ChunkBatchMerger::new(&s1, &s2, frontier.borrow());
654            loop {
655                let mut fuel = 1isize; // tiny → many yields, each settling
656                merger.work(&s1, &s2, &mut fuel);
657                if fuel > 0 { break; }
658            }
659            let result = merger.done();
660
661            // The produced batch is graded (grade-at-yield, so also at done).
662            assert!(is_graded(&result.chunks), "ungraded result: {:?}",
663                result.chunks.iter().map(Chunk::len).collect::<Vec<_>>());
664            // ...and its contents are the merged sources, advanced to `f`, consolidated.
665            let got = read(&result);
666            let mut want: Vec<_> = u1.iter().chain(u2.iter()).cloned().collect();
667            for u in want.iter_mut() { u.1 = u.1.max(f); }
668            consolidate_updates(&mut want);
669            assert_eq!(got, want, "fuel-driven merge mismatch\n  u1={u1:?}\n  u2={u2:?}\n  f={f}");
670        }
671    }
672
673    // `settle` must produce a *maximal packing*: adjacent sub-`TARGET` chunks
674    // that could combine into one legal chunk are coalesced, full chunks pass
675    // through as `Rc` moves, and contents are preserved exactly.
676    #[test]
677    fn settle_maximal_packing() {
678        use super::TARGET;
679        use crate::trace::chunk::is_graded;
680
681        // A mix of small and full chunks with distinct, increasing keys (so the
682        // concatenation is sorted and nothing consolidates away).
683        let t = TARGET;
684        let sizes = [t / 3, t / 3, t / 3, t, t / 2, t / 2, t, 1, t - 1];
685        let total: usize = sizes.iter().sum();
686        let mut key = 0u64;
687        let mut input = VecDeque::new();
688        let mut output = VecDeque::new();
689        for &s in &sizes {
690            let updates: Vec<_> = (0..s).map(|_| { let k = key; key += 1; ((k, 0u64), 0u64, 1i64) }).collect();
691            input.push_back(chunk(updates));
692            VecChunk::settle(&mut input, false, &mut output);
693        }
694        VecChunk::settle(&mut input, true, &mut output);
695        let chunks: Vec<_> = output.into();
696
697        assert!(is_graded(&chunks), "not graded: {:?}",
698            chunks.iter().map(Chunk::len).collect::<Vec<_>>());
699        // Nothing lost, and the keys stay strictly sorted across the new breaks.
700        let got: Vec<_> = chunks.into_iter().flat_map(|c| (*c.0).clone()).collect();
701        assert_eq!(got.len(), total);
702        assert!(got.windows(2).all(|w| w[0].0.0 < w[1].0.0));
703    }
704
705    // The indexed cursor must reconstruct the same grouped updates as a flat
706    // reference, even when a key — and a `(key, val)`'s times — straddle a
707    // chunk boundary.
708    #[test]
709    fn cursor_handles_straddle() {
710        use crate::trace::cursor::Cursor;
711        use crate::trace::Description;
712        use crate::trace::chunk::ChunkBatch;
713        use timely::progress::Antichain;
714
715        let chunks = vec![
716            chunk(vec![((0, 0), 0, 1), ((1, 0), 0, 1), ((1, 1), 0, 1)]),
717            chunk(vec![((1, 1), 1, 1), ((1, 2), 0, 1)]),
718            chunk(vec![((2, 0), 0, 1)]),
719        ];
720        let desc = Description::new(
721            Antichain::from_elem(0u64),
722            Antichain::from_elem(2u64),
723            Antichain::from_elem(0u64),
724        );
725        let batch = ChunkBatch::new(chunks, desc);
726
727        let mut cursor = batch.cursor();
728        let got = cursor.to_vec(&batch, |k| *k, |v| *v);
729        let want = vec![
730            ((0u64, 0u64), vec![(0u64, 1i64)]),
731            ((1, 0), vec![(0, 1)]),
732            ((1, 1), vec![(0, 1), (1, 1)]),
733            ((1, 2), vec![(0, 1)]),
734            ((2, 0), vec![(0, 1)]),
735        ];
736        assert_eq!(got, want);
737    }
738
739    // Isolated: gallop vs linear forward-seek over one big chunk, for sparse to
740    // dense probe sets. Run: cargo test seek_microbench -- --ignored --nocapture
741    #[test]
742    #[ignore]
743    fn seek_microbench() {
744        use std::time::Instant;
745        use std::hint::black_box;
746        use super::gallop;
747        let n = 1_000_000u64;
748        let data: Vec<((u64, ()), u64, isize)> = (0..n).map(|k| ((3 * k, ()), 0u64, 1isize)).collect();
749        for probes in [100u64, 10_000, 1_000_000] {
750            let targets: Vec<u64> = (0..probes).map(|i| 3 * (i * n / probes)).collect();
751            let best = |f: &dyn Fn() -> u64| {
752                let mut b = std::time::Duration::MAX;
753                for _ in 0..5 { let t = Instant::now(); black_box(f()); b = b.min(t.elapsed()); }
754                b
755            };
756            let data = black_box(&data[..]);
757            let g = best(&|| {
758                let (mut pos, mut acc) = (0usize, 0u64);
759                for &tgt in &targets { pos = gallop(data, pos, |u| u.0.0 < tgt); acc += pos as u64; }
760                acc
761            });
762            let l = best(&|| {
763                let (mut pos, mut acc) = (0usize, 0u64);
764                for &tgt in &targets { while pos < data.len() && data[pos].0.0 < tgt { pos += 1; } acc += pos as u64; }
765                acc
766            });
767            eprintln!("probes={probes:>7}: gallop={g:>12?}  linear={l:>12?}");
768        }
769    }
770
771    // `seek_key` must land at the first key `>= target` regardless of where the cursor
772    // starts, so the galloping hint (and its backward-seek fallback) never changes the
773    // answer. Probe every (start, target) pair — forward and backward — against an
774    // analytic oracle.
775    #[test]
776    fn seek_key_hint_is_direction_independent() {
777        use crate::trace::cursor::Cursor;
778        use crate::trace::Description;
779        use crate::trace::chunk::ChunkBatch;
780        use timely::progress::Antichain;
781
782        // One key per chunk (even keys 0, 2, .., 38) so seeks cross boundaries both ways.
783        let chunks: Vec<_> = (0..20u64).map(|k| chunk(vec![((2 * k, 0u64), 0u64, 1i64)])).collect();
784        let desc = Description::new(
785            Antichain::from_elem(0u64), Antichain::from_elem(1u64), Antichain::from_elem(0u64));
786        let batch = ChunkBatch::new(chunks, desc);
787
788        // First key `>= tgt`: the next even at or above `tgt`, or absent past the last (38).
789        let oracle = |tgt: u64| { let e = tgt + (tgt & 1); (e <= 38).then_some(e) };
790        for start in 0..=40u64 {
791            for tgt in 0..=40u64 {
792                let mut c = batch.cursor();
793                c.seek_key(&batch, &start);
794                c.seek_key(&batch, &tgt);
795                assert_eq!(c.get_key(&batch).copied(), oracle(tgt), "start={start} tgt={tgt}");
796            }
797        }
798    }
799
800}