1use std::hash::Hash;
13
14use columnar::{Columnar, Index, Len, Push};
15use differential_dataflow::Hashable;
16use differential_dataflow::difference::Semigroup;
17use differential_dataflow::lattice::Lattice;
18use differential_dataflow::trace::Batcher;
19use mz_timely_util::columnar::Column;
20use mz_timely_util::columnar::builder::ColumnBuilder;
21use mz_timely_util::columnar::chunk::{AccountedChunkBatcher, ChunkChunker, ComputeSpill};
22use mz_timely_util::columnar::columnar_exchange_data;
23use mz_timely_util::temporal::{Bucket, BucketChain, BucketRange, BucketTimestamp};
24use timely::Accountable;
25use timely::ExchangeData;
26use timely::container::{CapacityContainerBuilder, PushInto};
27use timely::dataflow::channels::pact::{Exchange, ExchangeCore};
28use timely::dataflow::operators::Operator;
29use timely::dataflow::{Stream, StreamVec};
30use timely::order::TotalOrder;
31use timely::progress::{Antichain, PathSummary, Timestamp};
32
33use crate::typedefs::MzData;
34
35pub trait TemporalBucketing<'scope, T: Timestamp>: Sized {
39 fn bucket(self, as_of: Antichain<T>, threshold: T::Summary) -> Self;
46}
47
48impl<'scope, T, D> TemporalBucketing<'scope, T> for Stream<'scope, T, Column<(D, T, mz_repr::Diff)>>
50where
51 T: Timestamp + Default + ExchangeData + MzData + BucketTimestamp + TotalOrder + Lattice,
52 for<'a> columnar::Ref<'a, T>: Copy + Ord,
53 D: ExchangeData + MzData + Ord + Clone + std::fmt::Debug + Hashable,
54 for<'a> columnar::Ref<'a, D>: Copy + Ord + Hash,
55 for<'a> columnar::Ref<'a, mz_repr::Diff>: Ord,
56 for<'a> <(D, T, mz_repr::Diff) as Columnar>::Container:
57 Push<columnar::Ref<'a, (D, T, mz_repr::Diff)>>,
58{
59 fn bucket(self, as_of: Antichain<T>, threshold: T::Summary) -> Self {
60 let scope = self.scope();
61 let logger = scope
62 .worker()
63 .logger_for("differential/arrange")
64 .map(Into::into);
65
66 type CB<D, T> = CapacityContainerBuilder<Column<(D, T, mz_repr::Diff)>>;
67
68 let pact = ExchangeCore::<ColumnBuilder<_>, _>::new_core(
69 columnar_exchange_data::<D, T, mz_repr::Diff>,
70 );
71 self.unary_frontier::<CB<D, T>, _, _, _>(pact, "Temporal delay", |cap, info| {
72 let mut chain = BucketChain::new(MergeBatcherWrapper::new(logger, info.global_id));
73 let activator = scope.activator_for(info.address);
74
75 let mut cap = Some(cap);
77
78 let mut buffer: Column<(D, T, mz_repr::Diff)> = Default::default();
81 let mut permutation: Vec<usize> = Vec::new();
83 let mut time_buf = T::minimum();
86
87 move |(input, frontier), output| {
88 let mut upper = Antichain::new();
91 for time1 in &frontier.frontier() {
92 for time2 in as_of.elements() {
93 if let Some(time) = threshold.results_in(&time1.join(time2)) {
95 upper.insert(time);
96 }
97 }
98 }
99
100 input.for_each_time(|time, data| {
101 let mut session = output.session_with_builder(&time);
102 for data in data {
103 let borrowed = data.borrow();
104
105 permutation.clear();
110 for index in 0..borrowed.len() {
111 let update = borrowed.get(index);
112 time_buf.copy_from(update.1);
113 if upper.less_equal(&time_buf) {
114 permutation.push(index);
115 } else {
116 session.give(update);
117 }
118 }
119
120 permutation.sort_unstable_by_key(|index| borrowed.get(*index).1);
124
125 let mut buffered_range = None;
127 for index in permutation.drain(..) {
128 let update = borrowed.get(index);
129 time_buf.copy_from(update.1);
130
131 let contained = match &buffered_range {
134 Some(range) => BucketRange::contains(range, &time_buf),
135 None => false,
136 };
137 if !contained {
138 if let Some(range) = buffered_range.take() {
139 let bucket = chain.find_mut(&range.start).expect("Must exist");
140 bucket.push_container(&mut buffer);
141 }
142 buffered_range =
143 Some(chain.range_of(&time_buf).expect("Must exist"));
144 }
145 buffer.push_into(update);
146 }
147
148 if let Some(range) = buffered_range.take() {
150 let bucket = chain.find_mut(&range.start).expect("Must exist");
151 bucket.push_container(&mut buffer);
152 }
153 }
154 });
155
156 let peeled = chain.peel(upper.borrow());
158 if let Some(cap) = cap.as_ref() {
159 let mut session = output.session_with_builder(cap);
160 for mut column in peeled.into_iter().flat_map(|x| x.done()) {
161 session.give_container(&mut column);
162 }
163 } else {
164 assert!(
166 peeled
167 .into_iter()
168 .flat_map(|x| x.done())
169 .all(|column| column.record_count() == 0),
170 "Unexpected data revealed without a cap."
171 );
172 }
173
174 if frontier.is_empty() || upper.is_empty() {
176 cap = None;
177 } else if let Some(cap) = cap.as_mut() {
178 cap.downgrade(&upper[0]);
180 }
181
182 let mut fuel = 1_000_000;
184 chain.restore(&mut fuel);
185 if fuel <= 0 {
186 activator.activate();
188 }
189 }
190 })
191 }
192}
193
194impl<'scope, T, D> TemporalBucketing<'scope, T> for StreamVec<'scope, T, (D, T, mz_repr::Diff)>
202where
203 T: Timestamp + Default + ExchangeData + MzData + BucketTimestamp + TotalOrder + Lattice,
204 D: ExchangeData + MzData + Ord + Clone + std::fmt::Debug + Hashable,
205 for<'a> <(D, T, mz_repr::Diff) as Columnar>::Container: Push<&'a (D, T, mz_repr::Diff)>,
206{
207 fn bucket(self, as_of: Antichain<T>, threshold: T::Summary) -> Self {
208 let scope = self.scope();
209 let logger = scope
210 .worker()
211 .logger_for("differential/arrange")
212 .map(Into::into);
213
214 let pact = Exchange::new(|(d, _, _): &(D, T, mz_repr::Diff)| d.hashed().into());
215 self.unary_frontier::<CapacityContainerBuilder<Vec<(D, T, mz_repr::Diff)>>, _, _, _>(
216 pact,
217 "Temporal delay",
218 |cap, info| {
219 let mut chain = BucketChain::new(MergeBatcherWrapper::new(logger, info.global_id));
220 let activator = scope.activator_for(info.address);
221
222 let mut cap = Some(cap);
224
225 let mut buffer: Column<(D, T, mz_repr::Diff)> = Default::default();
228
229 move |(input, frontier), output| {
230 let mut upper = Antichain::new();
233 for time1 in &frontier.frontier() {
234 for time2 in as_of.elements() {
235 if let Some(time) = threshold.results_in(&time1.join(time2)) {
237 upper.insert(time);
238 }
239 }
240 }
241
242 input.for_each_time(|time, data| {
243 let mut session = output.session_with_builder(&time);
244 for data in data {
245 let pass_through =
247 data.extract_if(.., |(_, t, _)| !upper.less_equal(t));
248 session.give_iterator(pass_through);
249
250 data.sort_unstable_by(|(_, t, _), (_, t2, _)| t.cmp(t2));
254
255 let mut drain = data.drain(..);
256 if let Some(update) = drain.next() {
257 let mut range = chain.range_of(&update.1).expect("Must exist");
258 buffer.push_into(&update);
259 for update in drain {
260 if !range.contains(&update.1) {
262 if !buffer.is_empty() {
265 let bucket =
266 chain.find_mut(&range.start).expect("Must exist");
267 bucket.push_container(&mut buffer);
268 }
269 range = chain.range_of(&update.1).expect("Must exist");
270 }
271 buffer.push_into(&update);
272 }
273
274 if !buffer.is_empty() {
276 let bucket = chain.find_mut(&range.start).expect("Must exist");
277 bucket.push_container(&mut buffer);
278 }
279 }
280 }
281 });
282
283 let peeled = chain.peel(upper.borrow());
285 if let Some(cap) = cap.as_ref() {
286 let mut session = output.session_with_builder(cap);
287 for column in peeled.into_iter().flat_map(|x| x.done()) {
288 session.give_iterator(
289 column
290 .borrow()
291 .into_index_iter()
292 .map(<(D, T, mz_repr::Diff)>::into_owned),
293 );
294 }
295 } else {
296 assert!(
298 peeled
299 .into_iter()
300 .flat_map(|x| x.done())
301 .all(|column| column.record_count() == 0),
302 "Unexpected data revealed without a cap."
303 );
304 }
305
306 if frontier.is_empty() || upper.is_empty() {
308 cap = None;
309 } else if let Some(cap) = cap.as_mut() {
310 cap.downgrade(&upper[0]);
312 }
313
314 let mut fuel = 1_000_000;
316 chain.restore(&mut fuel);
317 if fuel <= 0 {
318 activator.activate();
320 }
321 }
322 },
323 )
324 }
325}
326
327struct MergeBatcherWrapper<D, T, R>
346where
347 D: MzData,
348 T: MzData + Ord + Default + Timestamp + Lattice,
349 R: MzData + Semigroup + Default + for<'a> Semigroup<columnar::Ref<'a, R>>,
350 for<'a> columnar::Ref<'a, R>: Ord,
351{
352 logger: Option<differential_dataflow::logging::Logger>,
353 operator_id: usize,
354 chunker: ChunkChunker<(T, D), T, R, ComputeSpill>,
355 inner: AccountedChunkBatcher<(T, D), T, R, ComputeSpill>,
356}
357
358impl<D, T, R> MergeBatcherWrapper<D, T, R>
359where
360 D: MzData + Ord + Clone,
361 T: MzData + Ord + Clone + Default + Timestamp + Lattice,
362 R: MzData + Semigroup + Default + for<'a> Semigroup<columnar::Ref<'a, R>>,
363 for<'a> columnar::Ref<'a, R>: Ord,
364 for<'a> <D as Columnar>::Container: Push<columnar::Ref<'a, D>>,
365 for<'a> <T as Columnar>::Container: Push<columnar::Ref<'a, T>>,
366 for<'a> <R as Columnar>::Container: Push<&'a R>,
367{
368 fn new(logger: Option<differential_dataflow::logging::Logger>, operator_id: usize) -> Self {
370 Self {
371 logger: logger.clone(),
372 operator_id,
373 chunker: ChunkChunker::default(),
374 inner: AccountedChunkBatcher::new(logger, operator_id),
375 }
376 }
377
378 fn push_container(&mut self, buffer: &mut Column<(D, T, R)>) {
381 use columnar::Container as _;
382 use timely::container::{ContainerBuilder as _, PushInto as _};
383 if buffer.is_empty() {
384 return;
385 }
386 let mut staging = <((T, D), T, R) as Columnar>::Container::default();
390 let (data, time, diff) = buffer.borrow();
391 staging.0.0.extend_from_self(time, 0..time.len());
392 staging.0.1.extend_from_self(data, 0..data.len());
393 staging.1.extend_from_self(time, 0..time.len());
394 staging.2.extend_from_self(diff, 0..diff.len());
395 buffer.clear();
396 self.chunker.push_into(&mut Column::Typed(staging));
397 while let Some(chunk) = self.chunker.extract() {
398 self.inner.push_into(std::mem::take(chunk));
399 }
400 }
401
402 fn flush(&mut self) {
404 use timely::container::{ContainerBuilder as _, PushInto as _};
405 while let Some(chunk) = self.chunker.finish() {
406 self.inner.push_into(std::mem::take(chunk));
407 }
408 }
409
410 fn done(mut self) -> impl Iterator<Item = Column<(D, T, R)>> {
414 use columnar::Container as _;
415 self.flush();
416 let (chain, _description) = self.inner.seal(Antichain::new());
417 chain.into_iter().map(|chunk| {
418 let body = chunk.into_body();
419 let ((_, data), time, diff) = body.borrow();
420 let mut column = <(D, T, R) as Columnar>::Container::default();
421 column.0.extend_from_self(data, 0..data.len());
422 column.1.extend_from_self(time, 0..time.len());
423 column.2.extend_from_self(diff, 0..diff.len());
424 Column::Typed(column)
425 })
426 }
427}
428
429impl<D, T, R> Bucket for MergeBatcherWrapper<D, T, R>
430where
431 D: MzData + Ord + Clone,
432 T: MzData + Ord + Clone + Default + Lattice + BucketTimestamp,
433 R: MzData + Semigroup + Default + for<'a> Semigroup<columnar::Ref<'a, R>>,
434 for<'a> columnar::Ref<'a, R>: Ord,
435 for<'a> <D as Columnar>::Container: Push<columnar::Ref<'a, D>>,
436 for<'a> <T as Columnar>::Container: Push<columnar::Ref<'a, T>>,
437 for<'a> <R as Columnar>::Container: Push<&'a R>,
438{
439 type Timestamp = T;
440
441 fn split(mut self, timestamp: &Self::Timestamp, fuel: &mut i64) -> (Self, Self) {
442 use timely::container::PushInto as _;
443 self.flush();
444 let upper = Antichain::from_elem(timestamp.clone());
445 let mut lower = Self::new(self.logger.clone(), self.operator_id);
446 let (chain, _description) = self.inner.seal(upper);
451 for chunk in chain {
452 *fuel = fuel.saturating_sub(chunk.record_count());
453 lower.inner.push_into(chunk);
454 }
455 (lower, self)
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use std::collections::BTreeMap;
462
463 use mz_ore::pool::Pool;
464 use mz_repr::{Diff, Timestamp};
465 use mz_timely_util::columnar::chunk::set_spill_override;
466
467 use super::*;
468
469 type Wrapper = MergeBatcherWrapper<u64, Timestamp, Diff>;
470 type Update = (u64, Timestamp, Diff);
471
472 fn updates(round: u64) -> Vec<Update> {
475 (0..20_000u64)
476 .map(|i| {
477 let x = (i + round * 20_000).wrapping_mul(0x9E37_79B9_7F4A_7C15);
478 let data = (x >> 8) % 5_000;
479 let time = Timestamp::from(1_000 + (x >> 24) % 100_000);
480 let diff = if x & (1 << 40) == 0 {
481 Diff::ONE
482 } else {
483 Diff::MINUS_ONE
484 };
485 (data, time, diff)
486 })
487 .collect()
488 }
489
490 fn insert(chain: &mut BucketChain<Wrapper>, mut updates: Vec<Update>) {
493 updates.sort_by_key(|(_, time, _)| *time);
494 let mut buffer = Column::default();
495 let mut range: Option<BucketRange<Timestamp>> = None;
496 for update in &updates {
497 if !range
498 .as_ref()
499 .is_some_and(|range| range.contains(&update.1))
500 {
501 if let Some(range) = range.take() {
502 let bucket = chain.find_mut(&range.start).expect("must exist");
503 bucket.push_container(&mut buffer);
504 }
505 range = Some(chain.range_of(&update.1).expect("must exist"));
506 }
507 buffer.push_into(update);
508 }
509 if let Some(range) = range {
510 let bucket = chain.find_mut(&range.start).expect("must exist");
511 bucket.push_container(&mut buffer);
512 }
513 }
514
515 fn accumulate(
516 map: &mut BTreeMap<(u64, Timestamp), Diff>,
517 updates: impl Iterator<Item = Update>,
518 ) {
519 for (data, time, diff) in updates {
520 *map.entry((data, time)).or_default() += diff;
521 }
522 map.retain(|_, diff| !diff.is_zero());
523 }
524
525 fn peel_matches_reference() {
529 let mut chain = BucketChain::new(Wrapper::new(None, 0));
530 let mut reference = BTreeMap::new();
531 let mut lower = Timestamp::MIN;
532 for round in 0..6u64 {
533 let fresh: Vec<_> = updates(round)
534 .into_iter()
535 .filter(|(_, time, _)| lower <= *time)
536 .collect();
537 accumulate(&mut reference, fresh.iter().copied());
538 insert(&mut chain, fresh);
539
540 let upper = (round < 5).then(|| Timestamp::from(1_000 + (round + 1) * 17_000));
541 let frontier = Antichain::from_iter(upper);
542 let mut revealed = BTreeMap::new();
543 for column in chain
544 .peel(frontier.borrow())
545 .into_iter()
546 .flat_map(|x| x.done())
547 {
548 accumulate(
549 &mut revealed,
550 column.borrow().into_index_iter().map(Update::into_owned),
551 );
552 }
553 let mut expected = BTreeMap::new();
554 reference.retain(|(data, time), diff| {
555 if frontier.less_equal(time) {
556 true
557 } else {
558 expected.insert((*data, *time), *diff);
559 false
560 }
561 });
562 assert!(!expected.is_empty(), "round {round} reveals nothing");
563 assert_eq!(revealed, expected, "round {round}");
564
565 let mut fuel = 0;
566 while fuel <= 0 {
567 fuel = 1_000_000;
568 chain.restore(&mut fuel);
569 }
570 lower = upper.unwrap_or(Timestamp::MAX);
571 }
572 assert!(reference.is_empty());
573 }
574
575 #[mz_ore::test]
576 #[cfg_attr(miri, ignore)]
577 fn peel_matches_reference_resident() {
578 peel_matches_reference();
579 }
580
581 #[mz_ore::test]
582 #[cfg_attr(miri, ignore)]
583 fn peel_matches_reference_spilled() {
584 let pool = Pool::new().expect("pool creation");
585 set_spill_override(Some(pool.clone()));
586 peel_matches_reference();
587 set_spill_override(None);
588 assert!(pool.stats().inserts > 0, "nothing spilled");
589 }
590}