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