differential_dataflow/operators/int_proxy/reduce.rs
1//! The proxy reduce framework.
2//!
3//! A conventional differential reduce against ordered, copyable proxies.
4//! The backend supplies their interpretation.
5
6use super::pending::Pending;
7
8use timely::progress::{Antichain, Timestamp};
9use timely::progress::frontier::AntichainRef;
10
11use crate::difference::Semigroup;
12use crate::lattice::Lattice;
13use crate::trace::{Span, Description};
14use super::{KeyPosition, ProxyBridge};
15use crate::operators::reduce::{sort_dedup, ReduceTactic};
16use crate::operators::history::ValueHistory;
17
18/// A unit of proxied reduce work, presented to the backend.
19pub struct ReduceInstance<'a, T, B1, B2> {
20 /// The accumulated input history.
21 pub source_batches: &'a [B1],
22 /// The freshly arrived input delta.
23 pub input_batches: &'a [B1],
24 /// The accumulated output history.
25 pub output_batches: &'a [B2],
26 /// The compaction frontier for loading (the retire's lower bound).
27 pub lower: AntichainRef<'a, T>,
28}
29
30/// One window of the key space: the presentations a bounded, key-contiguous snip needs.
31///
32/// Seeds travel as times; records travel netted. The novel data's two roles are carried by two
33/// different channels: its TIME SUPPORT seeds interesting times and rides `seeds`, raw; its
34/// RECORDS are mere accumulants and join partners, so they ride `input` merged with the prior
35/// history, where they may net against it and be advanced like anything else. Consolidation can
36/// only cancel equal-`((key, id), time)` pairs, and such a time is necessarily in `seeds`, so no
37/// interesting time is lost to netting — the invariant that once forced the runs apart.
38///
39/// Owned by the harness and refilled by [`ProxyReduceBackend::next_window`].
40pub struct ReduceWindow<T, RIn, ROut, K = u64, VIn = u64, VOut = u64> {
41 /// The key's full input — novel and prior merged, netted — sorted & consolidated by `((key, value_id), time)`.
42 /// May be advanced to the compaction frontier.
43 pub input: ProxyBridge<T, RIn, K, VIn>,
44 /// The RAW novel time support: `(key, time)` pairs sorted by `(key, time)` and deduplicated.
45 /// Record these from the novel batches BEFORE any consolidation or advancement.
46 /// A netted-away record's time must still appear here.
47 pub seeds: Vec<(K, T)>,
48 /// Accumulated output preceding the retire's interval, same ordering as `input`.
49 pub output: ProxyBridge<T, ROut, K, VOut>,
50}
51
52impl<T, RIn, ROut, K, VIn, VOut> Default for ReduceWindow<T, RIn, ROut, K, VIn, VOut> {
53 fn default() -> Self { ReduceWindow { input: Vec::new(), seeds: Vec::new(), output: Vec::new() } }
54}
55
56impl<T, RIn, ROut, K, VIn, VOut> ReduceWindow<T, RIn, ROut, K, VIn, VOut> {
57 /// Clear the presentations, keeping their allocations.
58 pub fn clear(&mut self) {
59 self.input.clear();
60 self.seeds.clear();
61 self.output.clear();
62 }
63}
64
65/// The reduce backend: value semantics for a proxy-space reduction, driven by [`ProxyReduceTactic`].
66///
67/// The protocol for each round of invocation is
68/// `begin [ next_window reduce_corrections* emit ]* finish`,
69/// where the window loop runs until `next_window` reports the key space exhausted.
70pub trait ProxyReduceBackend<T, B1, B2> {
71 /// Independent groups, with identities stable across retires.
72 type Key: Copy + Ord;
73 /// Input value proxies, valid throughout a window.
74 type VIn: Copy + Ord;
75 /// Output value proxies, including newly produced values, valid throughout a window.
76 type VOut: Copy + Ord;
77 /// Diff type presented for the input.
78 type RIn: Semigroup;
79 /// Diff type of the output.
80 type ROut: Semigroup + 'static;
81
82 /// Initiate a session to create batches for these descriptions, which span `[lower, upper)`.
83 ///
84 /// It is the backend's job to prepare output batches for each of these descriptions.
85 /// The computation proceeds in windows of keys, where only the backend maintains this
86 /// work in progress, until `finish()` is called.
87 fn begin(&mut self, description: Description<T>);
88
89 /// Present the next window of the key space, and advance `from` past it.
90 ///
91 /// On entry `from` is `Start` or an inclusive `At(key)` lower bound on keys still to be covered.
92 /// The backend writes the window's exclusive upper bound as `At(key)`, or `End` when exhausted.
93 /// Each call must strictly advance `from`.
94 ///
95 /// The window must cover every key in `[from_before, from_after)` that carries a novel update or appears in `changed`.
96 /// For each key, present merged input (novel and prior together, netted), raw novel time support in `seeds`, and accumulated output.
97 /// A key must be reported entirely within the window that first mentions it: splitting one across windows drops the interaction between the halves.
98 /// `changed` is ascending; the harness reads no key outside the window's range, so a backend that keeps its own key order need not consult the whole space.
99 ///
100 /// `seeds` must be recorded from the novel batches before any consolidation or advancement:
101 /// a novel record that nets to zero against compacted history vanishes from `input`, but its
102 /// time must still seed — that cancellation is exactly the case that loses updates otherwise.
103 ///
104 /// The size of the window is up to the backend: large enough to amortize the crossings, small
105 /// enough that the presentations are affordable, as all are live at once.
106 fn next_window(
107 &mut self,
108 instance: &ReduceInstance<'_, T, B1, B2>,
109 changed: &[Self::Key],
110 from: &mut KeyPosition<Self::Key>,
111 window: &mut ReduceWindow<T, Self::RIn, Self::ROut, Self::Key, Self::VIn, Self::VOut>,
112 );
113
114 /// A wave of input-output reconciliation, in which the backend supplies necessary edits.
115 ///
116 /// Multiple keys are provided concurrently, for each an accumulated input and tentative output.
117 /// The backend should provide for each key the necessary output updates to bring the output in
118 /// with its desires. The `usize` integers upper bound the range for the corresponding key.
119 /// Each input and output bracket is sorted and consolidated by its value proxy.
120 fn reduce_corrections(
121 &mut self,
122 keys: &[Self::Key],
123 in_ends: &[usize],
124 input: &[(Self::VIn, Self::RIn)],
125 out_ends: &[usize],
126 output: &[(Self::VOut, Self::ROut)],
127 ) -> (Vec<(Self::VOut, Self::ROut)>, Vec<usize>);
128
129 /// Commit a collection of updates to the batch in progress.
130 fn emit(&mut self, records: &[((Self::Key, Self::VOut), T, Self::ROut)]);
131
132 /// Complete the session matching `begin`, yielding the batch it described,
133 /// or `None` when the span it described carries no updates.
134 fn finish(&mut self) -> Option<B2>;
135}
136
137/// A proxy-space [`ReduceTactic`]: matches input and output records by `key`.
138pub struct ProxyReduceTactic<T, Bk, K = u64> {
139 backend: Bk,
140 /// Maximum number of keys with live sweep state at once.
141 key_batch_size: usize,
142 /// Pending interesting times, shared across flat key ranges.
143 pending: Pending<T, K>,
144}
145
146impl<T, Bk, K> ProxyReduceTactic<T, Bk, K> {
147 /// A tactic deferring all value semantics to `backend`.
148 pub fn new(backend: Bk) -> Self {
149 ProxyReduceTactic { backend, key_batch_size: usize::MAX, pending: Pending::default() }
150 }
151
152 /// Limit simultaneous sweeps independently of the backend's presentation window.
153 ///
154 /// Complete proxy keys stay together, including all real keys sharing a proxy.
155 /// Sweep scratch is reused between groups within a retire. The bound does not
156 /// limit a single key's size, the presentation, or the output batch. Corrections
157 /// remain batched, and emission still happens once per backend window.
158 /// By default, all keys in the presentation can have live sweeps at once.
159 pub fn with_key_batch_size(mut self, key_batch_size: usize) -> Self {
160 assert!(key_batch_size > 0, "key batch size must be positive");
161 self.key_batch_size = key_batch_size;
162 self
163 }
164}
165
166impl<T, B1, B2, Bk, K: Copy + Ord> ReduceTactic<T, B1, B2> for ProxyReduceTactic<T, Bk, K>
167where
168 T: Timestamp + Lattice,
169 Bk: ProxyReduceBackend<T, B1, B2, Key = K>,
170{
171 fn retire(
172 &mut self,
173 source_batches: Vec<B1>,
174 output_batches: Vec<B2>,
175 input_batches: Vec<B1>,
176 lower: &Antichain<T>,
177 upper: &Antichain<T>,
178 held: &Antichain<T>,
179 ) -> (Option<Span<T, B2>>, Antichain<T>) {
180 if held.elements().iter().all(|t| upper.less_equal(t)) {
181 debug_assert!(
182 self.pending.frontier().iter().all(|time| held.less_equal(time)),
183 "held capabilities do not cover pending times",
184 );
185 return (None, held.clone());
186 }
187
188 let instance = ReduceInstance {
189 source_batches: &source_batches,
190 input_batches: &input_batches,
191 output_batches: &output_batches,
192 lower: lower.borrow(),
193 };
194
195 // Split the carried interesting times against `upper`.
196 // A time below it is DUE: its key must be re-evaluated this retire, so the key is `changed`.
197 // A time at or beyond it remains carried in `self.pending`.
198 // Activate time groups before visiting their keys. Due rows index a shared time table.
199 let due = self.pending.activate(upper.borrow());
200 // The keys the harness knows must be revisited. The backend adds those its novel batches
201 // touch, which it discovers while reading them; neither side scans the whole key space.
202 let mut changed: Vec<K> = due.rows.iter().map(|r| r.0).collect();
203 changed.dedup();
204 let mut deferred = Vec::new();
205 let mut due_pos = 0;
206
207 // Nothing due and nothing novel: no time in the interval can be interesting, so there is no
208 // work and no output. Return the frontier bounding the times still withheld — NOT an empty
209 // one. This is exactly where a due-only `changed` differs from the whole pending set: times
210 // beyond `upper` can remain when nothing is due, and releasing their capabilities would
211 // strand them (see the frontier clause of the `ReduceTactic::retire` contract).
212 if changed.is_empty() && instance.input_batches.is_empty() {
213 return (None, self.pending.frontier());
214 }
215
216 // The single output batch spans the retired interval.
217 let description = Description::new(lower.clone(), upper.clone(), Antichain::from_elem(T::minimum()));
218 self.backend.begin(description.clone());
219
220 // Start before every key; each window advances to an inclusive key bound or the end.
221 let mut from = KeyPosition::Start;
222 let mut window: ReduceWindow<T, Bk::RIn, Bk::ROut, K, Bk::VIn, Bk::VOut> = ReduceWindow::default();
223
224 // Retire-wide reusable scratch: cleared per group, window or wave, retaining capacity. Fresh
225 // per-key/per-wave `Vec`s were once the dominant cost here, which is why the slots and the
226 // staging buffers are held across the whole retire rather than built where they are used.
227 let mut slots: Vec<KeySweep<T, Bk::RIn, Bk::ROut, K, Bk::VIn, Bk::VOut>> = Vec::new();
228 let mut live: Vec<usize> = Vec::new();
229 let mut deltas: Vec<((K, Bk::VOut), T, Bk::ROut)> = Vec::new();
230 let mut batch_keys: Vec<K> = Vec::new();
231 let mut in_ends: Vec<usize> = Vec::new();
232 let mut in_all: Vec<(Bk::VIn, Bk::RIn)> = Vec::new();
233 let mut out_ends: Vec<usize> = Vec::new();
234 let mut out_all: Vec<(Bk::VOut, Bk::ROut)> = Vec::new();
235 let mut active: Vec<(usize, T)> = Vec::new();
236 let mut in_accum: Vec<(Bk::VIn, Bk::RIn)> = Vec::new();
237 let mut cur_out: Vec<(Bk::VOut, Bk::ROut)> = Vec::new();
238
239 while from != KeyPosition::End {
240 let before = from;
241 window.clear();
242 self.backend.next_window(&instance, &changed, &mut from, &mut window);
243 let p_in = &mut window.input;
244 let seeds = &window.seeds;
245 let p_out = &mut window.output;
246 super::debug_assert_sorted_bridge(p_in, "next_window.input");
247 super::debug_assert_sorted_bridge(p_out, "next_window.output");
248 debug_assert!(
249 seeds.windows(2).all(|w| w[0] < w[1]),
250 "next_window.seeds must be sorted by (key, time) and deduplicated",
251 );
252 // Without progress the window loop would never retire, so this guards liveness as well
253 // as contract; the range check catches a key reported outside the window that owns it,
254 // which would silently drop the interaction between its halves.
255 debug_assert!(
256 from > before,
257 "next_window must either advance `from` or report the key space exhausted",
258 );
259 debug_assert!(
260 {
261 let mut keys = p_in.iter().map(|r| r.0.0).chain(seeds.iter().map(|s| s.0)).chain(p_out.iter().map(|r| r.0.0));
262 keys.all(|k| before <= KeyPosition::At(k) && KeyPosition::At(k) < from)
263 },
264 "next_window must report a key entirely within the window that first mentions it",
265 );
266
267 deltas.clear();
268
269 // The window's keys are the proxies its presentations mention: the least of the three
270 // heads, each iteration, until all three are drained. A `changed` key that appears in
271 // none of them has no records at all, so its reduction has nothing to read and nothing
272 // to retract — the time its due moment would raise reaches the evaluation gate with an
273 // empty input and an empty output, and produces nothing. Skipping it is exactly what
274 // visiting it would do. (`changed` is still the backend's instruction about which keys
275 // to present; it is just not a source of keys here.)
276 //
277 // Each key gets a `Sweep`, which discovers and evaluates in ONE ascending pass,
278 // suspending where the conventional reduce would call user logic. Slots are reused
279 // across bounded groups as well as windows. A backend can present a large
280 // window without allocating sweep state for every key simultaneously.
281 let (mut is, mut ns, mut os) = (0usize, 0usize, 0usize);
282 while is < p_in.len() || ns < seeds.len() || os < p_out.len() {
283 let mut n_slots = 0usize;
284 live.clear();
285 // Mapped to keys before the min: the sources differ in shape.
286 while let Some(key) = [
287 p_in.get(is).map(|record| record.0.0),
288 seeds.get(ns).map(|seed| seed.0),
289 p_out.get(os).map(|record| record.0.0),
290 ].into_iter().flatten().min() {
291 let i0 = is;
292 while is < p_in.len() && p_in[is].0.0 == key { is += 1; }
293 let i1 = is;
294 let n0 = ns;
295 while ns < seeds.len() && seeds[ns].0 == key { ns += 1; }
296 let n1 = ns;
297 let o0 = os;
298 while os < p_out.len() && p_out[os].0.0 == key { os += 1; }
299 let o1 = os;
300
301 if n_slots == slots.len() { slots.push(KeySweep::empty(key)); }
302 let slot = &mut slots[n_slots];
303 slot.key = key;
304 slot.pended.clear();
305 // Only the DUE times seed the sweep; the carried ones remain in `self.pending`.
306 while due_pos < due.rows.len() && due.rows[due_pos].0 < key { due_pos += 1; }
307 let start = due_pos;
308 while due_pos < due.rows.len() && due.rows[due_pos].0 == key { due_pos += 1; }
309 let owed = &due.rows[start..due_pos];
310 let novel = &seeds[n0..n1];
311 // One seed dominating every record has exactly one possible evaluation.
312 let single = owed.first().map(|r| &due.times[r.1]).or_else(|| novel.first().map(|r| &r.1))
313 .filter(|at| owed.len() <= 1 && novel.len() <= 1
314 && novel.first().is_none_or(|r| &r.1 == *at)
315 && p_in[i0..i1].iter().all(|r| r.1.less_equal(at))
316 && p_out[o0..o1].iter().all(|r| r.1.less_equal(at)));
317 slot.direct = single.map(|_| (i0..i1, o0..o1));
318 slot.at = if let Some(at) = single {
319 if upper.less_equal(at) { slot.pended.push(at.clone()); None }
320 else { Some(at.clone()) }
321 } else {
322 slot.sweep.load(
323 owed.iter().map(|&(_, row)| due.times[row].clone()),
324 novel.iter().map(|r| r.1.clone()),
325 p_in[i0..i1].iter_mut().map(|r| (r.0.1, std::mem::replace(&mut r.1, T::minimum()), r.2.clone())),
326 p_out[o0..o1].iter_mut().map(|r| (r.0.1, std::mem::replace(&mut r.1, T::minimum()), r.2.clone())),
327 );
328 slot.sweep.next_crossing(upper, &mut slot.pended)
329 };
330 if slot.at.is_some() { live.push(n_slots); }
331 else if !slot.pended.is_empty() {
332 deferred.extend(slot.pended.drain(..).map(|time| (time, key)));
333 }
334 n_slots += 1;
335 if n_slots == self.key_batch_size { break; }
336 }
337
338 // Each wave: read every suspended key's accumulations, cross the non-empty ones in one
339 // call, hand the corrections back, and step every live key on. A key retires when its
340 // sweep runs dry, at which point its pended times are carried forward.
341 while !live.is_empty() {
342 batch_keys.clear();
343 in_ends.clear();
344 in_all.clear();
345 out_ends.clear();
346 out_all.clear();
347 active.clear();
348
349 for &si in live.iter() {
350 let at = slots[si].at.clone().expect("live slots are suspended at a time");
351 in_accum.clear();
352 cur_out.clear();
353 if let Some((ir, or)) = &slots[si].direct {
354 in_accum.extend(p_in[ir.clone()].iter().map(|r| (r.0.1, r.2.clone())));
355 cur_out.extend(p_out[or.clone()].iter().map(|r| (r.0.1, r.2.clone())));
356 crate::consolidation::consolidate(&mut in_accum);
357 crate::consolidation::consolidate(&mut cur_out);
358 } else {
359 slots[si].sweep.input_at(&at, &mut in_accum);
360 slots[si].sweep.output_at(&at, &mut cur_out);
361 }
362 // An interesting time can still reach the gate with nothing to read; the
363 // conventional reduce skips user logic there and so do we.
364 if in_accum.is_empty() && cur_out.is_empty() { continue; }
365 batch_keys.push(slots[si].key);
366 in_all.append(&mut in_accum);
367 in_ends.push(in_all.len());
368 out_all.append(&mut cur_out);
369 out_ends.push(out_all.len());
370 active.push((si, at));
371 }
372
373 if !batch_keys.is_empty() {
374 let (corr, corr_ends) = self.backend.reduce_corrections(&batch_keys, &in_ends, &in_all, &out_ends, &out_all);
375 let mut cstart = 0usize;
376 for (bi, (si, at)) in active.iter().enumerate() {
377 let cend = corr_ends[bi];
378 if cstart != cend {
379 debug_assert!(held.elements().iter().any(|h| h.less_equal(at)), "no held capability <= active time");
380 for (vid, d) in &corr[cstart..cend] {
381 deltas.push(((slots[*si].key, *vid), at.clone(), d.clone()));
382 }
383 if slots[*si].direct.is_none() {
384 slots[*si].sweep.commit(at, corr[cstart..cend].iter().cloned());
385 }
386 }
387 cstart = cend;
388 }
389 }
390
391 // Step every live key past the time it was suspended at, and retire the spent ones.
392 for &si in live.iter() {
393 let slot = &mut slots[si];
394 slot.at = if slot.direct.is_some() { None }
395 else { slot.sweep.next_crossing(upper, &mut slot.pended) };
396 if slot.at.is_none() && !slot.pended.is_empty() {
397 deferred.extend(slot.pended.drain(..).map(|time| (time, slot.key)));
398 }
399 }
400 live.retain(|&si| slots[si].at.is_some());
401 }
402 // Bound staging by the same key groups as the sweep scratch.
403 self.pending.insert(std::mem::take(&mut deferred));
404 }
405
406 if !deltas.is_empty() {
407 crate::consolidation::consolidate_updates(&mut deltas);
408 self.backend.emit(&deltas[..]);
409 }
410 }
411
412 let produced = Some(Span::new(description, self.backend.finish()));
413 (produced, self.pending.frontier())
414 }
415}
416
417/// One key's slot in a window: its [`Sweep`], the time it is suspended at, and the times it has
418/// pended so far. Slots and their scratch capacity are reused across groups and windows
419/// within a retire, then dropped when the retire completes.
420struct KeySweep<T, RIn, ROut, K, VIn, VOut> {
421 key: K,
422 sweep: Sweep<T, RIn, ROut, VIn, VOut>,
423 direct: Option<(std::ops::Range<usize>, std::ops::Range<usize>)>,
424 /// Times at or beyond `upper` the sweep has reached; carried forward when the slot retires.
425 pended: Vec<T>,
426 /// The time the sweep last suspended at, or `None` once it is spent.
427 at: Option<T>,
428}
429
430impl<T: Timestamp + Lattice, RIn: Semigroup + Clone, ROut: Semigroup + Clone, K, VIn: Copy + Ord, VOut: Copy + Ord> KeySweep<T, RIn, ROut, K, VIn, VOut> {
431 fn empty(key: K) -> Self {
432 KeySweep { key, sweep: Sweep::new(), direct: None, pended: Vec::new(), at: None }
433 }
434}
435
436/// Updates an optional meet by an optional time.
437fn update_meet<T: Lattice + Clone>(meet: &mut Option<T>, other: Option<&T>) {
438 if let Some(time) = other {
439 match meet.as_mut() {
440 Some(m) => m.meet_assign(time),
441 None => *meet = Some(time.clone()),
442 }
443 }
444}
445
446/// A resumable, fused determination-and-evaluation sweep over one key's times.
447///
448/// A determination pass would enumerate a key's interesting times up front, and the caller would
449/// then walk them again to evaluate. The conventional reduce does not: it runs ONE ascending pass
450/// and evaluates as it discovers. It can, because discovery never looks backwards — every
451/// synthesized time is
452/// `next_time.join(t)` for some `t` NOT at or below `next_time`, so it is strictly greater, and new
453/// work only ever lands ahead of the sweep.
454///
455/// This is that pass, cut at the point where the conventional operator would call user logic. Each
456/// [`next_crossing`](Self::next_crossing) returns the next in-interval time that needs evaluating,
457/// with the buffers positioned to read the accumulations; the caller evaluates and hands the
458/// corrections back through [`commit`](Self::commit); the next call resumes. Many keys can be run
459/// to their next crossing and evaluated together, which is what a batched backend wants, without
460/// any of them enumerating their times first.
461///
462/// The schedule is `formal/Differential/RoundCoverage.lean`'s `round_coverage`: a time carrying an
463/// output change lies in the join-closure of `prior ∪ novel` AND is at or above some novel time.
464/// The novel times arrive as the SEED LIST (the harness's warned times merged with the window's raw
465/// novel time support); the records themselves travel merged with the prior history and carry no
466/// witness duty, which is what lets them net and advance. Coverage is invariant under that move:
467/// the witness clause reads only times, and consolidation cancels only equal-time pairs whose time
468/// the seed list retains.
469struct Sweep<T, RIn, ROut, VIn, VOut> {
470 /// The accumulated input (novel and prior, merged and netted) and output: join partners, and
471 /// the accumulations to evaluate over. Both may be advanced freely — witness duty lives in
472 /// `seeds`, not in any record.
473 input: ValueHistory<VIn, T, RIn>,
474 output: ValueHistory<VOut, T, ROut>,
475 /// The key's seed times — the harness's due (warned) times merged with the raw novel time
476 /// support — ascending and deduplicated, with their suffix meets; `seed_pos` consumes them.
477 /// These are the ONLY source of interest: the schedule is stated over them, so they are held
478 /// raw, never advanced.
479 seeds: Vec<T>,
480 seed_meets: Vec<T>,
481 seed_pos: usize,
482 /// Synthesized times not yet visited, sorted DESCENDING so `last()` is the least.
483 synth: Vec<T>,
484 /// The seed times reached so far, compacted by the running meet. They are the witnesses the
485 /// absorption test looks for, and the partners a close joins against; keeping them collapsed is
486 /// what stops a key with many reached times rescanning all of them.
487 reached: Vec<T>,
488 /// Scratch for one step's synthesized times.
489 temporary: Vec<T>,
490 /// Corrections emitted so far this sweep, meet-collapsed; both a join partner and part of the
491 /// output accumulation.
492 produced: Vec<((VOut, T), ROut)>,
493 /// The meet of every time still to come.
494 meet: Option<T>,
495 /// Whether the last `next_crossing` returned a time whose step is not yet settled.
496 suspended: bool,
497}
498
499/// What one [`tick`](Sweep::tick) decided about the time it visited.
500enum Tick<T> {
501 /// No seed reaches this time; the sweep moved past it.
502 Passed,
503 /// Reached, but at or beyond `upper`: carried to a later round rather than evaluated.
504 Pended,
505 /// Reached and in the interval. The caller must evaluate here before the sweep goes on.
506 Crossing(T),
507 /// Every source is drained.
508 Done,
509}
510
511impl<T: Timestamp + Lattice, RIn: Semigroup + Clone, ROut: Semigroup + Clone, VIn: Copy + Ord, VOut: Copy + Ord> Sweep<T, RIn, ROut, VIn, VOut> {
512 /// An empty sweep, to be `load`ed and reused for successive keys.
513 fn new() -> Self {
514 Sweep {
515 input: ValueHistory::new(), output: ValueHistory::new(),
516 seeds: Vec::new(), seed_meets: Vec::new(), seed_pos: 0,
517 synth: Vec::new(), reached: Vec::new(), temporary: Vec::new(),
518 produced: Vec::new(), meet: None, suspended: false,
519 }
520 }
521
522 /// Position the sweep at the start of one key.
523 ///
524 /// `owed` is the harness's due (warned) times and `novel_times` the window's raw novel time
525 /// support, both ascending; they merge into the seed list, which is held raw — the schedule is
526 /// stated over these times, so they are never advanced. The records in `input` are merged and
527 /// netted (novel and prior together): a cancelled record's time survives in the seed list, so
528 /// netting loses nothing, and every record is a mere partner/accumulant that the meet may
529 /// advance freely.
530 fn load(
531 &mut self,
532 owed: impl Iterator<Item = T>,
533 novel_times: impl Iterator<Item = T>,
534 input: impl Iterator<Item = (VIn, T, RIn)>,
535 output: impl Iterator<Item = (VOut, T, ROut)>,
536 ) {
537 // Merge the two ascending seed sources, deduplicated.
538 self.seeds.clear();
539 let mut owed = owed.peekable();
540 let mut novel = novel_times.peekable();
541 loop {
542 let take_owed = match (owed.peek(), novel.peek()) {
543 (Some(a), Some(b)) => a <= b,
544 (Some(_), None) => true,
545 (None, Some(_)) => false,
546 (None, None) => break,
547 };
548 let time = if take_owed { owed.next() } else { novel.next() }.expect("peeked");
549 if self.seeds.last() != Some(&time) {
550 self.seeds.push(time);
551 }
552 }
553 self.seed_meets.clear();
554 self.seed_meets.extend(self.seeds.iter().cloned());
555 for i in (1..self.seed_meets.len()).rev() {
556 let (init, tail) = self.seed_meets.split_at_mut(i);
557 init[i - 1].meet_assign(&tail[0]);
558 }
559 self.seed_pos = 0;
560 self.synth.clear();
561 self.reached.clear();
562 self.temporary.clear();
563 self.produced.clear();
564 self.suspended = false;
565
566 // The meet of every seed bounds every time the sweep will visit, so the record buffers can
567 // be advanced by it at load.
568 let mut meet: Option<T> = None;
569 update_meet(&mut meet, self.seed_meets.first());
570 self.input.load_iter(input, meet.as_ref());
571 self.output.load_iter(output, meet.as_ref());
572 self.meet = meet;
573 }
574
575 /// Advance to the next in-interval time that needs evaluating, or `None` once the key is spent.
576 ///
577 /// Times at or beyond `upper` that the schedule reaches are appended to `pended` for the caller
578 /// to carry into a later round.
579 fn next_crossing(&mut self, upper: &Antichain<T>, pended: &mut Vec<T>) -> Option<T> {
580 loop {
581 // A crossing leaves its step half-finished, because `settle` must see the corrections
582 // the caller commits. Finishing it is the first thing the next call does.
583 if self.suspended {
584 self.suspended = false;
585 self.settle();
586 }
587 match self.tick(upper, pended) {
588 Tick::Done => return None,
589 Tick::Crossing(at) => {
590 self.suspended = true;
591 return Some(at);
592 }
593 Tick::Passed | Tick::Pended => {}
594 }
595 }
596 }
597
598 /// Visit one time: find it, decide whether it is reached, close it forward, and report.
599 fn tick(&mut self, upper: &Antichain<T>, pended: &mut Vec<T>) -> Tick<T> {
600 let Some(at) = self.frontier() else { return Tick::Done };
601 let reached = self.absorb(&at);
602 if upper.less_equal(&at) {
603 // Out of the interval: nothing can be emitted here, so there is nothing to close
604 // against either — a join with `at` is at or beyond `at`, hence also out of interval,
605 // and will be rediscovered from `at` in the round that admits it.
606 self.settle();
607 if reached { pended.push(at); return Tick::Pended; }
608 return Tick::Passed;
609 }
610 self.close(&at, reached, upper, pended);
611 if reached { return Tick::Crossing(at); }
612 self.settle();
613 Tick::Passed
614 }
615
616 /// The sweep's position: the least time any source still offers.
617 ///
618 /// The TOTAL order, not the partial one. Every time `close` produces is strictly greater than
619 /// the position that produced it, so new work only ever lands ahead of here and the sweep never
620 /// revisits.
621 fn frontier(&self) -> Option<T> {
622 [
623 self.seeds.get(self.seed_pos), self.input.time(),
624 self.output.time(), self.synth.last(),
625 ].into_iter().flatten().min().cloned()
626 }
627
628 /// Step every source sitting at `at`, and decide whether `at` is REACHED.
629 ///
630 /// Reached is clause two of `round_coverage` — `∃ nu ∈ novel, nu ≤ at` — evaluated
631 /// incrementally: either a seed lands exactly here, or one already stepped in lies below.
632 ///
633 /// Input and output are stepped whether or not `at` is reached, and that is forced rather than
634 /// eager: they are sources of the frontier, so leaving them would stall the sweep, and their
635 /// edits must reach the buffers or they are lost to every later accumulation. Stepping only
636 /// moves an edit across; it consolidates nothing. The expensive part — `advance_buffer_by`,
637 /// which joins every buffered time and re-consolidates — is deferred to `close`, and happens
638 /// only where the buffers are actually read.
639 fn absorb(&mut self, at: &T) -> bool {
640 self.input.step_while_time_is(at);
641 self.output.step_while_time_is(at);
642
643 // A seed here — a due time or a novel-support time — is consumed into the reached set,
644 // where it becomes a witness and a join partner for every later time. So is a synthetic
645 // join scheduled for here.
646 let mut reached = false;
647 while self.synth.last() == Some(at) {
648 self.reached.push(self.synth.pop().expect("nonempty"));
649 reached = true;
650 }
651 while self.seeds.get(self.seed_pos) == Some(at) {
652 self.reached.push(at.clone());
653 self.seed_pos += 1;
654 reached = true;
655 }
656 // Absorption: a time at or above a seed already consumed is itself reached, because
657 // joining that seed with it yields it back.
658 reached || self.reached.iter().any(|t| t.less_equal(at))
659 }
660
661 /// Close `at` forward under joins — clause one of `round_coverage`, the join-closure.
662 ///
663 /// Against the REACHED (seed-derived) times always, reached or not: an unreached time joined
664 /// with a seed lands at or above that seed, so it carries a witness and is on the schedule.
665 ///
666 /// Against the PRIOR times only when `at` is itself reached, because the join then inherits
667 /// `at`'s witness. A join of two prior times carries none and is deliberately never produced —
668 /// that asymmetry is the whole of why an incremental operator does less work than the closure
669 /// of everything.
670 ///
671 /// `produced` counts as prior. A correction emitted at `p` changes the accumulated output at
672 /// every time at or above `p`, so `p ∨ at` has to be visited; nothing else covers it, since
673 /// this round's corrections are not in the output history and `at` was not yet stepped in when
674 /// the sweep passed `p`.
675 fn close(&mut self, at: &T, reached: bool, upper: &Antichain<T>, pended: &mut Vec<T>) {
676 self.temporary.extend(self.reached.iter()
677 .filter(|t| !t.less_equal(at)).map(|t| t.join(at)));
678 if reached {
679 if let Some(meet) = self.meet.as_ref() {
680 self.input.advance_buffer_by(meet);
681 self.output.advance_buffer_by(meet);
682 }
683 self.temporary.extend(self.input.buffer().iter().map(|((_, t), _)| t)
684 .filter(|t| !t.less_equal(at)).map(|t| t.join(at)));
685 self.temporary.extend(self.output.buffer().iter().map(|((_, t), _)| t)
686 .filter(|t| !t.less_equal(at)).map(|t| t.join(at)));
687 self.temporary.extend(self.produced.iter().map(|((_, t), _)| t)
688 .filter(|t| !t.less_equal(at)).map(|t| t.join(at)));
689 }
690 sort_dedup(&mut self.temporary);
691 let before = self.synth.len();
692 for time in self.temporary.drain(..) {
693 if upper.less_equal(&time) { pended.push(time); } else { self.synth.push(time); }
694 }
695 if self.synth.len() > before {
696 self.synth.sort_by(|x, y| y.cmp(x));
697 self.synth.dedup();
698 }
699 }
700
701 /// The input accumulation at the suspended time.
702 fn input_at(&self, at: &T, into: &mut Vec<(VIn, RIn)>) {
703 for ((id, time), diff) in self.input.buffer().iter() {
704 if time.less_equal(at) { into.push((*id, diff.clone())); }
705 }
706 crate::consolidation::consolidate(into);
707 }
708
709 /// The tentative output accumulation at the suspended time, including this sweep's corrections.
710 fn output_at(&self, at: &T, into: &mut Vec<(VOut, ROut)>) {
711 for ((id, time), diff) in self.output.buffer().iter().chain(self.produced.iter()) {
712 if time.less_equal(at) { into.push((*id, diff.clone())); }
713 }
714 crate::consolidation::consolidate(into);
715 }
716
717 /// Record the corrections evaluated at the suspended time, and collapse them by the meet.
718 fn commit(&mut self, at: &T, corrections: impl Iterator<Item = (VOut, ROut)>) {
719 let before = self.produced.len();
720 for (id, diff) in corrections { self.produced.push(((id, at.clone()), diff)); }
721 if self.produced.len() > before {
722 if let Some(meet) = self.meet.as_ref() {
723 for entry in self.produced.iter_mut() { (entry.0).1.join_assign(meet); }
724 }
725 crate::consolidation::consolidate(&mut self.produced);
726 }
727 }
728
729 /// Close a step: recompute the meet of everything still to come, and compact the reached set by
730 /// it. This is what keeps a key with a long history linear rather than quadratic.
731 fn settle(&mut self) {
732 let mut meet: Option<T> = None;
733 update_meet(&mut meet, self.input.meet());
734 update_meet(&mut meet, self.output.meet());
735 for time in self.synth.iter() { update_meet(&mut meet, Some(time)); }
736 update_meet(&mut meet, self.seed_meets.get(self.seed_pos));
737 if let Some(m) = meet.as_ref() {
738 for time in self.reached.iter_mut() { *time = time.join(m); }
739 }
740 sort_dedup(&mut self.reached);
741 self.meet = meet;
742 }
743}