Skip to main content

differential_dataflow/operators/cursor/
reduce.rs

1//! Cursor-based reduce implementation.
2
3use std::marker::PhantomData;
4
5use timely::progress::{Antichain, Timestamp};
6
7use crate::Data;
8use crate::operators::arrange::{Arranged, TraceAgent};
9use crate::operators::reduce::{ReduceTactic, reduce_with_tactic, sort_dedup};
10use crate::trace::{Span, BatchCursor, BatchDiff, BatchVal, BatchValOwn, Builder, Cursor, Description, Navigable, Trace, TraceReader};
11use crate::trace::cursor::cursor_list;
12use crate::trace::implementations::containers::BatchContainer;
13
14/// The time type of the updates' cursor: the time coordinate the tactic works in.
15type TimeOf<B> = <<B as Navigable>::Cursor as Cursor>::Time;
16
17/// A key-wise reduction of values in an input trace.
18///
19/// This method exists to provide reduce functionality without opinions about qualifying trace types.
20///
21/// The `logic` closure is expected to take a key, accumulated input, and tentative accumulated output,
22/// and populate its final argument with whatever it feels to be appopriate updates. The behavior and
23/// correctness of the implementation rely on this making sense, and e.g. ideally the updates would if
24/// applied to the tentative output bring it in line with some function applied to the input.
25///
26/// The `push` closure is expected to clear its first argument, then populate it with the key and drain
27/// the value updates, as appropriate for the container. It is critical that it clear the container as
28/// the operator has no ability to do this otherwise, and failing to do so represents a leak from one
29/// key's computation to another, and will likely introduce non-determinism.
30pub fn reduce_trace<'scope, Tr1, Bu, Tr2, KC, L, P>(trace: Arranged<'scope, Tr1>, name: &str, logic: L, push: P) -> Arranged<'scope, TraceAgent<Tr2>>
31where
32    Tr1: TraceReader<Batch: Navigable> + 'static,
33    Tr2: Trace<Batch: Navigable, Time = Tr1::Time> + 'static,
34    KC: BatchContainer,
35    BatchCursor<Tr1>: Cursor<Time = Tr1::Time, KeyContainer = KC>,
36    for<'a> BatchCursor<Tr1>: Cursor<Key<'a> = KC::ReadItem<'a>>,
37    for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = KC::ReadItem<'a>, ValOwn: Data, Time = Tr2::Time>,
38    Bu: Builder<Time=Tr2::Time, Output: Into<Tr2::Batch>, Input: Default> + 'static,
39    L: FnMut(KC::ReadItem<'_>, &[(BatchVal<'_, Tr1>, BatchDiff<Tr1>)], &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>, &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>)+'static,
40    P: FnMut(&mut Bu::Input, KC::ReadItem<'_>, &mut Vec<(BatchValOwn<Tr2>, Tr2::Time, BatchDiff<Tr2>)>) + 'static,
41{
42    reduce_with_tactic(trace, name, CursorTactic::<Tr1::Batch, Tr2::Batch, Bu, L, P>::new(logic, push))
43}
44/// The conventional cursor-based [`ReduceTactic`].
45///
46/// It builds a [`CursorList`](crate::trace::cursor::CursorList) over the input, output, and new-batch
47/// updates and replays them together per key, applying `logic` and shaping output with `push`. It holds
48/// the outstanding synthetic interesting `(key, time)` moments across activations, and reasons only
49/// about times: capabilities are the driver's concern.
50
51/// The conventional cursor-based [`ReduceTactic`].
52pub struct CursorTactic<B1, B2, Bu, L, P>
53where
54    B1: Navigable,
55    B2: Navigable,
56    for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = TimeOf<B1>>,
57{
58    logic: L,
59    push: P,
60    // Outstanding `(key, time)` synthetic interesting moments, sorted by `(key, time)`, and the
61    // buffers into which we assemble the next round's moments.
62    pending_keys: <B1::Cursor as Cursor>::KeyContainer,
63    pending_time: <B1::Cursor as Cursor>::TimeContainer,
64    next_pending_keys: <B1::Cursor as Cursor>::KeyContainer,
65    next_pending_time: <B1::Cursor as Cursor>::TimeContainer,
66    // Buffers reused across activations.
67    interesting_times: Vec<TimeOf<B1>>,
68    new_interesting_times: Vec<TimeOf<B1>>,
69    // Output batches may need to be built piecemeal, and these temp storage help there.
70    _marker: PhantomData<(B2, Bu)>,
71}
72
73impl<B1, B2, Bu, L, P> CursorTactic<B1, B2, Bu, L, P>
74where
75    B1: Navigable,
76    B2: Navigable,
77    for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = TimeOf<B1>>,
78{
79    /// Construct a tactic that applies `logic` to each key and shapes output with `push`.
80    pub fn new(logic: L, push: P) -> Self {
81        CursorTactic {
82            logic,
83            push,
84            pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
85            pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
86            next_pending_keys: <B1::Cursor as Cursor>::KeyContainer::with_capacity(0),
87            next_pending_time: <B1::Cursor as Cursor>::TimeContainer::with_capacity(0),
88            interesting_times: Vec::new(),
89            new_interesting_times: Vec::new(),
90            _marker: PhantomData,
91        }
92    }
93}
94
95impl<B1, B2, Bu, L, P> ReduceTactic<TimeOf<B1>, B1, B2> for CursorTactic<B1, B2, Bu, L, P>
96where
97    B1: Navigable,
98    B2: Navigable,
99    for<'a> B2::Cursor: Cursor<Key<'a> = <B1::Cursor as Cursor>::Key<'a>, ValOwn: Data, Time = TimeOf<B1>>,
100    Bu: Builder<Time = TimeOf<B1>, Output: Into<B2>, Input: Default>,
101    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)>),
102    P: FnMut(&mut Bu::Input, <B1::Cursor as Cursor>::Key<'_>, &mut Vec<(<B2::Cursor as Cursor>::ValOwn, TimeOf<B1>, <B2::Cursor as Cursor>::Diff)>),
103{
104    fn retire(
105        &mut self,
106        source_batches: Vec<B1>,
107        output_batches: Vec<B2>,
108        input_batches: Vec<B1>,
109        lower: &Antichain<TimeOf<B1>>,
110        upper: &Antichain<TimeOf<B1>>,
111        held: &Antichain<TimeOf<B1>>,
112    ) -> (Option<Span<TimeOf<B1>, B2>>, Antichain<TimeOf<B1>>)
113    {
114        let mut produced = None;
115
116        // We have compute needs only if we hold a time in the interval [lower, upper); otherwise we
117        // could not transmit outputs even if they were (incorrectly) non-zero, and we leave the held
118        // times unchanged.
119        if held.elements().iter().any(|time| !upper.less_equal(time)) {
120
121            // cursors for navigating input, output, and new-batch updates.
122            let (mut source_cursor, ref source_storage) = cursor_list(source_batches);
123            let (mut output_cursor, ref output_storage) = cursor_list(output_batches);
124            let (mut batch_cursor, ref batch_storage) = cursor_list(input_batches);
125
126            // Prepare one output buffer and builder: the batch spans [lower, upper) and
127            // ships stamped with the held times that justify its contents.
128            let mut output_updates = Vec::<(<B2::Cursor as Cursor>::ValOwn, TimeOf<B1>, <B2::Cursor as Cursor>::Diff)>::new();
129            let mut builder = Bu::default();
130            // Temporary staging for output building.
131            let mut buffer = Bu::Input::default();
132
133            // Reuseable state for performing the computation.
134            let mut thinker = history_replay::HistoryReplayer::new();
135
136            // March through the keys we must work on, merging `batch_cursor` and pending keys.
137            // The interesting moments need to be in the interval to prompt work.
138            let mut pending_pos = 0;
139            while batch_cursor.key_valid(batch_storage) || pending_pos < self.pending_keys.len() {
140
141                // Determine the next key we will work on; could be synthetic, could be from a batch.
142                let key1 = self.pending_keys.get(pending_pos);
143                let key2 = batch_cursor.get_key(batch_storage);
144                let key = match (key1, key2) {
145                    (Some(key1), Some(key2)) => ::std::cmp::min(key1, key2),
146                    (Some(key1), None)       => key1,
147                    (None, Some(key2))       => key2,
148                    (None, None)             => unreachable!(),
149                };
150
151                // Populate `interesting_times` with interesting times not beyond `upper`.
152                // TODO: This could just be `pending_time` and indexes within `lower .. upper`.
153                let prior_pos = pending_pos;
154                self.interesting_times.clear();
155                while self.pending_keys.get(pending_pos) == Some(key) {
156                    let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pending_pos));
157                    if !upper.less_equal(&owned_time) { self.interesting_times.push(owned_time); }
158                    pending_pos += 1;
159                }
160
161                // tidy up times, removing redundancy.
162                sort_dedup(&mut self.interesting_times);
163
164                // If there are new updates, or pending times, we must investigate!
165                if batch_cursor.get_key(batch_storage) == Some(key) || !self.interesting_times.is_empty() {
166
167                    // do the per-key computation.
168                    thinker.compute(
169                        key,
170                        (&mut source_cursor, source_storage),
171                        (&mut output_cursor, output_storage),
172                        (&mut batch_cursor, batch_storage),
173                        &self.interesting_times,
174                        &mut self.logic,
175                        upper,
176                        &mut output_updates,
177                        held.elements(),
178                        &mut self.new_interesting_times,
179                    );
180
181                    // Advance the cursor if this key, so that the loop's validity check registers the work as done.
182                    if batch_cursor.get_key(batch_storage) == Some(key) { batch_cursor.step_key(batch_storage); }
183
184                    // Merge novel pending times with any prior pending times we did not process.
185                    // TODO: This could be a merge, not a sort_dedup, because both lists should be sorted.
186                    for pos in prior_pos .. pending_pos {
187                        let owned_time = <B1::Cursor as Cursor>::owned_time(self.pending_time.index(pos));
188                        if upper.less_equal(&owned_time) { self.new_interesting_times.push(owned_time); }
189                    }
190                    sort_dedup(&mut self.new_interesting_times);
191                    for time in self.new_interesting_times.drain(..) {
192                        self.next_pending_keys.push_ref(key);
193                        self.next_pending_time.push_own(&time);
194                    }
195
196                    // Sort the buffer by value and move into the builder.
197                    // TODO: This makes assumptions about at least one of (i) the stability of `sort_by`,
198                    //       (ii) that the buffer is time-ordered, and (iii) that the builders accept
199                    //       arbitrarily ordered times.
200                    output_updates.sort_by(|x,y| x.0.cmp(&y.0));
201                    (self.push)(&mut buffer, key, &mut output_updates);
202                    output_updates.clear();
203                    builder.push(&mut buffer);
204                }
205                else {
206                    // copy over the pending key and times.
207                    for pos in prior_pos .. pending_pos {
208                        self.next_pending_keys.push_ref(self.pending_keys.index(pos));
209                        self.next_pending_time.push_ref(self.pending_time.index(pos));
210                    }
211                }
212            }
213            // Drop to avoid lifetime issues that would lock `pending_{keys, time}`.
214            drop(thinker);
215
216            // Build the batch spanning the interval, and hand it back to the driver
217            // to ship and commit.
218            let description = Description::new(lower.clone(), upper.clone(), Antichain::from_elem(<TimeOf<B1> as Timestamp>::minimum()));
219            produced = Some(Span::new(description, builder.done().map(Into::into)));
220
221            // Refresh pending keys and times.
222            self.pending_keys.clear(); std::mem::swap(&mut self.next_pending_keys, &mut self.pending_keys);
223            self.pending_time.clear(); std::mem::swap(&mut self.next_pending_time, &mut self.pending_time);
224
225            // Compute the new frontier of interesting times for the operator to hold.
226            let mut frontier = Antichain::<TimeOf<B1>>::new();
227            let mut owned_time = <TimeOf<B1> as Timestamp>::minimum();
228            for pos in 0 .. self.pending_time.len() {
229                <B1::Cursor as Cursor>::clone_time_onto(self.pending_time.index(pos), &mut owned_time);
230                frontier.insert_ref(&owned_time);
231            }
232
233            (produced, frontier)
234        }
235        else {
236            // No work: leave the held times unchanged, so the driver's downgrade is a no-op.
237            (produced, held.clone())
238        }
239    }
240}
241
242
243/// Implementation based on replaying historical and new updates together.
244mod history_replay {
245
246    use timely::progress::Antichain;
247
248    use crate::lattice::Lattice;
249    use crate::operators::cursor::history::replay_key;
250    use crate::operators::history::ValueHistory;
251    use crate::trace::Cursor;
252
253    use crate::operators::reduce::sort_dedup;
254
255    /// The `HistoryReplayer` is a compute strategy based on moving through existing inputs, interesting times, etc in
256    /// time order, maintaining consolidated representations of updates with respect to future interesting times.
257    pub struct HistoryReplayer<V1, V2, V, T, D1, D2> {
258        input_history: ValueHistory<V1, T, D1>,
259        output_history: ValueHistory<V2, T, D2>,
260        batch_history: ValueHistory<V1, T, D1>,
261        input_buffer: Vec<(V1, D1)>,
262        output_buffer: Vec<(V, D2)>,
263        update_buffer: Vec<(V, D2)>,
264        output_produced: Vec<((V, T), D2)>,
265        synth_times: Vec<T>,
266        meets: Vec<T>,
267        times_current: Vec<T>,
268        temporary: Vec<T>,
269    }
270
271    impl<V1, V2, V, T, D1, D2> HistoryReplayer<V1, V2, V, T, D1, D2>
272    where
273        V1: Copy + Ord,
274        V2: Copy + Ord,
275        V: Clone + Ord,
276        T: Ord + Clone + Lattice,
277        D1: Clone + crate::difference::Semigroup,
278        D2: Clone + crate::difference::Semigroup,
279    {
280        pub fn new() -> Self {
281            HistoryReplayer {
282                input_history: ValueHistory::new(),
283                output_history: ValueHistory::new(),
284                batch_history: ValueHistory::new(),
285                input_buffer: Vec::new(),
286                output_buffer: Vec::new(),
287                update_buffer: Vec::new(),
288                output_produced: Vec::new(),
289                synth_times: Vec::new(),
290                meets: Vec::new(),
291                times_current: Vec::new(),
292                temporary: Vec::new(),
293            }
294        }
295        #[inline(never)]
296        pub fn compute<'a, K, C1, C2, C3, L>(
297            &mut self,
298            key: K,
299            (source_cursor, source_storage): (&mut C1, &'a C1::Storage),
300            (output_cursor, output_storage): (&mut C2, &'a C2::Storage),
301            (batch_cursor, batch_storage): (&mut C3, &'a C3::Storage),
302            times: &Vec<T>,
303            logic: &mut L,
304            upper_limit: &Antichain<T>,
305            outputs: &mut Vec<(V, T, D2)>,
306            held: &[T],
307            new_interesting: &mut Vec<T>)
308        where
309            C1: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
310            C2: Cursor<Key<'a> = K, Val<'a> = V2, ValOwn = V, Time = T, Diff = D2>,
311            C3: Cursor<Key<'a> = K, Val<'a> = V1, Time = T, Diff = D1>,
312            K: Copy + Ord,
313            L: FnMut(K, &[(V1, D1)], &mut Vec<(V, D2)>, &mut Vec<(V, D2)>),
314        {
315
316            // The work we need to perform is at times defined principally by the contents of `batch_cursor`
317            // and `times`, respectively "new work we just received" and "old times we were warned about".
318            //
319            // Our first step is to identify these times, so that we can use them to restrict the amount of
320            // information we need to recover from `input` and `output`; as all times of interest will have
321            // some time from `batch_cursor` or `times`, we can compute their meet and advance all other
322            // loaded times by performing the lattice `join` with this value.
323
324            // Load the batch contents.
325            let mut batch_replay = replay_key(&mut self.batch_history, batch_cursor, batch_storage, key, None);
326
327            // We determine the meet of times we must reconsider (those from `batch` and `times`). This meet
328            // can be used to advance other historical times, which may consolidate their representation. As
329            // a first step, we determine the meets of each *suffix* of `times`, which we will use as we play
330            // history forward.
331
332            self.meets.clear();
333            self.meets.extend(times.iter().cloned());
334            for index in (1 .. self.meets.len()).rev() {
335                self.meets[index-1] = self.meets[index-1].meet(&self.meets[index]);
336            }
337
338            // Determine the meet of times in `batch` and `times`.
339            let mut meet = None;
340            update_meet(&mut meet, self.meets.get(0));
341            update_meet(&mut meet, batch_replay.meet());
342
343            // Having determined the meet, we can load the input and output histories, where we
344            // advance all times by joining them with `meet`. The resulting times are more compact
345            // and guaranteed to accumulate identically for times greater or equal to `meet`.
346
347            // Load the input and output histories.
348            let mut input_replay =
349            replay_key(&mut self.input_history, source_cursor, source_storage, key, meet.as_ref());
350            let mut output_replay =
351            replay_key(&mut self.output_history, output_cursor, output_storage, key, meet.as_ref());
352
353            self.synth_times.clear();
354            self.times_current.clear();
355            self.output_produced.clear();
356
357            // The frontier of times we may still consider.
358            // Derived from frontiers of our update histories, supplied times, and synthetic times.
359
360            let mut times_slice = &times[..];
361            let mut meets_slice = &self.meets[..];
362
363            // We have candidate times from `batch` and `times`, as well as times identified by either
364            // `input` or `output`. Finally, we may have synthetic times produced as the join of times
365            // we consider in the course of evaluation. As long as any of these times exist, we need to
366            // keep examining times.
367            while let Some(next_time) = [   batch_replay.time(),
368                                            times_slice.first(),
369                                            input_replay.time(),
370                                            output_replay.time(),
371                                            self.synth_times.last(),
372                                        ].into_iter().flatten().min().cloned() {
373
374                // Advance input and output history replayers. This marks applicable updates as active.
375                input_replay.step_while_time_is(&next_time);
376                output_replay.step_while_time_is(&next_time);
377
378                // One of our goals is to determine if `next_time` is "interesting", meaning whether we
379                // have any evidence that we should re-evaluate the user logic at this time. For a time
380                // to be "interesting" it would need to be the join of times that include either a time
381                // from `batch`, `times`, or `synth`. Neither `input` nor `output` times are sufficient.
382
383                // Advance batch history, and capture whether an update exists at `next_time`.
384                let mut interesting = batch_replay.step_while_time_is(&next_time);
385                if interesting { if let Some(meet) = meet.as_ref() { batch_replay.advance_buffer_by(meet); } }
386
387                // advance both `synth_times` and `times_slice`, marking this time interesting if in either.
388                while self.synth_times.last() == Some(&next_time) {
389                    // We don't know enough about `next_time` to avoid putting it in to `times_current`.
390                    // TODO: If we knew that the time derived from a canceled batch update, we could remove the time.
391                    self.times_current.push(self.synth_times.pop().expect("failed to pop from synth_times")); // <-- TODO: this could be a min-heap.
392                    interesting = true;
393                }
394                while times_slice.first() == Some(&next_time) {
395                    // We know nothing about why we were warned about `next_time`, and must include it to scare future times.
396                    self.times_current.push(times_slice[0].clone());
397                    times_slice = &times_slice[1..];
398                    meets_slice = &meets_slice[1..];
399                    interesting = true;
400                }
401
402                // Times could also be interesting if an interesting time is less than them, as they would join
403                // and become the time itself. They may not equal the current time because whatever frontier we
404                // are tracking may not have advanced far enough.
405                // TODO: `batch_history` may or may not be super compact at this point, and so this check might
406                //       yield false positives if not sufficiently compact. Maybe we should look into this and see.
407                interesting = interesting || batch_replay.buffer().iter().any(|&((_, ref t),_)| t.less_equal(&next_time));
408                interesting = interesting || self.times_current.iter().any(|t| t.less_equal(&next_time));
409
410                // We should only process times that are not in advance of `upper_limit`.
411                //
412                // We have no particular guarantee that known times will not be in advance of `upper_limit`.
413                // We may have the guarantee that synthetic times will not be, as we test against the limit
414                // before we add the time to `synth_times`.
415                if !upper_limit.less_equal(&next_time) {
416
417                    // DETERMINATION (times only). Determine synthetic interesting times.
418                    //
419                    // Synthetic interesting times are produced differently for interesting and uninteresting
420                    // times. An uninteresting time must join with an interesting time to become interesting,
421                    // which means joins with `self.batch_history` and  `self.times_current`. I think we can
422                    // skip `self.synth_times` as we haven't gotten to them yet, but we will and they will be
423                    // joined against everything.
424
425                    // Any time, even uninteresting times, must be joined with the current accumulation of
426                    // batch times as well as the current accumulation of `times_current`.
427                    self.temporary.extend(batch_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
428                    self.temporary.extend(self.times_current.iter().filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
429
430                    // An interesting time additionally joins with `input` and `output` history and this round's
431                    // produced output: it carries the seed, so those joins stay interesting (an uninteresting
432                    // time does not, as `input`/`output` times are not themselves seeds). We advance the buffers
433                    // by `meet` first, exactly as evaluation reads them below; by join preservation the advanced
434                    // and unadvanced times spawn the same synthetics, so this matches the pre-split behavior.
435                    if interesting {
436                        if let Some(meet) = meet.as_ref() { input_replay.advance_buffer_by(meet) };
437                        if let Some(meet) = meet.as_ref() { output_replay.advance_buffer_by(meet) };
438                        self.temporary.extend(input_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
439                        self.temporary.extend(output_replay.buffer().iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
440                        self.temporary.extend(self.output_produced.iter().map(|((_,time),_)| time).filter(|time| !time.less_equal(&next_time)).map(|time| time.join(&next_time)));
441                    }
442                    sort_dedup(&mut self.temporary);
443
444                    // Introduce synthetic times, and re-organize if we add any.
445                    let synth_len = self.synth_times.len();
446                    for time in self.temporary.drain(..) {
447                        // We can either service `join` now, or must delay for the future.
448                        if upper_limit.less_equal(&time) {
449                            debug_assert!(held.iter().any(|t| t.less_equal(&time)));
450                            new_interesting.push(time);
451                        }
452                        else {
453                            self.synth_times.push(time);
454                        }
455                    }
456                    if self.synth_times.len() > synth_len {
457                        self.synth_times.sort_by(|x,y| y.cmp(x));
458                        self.synth_times.dedup();
459                    }
460
461                    // EVALUATION (values only).
462                    // We should re-evaluate the computation if this is an interesting time.
463                    // If the time is uninteresting (and our logic is sound) it is not possible for there to be
464                    // output produced. This sounds like a good test to have for debug builds!
465                    if interesting {
466
467                        // Assemble the input collection at `next_time`. (`self.input_buffer` cleared just after use).
468                        // The buffers were advanced by `meet` in the determination step above.
469                        debug_assert!(self.input_buffer.is_empty());
470                        for ((value, time), diff) in input_replay.buffer().iter() {
471                            if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
472                        }
473                        for ((value, time), diff) in batch_replay.buffer().iter() {
474                            if time.less_equal(&next_time) { self.input_buffer.push((*value, diff.clone())); }
475                        }
476                        crate::consolidation::consolidate(&mut self.input_buffer);
477
478                        // Assemble the output collection at `next_time`. (`self.output_buffer` cleared just after use).
479                        for ((value, time), diff) in output_replay.buffer().iter() {
480                            if time.less_equal(&next_time) { self.output_buffer.push((C2::owned_val(*value), diff.clone())); }
481                        }
482                        for ((value, time), diff) in self.output_produced.iter() {
483                            if time.less_equal(&next_time) { self.output_buffer.push(((*value).to_owned(), diff.clone())); }
484                        }
485                        crate::consolidation::consolidate(&mut self.output_buffer);
486
487                        // Apply user logic if non-empty input or output and see what happens!
488                        if !self.input_buffer.is_empty() || !self.output_buffer.is_empty() {
489                            logic(key, &self.input_buffer[..], &mut self.output_buffer, &mut self.update_buffer);
490                            self.input_buffer.clear();
491                            self.output_buffer.clear();
492
493                            // Having subtracted output updates from user output, consolidate the results to determine
494                            // if there is anything worth reporting. Note: this also orders the results by value, so
495                            // that could make the above merging plan even easier.
496                            //
497                            // Stash produced updates into both capability-indexed buffers and `output_produced`.
498                            // The two locations are important, in that we will compact `output_produced` as we move
499                            // through times, but we cannot compact the output buffers because we need their actual
500                            // times.
501                            crate::consolidation::consolidate(&mut self.update_buffer);
502                            if !self.update_buffer.is_empty() {
503
504                                // We *should* be able to find a capability for `next_time`. Any thing else would
505                                // indicate a logical error somewhere along the way; either we release a capability
506                                // we should have kept, or we have computed the output incorrectly (or both!)
507                                assert!(held.iter().any(|time| time.less_equal(&next_time)), "failed to find capability");
508                                for (val, diff) in self.update_buffer.drain(..) {
509                                    self.output_produced.push(((val.clone(), next_time.clone()), diff.clone()));
510                                    outputs.push((val, next_time.clone(), diff));
511                                }
512
513                                // Advance times in `self.output_produced` and consolidate the representation.
514                                // NOTE: We only do this when we add records; it could be that there are situations
515                                //       where we want to consolidate even without changes (because an initially
516                                //       large collection can now be collapsed).
517                                if let Some(meet) = meet.as_ref() { for entry in &mut self.output_produced { (entry.0).1.join_assign(meet); } }
518                                crate::consolidation::consolidate(&mut self.output_produced);
519                            }
520                        }
521                    }
522                }
523                else if interesting {
524                    // We cannot process `next_time` now, and must delay it.
525                    //
526                    // I think we are probably only here because of an uninteresting time declared interesting,
527                    // as initial interesting times are filtered to be in interval, and synthetic times are also
528                    // filtered before introducing them to `self.synth_times`.
529                    new_interesting.push(next_time.clone());
530                    debug_assert!(held.iter().any(|t| t.less_equal(&next_time)))
531                }
532
533                // Update `meet` to track the meet of each source of times.
534                meet = None;
535                update_meet(&mut meet, batch_replay.meet());
536                update_meet(&mut meet, input_replay.meet());
537                update_meet(&mut meet, output_replay.meet());
538                for time in self.synth_times.iter() { update_meet(&mut meet, Some(time)); }
539                update_meet(&mut meet, meets_slice.first());
540
541                // Update `times_current` by the frontier.
542                if let Some(meet) = meet.as_ref() {
543                    for time in self.times_current.iter_mut() {
544                        *time = time.join(meet);
545                    }
546                }
547
548                sort_dedup(&mut self.times_current);
549            }
550
551            // Normalize the representation of `new_interesting`, deduplicating and ordering.
552            sort_dedup(new_interesting);
553        }
554    }
555
556    /// Updates an optional meet by an optional time.
557    fn update_meet<T: Lattice+Clone>(meet: &mut Option<T>, other: Option<&T>) {
558        if let Some(time) = other {
559            if let Some(meet) = meet.as_mut() { meet.meet_assign(time); }
560            else { *meet = Some(time.clone()); }
561        }
562    }
563}