Skip to main content

differential_dataflow/operators/int_proxy/
vec_backend.rs

1//! A reference [`ProxyReduceBackend`] over [`VecChunk`] storage.
2//!
3//! This backend exists to demonstrate the backend recipe in plain `Vec`-of-rows form, and to give
4//! the proxy tactic a counterpart that can be tested and benchmarked against the cursor tactic on
5//! identical storage: both can drive a reduction over the same hash-keyed
6//! [`ChunkSpine`](crate::trace::chunk::vec::ChunkSpine) arrangement (see `tests/int_proxy.rs` and
7//! `tests/int_proxy_bench.rs`).
8//!
9//! # The recipe
10//!
11//! The wiring arranges `coll.map(|(k, v)| (k.hashed(), (k, v)))` into a `ChunkSpine` whose key is
12//! the `u64` key hash and whose value is the full `(key, val)` pair.
13//!
14//! Per window, the backend presents the input (the novel batches and the prior history, merged), the
15//! times of the novel records as seeds, and the prior output.
16//! Identifiers are ordinal positions minted by a single `MergedWalk` over the novel batches and the
17//! prior ones together, so a value carries ONE identifier whichever run it appears in and a novel
18//! retraction cancels against its own history. They are minted after the walk consolidates, so a value
19//! whose records all cancel never reaches the tactic and never spends an identifier.
20//!
21//! The backend clones keys and values like it is being paid to waste cycles.
22//! Generally, this is not a high-performance backend, but shouldn't be abysmal.
23
24use std::collections::{BTreeMap, HashMap};
25use std::rc::Rc;
26
27use timely::container::PushInto;
28use timely::progress::Timestamp;
29use timely::progress::frontier::AntichainRef;
30
31use crate::consolidation::{consolidate, consolidate_updates};
32use crate::difference::Semigroup;
33use crate::lattice::Lattice;
34use crate::trace::chunk::ChunkBatch;
35use crate::trace::chunk::vec::VecChunk;
36use crate::trace::Description;
37
38use super::{KeyPosition, ProxyReduceBackend, ReduceInstance, ReduceWindow};
39
40/// The batch type of a hash-keyed [`ChunkSpine`](crate::trace::chunk::vec::ChunkSpine): updates
41/// `D` is `(K, V)` on the input side and `(K, W)` on the output side.
42type VBatch<D, T, R> = Rc<ChunkBatch<VecChunk<u64, D, T, R>>>;
43
44/// A reference [`ProxyReduceBackend`] over `VBatch` storage.
45pub struct VecReduceBackend<K, V, W, T, R, L> {
46    /// User supplied reduce closure.
47    logic: L,
48    /// Configuration: records per window, bounding what the three presentations cost at once.
49    window_size: usize,
50
51    /// The current window's active keys, in ascending order: the input pass records them, and the
52    /// output pass replays them as its own active set.
53    keys_cache: Vec<u64>,
54    /// The current window's hashes carrying more than one real key, ascending.
55    collisions: Vec<u64>,
56    /// Per entry of `keys_cache`, the identifier of that key's first input value, if it had one.
57    reps: Vec<Option<u64>>,
58
59    /// Resolves an input value id (a per-window ordinal) to its `(key, val)` row.
60    in_pool: Vec<(K, V)>,
61    /// Resolves an output value id to its `(key, out)` row, for the current window only.
62    out_pool: Vec<(K, W)>,
63    /// Interns `(key, out)` rows to their output id, for the current window only.
64    /// Lookup-only: the non-determinism of the map's iteration order is never observed.
65    out_ids: HashMap<(K, W), u64>,
66
67    /// The chunks accumulated for the retire's output batch.
68    chunks: Vec<VecChunk<u64, (K, W), T, R>>,
69    /// Scratch to re-order one `emit`'s output by types, rather than transient identifiers.
70    stage: Vec<((u64, (K, W)), T, R)>,
71}
72
73impl<K, V, W, T, R, L> VecReduceBackend<K, V, W, T, R, L> {
74    /// A backend deferring value semantics to `logic`, covering the key space in windows.
75    pub fn new(logic: L) -> Self { Self::with_window(logic, 1 << 12) }
76
77    /// A backend with an explicit window budget, in presented records.
78    pub fn with_window(logic: L, window_size: usize) -> Self {
79        VecReduceBackend {
80            logic,
81            window_size: window_size.max(1),
82            keys_cache: Vec::new(),
83            collisions: Vec::new(),
84            reps: Vec::new(),
85            in_pool: Vec::new(),
86            out_pool: Vec::new(),
87            out_ids: HashMap::new(),
88            chunks: Vec::new(),
89            stage: Vec::new(),
90        }
91    }
92}
93
94impl<K, V, W, T, R, L> ProxyReduceBackend<T, VBatch<(K, V), T, R>, VBatch<(K, W), T, R>>
95    for VecReduceBackend<K, V, W, T, R, L>
96where
97    K: Ord + Clone + std::hash::Hash + 'static,
98    V: Ord + Clone + 'static,
99    W: Ord + Clone + std::hash::Hash + 'static,
100    T: Lattice + Timestamp,
101    R: Semigroup + Ord + Clone + 'static,
102    L: FnMut(&K, &[(V, R)], &mut Vec<(W, R)>, &mut Vec<(W, R)>),
103{
104    type Key = u64;
105    type VIn = u64;
106    type VOut = u64;
107    type RIn = R;
108    type ROut = R;
109
110    fn begin(&mut self, _description: Description<T>) {
111        self.chunks.clear();
112    }
113
114    #[inline(never)]
115    fn next_window(
116        &mut self,
117        instance: &ReduceInstance<'_, T, VBatch<(K, V), T, R>, VBatch<(K, W), T, R>>,
118        changed: &[u64],
119        from: &mut KeyPosition<u64>,
120        window: &mut ReduceWindow<T, R, R>,
121    ) {
122        let start = match *from { KeyPosition::Start => 0, KeyPosition::At(key) => key, KeyPosition::End => return };
123
124        // Populating the input bridge and the seeds.
125        self.in_pool.clear();
126        self.keys_cache.clear();
127        self.collisions.clear();
128        self.reps.clear();
129        let mut walk = MergedWalk::new(
130            instance.input_batches,
131            instance.source_batches,
132            changed,
133            start,
134            instance.lower,
135        );
136        let mut drawn: Vec<Drawn<(K, V), T, R>> = Vec::new();
137        let mut budget = self.window_size;
138        while let Some(key) = walk.advance(&mut drawn) {
139            self.keys_cache.push(key);
140            budget = budget.saturating_sub(drawn.len());
141            let rep = self.in_pool.len() as u64;
142            let (mut single, mut collides): (Option<&K>, bool) = (None, false);
143            let mut last: Option<&(K, V)> = None;
144            for ((data, time), diff) in drawn.drain(..) {
145                if last != Some(data) {
146                    match single {
147                        None => single = Some(&data.0),
148                        Some(only) => collides |= *only != data.0,
149                    }
150                    self.in_pool.push(data.clone());
151                    last = Some(data);
152                }
153                let id = (self.in_pool.len() - 1) as u64;
154                window.input.push(((key, id), time, diff));
155            }
156            window.seeds.extend(walk.seeds.drain(..).map(|t| (key, t)));
157            self.reps.push(single.map(|_| rep));
158            if collides { self.collisions.push(key); }
159            if budget == 0 { break; }
160        }
161        *from = walk.due().map_or(KeyPosition::End, KeyPosition::At);
162
163        // Populating the output bridge.
164        self.out_ids.clear();
165        self.out_pool.clear();
166        let mut owalk = MergedWalk::new(
167            &[],
168            instance.output_batches,
169            &self.keys_cache,
170            start,
171            instance.lower,
172        );
173        let mut odrawn: Vec<Drawn<(K, W), T, R>> = Vec::new();
174        let mut index = 0;
175        while let Some(key) = owalk.advance(&mut odrawn) {
176            debug_assert_eq!(self.keys_cache.get(index), Some(&key), "the output pass replays the input pass's keys");
177            let (mut single, mut collides): (Option<&K>, bool) = (None, false);
178            let mut last: Option<&(K, W)> = None;
179            for ((data, time), diff) in odrawn.drain(..) {
180                if last != Some(data) {
181                    match single {
182                        None => single = Some(&data.0),
183                        Some(only) => collides |= *only != data.0,
184                    }
185                    self.out_ids.insert(data.clone(), self.out_pool.len() as u64);
186                    self.out_pool.push(data.clone());
187                    last = Some(data);
188                }
189                let id = (self.out_pool.len() - 1) as u64;
190                window.output.push(((key, id), time, diff));
191            }
192            // The two passes agree or the hash collides. Neither can see this alone: a hash whose
193            // input has fully cancelled for one real key still carries that key's stale output.
194            if let (Some(out_key), Some(rep)) = (single, self.reps.get(index).copied().flatten()) {
195                collides |= self.in_pool[rep as usize].0 != *out_key;
196            }
197            if collides { self.collisions.push(key); }
198            index += 1;
199        }
200        // The two passes each contribute in ascending order, but one after the other.
201        self.collisions.sort_unstable();
202        self.collisions.dedup();
203    }
204
205    #[inline(never)]
206    fn reduce_corrections(
207        &mut self,
208        keys: &[u64],
209        in_ends: &[usize],
210        input: &[(u64, R)],
211        out_ends: &[usize],
212        output: &[(u64, R)],
213    ) -> (Vec<(u64, R)>, Vec<usize>) {
214        let mut corr: Vec<(u64, R)> = Vec::new();
215        let mut corr_ends: Vec<usize> = Vec::with_capacity(keys.len());
216        let (mut is, mut os) = (0usize, 0usize);
217        let mut updates: Vec<(W, R)> = Vec::new();
218        let mut input_vals: Vec<(V, R)> = Vec::new();
219        let mut current: Vec<(W, R)> = Vec::new();
220        for i in 0..keys.len() {
221            let (ie, oe) = (in_ends[i], out_ends[i]);
222            // No hash collision fast path, expected to be the most common case.
223            let collides = !self.collisions.is_empty() && self.collisions.binary_search(&keys[i]).is_ok();
224            let single_key: Option<K> = if collides { None } else {
225                input[is..ie].first().map(|(vid, _)| self.in_pool[*vid as usize].0.clone())
226                    .or_else(|| output[os..oe].first().map(|(vid, _)| self.out_pool[*vid as usize].0.clone()))
227            };
228            if let Some(key) = single_key {
229                input_vals.clear();
230                input_vals.extend(input[is..ie].iter().map(|(vid, d)| (self.in_pool[*vid as usize].1.clone(), d.clone())));
231                consolidate(&mut input_vals);
232                current.clear();
233                current.extend(output[os..oe].iter().map(|(vid, d)| (self.out_pool[*vid as usize].1.clone(), d.clone())));
234                consolidate(&mut current);
235                updates.clear();
236                (self.logic)(&key, &input_vals, &mut current, &mut updates);
237                consolidate(&mut updates);
238                for (w, d) in updates.drain(..) {
239                    let key_w = (key.clone(), w);
240                    let id = *self.out_ids.entry(key_w.clone()).or_insert_with(|| {
241                        self.out_pool.push(key_w);
242                        (self.out_pool.len() - 1) as u64
243                    });
244                    corr.push((id, d));
245                }
246            } else {
247                let mut ins: BTreeMap<K, Vec<(V, R)>> = BTreeMap::new();
248                for (vid, d) in &input[is..ie] {
249                    let (k, v) = &self.in_pool[*vid as usize];
250                    ins.entry(k.clone()).or_default().push((v.clone(), d.clone()));
251                }
252                let mut outs: BTreeMap<K, Vec<(W, R)>> = BTreeMap::new();
253                for (vid, d) in &output[os..oe] {
254                    let (k, w) = &self.out_pool[*vid as usize];
255                    outs.entry(k.clone()).or_default().push((w.clone(), d.clone()));
256                }
257                let mut real_keys: Vec<K> = ins.keys().chain(outs.keys()).cloned().collect();
258                real_keys.sort();
259                real_keys.dedup();
260                for key in real_keys {
261                    let mut ivals = ins.remove(&key).unwrap_or_default();
262                    consolidate(&mut ivals);
263                    let mut cur = outs.remove(&key).unwrap_or_default();
264                    consolidate(&mut cur);
265                    updates.clear();
266                    (self.logic)(&key, &ivals, &mut cur, &mut updates);
267                    consolidate(&mut updates);
268                    for (w, d) in updates.drain(..) {
269                        let key_w = (key.clone(), w);
270                        let id = *self.out_ids.entry(key_w.clone()).or_insert_with(|| {
271                            self.out_pool.push(key_w);
272                            (self.out_pool.len() - 1) as u64
273                        });
274                        corr.push((id, d));
275                    }
276                }
277            }
278            corr_ends.push(corr.len());
279            is = ie;
280            os = oe;
281        }
282        (corr, corr_ends)
283    }
284
285    #[inline(never)]
286    fn emit(&mut self, records: &[((u64, u64), T, R)]) {
287        self.stage.clear();
288        for ((h, vid), t, d) in records {
289            let row = self.out_pool[*vid as usize].clone();
290            self.stage.push(((*h, row), t.clone(), d.clone()));
291        }
292        // TODO: could consolidate only within a hash key, rather than the whole chunk.
293        consolidate_updates(&mut self.stage);
294        let chunks = &mut self.chunks;
295        for update in self.stage.drain(..) {
296            if chunks.last().is_none_or(|c| c.as_slice().len() >= <VecChunk<u64, (K, W), T, R> as crate::trace::chunk::Chunk>::TARGET) {
297                chunks.push(VecChunk::default());
298            }
299            chunks.last_mut().expect("pushed above if absent").push_into(update);
300        }
301    }
302
303    #[inline(never)]
304    fn finish(&mut self) -> Option<VBatch<(K, W), T, R>> {
305        self.in_pool.clear();
306        self.out_pool.clear();
307        self.out_ids.clear();
308        let chunks = std::mem::take(&mut self.chunks);
309        if chunks.is_empty() { None } else { Some(Rc::new(ChunkBatch::new(chunks))) }
310    }
311}
312
313/// One record drawn by [`MergedWalk::advance`], keyed for consolidation.
314///
315/// The key is `(data, time)`: grouping by data is what mints one identifier per distinct value.
316/// The data is borrowed from the batch — it is cloned once per surviving value, when its
317/// identifier is minted, and never per record.
318type Drawn<'a, D, T, R> = ((&'a D, T), R);
319
320/// The length of `slice`'s prefix of records whose hash is `key`, which must be non-empty.
321///
322/// Found by doubling rather than by bisecting the whole slice: a key's run within one chunk is
323/// usually a handful of records, and a binary search over the thousands that follow it would cost
324/// several times the run it is measuring.
325fn run_len<D, T, R>(slice: &[((u64, D), T, R)], key: u64) -> usize {
326    debug_assert!(slice.first().is_some_and(|r| r.0.0 == key), "the run must start at `key`");
327    let mut step = 1;
328    while step < slice.len() && slice[step].0.0 == key { step <<= 1; }
329    let lower = step >> 1;
330    let upper = step.min(slice.len());
331    lower + slice[lower..upper].partition_point(|r| r.0.0 <= key)
332}
333
334/// A resumable merge-walk over a list of batches, delivering one key at a time in `(hash, data,
335/// time)` order.
336///
337/// The list is logically one, with batches `[0, novel)` the retire's novel input and the rest its
338/// prior history. Only the novel batches and `changed` make a key ACTIVE. The prior batches are
339/// seeked to whatever key the active set names and are never walked in search of one, which is what
340/// keeps a window proportional to the work asked for rather than to the accumulated trace: on an
341/// iterative computation, where a retire asks for a scattered handful of keys, the difference is
342/// between presenting a window and re-reading the whole trace.
343///
344/// The state is a `(chunk, offset)` cursor per batch, an index into `changed`, and the next active
345/// hash. Nothing is materialized ahead of the walk, so a caller can stop at any key boundary and
346/// resume from the hash left in [`due`](Self::due) — which is exactly the windowing the backend owes
347/// [`ProxyReduceBackend::next_window`].
348struct MergedWalk<'a, D: Ord + Clone + 'static, T: Lattice + Timestamp, R: Semigroup + Ord + Clone + 'static> {
349    /// The novel batches, then the prior ones; [`batch`](Self::batch) reads the pair as one list.
350    novel: &'a [VBatch<D, T, R>],
351    prior: &'a [VBatch<D, T, R>],
352    /// Externally flagged keys, ascending: active whatever the batches hold.
353    changed: &'a [u64],
354    /// Cursor into `changed`.
355    ci: usize,
356    /// Per-batch `(chunk, offset)` cursor, in `batch` order.
357    pos: Vec<(usize, usize)>,
358    /// The least active hash at or above the cursors, or `None` once the key space is spent.
359    due: Option<u64>,
360    /// The compaction frontier prior records are advanced to as they are drawn. Advancing before
361    /// the sort is what lets the consolidation that follows collapse a long-but-quiet history to one
362    /// record per value: advancement is only useful because something merges the collisions it
363    /// creates. An empty frontier advances nothing.
364    frontier: AntichainRef<'a, T>,
365    /// The times of the novel records of the key last drawn, sorted and deduplicated: the
366    /// interesting-time seeds. They are recorded as drawn, before consolidation can net a novel
367    /// record against the history it changes.
368    seeds: Vec<T>,
369}
370
371impl<'a, D, T, R> MergedWalk<'a, D, T, R>
372where
373    D: Ord + Clone + 'static,
374    T: Lattice + Timestamp,
375    R: Semigroup + Ord + Clone + 'static,
376{
377    /// A walk over `novel ++ prior`, positioned at the first active key at or above `from`.
378    fn new(
379        novel: &'a [VBatch<D, T, R>],
380        prior: &'a [VBatch<D, T, R>],
381        changed: &'a [u64],
382        from: u64,
383        frontier: AntichainRef<'a, T>,
384    ) -> Self {
385        let count = novel.len() + prior.len();
386        let mut walk = MergedWalk {
387            novel,
388            prior,
389            changed,
390            ci: changed.partition_point(|k| *k < from),
391            pos: vec![(0, 0); count],
392            due: None,
393            frontier,
394            seeds: Vec::new(),
395        };
396        for b in 0..count {
397            walk.seek_to(b, from);
398        }
399        walk.refresh();
400        walk
401    }
402
403    /// The number of batches, novel and prior together.
404    fn count(&self) -> usize { self.novel.len() + self.prior.len() }
405
406    /// The `b`-th batch of the logical list.
407    fn batch(&self, b: usize) -> &'a ChunkBatch<VecChunk<u64, D, T, R>> {
408        if b < self.novel.len() { &self.novel[b] } else { &self.prior[b - self.novel.len()] }
409    }
410
411    /// The hash the `b`-th batch's cursor sits on, or `None` if it is drained.
412    fn head(&self, b: usize) -> Option<u64> {
413        let (chunk, offset) = self.pos[b];
414        self.batch(b).chunks.get(chunk).and_then(|c| c.as_slice().get(offset)).map(|r| r.0.0)
415    }
416
417    /// The least active hash at or above the cursors, or `None` once the key space is spent.
418    ///
419    /// A key is active if a novel batch holds a record for it or it appears in `changed`. Prior
420    /// history alone never activates a key: with no novel update and nothing due, its reduction has
421    /// the same input and the same output as it did last round, so there is nothing to reconcile.
422    fn due(&self) -> Option<u64> { self.due }
423
424    /// Recompute the next active hash from the novel heads and the `changed` cursor.
425    fn refresh(&mut self) {
426        let from_novel = (0..self.novel.len()).filter_map(|b| self.head(b)).min();
427        let from_changed = self.changed.get(self.ci).copied();
428        self.due = [from_novel, from_changed].into_iter().flatten().min();
429    }
430
431    /// Seek the `b`-th batch's cursor to the first record with hash at or above `key`.
432    ///
433    /// Both steps are binary searches: over the chunk last-keys, which ascend across a batch and so
434    /// form a sparse index that never touches a record, and then over the landed chunk's records.
435    fn seek_to(&mut self, b: usize, key: u64) {
436        // Already positioned, or drained. Keys are drawn in ascending order, so a cursor sitting at
437        // or above `key` is already at the FIRST record at or above it: everything it passed was at
438        // or below the previous key. This is the dense case — most keys, most batches — and paying
439        // two binary searches per batch per key there costs more than the whole walk.
440        if self.head(b).is_none_or(|h| h >= key) { return; }
441        let chunks = &self.batch(b).chunks;
442        let (mut chunk, mut offset) = self.pos[b];
443        let skip = chunks[chunk.min(chunks.len())..]
444            .partition_point(|c| c.as_slice().last().is_some_and(|r| r.0.0 < key));
445        if skip > 0 {
446            chunk += skip;
447            offset = 0;
448        }
449        if let Some(slice) = chunks.get(chunk).map(|c| c.as_slice()) {
450            offset += slice[offset..].partition_point(|r| r.0.0 < key);
451        }
452        self.pos[b] = (chunk, offset);
453    }
454
455    /// Draw the `b`-th batch's run of records for `key` into `into`, advancing its cursor past them.
456    ///
457    /// A key's run can straddle chunks, so this walks chunks until one ends the run; within each it
458    /// takes the whole run at once rather than a record at a time.
459    fn take_run(&mut self, b: usize, key: u64, into: &mut Vec<Drawn<'a, D, T, R>>) {
460        let novel = b < self.novel.len();
461        let chunks = &self.batch(b).chunks;
462        while let Some(slice) = chunks.get(self.pos[b].0).map(|c| c.as_slice()) {
463            let start = self.pos[b].1;
464            // The cursor is at or above `key`; if it is above, this batch holds nothing for it.
465            // Most batches hold nothing for most keys, so this is the common exit and it must cost
466            // one comparison rather than a search.
467            if slice.get(start).is_none_or(|r| r.0.0 != key) { return; }
468            let end = start + run_len(&slice[start..], key);
469            let run = &slice[start..end];
470            // Novel records are at or beyond the frontier, so only the prior history is advanced.
471            if novel {
472                self.seeds.extend(run.iter().map(|record| record.1.clone()));
473                into.extend(run.iter().map(|record| ((&record.0.1, record.1.clone()), record.2.clone())));
474            } else {
475                into.extend(run.iter().map(|record| {
476                    let mut time = record.1.clone();
477                    time.advance_by(self.frontier);
478                    ((&record.0.1, time), record.2.clone())
479                }));
480            }
481            if end < slice.len() {
482                self.pos[b].1 = end;
483                return;
484            }
485            self.pos[b] = (self.pos[b].0 + 1, 0);
486        }
487    }
488
489    /// Draw every record of the next active key, and advance to the one after it.
490    ///
491    /// Returns the key drawn, or `None` once the key space is spent. `into` is left sorted by
492    /// `(data, time)` and consolidated, so a value whose records all cancel is gone before the
493    /// caller ever sees it — and so never spends an identifier. The key's seeds are left in
494    /// [`seeds`](Self::seeds).
495    fn advance(&mut self, into: &mut Vec<Drawn<'a, D, T, R>>) -> Option<u64> {
496        let key = self.due?;
497        into.clear();
498        self.seeds.clear();
499        for b in 0..self.count() {
500            self.seek_to(b, key);
501            self.take_run(b, key, into);
502        }
503        consolidate(into);
504        self.seeds.sort();
505        self.seeds.dedup();
506        if self.changed.get(self.ci) == Some(&key) { self.ci += 1; }
507        self.refresh();
508        Some(key)
509    }
510}