differential_dataflow/operators/reduce.rs
1//! Applies a reduction function on records grouped by key.
2//!
3//! The `reduce` operator acts on `(key, val)` data.
4//! Records with the same key are grouped together, and a user-supplied reduction function is applied
5//! to the key and the list of values.
6//! The function is expected to populate a list of output values.
7//!
8//! The output can change at times that are joins of input times, not only at input times themselves,
9//! and the operator must determine at which times to re-evaluate the reduction. A machine-checked
10//! account of which times suffice lives in `formal/Differential/Coverage.lean`.
11
12use crate::Data;
13
14use std::marker::PhantomData;
15
16use timely::progress::frontier::Antichain;
17use timely::progress::Timestamp;
18use timely::dataflow::operators::Operator;
19use timely::dataflow::operators::CapabilitySet;
20use timely::dataflow::channels::pact::Pipeline;
21
22use crate::operators::arrange::{Arranged, TraceAgent};
23use crate::trace::{BatchCursor, BatchDiff, BatchKey, BatchReader, BatchVal, BatchValOwn, Builder, Cursor, Description, ExertionLogic, Navigable, Trace, TraceReader};
24use crate::trace::cursor::cursor_list;
25use crate::trace::implementations::containers::BatchContainer;
26
27/// Sort and deduplicate a list. Shared by the cursor and reference tactics (via their
28/// `use super::*`) and the proxy tactic (`crate::operators::reduce::sort_dedup`), which
29/// each previously carried an identical copy.
30#[inline(never)]
31pub(crate) fn sort_dedup<T: Ord>(list: &mut Vec<T>) {
32 list.dedup();
33 list.sort();
34 list.dedup();
35}
36
37/// A type that resolves a key-wise reduction over batches arriving on the input.
38///
39/// Unlike join, reduce does not suspend: its output is at most linear in its input, so a single
40/// `retire` runs the whole `[lower, upper)` interval to completion rather than yielding under a fuel
41/// budget.
42pub trait ReduceTactic<B1: BatchReader, B2: BatchReader<Time = B1::Time>> {
43 /// Retire the interval `[lower, upper)`, producing the output batches it informs.
44 ///
45 /// It is presented with the pre-existing input batches and output batches (those before `lower`),
46 /// the new input batches, and `held`: the times the operator currently holds capabilities for. It
47 /// reasons only about times, returning the output batches to ship — each tagged with the time at
48 /// which to ship it — and the new frontier of interesting times for the operator to hold.
49 ///
50 /// # Contract
51 ///
52 /// The driver ([`reduce_with_tactic`]) relies on the following; the first two are cheap to check
53 /// and are `debug_assert!`ed there.
54 ///
55 /// * **Ordered, tiling output.** The returned `(time, batch)` pairs are in ascending order and
56 /// their descriptions *tile* `[lower, upper)`: the first batch's lower is `lower`, each batch's
57 /// upper is the next batch's lower, and the last batch's upper is `upper` — no gaps, no overlaps.
58 /// Sub-intervals with no updates are skipped; the next batch's lower simply picks up where the
59 /// last left off. Producing *in order* is a requirement, not a convenience — it is what lets the
60 /// driver check the tiling with a single linear scan.
61 /// * **Shipped at a held time.** Each batch's `time` tag is an element of `held`; the driver mints
62 /// a capability at it, which is only valid for a held time.
63 /// * **Frontier bounds withheld work, and collapses to empty when there is none.** The returned
64 /// frontier must be at-or-below every time the tactic defers, so the driver knows what is safe to
65 /// release. In particular, with no work to defer it must be the *empty* antichain. Derive it from
66 /// the actual withheld set rather than constructing it and this holds for free; returning a
67 /// non-empty frontier with nothing pending holds capabilities forever and **deadlocks recursive
68 /// scopes**. (Not driver-checkable — the withheld set is tactic-internal — so tactics self-enforce.)
69 fn retire(
70 &mut self,
71 source_batches: Vec<B1>,
72 output_batches: Vec<B2>,
73 input_batches: Vec<B1>,
74 lower: &Antichain<B1::Time>,
75 upper: &Antichain<B1::Time>,
76 held: &Antichain<B1::Time>,
77 ) -> (Vec<(B1::Time, B2)>, Antichain<B1::Time>);
78}
79
80/// A key-wise reduction of values in an input trace.
81///
82/// This method exists to provide reduce functionality without opinions about qualifying trace types.
83///
84/// The `logic` closure is expected to take a key, accumulated input, and tentative accumulated output,
85/// and populate its final argument with whatever it feels to be appopriate updates. The behavior and
86/// correctness of the implementation rely on this making sense, and e.g. ideally the updates would if
87/// applied to the tentative output bring it in line with some function applied to the input.
88///
89/// The `push` closure is expected to clear its first argument, then populate it with the key and drain
90/// the value updates, as appropriate for the container. It is critical that it clear the container as
91/// the operator has no ability to do this otherwise, and failing to do so represents a leak from one
92/// key's computation to another, and will likely introduce non-determinism.
93pub fn reduce_trace<'scope, Tr1, Bu, Tr2, KC, L, P>(trace: Arranged<'scope, Tr1>, name: &str, logic: L, push: P) -> Arranged<'scope, TraceAgent<Tr2>>
94where
95 Tr1: TraceReader<Batch: Navigable> + 'static,
96 Tr2: Trace<Batch: Navigable, Time = Tr1::Time> + 'static,
97 KC: BatchContainer,
98 BatchCursor<Tr1>: Cursor<Time = Tr1::Time, KeyContainer = KC>,
99 for<'a> BatchCursor<Tr1>: Cursor<Key<'a> = KC::ReadItem<'a>>,
100 for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = KC::ReadItem<'a>, ValOwn: Data, Time = Tr2::Time>,
101 Bu: Builder<Time=Tr2::Time, Output = Tr2::Batch, Input: Default> + 'static,
102 L: FnMut(KC::ReadItem<'_>, &[(BatchVal<'_, Tr1>, BatchDiff<Tr1>)], &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>, &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>)+'static,
103 P: FnMut(&mut Bu::Input, KC::ReadItem<'_>, &mut Vec<(BatchValOwn<Tr2>, Tr2::Time, BatchDiff<Tr2>)>) + 'static,
104{
105 reduce_with_tactic(trace, name, cursors::CursorTactic::<Tr1::Batch, Tr2::Batch, Bu, L, P>::new(logic, push))
106}
107
108// The model-derived reference tactic and its entry point live in `mod reference`; re-exported here
109// (doc-hidden) as the sole public handle for its differential and oracle tests.
110#[doc(hidden)]
111pub use reference::reduce_trace_reference;
112
113/// Drives a key-wise reduction using a supplied [`ReduceTactic`].
114///
115/// This is the general reduce operator: it does the dataflow plumbing (frontiers, capabilities, output
116/// trace maintenance) and routes the per-interval work through the tactic. It requires only
117/// `TraceReader` of its input and `Trace` of its output, never `Navigable`: it extracts batches via
118/// `batches_through`, and building cursors over them (if that is how the reduce proceeds) is the
119/// tactic's concern.
120pub fn reduce_with_tactic<'scope, Tr1, Tr2, T>(trace: Arranged<'scope, Tr1>, name: &str, mut tactic: T) -> Arranged<'scope, TraceAgent<Tr2>>
121where
122 Tr1: TraceReader + 'static,
123 Tr2: Trace<Time = Tr1::Time> + 'static,
124 T: ReduceTactic<Tr1::Batch, Tr2::Batch> + 'static,
125{
126 let mut result_trace = None;
127
128 // fabricate a data-parallel operator using the `unary_notify` pattern.
129 let stream = {
130
131 let mut source_trace = trace.trace;
132 let result_trace = &mut result_trace;
133 let scope = trace.stream.scope();
134 trace.stream.unary_frontier(Pipeline, name, move |_capability, operator_info| {
135
136 // Acquire a logger for arrange events.
137 let logger = scope.worker().logger_for::<crate::logging::DifferentialEventBuilder>("differential/arrange").map(Into::into);
138
139 let activator = Some(scope.activator_for(std::rc::Rc::clone(&operator_info.address)));
140 let mut empty = Tr2::new(operator_info.clone(), logger.clone(), activator);
141 // If there is default exert logic set, install it.
142 if let Some(exert_logic) = scope.worker().config().get::<ExertionLogic>("differential/default_exert_logic").cloned() {
143 empty.set_exert_logic(exert_logic);
144 }
145
146 let (mut output_reader, mut output_writer) = TraceAgent::new(empty, operator_info, logger);
147
148 *result_trace = Some(output_reader.clone());
149
150 // Capabilities for the lower envelope of the interesting times the operator holds.
151 let mut capabilities = CapabilitySet::<Tr1::Time>::default();
152
153 // Upper and lower frontiers for the pending input and output batches to process.
154 let mut upper_limit = Antichain::from_elem(<Tr1::Time as Timestamp>::minimum());
155 let mut lower_limit = Antichain::from_elem(<Tr1::Time as Timestamp>::minimum());
156
157 move |(input, frontier), output| {
158
159 // The operator receives input batches, which it treats as contiguous and will collect and
160 // then process as one batch. It captures the input frontier from the batches, from the upstream
161 // trace, and from the input frontier, and retires the work through that interval.
162 //
163 // Reduce may retain capabilities and need to perform work and produce output at times that
164 // may not be seen in its input. The standard example is that updates at `(0, 1)` and `(1, 0)`
165 // may result in outputs at `(1, 1)` as well, even with no input at that time.
166
167 let mut batch_storage = Vec::new();
168
169 // Downgrade previous upper limit to be current lower limit.
170 lower_limit.clear();
171 lower_limit.extend(upper_limit.borrow().iter().cloned());
172
173 // Drain input batches in order, capturing capabilities and the last upper.
174 input.for_each(|capability, batches| {
175 capabilities.insert(capability.retain(0));
176 for batch in batches.drain(..) {
177 upper_limit.clone_from(batch.upper());
178 batch_storage.push(batch);
179 }
180 });
181
182 // Pull in any subsequent empty batches we believe to exist.
183 source_trace.advance_upper(&mut upper_limit);
184 // Incorporate the input frontier guarantees as well.
185 let mut joined = Antichain::new();
186 crate::lattice::antichain_join_into(&upper_limit.borrow()[..], &frontier.frontier()[..], &mut joined);
187 upper_limit = joined;
188
189 // We plan to retire the interval [lower_limit, upper_limit), which should be non-empty to proceed.
190 if upper_limit != lower_limit {
191
192 // Acquire the pre-existing input and output batches preceding the interval. Batch handles
193 // are cheap to clone, so we fetch them whether or not the tactic finds work to do.
194 let source_batches = source_trace.batches_through(lower_limit.borrow()).expect("failed to acquire source batches");
195 let output_batches = output_reader.batches_through(lower_limit.borrow()).expect("failed to acquire output batches");
196
197 // The times the operator currently holds capabilities for, as an antichain.
198 let held: Antichain<Tr1::Time> = capabilities.iter().map(|c| c.time().clone()).collect();
199
200 // Retire the interval. The tactic reasons only about times: it returns output batches
201 // each tagged with the time to ship it at, and the new frontier of interesting times.
202 let (produced, new_frontier) = tactic.retire(source_batches, output_batches, batch_storage, &lower_limit, &upper_limit, &held);
203
204 // Contract checks (see `ReduceTactic::retire`). Cheap, debug-only.
205 debug_assert!(
206 produced.iter().all(|(time, _)| held.elements().contains(time)),
207 "ReduceTactic::retire shipped a batch at a time not held as a capability",
208 );
209 debug_assert!(
210 {
211 // Ordered output makes tiling a single linear scan: each description's lower
212 // must meet the previous upper (starting at `lower_limit`), ending at `upper_limit`.
213 let mut edge = lower_limit.clone();
214 let abutting = produced.iter().all(|(_, batch)| {
215 let matches = batch.description().lower() == &edge;
216 edge.clone_from(batch.description().upper());
217 matches
218 });
219 abutting && (produced.is_empty() || edge == upper_limit)
220 },
221 "ReduceTactic::retire output must be ordered and tile [lower, upper)",
222 );
223
224 // Ship each batch at a capability minted from the set at its time, and commit it to the
225 // output trace. The times are elements of `held`, so they stay valid until we downgrade.
226 for (time, batch) in produced {
227 let capability = capabilities.delayed(&time);
228 output.session(&capability).give(batch.clone());
229 output_writer.insert(batch, Some(time));
230 }
231
232 // Downgrade to the frontier the tactic handed back (a no-op when it found no work).
233 capabilities.downgrade(new_frontier);
234
235 // ensure that observed progress is reflected in the output.
236 output_writer.seal(upper_limit.clone());
237
238 // We only anticipate future times in advance of `upper_limit`.
239 source_trace.set_logical_compaction(upper_limit.borrow());
240 output_reader.set_logical_compaction(upper_limit.borrow());
241
242 // We will only slice the data between future batches.
243 source_trace.set_physical_compaction(upper_limit.borrow());
244 output_reader.set_physical_compaction(upper_limit.borrow());
245 }
246
247 // Exert trace maintenance if we have been so requested.
248 output_writer.exert();
249 }
250 }
251 )
252 };
253
254 Arranged { stream, trace: result_trace.unwrap() }
255}
256
257/// The conventional cursor-based [`ReduceTactic`].
258///
259/// It builds a [`CursorList`](crate::trace::cursor::CursorList) over the input, output, and new-batch
260/// updates and replays them together per key, applying `logic` and shaping output with `push`. It holds
261/// the outstanding synthetic interesting `(key, time)` moments across activations, and reasons only
262/// about times: capabilities are the driver's concern.
263mod cursors {
264
265 use super::*;
266
267 /// The conventional cursor-based [`ReduceTactic`].
268 pub struct CursorTactic<B1, B2, Bu, L, P>
269 where
270 B1: BatchReader + Navigable,
271 B2: BatchReader<Time = B1::Time> + Navigable,
272 B1::Cursor: Cursor<Time = B1::Time>,
273 for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
274 {
275 logic: L,
276 push: P,
277 // Outstanding `(key, time)` synthetic interesting moments, sorted by `(key, time)`, and the
278 // buffers into which we assemble the next round's moments.
279 pending_keys: <B1::Cursor as Cursor>::KeyContainer,
280 pending_time: <B1::Cursor as Cursor>::TimeContainer,
281 next_pending_keys: <B1::Cursor as Cursor>::KeyContainer,
282 next_pending_time: <B1::Cursor as Cursor>::TimeContainer,
283 // Buffers reused across activations.
284 interesting_times: Vec<B1::Time>,
285 new_interesting_times: Vec<B1::Time>,
286 // Output batches may need to be built piecemeal, and these temp storage help there.
287 output_upper: Antichain<B1::Time>,
288 output_lower: Antichain<B1::Time>,
289 _marker: PhantomData<(B2, Bu)>,
290 }
291
292 impl<B1, B2, Bu, L, P> CursorTactic<B1, B2, Bu, L, P>
293 where
294 B1: BatchReader + Navigable,
295 B2: BatchReader<Time = B1::Time> + Navigable,
296 B1::Cursor: Cursor<Time = B1::Time>,
297 for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
298 {
299 /// Construct a tactic that applies `logic` to each key and shapes output with `push`.
300 pub fn new(logic: L, push: P) -> Self {
301 CursorTactic {
302 logic,
303 push,
304 pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
305 pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
306 next_pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
307 next_pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
308 interesting_times: Vec::new(),
309 new_interesting_times: Vec::new(),
310 output_upper: Antichain::from_elem(<B1::Time as Timestamp>::minimum()),
311 output_lower: Antichain::from_elem(<B1::Time as Timestamp>::minimum()),
312 _marker: PhantomData,
313 }
314 }
315 }
316
317 impl<B1, B2, Bu, L, P> ReduceTactic<B1, B2> for CursorTactic<B1, B2, Bu, L, P>
318 where
319 B1: BatchReader + Navigable,
320 B2: BatchReader<Time = B1::Time> + Navigable,
321 B1::Cursor: Cursor<Time = B1::Time>,
322 for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
323 Bu: Builder<Time = B1::Time, Output = B2, Input: Default>,
324 L: FnMut(<B1::Cursor as Cursor>::Key<'_>, &[(<B1::Cursor as Cursor>::Val<'_>, <B1::Cursor as Cursor>::Diff)], &mut Vec<(<B2::Cursor as Cursor>::ValOwn, <B2::Cursor as Cursor>::Diff)>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, <B2::Cursor as Cursor>::Diff)>),
325 P: FnMut(&mut Bu::Input, <B1::Cursor as Cursor>::Key<'_>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, B1::Time, <B2::Cursor as Cursor>::Diff)>),
326 {
327 fn retire(
328 &mut self,
329 source_batches: Vec<B1>,
330 output_batches: Vec<B2>,
331 input_batches: Vec<B1>,
332 lower: &Antichain<B1::Time>,
333 upper: &Antichain<B1::Time>,
334 held: &Antichain<B1::Time>,
335 ) -> (Vec<(B1::Time, B2)>, Antichain<B1::Time>)
336 {
337 let mut produced = Vec::new();
338
339 // We have compute needs only if we hold a time in the interval [lower, upper); otherwise we
340 // could not transmit outputs even if they were (incorrectly) non-zero, and we leave the held
341 // times unchanged.
342 if held.elements().iter().any(|time| !upper.less_equal(time)) {
343
344 // cursors for navigating input, output, and new-batch updates.
345 let (mut source_cursor, ref source_storage) = cursor_list(source_batches);
346 let (mut output_cursor, ref output_storage) = cursor_list(output_batches);
347 let (mut batch_cursor, ref batch_storage) = cursor_list(input_batches);
348
349 // Prepare an output buffer and builder for each held time.
350 // TODO: It would be better if all updates went into one batch, but timely dataflow prevents
351 // this as long as it requires that there is only one capability for each message.
352 let mut buffers = Vec::<(B1::Time, Vec<(<B2::Cursor as Cursor>::ValOwn, B1::Time, <B2::Cursor as Cursor>::Diff)>)>::new();
353 let mut builders = Vec::new();
354 for time in held.elements().iter() {
355 buffers.push((time.clone(), Vec::new()));
356 builders.push(Bu::new());
357 }
358 // Temporary staging for output building.
359 let mut buffer = Bu::Input::default();
360
361 // Reuseable state for performing the computation.
362 let mut thinker = history_replay::HistoryReplayer::new();
363
364 // March through the keys we must work on, merging `batch_cursor` and pending keys.
365 // The interesting moments need to be in the interval to prompt work.
366 let mut pending_pos = 0;
367 while batch_cursor.key_valid(batch_storage) || pending_pos < self.pending_keys.len() {
368
369 // Determine the next key we will work on; could be synthetic, could be from a batch.
370 let key1 = self.pending_keys.get(pending_pos);
371 let key2 = batch_cursor.get_key(batch_storage);
372 let key = match (key1, key2) {
373 (Some(key1), Some(key2)) => ::std::cmp::min(key1, key2),
374 (Some(key1), None) => key1,
375 (None, Some(key2)) => key2,
376 (None, None) => unreachable!(),
377 };
378
379 // Populate `interesting_times` with interesting times not beyond `upper`.
380 // TODO: This could just be `pending_time` and indexes within `lower .. upper`.
381 let prior_pos = pending_pos;
382 self.interesting_times.clear();
383 while self.pending_keys.get(pending_pos) == Some(key) {
384 let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pending_pos));
385 if !upper.less_equal(&owned_time) { self.interesting_times.push(owned_time); }
386 pending_pos += 1;
387 }
388
389 // tidy up times, removing redundancy.
390 sort_dedup(&mut self.interesting_times);
391
392 // If there are new updates, or pending times, we must investigate!
393 if batch_cursor.get_key(batch_storage) == Some(key) || !self.interesting_times.is_empty() {
394
395 // do the per-key computation.
396 thinker.compute(
397 key,
398 (&mut source_cursor, source_storage),
399 (&mut output_cursor, output_storage),
400 (&mut batch_cursor, batch_storage),
401 &self.interesting_times,
402 &mut self.logic,
403 upper,
404 &mut buffers[..],
405 &mut self.new_interesting_times,
406 );
407
408 // Advance the cursor if this key, so that the loop's validity check registers the work as done.
409 if batch_cursor.get_key(batch_storage) == Some(key) { batch_cursor.step_key(batch_storage); }
410
411 // Merge novel pending times with any prior pending times we did not process.
412 // TODO: This could be a merge, not a sort_dedup, because both lists should be sorted.
413 for pos in prior_pos .. pending_pos {
414 let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pos));
415 if upper.less_equal(&owned_time) { self.new_interesting_times.push(owned_time); }
416 }
417 sort_dedup(&mut self.new_interesting_times);
418 for time in self.new_interesting_times.drain(..) {
419 self.next_pending_keys.push_ref(key);
420 self.next_pending_time.push_own(&time);
421 }
422
423 // Sort each buffer by value and move into the corresponding builder.
424 // TODO: This makes assumptions about at least one of (i) the stability of `sort_by`,
425 // (ii) that the buffers are time-ordered, and (iii) that the builders accept
426 // arbitrarily ordered times.
427 for index in 0 .. buffers.len() {
428 buffers[index].1.sort_by(|x,y| x.0.cmp(&y.0));
429 (self.push)(&mut buffer, key, &mut buffers[index].1);
430 buffers[index].1.clear();
431 builders[index].push(&mut buffer);
432
433 }
434 }
435 else {
436 // copy over the pending key and times.
437 for pos in prior_pos .. pending_pos {
438 self.next_pending_keys.push_ref(self.pending_keys.index(pos));
439 self.next_pending_time.push_ref(self.pending_time.index(pos));
440 }
441 }
442 }
443 // Drop to avoid lifetime issues that would lock `pending_{keys, time}`.
444 drop(thinker);
445
446 // We start sealing output batches from the lower limit (previous upper limit).
447 // In principle, we could update `lower` itself, and it should arrive at `upper` by the
448 // end of the process.
449 self.output_lower.clear();
450 self.output_lower.extend(lower.borrow().iter().cloned());
451
452 // build each batch (because only one capability per message).
453 for (index, builder) in builders.drain(..).enumerate() {
454
455 // Form the upper limit of the next batch, which includes all times greater
456 // than the input batch, or the held times from i + 1 onward.
457 self.output_upper.clear();
458 self.output_upper.extend(upper.borrow().iter().cloned());
459 for time in &held.elements()[index + 1 ..] {
460 self.output_upper.insert_ref(time);
461 }
462
463 if self.output_upper.borrow() != self.output_lower.borrow() {
464
465 let description = Description::new(self.output_lower.clone(), self.output_upper.clone(), Antichain::from_elem(<B1::Time as Timestamp>::minimum()));
466 let batch = builder.done(description);
467
468 // hand the batch back to the driver to ship and commit, tagged with its time.
469 produced.push((held.elements()[index].clone(), batch));
470
471 self.output_lower.clear();
472 self.output_lower.extend(self.output_upper.borrow().iter().cloned());
473 }
474 }
475 // This should be true, as the final iteration introduces no held times, and
476 // uses exactly `upper` to determine the upper bound. Good to check though.
477 assert!(self.output_upper.borrow() == upper.borrow());
478
479 // Refresh pending keys and times.
480 self.pending_keys.clear(); std::mem::swap(&mut self.next_pending_keys, &mut self.pending_keys);
481 self.pending_time.clear(); std::mem::swap(&mut self.next_pending_time, &mut self.pending_time);
482
483 // Compute the new frontier of interesting times for the operator to hold.
484 let mut frontier = Antichain::<B1::Time>::new();
485 let mut owned_time = <B1::Time as Timestamp>::minimum();
486 for pos in 0 .. self.pending_time.len() {
487 <B1::Cursor as Cursor>::clone_time_onto(self.pending_time.index(pos), &mut owned_time);
488 frontier.insert_ref(&owned_time);
489 }
490
491 (produced, frontier)
492 }
493 else {
494 // No work: leave the held times unchanged, so the driver's downgrade is a no-op.
495 (produced, held.clone())
496 }
497 }
498 }
499
500
501 /// Implementation based on replaying historical and new updates together.
502 mod history_replay {
503
504 use timely::progress::Antichain;
505
506 use crate::lattice::Lattice;
507 use crate::trace::Cursor;
508 use crate::operators::ValueHistory;
509
510 use crate::operators::reduce::sort_dedup;
511
512 /// The `HistoryReplayer` is a compute strategy based on moving through existing inputs, interesting times, etc in
513 /// time order, maintaining consolidated representations of updates with respect to future interesting times.
514 pub struct HistoryReplayer<V1, V2, V, T, D1, D2> {
515 input_history: ValueHistory<V1, T, D1>,
516 output_history: ValueHistory<V2, T, D2>,
517 batch_history: ValueHistory<V1, T, D1>,
518 input_buffer: Vec<(V1, D1)>,
519 output_buffer: Vec<(V, D2)>,
520 update_buffer: Vec<(V, D2)>,
521 output_produced: Vec<((V, T), D2)>,
522 synth_times: Vec<T>,
523 meets: Vec<T>,
524 times_current: Vec<T>,
525 temporary: Vec<T>,
526 }
527
528 impl<V1, V2, V, T, D1, D2> HistoryReplayer<V1, V2, V, T, D1, D2>
529 where
530 V1: Copy + Ord,
531 V2: Copy + Ord,
532 V: Clone + Ord,
533 T: Ord + Clone + Lattice,
534 D1: Clone + crate::difference::Semigroup,
535 D2: Clone + crate::difference::Semigroup,
536 {
537 pub fn new() -> Self {
538 HistoryReplayer {
539 input_history: ValueHistory::new(),
540 output_history: ValueHistory::new(),
541 batch_history: ValueHistory::new(),
542 input_buffer: Vec::new(),
543 output_buffer: Vec::new(),
544 update_buffer: Vec::new(),
545 output_produced: Vec::new(),
546 synth_times: Vec::new(),
547 meets: Vec::new(),
548 times_current: Vec::new(),
549 temporary: Vec::new(),
550 }
551 }
552 #[inline(never)]
553 pub fn compute<'a, K, C1, C2, C3, L>(
554 &mut self,
555 key: K,
556 (source_cursor, source_storage): (&mut C1, &'a C1::Storage),
557 (output_cursor, output_storage): (&mut C2, &'a C2::Storage),
558 (batch_cursor, batch_storage): (&mut C3, &'a C3::Storage),
559 times: &Vec<T>,
560 logic: &mut L,
561 upper_limit: &Antichain<T>,
562 outputs: &mut [(T, Vec<(V, T, D2)>)],
563 new_interesting: &mut Vec<T>)
564 where
565 C1: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
566 C2: Cursor<Key<'a> = K, Val<'a> = V2, ValOwn = V, Time = T, Diff = D2>,
567 C3: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
568 K: Copy + Ord,
569 L: FnMut(K, &[(V1, D1)], &mut Vec<(V, D2)>, &mut Vec<(V, D2)>),
570 {
571
572 // The work we need to perform is at times defined principally by the contents of `batch_cursor`
573 // and `times`, respectively "new work we just received" and "old times we were warned about".
574 //
575 // Our first step is to identify these times, so that we can use them to restrict the amount of
576 // information we need to recover from `input` and `output`; as all times of interest will have
577 // some time from `batch_cursor` or `times`, we can compute their meet and advance all other
578 // loaded times by performing the lattice `join` with this value.
579
580 // Load the batch contents.
581 let mut batch_replay = self.batch_history.replay_key(batch_cursor, batch_storage, key, None);
582
583 // We determine the meet of times we must reconsider (those from `batch` and `times`). This meet
584 // can be used to advance other historical times, which may consolidate their representation. As
585 // a first step, we determine the meets of each *suffix* of `times`, which we will use as we play
586 // history forward.
587
588 self.meets.clear();
589 self.meets.extend(times.iter().cloned());
590 for index in (1 .. self.meets.len()).rev() {
591 self.meets[index-1] = self.meets[index-1].meet(&self.meets[index]);
592 }
593
594 // Determine the meet of times in `batch` and `times`.
595 let mut meet = None;
596 update_meet(&mut meet, self.meets.get(0));
597 update_meet(&mut meet, batch_replay.meet());
598
599 // Having determined the meet, we can load the input and output histories, where we
600 // advance all times by joining them with `meet`. The resulting times are more compact
601 // and guaranteed to accumulate identically for times greater or equal to `meet`.
602
603 // Load the input and output histories.
604 let mut input_replay =
605 self.input_history.replay_key(source_cursor, source_storage, key, meet.as_ref());
606 let mut output_replay =
607 self.output_history.replay_key(output_cursor, output_storage, key, meet.as_ref());
608
609 self.synth_times.clear();
610 self.times_current.clear();
611 self.output_produced.clear();
612
613 // The frontier of times we may still consider.
614 // Derived from frontiers of our update histories, supplied times, and synthetic times.
615
616 let mut times_slice = ×[..];
617 let mut meets_slice = &self.meets[..];
618
619 // We have candidate times from `batch` and `times`, as well as times identified by either
620 // `input` or `output`. Finally, we may have synthetic times produced as the join of times
621 // we consider in the course of evaluation. As long as any of these times exist, we need to
622 // keep examining times.
623 while let Some(next_time) = [ batch_replay.time(),
624 times_slice.first(),
625 input_replay.time(),
626 output_replay.time(),
627 self.synth_times.last(),
628 ].into_iter().flatten().min().cloned() {
629
630 // Advance input and output history replayers. This marks applicable updates as active.
631 input_replay.step_while_time_is(&next_time);
632 output_replay.step_while_time_is(&next_time);
633
634 // One of our goals is to determine if `next_time` is "interesting", meaning whether we
635 // have any evidence that we should re-evaluate the user logic at this time. For a time
636 // to be "interesting" it would need to be the join of times that include either a time
637 // from `batch`, `times`, or `synth`. Neither `input` nor `output` times are sufficient.
638
639 // Advance batch history, and capture whether an update exists at `next_time`.
640 let mut interesting = batch_replay.step_while_time_is(&next_time);
641 if interesting { if let Some(meet) = meet.as_ref() { batch_replay.advance_buffer_by(meet); } }
642
643 // advance both `synth_times` and `times_slice`, marking this time interesting if in either.
644 while self.synth_times.last() == Some(&next_time) {
645 // We don't know enough about `next_time` to avoid putting it in to `times_current`.
646 // TODO: If we knew that the time derived from a canceled batch update, we could remove the time.
647 self.times_current.push(self.synth_times.pop().expect("failed to pop from synth_times")); // <-- TODO: this could be a min-heap.
648 interesting = true;
649 }
650 while times_slice.first() == Some(&next_time) {
651 // We know nothing about why we were warned about `next_time`, and must include it to scare future times.
652 self.times_current.push(times_slice[0].clone());
653 times_slice = ×_slice[1..];
654 meets_slice = &meets_slice[1..];
655 interesting = true;
656 }
657
658 // Times could also be interesting if an interesting time is less than them, as they would join
659 // and become the time itself. They may not equal the current time because whatever frontier we
660 // are tracking may not have advanced far enough.
661 // TODO: `batch_history` may or may not be super compact at this point, and so this check might
662 // yield false positives if not sufficiently compact. Maybe we should look into this and see.
663 interesting = interesting || batch_replay.buffer().iter().any(|&((_, ref t),_)| t.less_equal(&next_time));
664 interesting = interesting || self.times_current.iter().any(|t| t.less_equal(&next_time));
665
666 // We should only process times that are not in advance of `upper_limit`.
667 //
668 // We have no particular guarantee that known times will not be in advance of `upper_limit`.
669 // We may have the guarantee that synthetic times will not be, as we test against the limit
670 // before we add the time to `synth_times`.
671 if !upper_limit.less_equal(&next_time) {
672
673 // DETERMINATION (times only). Determine synthetic interesting times.
674 //
675 // Synthetic interesting times are produced differently for interesting and uninteresting
676 // times. An uninteresting time must join with an interesting time to become interesting,
677 // which means joins with `self.batch_history` and `self.times_current`. I think we can
678 // skip `self.synth_times` as we haven't gotten to them yet, but we will and they will be
679 // joined against everything.
680
681 // Any time, even uninteresting times, must be joined with the current accumulation of
682 // batch times as well as the current accumulation of `times_current`.
683 self.temporary.extend(batch_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
684 self.temporary.extend(self.times_current.iter().filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
685
686 // An interesting time additionally joins with `input` and `output` history and this round's
687 // produced output: it carries the seed, so those joins stay interesting (an uninteresting
688 // time does not, as `input`/`output` times are not themselves seeds). We advance the buffers
689 // by `meet` first, exactly as evaluation reads them below; by join preservation the advanced
690 // and unadvanced times spawn the same synthetics, so this matches the pre-split behavior.
691 if interesting {
692 if let Some(meet) = meet.as_ref() { input_replay.advance_buffer_by(meet) };
693 if let Some(meet) = meet.as_ref() { output_replay.advance_buffer_by(meet) };
694 self.temporary.extend(input_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
695 self.temporary.extend(output_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
696 self.temporary.extend(self.output_produced.iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
697 }
698 sort_dedup(&mut self.temporary);
699
700 // Introduce synthetic times, and re-organize if we add any.
701 let synth_len = self.synth_times.len();
702 for time in self.temporary.drain(..) {
703 // We can either service `join` now, or must delay for the future.
704 if upper_limit.less_equal(&time) {
705 debug_assert!(outputs.iter().any(|(t,_)| t.less_equal(&time)));
706 new_interesting.push(time);
707 }
708 else {
709 self.synth_times.push(time);
710 }
711 }
712 if self.synth_times.len() > synth_len {
713 self.synth_times.sort_by(|x,y| y.cmp(x));
714 self.synth_times.dedup();
715 }
716
717 // EVALUATION (values only).
718 // We should re-evaluate the computation if this is an interesting time.
719 // If the time is uninteresting (and our logic is sound) it is not possible for there to be
720 // output produced. This sounds like a good test to have for debug builds!
721 if interesting {
722
723 // Assemble the input collection at `next_time`. (`self.input_buffer` cleared just after use).
724 // The buffers were advanced by `meet` in the determination step above.
725 debug_assert!(self.input_buffer.is_empty());
726 for ((value, time), diff) in input_replay.buffer().iter() {
727 if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
728 }
729 for ((value, time), diff) in batch_replay.buffer().iter() {
730 if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
731 }
732 crate::consolidation::consolidate(&mut self.input_buffer);
733
734 // Assemble the output collection at `next_time`. (`self.output_buffer` cleared just after use).
735 for ((value, time), diff) in output_replay.buffer().iter() {
736 if time.less_equal(&next_time) { self.output_buffer.push((C2::owned_val(*value), diff.clone())); }
737 }
738 for ((value, time), diff) in self.output_produced.iter() {
739 if time.less_equal(&next_time) { self.output_buffer.push(((*value).to_owned(), diff.clone())); }
740 }
741 crate::consolidation::consolidate(&mut self.output_buffer);
742
743 // Apply user logic if non-empty input or output and see what happens!
744 if !self.input_buffer.is_empty() || !self.output_buffer.is_empty() {
745 logic(key, &self.input_buffer[..], &mut self.output_buffer, &mut self.update_buffer);
746 self.input_buffer.clear();
747 self.output_buffer.clear();
748
749 // Having subtracted output updates from user output, consolidate the results to determine
750 // if there is anything worth reporting. Note: this also orders the results by value, so
751 // that could make the above merging plan even easier.
752 //
753 // Stash produced updates into both capability-indexed buffers and `output_produced`.
754 // The two locations are important, in that we will compact `output_produced` as we move
755 // through times, but we cannot compact the output buffers because we need their actual
756 // times.
757 crate::consolidation::consolidate(&mut self.update_buffer);
758 if !self.update_buffer.is_empty() {
759
760 // We *should* be able to find a capability for `next_time`. Any thing else would
761 // indicate a logical error somewhere along the way; either we release a capability
762 // we should have kept, or we have computed the output incorrectly (or both!)
763 let idx = outputs.iter().rev().position(|(time, _)| time.less_equal(&next_time));
764 let idx = outputs.len() - idx.expect("failed to find index") - 1;
765 for (val, diff) in self.update_buffer.drain(..) {
766 self.output_produced.push(((val.clone(), next_time.clone()), diff.clone()));
767 outputs[idx].1.push((val, next_time.clone(), diff));
768 }
769
770 // Advance times in `self.output_produced` and consolidate the representation.
771 // NOTE: We only do this when we add records; it could be that there are situations
772 // where we want to consolidate even without changes (because an initially
773 // large collection can now be collapsed).
774 if let Some(meet) = meet.as_ref() { for entry in &mut self.output_produced { (entry.0).1.join_assign(meet); } }
775 crate::consolidation::consolidate(&mut self.output_produced);
776 }
777 }
778 }
779 }
780 else if interesting {
781 // We cannot process `next_time` now, and must delay it.
782 //
783 // I think we are probably only here because of an uninteresting time declared interesting,
784 // as initial interesting times are filtered to be in interval, and synthetic times are also
785 // filtered before introducing them to `self.synth_times`.
786 new_interesting.push(next_time.clone());
787 debug_assert!(outputs.iter().any(|(t,_)| t.less_equal(&next_time)))
788 }
789
790 // Update `meet` to track the meet of each source of times.
791 meet = None;
792 update_meet(&mut meet, batch_replay.meet());
793 update_meet(&mut meet, input_replay.meet());
794 update_meet(&mut meet, output_replay.meet());
795 for time in self.synth_times.iter() { update_meet(&mut meet, Some(time)); }
796 update_meet(&mut meet, meets_slice.first());
797
798 // Update `times_current` by the frontier.
799 if let Some(meet) = meet.as_ref() {
800 for time in self.times_current.iter_mut() {
801 *time = time.join(meet);
802 }
803 }
804
805 sort_dedup(&mut self.times_current);
806 }
807
808 // Normalize the representation of `new_interesting`, deduplicating and ordering.
809 sort_dedup(new_interesting);
810 }
811 }
812
813 /// Updates an optional meet by an optional time.
814 fn update_meet<T: Lattice+Clone>(meet: &mut Option<T>, other: Option<&T>) {
815 if let Some(time) = other {
816 if let Some(meet) = meet.as_mut() { meet.meet_assign(time); }
817 else { *meet = Some(time.clone()); }
818 }
819 }
820 }
821}
822
823/// A second [`ReduceTactic`], written directly from the incremental model in
824/// `formal/Differential/Model.lean`.
825///
826/// Per key it runs two phases over one cursor walk. Phase 1 (determination) computes the
827/// interesting times as the truncated join-closure over {input, output, seeds} — the model's
828/// `Reached` — advancing by meets so the synthetic set stays bounded. Phase 2 (application) walks
829/// exactly those times in order, maintaining tight input/output accumulations by meets, and emits
830/// the corrections — the model's `emit_correct`. Determination never consults the output produced
831/// this round (it finishes first), so this tactic embodies the proven algorithm exactly and is the
832/// clean subject for differential testing against [`cursors::CursorTactic`].
833pub(crate) mod reference {
834
835 use super::*;
836 use crate::lattice::Lattice;
837 use crate::operators::ValueHistory;
838
839 /// Drives a key-wise reduction with the model-derived [`ReferenceTactic`], the analogue of the
840 /// default [`super::reduce_trace`]. Same result contract; intended for differential testing of the
841 /// two tactics against each other. Re-exported (doc-hidden) from the parent module as the sole
842 /// public handle: the reference tactic is a testing and demonstration oracle, not a stable entry
843 /// point to build on.
844 pub fn reduce_trace_reference<'scope, Tr1, Bu, Tr2, L, P>(trace: Arranged<'scope, Tr1>, name: &str, logic: L, push: P) -> Arranged<'scope, TraceAgent<Tr2>>
845 where
846 Tr1: TraceReader<Batch: Navigable> + 'static,
847 Tr2: Trace<Batch: Navigable, Time = Tr1::Time> + 'static,
848 BatchCursor<Tr1>: Cursor<Time = Tr1::Time>,
849 for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = BatchKey<'a, Tr1>, ValOwn: Data, Time = Tr2::Time>,
850 Bu: Builder<Time=Tr2::Time, Output = Tr2::Batch, Input: Default> + 'static,
851 L: FnMut(BatchKey<'_, Tr1>, &[(BatchVal<'_, Tr1>, BatchDiff<Tr1>)], &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>, &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>)+'static,
852 P: FnMut(&mut Bu::Input, BatchKey<'_, Tr1>, &mut Vec<(BatchValOwn<Tr2>, Tr2::Time, BatchDiff<Tr2>)>) + 'static,
853 {
854 reduce_with_tactic(trace, name, ReferenceTactic::<Tr1::Batch, Tr2::Batch, Bu, L, P>::new(logic, push))
855 }
856
857
858 /// Updates an optional meet by an optional time.
859 fn update_meet<T: Lattice+Clone>(meet: &mut Option<T>, other: Option<&T>) {
860 if let Some(time) = other {
861 if let Some(meet) = meet.as_mut() { meet.meet_assign(time); }
862 else { *meet = Some(time.clone()); }
863 }
864 }
865
866 /// The model-derived [`ReduceTactic`]. Structurally a twin of [`cursors::CursorTactic`]; only the
867 /// per-key engine differs.
868 pub struct ReferenceTactic<B1, B2, Bu, L, P>
869 where
870 B1: BatchReader + Navigable,
871 B2: BatchReader<Time = B1::Time> + Navigable,
872 B1::Cursor: Cursor<Time = B1::Time>,
873 for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
874 {
875 logic: L,
876 push: P,
877 pending_keys: <B1::Cursor as Cursor>::KeyContainer,
878 pending_time: <B1::Cursor as Cursor>::TimeContainer,
879 next_pending_keys: <B1::Cursor as Cursor>::KeyContainer,
880 next_pending_time: <B1::Cursor as Cursor>::TimeContainer,
881 interesting_times: Vec<B1::Time>,
882 new_interesting_times: Vec<B1::Time>,
883 output_upper: Antichain<B1::Time>,
884 output_lower: Antichain<B1::Time>,
885 _marker: PhantomData<(B2, Bu)>,
886 }
887
888 impl<B1, B2, Bu, L, P> ReferenceTactic<B1, B2, Bu, L, P>
889 where
890 B1: BatchReader + Navigable,
891 B2: BatchReader<Time = B1::Time> + Navigable,
892 B1::Cursor: Cursor<Time = B1::Time>,
893 for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
894 {
895 /// Construct a tactic that applies `logic` to each key and shapes output with `push`.
896 pub fn new(logic: L, push: P) -> Self {
897 ReferenceTactic {
898 logic,
899 push,
900 pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
901 pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
902 next_pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
903 next_pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
904 interesting_times: Vec::new(),
905 new_interesting_times: Vec::new(),
906 output_upper: Antichain::from_elem(<B1::Time as Timestamp>::minimum()),
907 output_lower: Antichain::from_elem(<B1::Time as Timestamp>::minimum()),
908 _marker: PhantomData,
909 }
910 }
911 }
912
913 impl<B1, B2, Bu, L, P> ReduceTactic<B1, B2> for ReferenceTactic<B1, B2, Bu, L, P>
914 where
915 B1: BatchReader + Navigable,
916 B2: BatchReader<Time = B1::Time> + Navigable,
917 B1::Cursor: Cursor<Time = B1::Time>,
918 for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = B1::Time>,
919 Bu: Builder<Time = B1::Time, Output = B2, Input: Default>,
920 L: FnMut(<B1::Cursor as Cursor>::Key<'_>, &[(<B1::Cursor as Cursor>::Val<'_>, <B1::Cursor as Cursor>::Diff)], &mut Vec<(<B2::Cursor as Cursor>::ValOwn, <B2::Cursor as Cursor>::Diff)>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, <B2::Cursor as Cursor>::Diff)>),
921 P: FnMut(&mut Bu::Input, <B1::Cursor as Cursor>::Key<'_>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, B1::Time, <B2::Cursor as Cursor>::Diff)>),
922 {
923 fn retire(
924 &mut self,
925 source_batches: Vec<B1>,
926 output_batches: Vec<B2>,
927 input_batches: Vec<B1>,
928 lower: &Antichain<B1::Time>,
929 upper: &Antichain<B1::Time>,
930 held: &Antichain<B1::Time>,
931 ) -> (Vec<(B1::Time, B2)>, Antichain<B1::Time>)
932 {
933 let mut produced = Vec::new();
934
935 if held.elements().iter().any(|time| !upper.less_equal(time)) {
936
937 let (mut source_cursor, ref source_storage) = cursor_list(source_batches);
938 let (mut output_cursor, ref output_storage) = cursor_list(output_batches);
939 let (mut batch_cursor, ref batch_storage) = cursor_list(input_batches);
940
941 let mut buffers = Vec::<(B1::Time, Vec<(<B2::Cursor as Cursor>::ValOwn, B1::Time, <B2::Cursor as Cursor>::Diff)>)>::new();
942 let mut builders = Vec::new();
943 for time in held.elements().iter() {
944 buffers.push((time.clone(), Vec::new()));
945 builders.push(Bu::new());
946 }
947 let mut buffer = Bu::Input::default();
948
949 // Reuseable state for performing the computation.
950 let mut thinker = ReferenceThinker::new();
951
952 let mut pending_pos = 0;
953 while batch_cursor.key_valid(batch_storage) || pending_pos < self.pending_keys.len() {
954
955 let key1 = self.pending_keys.get(pending_pos);
956 let key2 = batch_cursor.get_key(batch_storage);
957 let key = match (key1, key2) {
958 (Some(key1), Some(key2)) => ::std::cmp::min(key1, key2),
959 (Some(key1), None) => key1,
960 (None, Some(key2)) => key2,
961 (None, None) => unreachable!(),
962 };
963
964 let prior_pos = pending_pos;
965 self.interesting_times.clear();
966 while self.pending_keys.get(pending_pos) == Some(key) {
967 let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pending_pos));
968 if !upper.less_equal(&owned_time) { self.interesting_times.push(owned_time); }
969 pending_pos += 1;
970 }
971
972 sort_dedup(&mut self.interesting_times);
973
974 if batch_cursor.get_key(batch_storage) == Some(key) || !self.interesting_times.is_empty() {
975
976 thinker.compute(
977 key,
978 (&mut source_cursor, source_storage),
979 (&mut output_cursor, output_storage),
980 (&mut batch_cursor, batch_storage),
981 &self.interesting_times,
982 &mut self.logic,
983 upper,
984 &mut buffers[..],
985 &mut self.new_interesting_times,
986 );
987
988 if batch_cursor.get_key(batch_storage) == Some(key) { batch_cursor.step_key(batch_storage); }
989
990 for pos in prior_pos .. pending_pos {
991 let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pos));
992 if upper.less_equal(&owned_time) { self.new_interesting_times.push(owned_time); }
993 }
994 sort_dedup(&mut self.new_interesting_times);
995 for time in self.new_interesting_times.drain(..) {
996 self.next_pending_keys.push_ref(key);
997 self.next_pending_time.push_own(&time);
998 }
999
1000 for index in 0 .. buffers.len() {
1001 buffers[index].1.sort_by(|x,y| x.0.cmp(&y.0));
1002 (self.push)(&mut buffer, key, &mut buffers[index].1);
1003 buffers[index].1.clear();
1004 builders[index].push(&mut buffer);
1005
1006 }
1007 }
1008 else {
1009 for pos in prior_pos .. pending_pos {
1010 self.next_pending_keys.push_ref(self.pending_keys.index(pos));
1011 self.next_pending_time.push_ref(self.pending_time.index(pos));
1012 }
1013 }
1014 }
1015 drop(thinker);
1016
1017 self.output_lower.clear();
1018 self.output_lower.extend(lower.borrow().iter().cloned());
1019
1020 for (index, builder) in builders.drain(..).enumerate() {
1021
1022 self.output_upper.clear();
1023 self.output_upper.extend(upper.borrow().iter().cloned());
1024 for time in &held.elements()[index + 1 ..] {
1025 self.output_upper.insert_ref(time);
1026 }
1027
1028 if self.output_upper.borrow() != self.output_lower.borrow() {
1029
1030 let description = Description::new(self.output_lower.clone(), self.output_upper.clone(), Antichain::from_elem(<B1::Time as Timestamp>::minimum()));
1031 let batch = builder.done(description);
1032
1033 produced.push((held.elements()[index].clone(), batch));
1034
1035 self.output_lower.clear();
1036 self.output_lower.extend(self.output_upper.borrow().iter().cloned());
1037 }
1038 }
1039 assert!(self.output_upper.borrow() == upper.borrow());
1040
1041 self.pending_keys.clear(); std::mem::swap(&mut self.next_pending_keys, &mut self.pending_keys);
1042 self.pending_time.clear(); std::mem::swap(&mut self.next_pending_time, &mut self.pending_time);
1043
1044 let mut frontier = Antichain::<B1::Time>::new();
1045 let mut owned_time = <B1::Time as Timestamp>::minimum();
1046 for pos in 0 .. self.pending_time.len() {
1047 <B1::Cursor as Cursor>::clone_time_onto(self.pending_time.index(pos), &mut owned_time);
1048 frontier.insert_ref(&owned_time);
1049 }
1050
1051 (produced, frontier)
1052 }
1053 else {
1054 (produced, held.clone())
1055 }
1056 }
1057 }
1058
1059 /// The two-phase per-key engine.
1060 ///
1061 /// Phase 1 (determination) reads the input/output/seed *times* and closes them into `active`
1062 /// (the interesting times) and the pended set — Model.lean's `Reached`, directly. Phase 2
1063 /// (application) walks the same, still-loaded histories for *values* and emits corrections.
1064 pub struct ReferenceThinker<V1, V2, V, T, D1, D2> {
1065 input_history: ValueHistory<V1, T, D1>,
1066 output_history: ValueHistory<V2, T, D2>,
1067 batch_history: ValueHistory<V1, T, D1>,
1068 input_buffer: Vec<(V1, D1)>,
1069 output_buffer: Vec<(V, D2)>,
1070 update_buffer: Vec<(V, D2)>,
1071 output_produced: Vec<((V, T), D2)>,
1072 // Phase 1 (the compacted closure): synthetic reached times still to visit, the reached times
1073 // in play as join partners, scratch for the joins, and suffix-meets of the supplied times.
1074 synth_times: Vec<T>,
1075 times_current: Vec<T>,
1076 temporary: Vec<T>,
1077 meets: Vec<T>,
1078 // Reusable time-only buffers for phase 1's `TimeReplay` walks. Pooled here (rather than in
1079 // `ValueHistory`) so the reference tactic pays for them and the standard value walk does not.
1080 batch_times: Vec<T>,
1081 input_times: Vec<T>,
1082 output_times: Vec<T>,
1083 // The interesting (in-band reached) times, handed from phase 1 to phase 2.
1084 active: Vec<T>,
1085 }
1086
1087 impl<V1, V2, V, T, D1, D2> ReferenceThinker<V1, V2, V, T, D1, D2>
1088 where
1089 V1: Copy + Ord,
1090 V2: Copy + Ord,
1091 V: Clone + Ord,
1092 T: Ord + Clone + Lattice + 'static,
1093 D1: Clone + crate::difference::Semigroup,
1094 D2: Clone + crate::difference::Semigroup,
1095 {
1096 pub fn new() -> Self {
1097 ReferenceThinker {
1098 input_history: ValueHistory::new(),
1099 output_history: ValueHistory::new(),
1100 batch_history: ValueHistory::new(),
1101 input_buffer: Vec::new(),
1102 output_buffer: Vec::new(),
1103 update_buffer: Vec::new(),
1104 output_produced: Vec::new(),
1105 synth_times: Vec::new(),
1106 times_current: Vec::new(),
1107 temporary: Vec::new(),
1108 meets: Vec::new(),
1109 batch_times: Vec::new(),
1110 input_times: Vec::new(),
1111 output_times: Vec::new(),
1112 active: Vec::new(),
1113 }
1114 }
1115
1116 #[inline(never)]
1117 pub fn compute<'a, K, C1, C2, C3, L>(
1118 &mut self,
1119 key: K,
1120 (source_cursor, source_storage): (&mut C1, &'a C1::Storage),
1121 (output_cursor, output_storage): (&mut C2, &'a C2::Storage),
1122 (batch_cursor, batch_storage): (&mut C3, &'a C3::Storage),
1123 times: &Vec<T>,
1124 logic: &mut L,
1125 upper_limit: &Antichain<T>,
1126 outputs: &mut [(T, Vec<(V, T, D2)>)],
1127 new_interesting: &mut Vec<T>)
1128 where
1129 C1: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
1130 C2: Cursor<Key<'a> = K, Val<'a> = V2, ValOwn = V, Time = T, Diff = D2>,
1131 C3: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
1132 K: Copy + Ord,
1133 L: FnMut(K, &[(V1, D1)], &mut Vec<(V, D2)>, &mut Vec<(V, D2)>),
1134 {
1135 // ================== PHASE 1 — DETERMINATION (`Reached`, compacted) ==================
1136 // The interesting times are Model.lean's `Reached` — but computed the non-quadratic way.
1137 // Walk the input/output/seed times in increasing order; a time is *reached* only via a
1138 // seed (a batch update, a due pending time, or a synthetic join of earlier reached times);
1139 // a reached in-band time joins against the live partners to spawn more reached times, and
1140 // a join beyond `upper` is pended. The live partner sets are kept an antichain by
1141 // `advance_buffer_by(meet)` — coincident times collapse under the running meet — so this is
1142 // the closure without the all-pairs blow-up. Time-only: `TimeReplay` reads the histories
1143 // without touching values or stepping the underlying `history`, so phase 2 can walk them.
1144 {
1145 // Suffix-meets of the supplied (due pending) times, consumed as we pass them.
1146 self.meets.clear();
1147 self.meets.extend(times.iter().cloned());
1148 for index in (1 .. self.meets.len()).rev() {
1149 self.meets[index-1] = self.meets[index-1].meet(&self.meets[index]);
1150 }
1151
1152 // Build each history, then read it time-only (leaving it intact for phase 2).
1153 drop(self.batch_history.replay_key(batch_cursor, batch_storage, key, None));
1154 drop(self.input_history.replay_key(source_cursor, source_storage, key, None));
1155 drop(self.output_history.replay_key(output_cursor, output_storage, key, None));
1156 let mut batch_replay = self.batch_history.replay_times(&mut self.batch_times);
1157 let mut input_replay = self.input_history.replay_times(&mut self.input_times);
1158 let mut output_replay = self.output_history.replay_times(&mut self.output_times);
1159
1160 self.synth_times.clear();
1161 self.times_current.clear();
1162 self.temporary.clear();
1163 self.active.clear();
1164
1165 let mut times_slice = ×[..];
1166 let mut meets_slice = &self.meets[..];
1167 let mut meet: Option<T> = None;
1168
1169 while let Some(next_time) = [ batch_replay.time(),
1170 times_slice.first(),
1171 input_replay.time(),
1172 output_replay.time(),
1173 self.synth_times.last(),
1174 ].into_iter().flatten().min().cloned() {
1175
1176 input_replay.step_while_time_is(&next_time);
1177 output_replay.step_while_time_is(&next_time);
1178
1179 // Reached via a seed: a batch update, a due pending time, or a synthetic join.
1180 // (Input/output times alone are not reached — they are only join partners.)
1181 let mut interesting = batch_replay.step_while_time_is(&next_time);
1182 if interesting { if let Some(m) = meet.as_ref() { batch_replay.advance_buffer_by(m); } }
1183 while self.synth_times.last() == Some(&next_time) {
1184 self.times_current.push(self.synth_times.pop().unwrap());
1185 interesting = true;
1186 }
1187 while times_slice.first() == Some(&next_time) {
1188 self.times_current.push(next_time.clone());
1189 times_slice = ×_slice[1..];
1190 meets_slice = &meets_slice[1..];
1191 interesting = true;
1192 }
1193 // Absorb: a time at or above a reached time is itself reached.
1194 interesting = interesting
1195 || batch_replay.buffer().iter().any(|t| t.less_equal(&next_time))
1196 || self.times_current.iter().any(|t| t.less_equal(&next_time));
1197
1198 if !upper_limit.less_equal(&next_time) {
1199 // A reached in-band time is `active`; join it against the live partners —
1200 // input/output (`joinBase`) and reached-so-far (`joinActive`) — for new times.
1201 if interesting {
1202 self.active.push(next_time.clone());
1203 if let Some(m) = meet.as_ref() { input_replay.advance_buffer_by(m); output_replay.advance_buffer_by(m); }
1204 self.temporary.extend(input_replay.buffer().iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
1205 self.temporary.extend(output_replay.buffer().iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
1206 }
1207 self.temporary.extend(batch_replay.buffer().iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
1208 self.temporary.extend(self.times_current.iter().filter(|t| !t.less_equal(&next_time)).map(|t| t.join(&next_time)));
1209 sort_dedup(&mut self.temporary);
1210
1211 let synth_len = self.synth_times.len();
1212 for time in self.temporary.drain(..) {
1213 if upper_limit.less_equal(&time) { new_interesting.push(time); } // pended
1214 else { self.synth_times.push(time); } // reached, later
1215 }
1216 if self.synth_times.len() > synth_len {
1217 self.synth_times.sort_by(|x,y| y.cmp(x));
1218 self.synth_times.dedup();
1219 }
1220 }
1221 else if interesting {
1222 new_interesting.push(next_time.clone());
1223 }
1224
1225 // Running meet (a lower bound on every time still to visit); compact the reached
1226 // partners `times_current` with it.
1227 meet = None;
1228 update_meet(&mut meet, batch_replay.meet());
1229 update_meet(&mut meet, input_replay.meet());
1230 update_meet(&mut meet, output_replay.meet());
1231 for time in self.synth_times.iter() { update_meet(&mut meet, Some(time)); }
1232 update_meet(&mut meet, meets_slice.first());
1233 if let Some(m) = meet.as_ref() {
1234 for time in self.times_current.iter_mut() { *time = time.join(m); }
1235 }
1236 sort_dedup(&mut self.times_current);
1237 }
1238
1239 sort_dedup(&mut self.active);
1240 }
1241
1242 // ===================== PHASE 2 — APPLICATION (`emit_correct`) =====================
1243 // Walk `self.active` in order over the SAME per-key edits (via `replay`, no cursor), keep
1244 // the input/output accumulations tight by advancing to the meet of the times still to be
1245 // produced, apply `logic`, and emit corrections.
1246 {
1247 self.meets.clear();
1248 self.meets.extend(self.active.iter().cloned());
1249 for index in (1 .. self.meets.len()).rev() {
1250 self.meets[index-1] = self.meets[index-1].meet(&self.meets[index]);
1251 }
1252
1253 // Walk the histories loaded (and left intact) by phase 1 — no cursor re-read, no
1254 // rebuild, no re-sort; just a fresh walk of the same sorted `history` for values.
1255 let mut batch_replay = self.batch_history.walk();
1256 let mut input_replay = self.input_history.walk();
1257 let mut output_replay = self.output_history.walk();
1258
1259 self.output_produced.clear();
1260
1261 for index in 0 .. self.active.len() {
1262 let next_time = self.active[index].clone();
1263 let meet = self.meets[index].clone();
1264
1265 // Phase 2 visits only the active times, so at each we must catch up the histories to
1266 // include every edit that will contribute to the accumulation at `next_time` (edits
1267 // at non-active times count too). `history` is sorted by the total `Ord` and `step`
1268 // pops the least, so we step the `Ord`-prefix `t <= next_time`, NOT the partial
1269 // `t.less_equal(next_time)`: the partial order interleaves with the sort, so an
1270 // `Ord`-earlier time incomparable to `next_time` would halt a `less_equal` walk early
1271 // and strand later edits that *are* `less_equal(next_time)`. Stepping the `Ord`-prefix
1272 // takes a superset; the `less_equal` filter below then selects the true `<= next_time`
1273 // edits. (Assembling with `less_equal` while stepping with `<=` is what the value-aware
1274 // cursor does via its full time-order frontier walk.)
1275 while input_replay.time().map_or(false, |t| *t <= next_time) { input_replay.step(); }
1276 while batch_replay.time().map_or(false, |t| *t <= next_time) { batch_replay.step(); }
1277 while output_replay.time().map_or(false, |t| *t <= next_time) { output_replay.step(); }
1278 input_replay.advance_buffer_by(&meet);
1279 batch_replay.advance_buffer_by(&meet);
1280 output_replay.advance_buffer_by(&meet);
1281
1282 debug_assert!(self.input_buffer.is_empty());
1283 for ((value, time), diff) in input_replay.buffer().iter() {
1284 if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
1285 }
1286 for ((value, time), diff) in batch_replay.buffer().iter() {
1287 if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
1288 }
1289 crate::consolidation::consolidate(&mut self.input_buffer);
1290
1291 for ((value, time), diff) in output_replay.buffer().iter() {
1292 if time.less_equal(&next_time) { self.output_buffer.push((C2::owned_val(*value), diff.clone())); }
1293 }
1294 for ((value, time), diff) in self.output_produced.iter() {
1295 if time.less_equal(&next_time) { self.output_buffer.push((value.clone(), diff.clone())); }
1296 }
1297 crate::consolidation::consolidate(&mut self.output_buffer);
1298
1299 if !self.input_buffer.is_empty() || !self.output_buffer.is_empty() {
1300 logic(key, &self.input_buffer[..], &mut self.output_buffer, &mut self.update_buffer);
1301 self.input_buffer.clear();
1302 self.output_buffer.clear();
1303
1304 crate::consolidation::consolidate(&mut self.update_buffer);
1305 if !self.update_buffer.is_empty() {
1306
1307 let idx = outputs.iter().rev().position(|(time, _)| time.less_equal(&next_time));
1308 let idx = outputs.len() - idx.expect("failed to find index") - 1;
1309 for (val, diff) in self.update_buffer.drain(..) {
1310 self.output_produced.push(((val.clone(), next_time.clone()), diff.clone()));
1311 outputs[idx].1.push((val, next_time.clone(), diff));
1312 }
1313
1314 for entry in &mut self.output_produced { (entry.0).1.join_assign(&meet); }
1315 crate::consolidation::consolidate(&mut self.output_produced);
1316 }
1317 }
1318 }
1319 }
1320 }
1321 }
1322}