Skip to main content

mz_compute/extensions/
arrange.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::collections::BTreeMap;
11use std::rc::Rc;
12use std::sync::{Arc, Weak};
13
14use differential_dataflow::difference::Semigroup;
15use differential_dataflow::lattice::Lattice;
16use differential_dataflow::operators::arrange::arrangement::arrange_core;
17use differential_dataflow::operators::arrange::{Arranged, TraceAgent};
18use differential_dataflow::trace::implementations::BatchContainer;
19use differential_dataflow::trace::implementations::spine_fueled::Spine;
20use differential_dataflow::trace::{Batch, Batcher, Builder, Trace, TraceReader};
21use differential_dataflow::{Collection, Data, ExchangeData, Hashable, VecCollection};
22use mz_compute_types::dyncfgs::{ENABLE_COLUMN_PAGED_BATCHER, ENABLE_COLUMNAR_MERGE_BATCHER};
23use mz_dyncfg::ConfigSet;
24use mz_row_spine::ArcBatch;
25use mz_timely_util::containers::HeapSize;
26use timely::Container;
27use timely::container::{ContainerBuilder, PushInto};
28use timely::dataflow::Stream;
29use timely::dataflow::channels::pact::{Exchange, ParallelizationContract, Pipeline};
30use timely::dataflow::operators::Operator;
31use timely::progress::Timestamp;
32
33use crate::logging::compute::{
34    ArrangementHeapAllocations, ArrangementHeapCapacity, ArrangementHeapSize,
35    ArrangementHeapSizeOperator, ComputeEvent, ComputeEventBuilder,
36};
37use crate::typedefs::{
38    KeyAgent, KeyValAgent, MzArrangeData, MzData, MzTimestamp, RowAgent, RowRowAgent, RowValAgent,
39};
40
41/// Which merge batcher an arrange site should instantiate.
42///
43/// The three parameters `mz_arrange_core` takes are one unit, not three
44/// knobs: the chunker's container and the builder's input are both pinned to
45/// `Batcher::Output`, so the chunker and builder follow from the batcher and
46/// a call site has to spell out a whole arm per variant.
47pub enum ArrangementBatcher {
48    /// `Chunker<ColumnationStack<_>>` + `Col2ValBatcher` + `RowRowBuilder`.
49    /// Chains are columnation stacks.
50    Columnation,
51    /// `ColumnChunker` + `Col2ValColBatcher` + `RowRowColPagedBuilder`.
52    /// Chains are resident `Column`s.
53    Columnar,
54    /// `ColumnChunker` + `Col2ValPagedBatcher` + `RowRowColPagedBuilder`.
55    /// Chains are `Column`s routed through the pager, which may spill them.
56    ColumnarPaged,
57}
58
59impl ArrangementBatcher {
60    /// Resolve the batcher from the replica's config set.
61    ///
62    /// `ENABLE_COLUMN_PAGED_BATCHER` wins over
63    /// `ENABLE_COLUMNAR_MERGE_BATCHER`, because it asks for the same columnar
64    /// chains plus paging. Call this once per arrange site at operator
65    /// construction time, so a dataflow keeps one batcher for its whole life
66    /// even if the flags flip underneath it.
67    pub fn from_config(config: &ConfigSet) -> Self {
68        if ENABLE_COLUMN_PAGED_BATCHER.get(config) {
69            Self::ColumnarPaged
70        } else if ENABLE_COLUMNAR_MERGE_BATCHER.get(config) {
71            Self::Columnar
72        } else {
73            Self::Columnation
74        }
75    }
76}
77
78/// Extension trait to arrange data.
79pub trait MzArrange<'scope>: MzArrangeCore<'scope> {
80    /// Arranges a stream of `(Key, Val)` updates by `Key` into a trace of type `Tr`.
81    ///
82    /// This operator arranges a stream of values into a shared trace, whose contents it maintains.
83    /// This trace is current for all times marked completed in the output stream, and probing this stream
84    /// is the correct way to determine that times in the shared trace are committed.
85    fn mz_arrange<Chu, Ba, Bu, Tr>(self, name: &str) -> Arranged<'scope, TraceAgent<Tr>>
86    where
87        Ba: Batcher<Time = Self::Timestamp> + 'static,
88        Chu: ContainerBuilder<Container = Ba::Output>
89            + for<'a> PushInto<&'a mut Self::Input>
90            + 'static,
91        Bu: Builder<Time = Self::Timestamp, Input = Ba::Output, Output = Tr::Batch>,
92        Tr: Trace + TraceReader<Time = Self::Timestamp> + 'static,
93        Tr::Batch: Batch,
94        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize;
95}
96
97/// Extension trait to arrange data.
98pub trait MzArrangeCore<'scope> {
99    /// The current scope.
100    type Timestamp: Timestamp + Lattice;
101    /// The data input container type.
102    type Input: Container + Clone + 'static;
103
104    /// Arranges a stream of `(Key, Val)` updates by `Key` into a trace of type `Tr`. Partitions
105    /// the data according to `pact`.
106    ///
107    /// This operator arranges a stream of values into a shared trace, whose contents it maintains.
108    /// This trace is current for all times marked completed in the output stream, and probing this stream
109    /// is the correct way to determine that times in the shared trace are committed.
110    fn mz_arrange_core<P, Chu, Ba, Bu, Tr>(
111        self,
112        pact: P,
113        name: &str,
114    ) -> Arranged<'scope, TraceAgent<Tr>>
115    where
116        P: ParallelizationContract<Self::Timestamp, Self::Input>,
117        Ba: Batcher<Time = Self::Timestamp> + 'static,
118        Chu: ContainerBuilder<Container = Ba::Output>
119            + for<'a> PushInto<&'a mut Self::Input>
120            + 'static,
121        Bu: Builder<Time = Self::Timestamp, Input = Ba::Output, Output = Tr::Batch>,
122        Tr: Trace + TraceReader<Time = Self::Timestamp> + 'static,
123        Tr::Batch: Batch,
124        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize;
125}
126
127impl<'scope, T, C> MzArrangeCore<'scope> for Stream<'scope, T, C>
128where
129    T: Timestamp + Lattice,
130    C: Container + Clone + 'static,
131{
132    type Timestamp = T;
133    type Input = C;
134
135    fn mz_arrange_core<P, Chu, Ba, Bu, Tr>(
136        self,
137        pact: P,
138        name: &str,
139    ) -> Arranged<'scope, TraceAgent<Tr>>
140    where
141        P: ParallelizationContract<T, Self::Input>,
142        Ba: Batcher<Time = T> + 'static,
143        Chu: ContainerBuilder<Container = Ba::Output>
144            + for<'a> PushInto<&'a mut Self::Input>
145            + 'static,
146        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
147        Tr: Trace + TraceReader<Time = T> + 'static,
148        Tr::Batch: Batch,
149        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
150    {
151        // Allow access to `arrange_named` because we're within Mz's wrapper.
152        #[allow(clippy::disallowed_methods)]
153        arrange_core::<_, _, Chu, Ba, Bu, _>(self, pact, name).log_arrangement_size()
154    }
155}
156
157impl<'scope, T, K, V, R> MzArrange<'scope> for VecCollection<'scope, T, (K, V), R>
158where
159    T: Timestamp + Lattice,
160    K: ExchangeData + Hashable,
161    V: ExchangeData,
162    R: ExchangeData,
163{
164    fn mz_arrange<Chu, Ba, Bu, Tr>(self, name: &str) -> Arranged<'scope, TraceAgent<Tr>>
165    where
166        Ba: Batcher<Time = T> + 'static,
167        Chu: ContainerBuilder<Container = Ba::Output>
168            + for<'a> PushInto<&'a mut Self::Input>
169            + 'static,
170        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
171        Tr: Trace + TraceReader<Time = T> + 'static,
172        Tr::Batch: Batch,
173        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
174    {
175        let exchange = Exchange::new(move |update: &((K, V), T, R)| (update.0).0.hashed().into());
176        self.mz_arrange_core::<_, Chu, Ba, Bu, _>(exchange, name)
177    }
178}
179
180impl<'scope, T, C> MzArrangeCore<'scope> for Collection<'scope, T, C>
181where
182    T: Timestamp + Lattice,
183    C: Container + Clone + 'static,
184{
185    type Timestamp = T;
186    type Input = C;
187
188    fn mz_arrange_core<P, Chu, Ba, Bu, Tr>(
189        self,
190        pact: P,
191        name: &str,
192    ) -> Arranged<'scope, TraceAgent<Tr>>
193    where
194        P: ParallelizationContract<T, Self::Input>,
195        Ba: Batcher<Time = T> + 'static,
196        Chu: ContainerBuilder<Container = Ba::Output>
197            + for<'a> PushInto<&'a mut Self::Input>
198            + 'static,
199        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
200        Tr: Trace + TraceReader<Time = T> + 'static,
201        Tr::Batch: Batch,
202        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
203    {
204        self.inner.mz_arrange_core::<_, Chu, Ba, Bu, _>(pact, name)
205    }
206}
207
208/// A specialized collection where data only has a key, but no associated value.
209///
210/// Created by calling `collection.into()`.
211pub struct KeyCollection<'scope, T: Timestamp, K: 'static, R: 'static = usize>(
212    VecCollection<'scope, T, K, R>,
213);
214
215impl<'scope, T: Timestamp, K, R: Semigroup> From<VecCollection<'scope, T, K, R>>
216    for KeyCollection<'scope, T, K, R>
217{
218    fn from(value: VecCollection<'scope, T, K, R>) -> Self {
219        KeyCollection(value)
220    }
221}
222
223impl<'scope, T, K, R> MzArrange<'scope> for KeyCollection<'scope, T, K, R>
224where
225    T: Timestamp + Lattice,
226    K: ExchangeData + Hashable,
227    R: ExchangeData,
228{
229    fn mz_arrange<Chu, Ba, Bu, Tr>(self, name: &str) -> Arranged<'scope, TraceAgent<Tr>>
230    where
231        Ba: Batcher<Time = T> + 'static,
232        Chu: ContainerBuilder<Container = Ba::Output>
233            + for<'a> PushInto<&'a mut Self::Input>
234            + 'static,
235        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
236        Tr: Trace + TraceReader<Time = T> + 'static,
237        Tr::Batch: Batch,
238        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
239    {
240        self.0.map(|d| (d, ())).mz_arrange::<Chu, Ba, Bu, _>(name)
241    }
242}
243
244impl<'scope, T, K, R> MzArrangeCore<'scope> for KeyCollection<'scope, T, K, R>
245where
246    T: Timestamp + Lattice,
247    K: Clone + 'static,
248    R: Clone + 'static,
249{
250    type Timestamp = T;
251    type Input = Vec<((K, ()), T, R)>;
252
253    fn mz_arrange_core<P, Chu, Ba, Bu, Tr>(
254        self,
255        pact: P,
256        name: &str,
257    ) -> Arranged<'scope, TraceAgent<Tr>>
258    where
259        P: ParallelizationContract<T, Self::Input>,
260        Ba: Batcher<Time = T> + 'static,
261        Chu: ContainerBuilder<Container = Ba::Output>
262            + for<'a> PushInto<&'a mut Self::Input>
263            + 'static,
264        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
265        Tr: Trace + TraceReader<Time = T> + 'static,
266        Tr::Batch: Batch,
267        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
268    {
269        self.0
270            .map(|d| (d, ()))
271            .mz_arrange_core::<_, Chu, Ba, Bu, _>(pact, name)
272    }
273}
274
275/// A type that can log its heap size.
276pub trait ArrangementSize {
277    /// Install a logger to track the heap size of the target.
278    fn log_arrangement_size(self) -> Self;
279}
280
281/// Helper for [`ArrangementSize`] to install a common operator holding on to a trace.
282///
283/// * `arranged`: The arrangement to inspect.
284/// * `logic`: Closure that calculates the heap size/capacity/allocations for a batch. The return
285///    value are size and capacity in bytes, and number of allocations, all in absolute values.
286///
287/// Batch-size logging identifies each batch by the address of its backing allocation and holds a
288/// weak reference to it, so it needs the `Arc` underlying the spine's [`ArcBatch<B>`] batches;
289/// `batch.0` reaches straight through the newtype to it.
290fn log_arrangement_size_inner<'scope, B, L>(
291    arranged: Arranged<'scope, TraceAgent<Spine<ArcBatch<B>>>>,
292    mut logic: L,
293) -> Arranged<'scope, TraceAgent<Spine<ArcBatch<B>>>>
294where
295    B: Batch + 'static,
296    L: FnMut(&B) -> (usize, usize, usize) + 'static,
297{
298    let scope = arranged.stream.scope();
299    let Some(logger) = scope
300        .worker()
301        .logger_for::<ComputeEventBuilder>("materialize/compute")
302    else {
303        return arranged;
304    };
305    let operator_id = arranged.trace.operator().global_id;
306    let trace = Rc::downgrade(&arranged.trace.trace_box_unstable());
307
308    let (mut old_size, mut old_capacity, mut old_allocations) = (0isize, 0isize, 0isize);
309
310    let stream = arranged
311        .stream
312        .unary(Pipeline, "ArrangementSize", |_cap, info| {
313            let address = info.address;
314            logger.log(&ComputeEvent::ArrangementHeapSizeOperator(
315                ArrangementHeapSizeOperator {
316                    operator_id,
317                    address: address.to_vec(),
318                },
319            ));
320
321            // Weak references to batches, so we can observe batches outside the trace.
322            // Batches are immutable once sealed, so we compute their size exactly
323            // once (when first observed) and cache it alongside the weak reference.
324            // Subsequent activations only sum the cached values for live batches,
325            // avoiding a repeated walk of every batch's backing regions.
326            let mut batches: BTreeMap<*const B, (Weak<B>, (usize, usize, usize))> = BTreeMap::new();
327
328            move |input, output| {
329                input.for_each(|time, data| {
330                    for batch in data.iter() {
331                        batches
332                            .entry(Arc::as_ptr(&batch.0))
333                            .or_insert_with(|| (Arc::downgrade(&batch.0), logic(&batch.0)));
334                    }
335                    output.session(&time).give_container(data);
336                });
337                let Some(trace) = trace.upgrade() else {
338                    // Invariant: `batches` holds no entries once the trace is gone. Each entry's
339                    // `Weak` keeps its batch's `ArcInner` allocation reserved, and the `retain` below
340                    // that would drop it is unreachable on this path, so the entries have to go
341                    // here. The upgrade cannot start succeeding again, hence clearing on every
342                    // activation that takes this path also covers batches that arrive on the
343                    // input afterwards.
344                    batches.clear();
345                    return;
346                };
347
348                trace.borrow().trace().map_batches(|batch| {
349                    batches
350                        .entry(Arc::as_ptr(&batch.0))
351                        .or_insert_with(|| (Arc::downgrade(&batch.0), logic(&batch.0)));
352                });
353
354                let (mut size, mut capacity, mut allocations) = (0, 0, 0);
355                batches.retain(|_, (weak, cached)| {
356                    if weak.strong_count() > 0 {
357                        let (sz, c, a) = *cached;
358                        (size += sz, capacity += c, allocations += a);
359                        true
360                    } else {
361                        false
362                    }
363                });
364
365                let size = size.try_into().expect("must fit");
366                if size != old_size {
367                    logger.log(&ComputeEvent::ArrangementHeapSize(ArrangementHeapSize {
368                        operator_id,
369                        delta_size: size - old_size,
370                    }));
371                }
372
373                let capacity = capacity.try_into().expect("must fit");
374                if capacity != old_capacity {
375                    logger.log(&ComputeEvent::ArrangementHeapCapacity(
376                        ArrangementHeapCapacity {
377                            operator_id,
378                            delta_capacity: capacity - old_capacity,
379                        },
380                    ));
381                }
382
383                let allocations = allocations.try_into().expect("must fit");
384                if allocations != old_allocations {
385                    logger.log(&ComputeEvent::ArrangementHeapAllocations(
386                        ArrangementHeapAllocations {
387                            operator_id,
388                            delta_allocations: allocations - old_allocations,
389                        },
390                    ));
391                }
392
393                old_size = size;
394                old_capacity = capacity;
395                old_allocations = allocations;
396            }
397        });
398    Arranged {
399        trace: arranged.trace,
400        stream,
401    }
402}
403
404impl<'scope, T, K, V, R> ArrangementSize for Arranged<'scope, KeyValAgent<K, V, T, R>>
405where
406    T: MzTimestamp,
407    K: Data + MzData,
408    V: Data + MzData,
409    R: Semigroup + Ord + MzData + 'static,
410{
411    fn log_arrangement_size(self) -> Self {
412        log_arrangement_size_inner(self, |batch| {
413            let (mut size, mut capacity, mut allocations) = (0, 0, 0);
414            let mut callback = |siz, cap| {
415                size += siz;
416                capacity += cap;
417                allocations += usize::from(cap > 0);
418            };
419            batch.storage.keys.heap_size(&mut callback);
420            batch.storage.vals.offs.heap_size(&mut callback);
421            batch.storage.vals.vals.heap_size(&mut callback);
422            batch.storage.upds.offs.heap_size(&mut callback);
423            batch.storage.upds.times.heap_size(&mut callback);
424            batch.storage.upds.diffs.heap_size(&mut callback);
425            (size, capacity, allocations)
426        })
427    }
428}
429
430impl<'scope, T, K, R> ArrangementSize for Arranged<'scope, KeyAgent<K, T, R>>
431where
432    T: MzTimestamp,
433    K: Data + MzArrangeData,
434    R: Semigroup + Ord + MzData + 'static,
435{
436    fn log_arrangement_size(self) -> Self {
437        log_arrangement_size_inner(self, |batch| {
438            let (mut size, mut capacity, mut allocations) = (0, 0, 0);
439            let mut callback = |siz, cap| {
440                size += siz;
441                capacity += cap;
442                allocations += usize::from(cap > 0);
443            };
444            batch.storage.keys.heap_size(&mut callback);
445            batch.storage.upds.offs.heap_size(&mut callback);
446            batch.storage.upds.times.heap_size(&mut callback);
447            batch.storage.upds.diffs.heap_size(&mut callback);
448            (size, capacity, allocations)
449        })
450    }
451}
452
453impl<'scope, T, V, R> ArrangementSize for Arranged<'scope, RowValAgent<V, T, R>>
454where
455    T: MzTimestamp,
456    V: Data + MzArrangeData,
457    R: Semigroup + Ord + MzArrangeData + 'static,
458{
459    fn log_arrangement_size(self) -> Self {
460        log_arrangement_size_inner(self, |batch| {
461            let (mut size, mut capacity, mut allocations) = (0, 0, 0);
462            let mut callback = |siz, cap| {
463                size += siz;
464                capacity += cap;
465                allocations += usize::from(cap > 0);
466            };
467            batch.storage.keys.heap_size(&mut callback);
468            batch.storage.vals.offs.heap_size(&mut callback);
469            batch.storage.vals.vals.heap_size(&mut callback);
470            batch.storage.upds.offs.heap_size(&mut callback);
471            batch.storage.upds.times.heap_size(&mut callback);
472            batch.storage.upds.diffs.heap_size(&mut callback);
473            (size, capacity, allocations)
474        })
475    }
476}
477
478impl<'scope, T, R> ArrangementSize for Arranged<'scope, RowRowAgent<T, R>>
479where
480    T: MzTimestamp,
481    R: Semigroup + Ord + MzArrangeData + 'static,
482{
483    fn log_arrangement_size(self) -> Self {
484        log_arrangement_size_inner(self, |batch| {
485            let (mut size, mut capacity, mut allocations) = (0, 0, 0);
486            let mut callback = |siz, cap| {
487                size += siz;
488                capacity += cap;
489                allocations += usize::from(cap > 0);
490            };
491            batch.storage.keys.heap_size(&mut callback);
492            batch.storage.vals.offs.heap_size(&mut callback);
493            batch.storage.vals.vals.heap_size(&mut callback);
494            batch.storage.upds.offs.heap_size(&mut callback);
495            batch.storage.upds.times.heap_size(&mut callback);
496            batch.storage.upds.diffs.heap_size(&mut callback);
497            (size, capacity, allocations)
498        })
499    }
500}
501
502impl<'scope, T, DC> ArrangementSize for Arranged<'scope, RowAgent<T, DC::Owned, DC>>
503where
504    T: MzTimestamp,
505    DC: BatchContainer<Owned: Semigroup + 'static> + HeapSize,
506{
507    fn log_arrangement_size(self) -> Self {
508        log_arrangement_size_inner(self, |batch| {
509            let (mut size, mut capacity, mut allocations) = (0, 0, 0);
510            let mut callback = |siz, cap| {
511                size += siz;
512                capacity += cap;
513                allocations += usize::from(cap > 0);
514            };
515            batch.storage.keys.heap_size(&mut callback);
516            batch.storage.upds.offs.heap_size(&mut callback);
517            batch.storage.upds.times.heap_size(&mut callback);
518            batch.storage.upds.diffs.heap_size(&mut callback);
519            (size, capacity, allocations)
520        })
521    }
522}