Skip to main content

differential_dataflow/operators/
common.rs

1//! Types and methods generally useful for differential computation.
2//!
3//! The contents operate on `(id, time, diff)` update streams and lattice operations, with key
4//! and value representation left to the caller: replaying histories in time order with
5//! meet-advanced buffers ([`ValueHistory`], [`TimeHistory`]), determining the times at which a
6//! key's reduction must be re-evaluated ([`discover_times`]), producing the join of two
7//! histories in time order ([`bilinear_wave`]), and cutting an output interval into batch
8//! descriptions along held capabilities ([`tile_descriptions`]).
9//!
10//! Each item's correctness is a property of its own inputs and outputs, so they can be used
11//! and tested in isolation; sequencing obligations that span calls (for example, where
12//! interesting-time seeds must come from) are stated at the item that imposes them and are
13//! the caller's responsibility.
14
15use timely::progress::{Antichain, Timestamp};
16
17use crate::difference::{Multiply, Semigroup};
18use crate::lattice::Lattice;
19use crate::trace::Description;
20use crate::operators::reduce::sort_dedup;
21
22pub use crate::operators::ValueHistory;
23
24/// Replays a set of times in ascending order, maintaining the meet of the times not yet
25/// replayed and a deduplicated buffer of replayed times advanced by that meet. A cheaper
26/// [`ValueHistory`] for callers that need time structure only (no values, no cancellation).
27pub struct TimeHistory<T> {
28    /// Un-replayed `(time, meet)`, sorted descending by time so popping replays ascending;
29    /// `meet` is the meet of this time with all times later in the replay.
30    history: Vec<(T, T)>,
31    /// Stepped-in times, advanced and deduplicated, sorted ascending.
32    buffer: Vec<T>,
33}
34
35impl<T> Default for TimeHistory<T> {
36    fn default() -> Self { TimeHistory { history: Vec::new(), buffer: Vec::new() } }
37}
38
39impl<T: Lattice + Clone + Ord> TimeHistory<T> {
40    /// An empty history, to be `load`ed.
41    pub fn new() -> Self { TimeHistory { history: Vec::new(), buffer: Vec::new() } }
42
43    /// Load `times`, advancing each by `advance_by` if supplied, and organize the replay
44    /// (sort + suffix meets).
45    pub fn load(&mut self, times: impl Iterator<Item = T>, advance_by: Option<&T>) {
46        self.history.clear();
47        self.buffer.clear();
48        for mut time in times {
49            if let Some(m) = advance_by {
50                time = time.join(m);
51            }
52            self.history.push((time.clone(), time));
53        }
54        self.history.sort_by(|x, y| y.0.cmp(&x.0));
55        self.history.iter_mut().reduce(|prev, cur| {
56            cur.1.meet_assign(&prev.1);
57            cur
58        });
59    }
60
61    /// The next (least) un-replayed time.
62    pub fn time(&self) -> Option<&T> {
63        self.history.last().map(|x| &x.0)
64    }
65    /// The meet of all un-replayed times.
66    pub fn meet(&self) -> Option<&T> {
67        self.history.last().map(|x| &x.1)
68    }
69
70    /// Step times while the next equals `time`; true iff any did.
71    pub fn step_while_time_is(&mut self, time: &T) -> bool {
72        let mut found = false;
73        while self.time() == Some(time) {
74            found = true;
75            let (t, _) = self.history.pop().unwrap();
76            self.buffer.push(t);
77        }
78        found
79    }
80
81    /// Advance buffered times by `meet` and deduplicate — the collapse that keeps replay
82    /// linear.
83    pub fn advance_buffer_by(&mut self, meet: &T) {
84        for time in self.buffer.iter_mut() {
85            *time = time.join(meet);
86        }
87        self.buffer.sort();
88        self.buffer.dedup();
89    }
90
91    /// The buffered (stepped-in, advanced) times.
92    pub fn buffer(&self) -> &[T] {
93        &self.buffer
94    }
95}
96
97/// Produces the join of two histories: every pair of edits, diffs multiplied and times
98/// joined, visited in time order. Repeatedly steps the history with the earlier un-replayed
99/// edit and multiplies it against the other's buffer, which is consolidated under the meet of
100/// its remaining times as the wave advances — so work is bounded by the netted accumulation
101/// sizes rather than the raw history lengths.
102///
103/// `emit` receives every produced `(id0, id1, joined time, multiplied diff)`. Both histories
104/// must be pre-loaded (`load`/`load_iter`) and are fully drained. For small histories a plain
105/// cross product is cheaper; callers should gate on size.
106pub fn bilinear_wave<V, T, R0, R1, RO>(
107    h0: &mut ValueHistory<V, T, R0>,
108    h1: &mut ValueHistory<V, T, R1>,
109    mut emit: impl FnMut(V, V, T, RO),
110) where
111    V: Copy + Ord,
112    T: Ord + Clone + Lattice,
113    R0: Semigroup + Multiply<R1, Output = RO> + Clone,
114    R1: Semigroup + Clone,
115{
116    while h0.time().is_some() && h1.time().is_some() {
117        if h0.time().unwrap() < h1.time().unwrap() {
118            h1.advance_buffer_by(h0.meet().unwrap());
119            let (v0, t0, d0) = h0.edit().unwrap();
120            for ((v1, t1), d1) in h1.buffer() {
121                emit(v0, *v1, t0.join(t1), d0.clone().multiply(d1));
122            }
123            h0.step();
124        } else {
125            h0.advance_buffer_by(h1.meet().unwrap());
126            let (v1, t1, d1) = h1.edit().unwrap();
127            for ((v0, t0), d0) in h0.buffer() {
128                emit(*v0, v1, t0.join(t1), d0.clone().multiply(d1));
129            }
130            h1.step();
131        }
132    }
133    while h0.time().is_some() {
134        h1.advance_buffer_by(h0.meet().unwrap());
135        let (v0, t0, d0) = h0.edit().unwrap();
136        for ((v1, t1), d1) in h1.buffer() {
137            emit(v0, *v1, t0.join(t1), d0.clone().multiply(d1));
138        }
139        h0.step();
140    }
141    while h1.time().is_some() {
142        h0.advance_buffer_by(h1.meet().unwrap());
143        let (v1, t1, d1) = h1.edit().unwrap();
144        for ((v0, t0), d0) in h0.buffer() {
145            emit(*v0, v1, t0.join(t1), d0.clone().multiply(d1));
146        }
147        h1.step();
148    }
149}
150
151/// Cuts the interval `[lower, upper)` into consecutive batch descriptions along `held`, which
152/// must be sorted: the `i`-th cut point is the frontier formed by inserting `held[i+1..]` into
153/// `upper`, so description `i` covers the part of the interval not greater-or-equal any held
154/// time after `held[i]` (and not covered by an earlier description). Descriptions whose
155/// interval is empty are skipped. Returns the descriptions, the held time associated with
156/// each, and, per held index, the index of its description (`None` if skipped). A batch built
157/// to description `i` can be committed at the capability `held[i]`.
158pub fn tile_descriptions<T: Timestamp + Lattice>(
159    lower: &Antichain<T>,
160    upper: &Antichain<T>,
161    held: &[T],
162) -> (Vec<Description<T>>, Vec<T>, Vec<Option<usize>>) {
163    let mut tile_descs: Vec<Description<T>> = Vec::new();
164    let mut tile_held: Vec<T> = Vec::new();
165    let mut tile_of: Vec<Option<usize>> = vec![None; held.len()];
166    let mut out_lower = lower.clone();
167    for index in 0..held.len() {
168        let mut out_upper = upper.clone();
169        for t in &held[index + 1..] {
170            out_upper.insert(t.clone());
171        }
172        if out_upper != out_lower {
173            tile_of[index] = Some(tile_descs.len());
174            tile_descs.push(Description::new(out_lower.clone(), out_upper.clone(), Antichain::from_elem(T::minimum())));
175            tile_held.push(held[index].clone());
176            out_lower = out_upper;
177        }
178    }
179    (tile_descs, tile_held, tile_of)
180}
181
182/// A one-key view into an input presentation: the read-only arguments [`discover_times`] needs
183/// about a single key — its slice `[i0, i1)` of the merged `(id, time, diff)` run `p_in` and
184/// the carried `pending` times.
185pub struct KeyView<'a, T, RIn> {
186    /// The presented `((key_hash, value_id), time, diff)` run the key's records live in.
187    pub p_in: &'a [((u64, u64), T, RIn)],
188    /// The key's first record.
189    pub i0: usize,
190    /// One past the key's last record.
191    pub i1: usize,
192    /// Interesting times pended for this key by earlier retires.
193    pub pending: &'a [T],
194}
195
196/// Updates an optional meet by an optional time.
197fn update_meet<T: Lattice + Clone>(meet: &mut Option<T>, other: Option<&T>) {
198    if let Some(time) = other {
199        match meet.as_mut() {
200            Some(m) => m.meet_assign(time),
201            None => *meet = Some(time.clone()),
202        }
203    }
204}
205
206/// Reusable per-key scratch for [`discover_times`]: held once and threaded through every key,
207/// so replays and time buffers are cleared and refilled rather than reallocated per key.
208pub struct DiscoverScratch<T, RIn> {
209    batch_replay: TimeHistory<T>,
210    input_replay: ValueHistory<u64, T, RIn>,
211    output_replay: TimeHistory<T>,
212    synth: Vec<T>,
213    times_current: Vec<T>,
214    temporary: Vec<T>,
215    meets: Vec<T>,
216}
217
218impl<T: Timestamp + Lattice, RIn: Semigroup + Clone> DiscoverScratch<T, RIn> {
219    /// Fresh scratch; hold one per retire and thread it through every key.
220    pub fn new() -> Self {
221        DiscoverScratch {
222            batch_replay: TimeHistory::new(),
223            input_replay: ValueHistory::new(),
224            output_replay: TimeHistory::new(),
225            synth: Vec::new(),
226            times_current: Vec::new(),
227            temporary: Vec::new(),
228            meets: Vec::new(),
229        }
230    }
231}
232
233impl<T: Timestamp + Lattice, RIn: Semigroup + Clone> Default for DiscoverScratch<T, RIn> {
234    fn default() -> Self { Self::new() }
235}
236
237/// Determines the times in `[lower, upper)` at which a key's reduction must be re-evaluated
238/// (`moments`), and the times at or beyond `upper` to carry into the next invocation
239/// (`pended`). Replays the key's `seed_times` and `pending` times in ascending order, marking
240/// those that carry updates and closing the set under joins with the input and output
241/// histories' times and with each other. No input collection is materialized, so peak memory
242/// is O(times); buffers are advanced by the meet of the times still to come, keeping a key
243/// with many distinct times linear rather than quadratic.
244///
245/// `seed_times` must be the novel batch's own time support for this key. Seeding from a
246/// consolidated view is unsound: compaction may advance a history record onto a novel time,
247/// where consolidation cancels the novel update and its interesting time is missed.
248#[allow(clippy::too_many_arguments)]
249pub fn discover_times<T, RIn>(
250    key: KeyView<'_, T, RIn>,
251    seed_times: impl Iterator<Item = T>,
252    out_times: impl Iterator<Item = T>,
253    upper: &Antichain<T>,
254    scratch: &mut DiscoverScratch<T, RIn>,
255    moments: &mut Vec<T>,
256    pended: &mut Vec<T>,
257) where
258    T: Timestamp + Lattice,
259    RIn: Semigroup + Clone,
260{
261    // Reuse the retire's scratch: `load`/`load_iter` reset the replays (keeping capacity); the plain
262    // buffers are cleared here. `meets_slice` reborrows `meets` immutably; the rest stay disjoint.
263    let DiscoverScratch { batch_replay, input_replay, output_replay, synth, times_current, temporary, meets } = scratch;
264    synth.clear();
265    times_current.clear();
266    temporary.clear();
267
268    batch_replay.load(seed_times, None);
269
270    meets.clear();
271    meets.extend(key.pending.iter().cloned());
272    for i in (1..meets.len()).rev() {
273        let m = meets[i].clone();
274        meets[i - 1].meet_assign(&m);
275    }
276
277    let mut meet: Option<T> = None;
278    update_meet(&mut meet, meets.first());
279    update_meet(&mut meet, batch_replay.meet());
280
281    // The merged (history ⊎ novel) run — replayed for its TIMES only (join base), never
282    // accumulated. Output times likewise: base joins, never seeds.
283    input_replay.load_iter(
284        (key.i0..key.i1).map(|i| (key.p_in[i].0.1, key.p_in[i].1.clone(), key.p_in[i].2.clone())),
285        meet.as_ref(),
286    );
287    output_replay.load(out_times, meet.as_ref());
288
289    let mut times_slice = key.pending;
290    let mut meets_slice = &meets[..];
291
292    while let Some(next_time) = [batch_replay.time(), times_slice.first(), input_replay.time(), output_replay.time(), synth.last()]
293        .into_iter()
294        .flatten()
295        .min()
296        .cloned()
297    {
298        input_replay.step_while_time_is(&next_time);
299        output_replay.step_while_time_is(&next_time);
300        let mut interesting = batch_replay.step_while_time_is(&next_time);
301        if interesting {
302            if let Some(m) = meet.as_ref() {
303                batch_replay.advance_buffer_by(m);
304            }
305        }
306        while synth.last() == Some(&next_time) {
307            times_current.push(synth.pop().expect("nonempty"));
308            interesting = true;
309        }
310        while times_slice.first() == Some(&next_time) {
311            times_current.push(times_slice[0].clone());
312            times_slice = &times_slice[1..];
313            meets_slice = &meets_slice[1..];
314            interesting = true;
315        }
316        interesting = interesting || batch_replay.buffer().iter().any(|t| t.less_equal(&next_time));
317        interesting = interesting || times_current.iter().any(|t| t.less_equal(&next_time));
318
319        if !upper.less_equal(&next_time) {
320            if interesting {
321                // Synthesize joins against the input/output histories (times only — no
322                // accumulation), then record `next_time` as an interesting moment.
323                if let Some(m) = meet.as_ref() {
324                    input_replay.advance_buffer_by(m);
325                }
326                for ((_, t), _) in input_replay.buffer().iter() {
327                    if !t.less_equal(&next_time) {
328                        temporary.push(next_time.join(t));
329                    }
330                }
331                if let Some(m) = meet.as_ref() {
332                    output_replay.advance_buffer_by(m);
333                }
334                for t in output_replay.buffer().iter() {
335                    if !t.less_equal(&next_time) {
336                        temporary.push(next_time.join(t));
337                    }
338                }
339                moments.push(next_time.clone());
340            }
341            temporary.extend(batch_replay.buffer().iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
342            temporary.extend(times_current.iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
343            sort_dedup(temporary);
344            let synth_len = synth.len();
345            for time in temporary.drain(..) {
346                if upper.less_equal(&time) {
347                    pended.push(time);
348                } else {
349                    synth.push(time);
350                }
351            }
352            if synth.len() > synth_len {
353                synth.sort_by(|x, y| y.cmp(x));
354                synth.dedup();
355            }
356        } else if interesting {
357            pended.push(next_time.clone());
358        }
359
360        meet = None;
361        update_meet(&mut meet, batch_replay.meet());
362        update_meet(&mut meet, input_replay.meet());
363        update_meet(&mut meet, output_replay.meet());
364        for t in synth.iter() {
365            update_meet(&mut meet, Some(t));
366        }
367        update_meet(&mut meet, meets_slice.first());
368        if let Some(m) = meet.as_ref() {
369            for t in times_current.iter_mut() {
370                *t = t.join(m);
371            }
372        }
373        sort_dedup(times_current);
374    }
375    sort_dedup(pended);
376}