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 = ×[..];
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 = ×_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}