Skip to main content

differential_dataflow/trace/implementations/
merge_batcher.rs

1//! A `Batcher` implementation based on merge sort.
2//!
3//! The `MergeBatcher` requires a "merger" that implements the [`Merger`] trait, which provides
4//! hooks for manipulating sorted "chains" of chunks as needed by the merge batcher: merging
5//! chunks and also splitting them apart based on time.
6//!
7//! Callers feed already-chunked, sorted-and-consolidated input into the batcher via [`PushInto`].
8//! Forming such chunks from raw data is the responsibility of the caller (typically a chunker
9//! living in the surrounding dataflow operator).
10
11use timely::progress::frontier::AntichainRef;
12use timely::progress::{frontier::Antichain, Timestamp};
13use timely::container::PushInto;
14
15use crate::logging::{BatcherEvent, Logger};
16use crate::trace::{Batcher, Description};
17
18/// Creates batches from chunks of sorted, consolidated tuples.
19pub struct MergeBatcher<M: Merger> {
20    /// Sorted, consolidated chains, each paired with its cached summed update count.
21    ///
22    /// The cached count is the chain's *merge weight*: the geometric ladder weighs
23    /// chains by updates, not chunk counts, since regrading decouples the two. A
24    /// chain is immutable until merged, so the weight is computed once at push.
25    ///
26    /// Do not push/pop directly but use the corresponding functions ([`Self::chain_push`]/[`Self::chain_pop`]).
27    chains: Vec<(usize, Vec<M::Chunk>)>,
28    /// Stash of empty chunks, recycled through the merging process.
29    stash: Vec<M::Chunk>,
30    /// Merges consolidated chunks, and extracts the subset of an update chain that lies in an interval of time.
31    merger: M,
32    /// Current lower frontier, we sealed up to here.
33    lower: Antichain<M::Time>,
34    /// The lower-bound frontier of the data, after the last call to seal.
35    frontier: Antichain<M::Time>,
36    /// Logger for size accounting.
37    logger: Option<Logger>,
38    /// Timely operator ID.
39    operator_id: usize,
40}
41
42impl<M> Batcher for MergeBatcher<M>
43where
44    M: Merger<Time: Timestamp>,
45{
46    type Time = M::Time;
47    type Output = M::Chunk;
48
49    fn new(logger: Option<Logger>, operator_id: usize) -> Self {
50        Self {
51            logger,
52            operator_id,
53            merger: M::default(),
54            chains: Vec::new(),
55            stash: Vec::new(),
56            frontier: Antichain::new(),
57            lower: Antichain::from_elem(M::Time::minimum()),
58        }
59    }
60
61    // Sealing a batch means finding those updates with times not greater or equal to any time
62    // in `upper`. All updates must have time greater or equal to the previously used `upper`,
63    // which we call `lower`, by assumption that after sealing a batcher we receive no more
64    // updates with times not greater or equal to `upper`.
65    fn seal(&mut self, upper: Antichain<M::Time>) -> (Vec<Self::Output>, Description<M::Time>) {
66        // Merge all remaining chains into a single chain.
67        while self.chains.len() > 1 {
68            let list1 = self.chain_pop().unwrap();
69            let list2 = self.chain_pop().unwrap();
70            let merged = self.merge_by(list1, list2);
71            self.chain_push(merged);
72        }
73        let merged = self.chain_pop().unwrap_or_default();
74
75        // Extract readied data.
76        let mut kept = Vec::new();
77        let mut readied = Vec::new();
78        self.frontier.clear();
79
80        self.merger.extract(merged, upper.borrow(), &mut self.frontier, &mut readied, &mut kept, &mut self.stash);
81
82        if !kept.is_empty() {
83            self.chain_push(kept);
84        }
85
86        self.stash.clear();
87
88        let description = Description::new(self.lower.clone(), upper.clone(), Antichain::from_elem(M::Time::minimum()));
89        self.lower = upper;
90        (readied, description)
91    }
92
93    /// The frontier of elements remaining after the most recent call to `self.seal`.
94    #[inline]
95    fn frontier(&mut self) -> AntichainRef<'_, M::Time> {
96        self.frontier.borrow()
97    }
98}
99
100impl<M: Merger> PushInto<M::Chunk> for MergeBatcher<M> {
101    fn push_into(&mut self, chunk: M::Chunk) {
102        self.insert_chain(vec![chunk]);
103    }
104}
105
106impl<M: Merger> MergeBatcher<M> {
107    /// Insert a chain and maintain chain properties: Chains are geometrically sized
108    /// (by summed updates) and ordered by decreasing update weight.
109    fn insert_chain(&mut self, chain: Vec<M::Chunk>) {
110        if !chain.is_empty() {
111            self.chain_push(chain);
112            while self.chains.len() > 1 && (self.chains[self.chains.len() - 1].0 >= self.chains[self.chains.len() - 2].0 / 2) {
113                let list1 = self.chain_pop().unwrap();
114                let list2 = self.chain_pop().unwrap();
115                let merged = self.merge_by(list1, list2);
116                self.chain_push(merged);
117            }
118        }
119    }
120
121    // merges two sorted input lists into one sorted output list.
122    fn merge_by(&mut self, list1: Vec<M::Chunk>, list2: Vec<M::Chunk>) -> Vec<M::Chunk> {
123        // TODO: `list1` and `list2` get dropped; would be better to reuse?
124        let mut output = Vec::with_capacity(list1.len() + list2.len());
125        self.merger.merge(list1, list2, &mut output, &mut self.stash);
126
127        output
128    }
129
130    /// Pop a chain and account size changes.
131    #[inline]
132    fn chain_pop(&mut self) -> Option<Vec<M::Chunk>> {
133        let (_weight, chain) = self.chains.pop()?;
134        self.account(chain.iter().map(Self::record), -1);
135        Some(chain)
136    }
137
138    /// Push a chain and account size changes.
139    ///
140    /// Caches the chain's summed update count alongside it for the ladder.
141    #[inline]
142    fn chain_push(&mut self, chain: Vec<M::Chunk>) {
143        let weight = chain.iter().map(M::len).sum();
144        self.account(chain.iter().map(Self::record), 1);
145        self.chains.push((weight, chain));
146    }
147
148    /// The `(records, size, capacity, allocations)` logger tuple for one chunk,
149    /// assembled from the two focused `Merger` methods.
150    #[inline]
151    fn record(chunk: &M::Chunk) -> (usize, usize, usize, usize) {
152        let (size, capacity, allocations) = M::allocation(chunk);
153        (M::len(chunk), size, capacity, allocations)
154    }
155
156    /// Account size changes. Only performs work if a logger exists.
157    ///
158    /// Calculate the size based on the iterator passed along, with each attribute
159    /// multiplied by `diff`. Usually, one wants to pass 1 or -1 as the diff.
160    #[inline]
161    fn account<I: IntoIterator<Item = (usize, usize, usize, usize)>>(&self, items: I, diff: isize) {
162        if let Some(logger) = &self.logger {
163            let (mut records, mut size, mut capacity, mut allocations) = (0isize, 0isize, 0isize, 0isize);
164            for (records_, size_, capacity_, allocations_) in items {
165                records = records.saturating_add_unsigned(records_);
166                size = size.saturating_add_unsigned(size_);
167                capacity = capacity.saturating_add_unsigned(capacity_);
168                allocations = allocations.saturating_add_unsigned(allocations_);
169            }
170            logger.log(BatcherEvent {
171                operator: self.operator_id,
172                records_diff: records * diff,
173                size_diff: size * diff,
174                capacity_diff: capacity * diff,
175                allocations_diff: allocations * diff,
176            })
177        }
178    }
179}
180
181impl<M: Merger> Drop for MergeBatcher<M> {
182    fn drop(&mut self) {
183        // Cleanup chain to retract accounting information.
184        while self.chain_pop().is_some() {}
185    }
186}
187
188/// A trait to describe interesting moments in a merge batcher.
189pub trait Merger: Default {
190    /// The internal representation of chunks of data.
191    type Chunk: Default;
192    /// The type of time in frontiers to extract updates.
193    type Time;
194    /// Merge chains into an output chain.
195    fn merge(&mut self, list1: Vec<Self::Chunk>, list2: Vec<Self::Chunk>, output: &mut Vec<Self::Chunk>, stash: &mut Vec<Self::Chunk>);
196    /// Extract ready updates based on the `upper` frontier.
197    fn extract(
198        &mut self,
199        merged: Vec<Self::Chunk>,
200        upper: AntichainRef<Self::Time>,
201        frontier: &mut Antichain<Self::Time>,
202        readied: &mut Vec<Self::Chunk>,
203        kept: &mut Vec<Self::Chunk>,
204        stash: &mut Vec<Self::Chunk>,
205    );
206
207    /// The number of updates in a chunk.
208    ///
209    /// Drives the geometric ladder (chains are weighed by summed updates, not chunk
210    /// counts, since regrading decouples the two) and the `records` field of the
211    /// size logger.
212    fn len(chunk: &Self::Chunk) -> usize;
213
214    /// Backing-allocation figures for a chunk: `(size, capacity, allocations)`, for
215    /// the size logger's memory telemetry.
216    ///
217    /// Defaults to zero — most chunk types do not track this. Override to report
218    /// real figures (e.g. Materialize's memory accounting).
219    fn allocation(_chunk: &Self::Chunk) -> (usize, usize, usize) { (0, 0, 0) }
220}
221
222/// A `Merger` implementation for vector update containers.
223pub mod vec {
224
225    use std::marker::PhantomData;
226    use timely::container::SizableContainer;
227    use timely::progress::frontier::{Antichain, AntichainRef};
228    use timely::PartialOrder;
229    use crate::trace::implementations::merge_batcher::Merger;
230
231    /// A `Merger` implementation for `Vec<(D, T, R)>` that drains owned inputs.
232    pub struct VecMerger<D, T, R> {
233        _marker: PhantomData<(D, T, R)>,
234    }
235
236    impl<D, T, R> Default for VecMerger<D, T, R> {
237        fn default() -> Self { Self { _marker: PhantomData } }
238    }
239
240    impl<D, T, R> VecMerger<D, T, R> {
241        /// The target capacity for output buffers, as a power of two.
242        ///
243        /// This amount is used to size vectors, where vectors not exactly this capacity are dropped.
244        /// If this is mis-set, there is the potential for more memory churn than anticipated.
245        fn target_capacity() -> usize {
246            timely::container::buffer::default_capacity::<(D, T, R)>().next_power_of_two()
247        }
248        /// Acquire a buffer with the target capacity.
249        fn empty(&self, stash: &mut Vec<Vec<(D, T, R)>>) -> Vec<(D, T, R)> {
250            let target = Self::target_capacity();
251            let mut container = stash.pop().unwrap_or_default();
252            container.clear();
253            // Reuse if at target; otherwise allocate fresh.
254            if container.capacity() != target {
255                container = Vec::with_capacity(target);
256            }
257            container
258        }
259        /// Refill `queue` from `iter` if empty. Recycles drained queues into `stash`.
260        fn refill(queue: &mut std::collections::VecDeque<(D, T, R)>, iter: &mut impl Iterator<Item = Vec<(D, T, R)>>, stash: &mut Vec<Vec<(D, T, R)>>) {
261            if queue.is_empty() {
262                let target = Self::target_capacity();
263                if stash.len() < 2 {
264                    let mut recycled = Vec::from(std::mem::take(queue));
265                    recycled.clear();
266                    if recycled.capacity() == target {
267                        stash.push(recycled);
268                    }
269                }
270                if let Some(chunk) = iter.next() {
271                    *queue = std::collections::VecDeque::from(chunk);
272                }
273            }
274        }
275    }
276
277    impl<D, T, R> Merger for VecMerger<D, T, R>
278    where
279        D: Ord + Clone + 'static,
280        T: Ord + Clone + PartialOrder + 'static,
281        R: crate::difference::Semigroup + 'static,
282    {
283        type Chunk = Vec<(D, T, R)>;
284        type Time = T;
285
286        fn merge(
287            &mut self,
288            list1: Vec<Vec<(D, T, R)>>,
289            list2: Vec<Vec<(D, T, R)>>,
290            output: &mut Vec<Vec<(D, T, R)>>,
291            stash: &mut Vec<Vec<(D, T, R)>>,
292        ) {
293            use std::cmp::Ordering;
294            use std::collections::VecDeque;
295
296            let mut iter1 = list1.into_iter();
297            let mut iter2 = list2.into_iter();
298            let mut q1 = VecDeque::<(D,T,R)>::from(iter1.next().unwrap_or_default());
299            let mut q2 = VecDeque::<(D,T,R)>::from(iter2.next().unwrap_or_default());
300
301            let mut result = self.empty(stash);
302
303            // Merge while both queues are non-empty.
304            while let (Some((d1, t1, _)), Some((d2, t2, _))) = (q1.front(), q2.front()) {
305                match (d1, t1).cmp(&(d2, t2)) {
306                    Ordering::Less => {
307                        result.push(q1.pop_front().unwrap());
308                    }
309                    Ordering::Greater => {
310                        result.push(q2.pop_front().unwrap());
311                    }
312                    Ordering::Equal => {
313                        let (d, t, mut r1) = q1.pop_front().unwrap();
314                        let (_, _, r2) = q2.pop_front().unwrap();
315                        r1.plus_equals(&r2);
316                        if !r1.is_zero() {
317                            result.push((d, t, r1));
318                        }
319                    }
320                }
321
322                if result.at_capacity() {
323                    output.push(std::mem::take(&mut result));
324                    result = self.empty(stash);
325                }
326
327                // Refill emptied queues from their chains.
328                if q1.is_empty() { Self::refill(&mut q1, &mut iter1, stash); }
329                if q2.is_empty() { Self::refill(&mut q2, &mut iter2, stash); }
330            }
331
332            // Push partial result and remaining data from both sides.
333            if !result.is_empty() { output.push(result); }
334            for q in [q1, q2] {
335                if !q.is_empty() { output.push(Vec::from(q)); }
336            }
337            output.extend(iter1);
338            output.extend(iter2);
339        }
340
341        fn extract(
342            &mut self,
343            merged: Vec<Vec<(D, T, R)>>,
344            upper: AntichainRef<T>,
345            frontier: &mut Antichain<T>,
346            ship: &mut Vec<Vec<(D, T, R)>>,
347            kept: &mut Vec<Vec<(D, T, R)>>,
348            stash: &mut Vec<Vec<(D, T, R)>>,
349        ) {
350            let mut keep = self.empty(stash);
351            let mut ready = self.empty(stash);
352
353            for mut chunk in merged {
354                // Go update-by-update to swap out full containers.
355                for (data, time, diff) in chunk.drain(..) {
356                    if upper.less_equal(&time) {
357                        frontier.insert_with(&time, |time| time.clone());
358                        keep.push((data, time, diff));
359                    } else {
360                        ready.push((data, time, diff));
361                    }
362                    if keep.at_capacity() {
363                        kept.push(std::mem::take(&mut keep));
364                        keep = self.empty(stash);
365                    }
366                    if ready.at_capacity() {
367                        ship.push(std::mem::take(&mut ready));
368                        ready = self.empty(stash);
369                    }
370                }
371                // Recycle the now-empty chunk if it has the right capacity.
372                if chunk.capacity() == Self::target_capacity() {
373                    stash.push(chunk);
374                }
375            }
376            if !keep.is_empty() { kept.push(keep); }
377            if !ready.is_empty() { ship.push(ready); }
378        }
379
380        fn len(chunk: &Vec<(D, T, R)>) -> usize { chunk.len() }
381    }
382}
383
384#[cfg(test)]
385mod test {
386    use timely::progress::frontier::Antichain;
387    use crate::trace::Batcher;
388    use super::MergeBatcher;
389    use super::vec::VecMerger;
390
391    type Bt = MergeBatcher<VecMerger<u64, u64, i64>>;
392
393    /// The sealed frontier must reflect the POST-CONSOLIDATION set of distinct kept times:
394    /// two chains carry cancelling updates at a kept time (`t=5`), plus a survivor at a later
395    /// kept time (`t=7`). After `seal(upper=[3])` the frontier must be `{7}` — `(100, 5)` nets
396    /// to zero and needs no capability. (A per-chain extract that folds the frontier before
397    /// consolidating would wrongly report `{5}`.)
398    #[test]
399    fn frontier_is_post_consolidation() {
400        let mut b = Bt::new(None, 0);
401        b.chain_push(vec![vec![(100u64, 5u64, 1i64), (200u64, 7u64, 1i64)]]);
402        b.chain_push(vec![vec![(100u64, 5u64, -1i64)]]);
403        let _ = b.seal(Antichain::from_elem(3));
404        let got: Vec<u64> = b.frontier().iter().cloned().collect();
405        assert_eq!(got, vec![7u64],
406            "frontier held a capability at t=5, which consolidates to zero (got {got:?})");
407    }
408
409    /// Sanity: with no cross-chain cancellation, the frontier is the minimal kept time.
410    #[test]
411    fn frontier_survivor_minimum() {
412        let mut b = Bt::new(None, 0);
413        b.chain_push(vec![vec![(100u64, 5u64, 1i64)]]);
414        b.chain_push(vec![vec![(200u64, 7u64, 1i64)]]);
415        let _ = b.seal(Antichain::from_elem(3));
416        let got: Vec<u64> = b.frontier().iter().cloned().collect();
417        assert_eq!(got, vec![5u64]);
418    }
419}