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