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::spine_fueled::Spine;
19use differential_dataflow::trace::{Batch, Batcher, Builder, Trace, TraceReader};
20use differential_dataflow::{Collection, Data, ExchangeData, Hashable, VecCollection};
21use mz_row_spine::ArcBatch;
22use timely::Container;
23use timely::container::{ContainerBuilder, PushInto};
24use timely::dataflow::Stream;
25use timely::dataflow::channels::pact::{Exchange, ParallelizationContract, Pipeline};
26use timely::dataflow::operators::Operator;
27use timely::progress::Timestamp;
28
29use crate::logging::compute::{
30    ArrangementHeapAllocations, ArrangementHeapCapacity, ArrangementHeapSize,
31    ArrangementHeapSizeOperator, ComputeEvent, ComputeEventBuilder,
32};
33use crate::typedefs::{
34    KeyAgent, KeyValAgent, MzArrangeData, MzData, MzTimestamp, RowAgent, RowRowAgent, RowValAgent,
35};
36
37/// Extension trait to arrange data.
38pub trait MzArrange<'scope>: MzArrangeCore<'scope> {
39    /// Arranges a stream of `(Key, Val)` updates by `Key` into a trace of type `Tr`.
40    ///
41    /// This operator arranges a stream of values into a shared trace, whose contents it maintains.
42    /// This trace is current for all times marked completed in the output stream, and probing this stream
43    /// is the correct way to determine that times in the shared trace are committed.
44    fn mz_arrange<Chu, Ba, Bu, Tr>(self, name: &str) -> Arranged<'scope, TraceAgent<Tr>>
45    where
46        Ba: Batcher<Time = Self::Timestamp> + 'static,
47        Chu: ContainerBuilder<Container = Ba::Output>
48            + for<'a> PushInto<&'a mut Self::Input>
49            + 'static,
50        Bu: Builder<Time = Self::Timestamp, Input = Ba::Output, Output = Tr::Batch>,
51        Tr: Trace + TraceReader<Time = Self::Timestamp> + 'static,
52        Tr::Batch: Batch,
53        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize;
54}
55
56/// Extension trait to arrange data.
57pub trait MzArrangeCore<'scope> {
58    /// The current scope.
59    type Timestamp: Timestamp + Lattice;
60    /// The data input container type.
61    type Input: Container + Clone + 'static;
62
63    /// Arranges a stream of `(Key, Val)` updates by `Key` into a trace of type `Tr`. Partitions
64    /// the data according to `pact`.
65    ///
66    /// This operator arranges a stream of values into a shared trace, whose contents it maintains.
67    /// This trace is current for all times marked completed in the output stream, and probing this stream
68    /// is the correct way to determine that times in the shared trace are committed.
69    fn mz_arrange_core<P, Chu, Ba, Bu, Tr>(
70        self,
71        pact: P,
72        name: &str,
73    ) -> Arranged<'scope, TraceAgent<Tr>>
74    where
75        P: ParallelizationContract<Self::Timestamp, Self::Input>,
76        Ba: Batcher<Time = Self::Timestamp> + 'static,
77        Chu: ContainerBuilder<Container = Ba::Output>
78            + for<'a> PushInto<&'a mut Self::Input>
79            + 'static,
80        Bu: Builder<Time = Self::Timestamp, Input = Ba::Output, Output = Tr::Batch>,
81        Tr: Trace + TraceReader<Time = Self::Timestamp> + 'static,
82        Tr::Batch: Batch,
83        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize;
84}
85
86impl<'scope, T, C> MzArrangeCore<'scope> for Stream<'scope, T, C>
87where
88    T: Timestamp + Lattice,
89    C: Container + Clone + 'static,
90{
91    type Timestamp = T;
92    type Input = C;
93
94    fn mz_arrange_core<P, Chu, Ba, Bu, Tr>(
95        self,
96        pact: P,
97        name: &str,
98    ) -> Arranged<'scope, TraceAgent<Tr>>
99    where
100        P: ParallelizationContract<T, Self::Input>,
101        Ba: Batcher<Time = T> + 'static,
102        Chu: ContainerBuilder<Container = Ba::Output>
103            + for<'a> PushInto<&'a mut Self::Input>
104            + 'static,
105        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
106        Tr: Trace + TraceReader<Time = T> + 'static,
107        Tr::Batch: Batch,
108        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
109    {
110        // Allow access to `arrange_named` because we're within Mz's wrapper.
111        #[allow(clippy::disallowed_methods)]
112        arrange_core::<_, _, Chu, Ba, Bu, _>(self, pact, name).log_arrangement_size()
113    }
114}
115
116impl<'scope, T, K, V, R> MzArrange<'scope> for VecCollection<'scope, T, (K, V), R>
117where
118    T: Timestamp + Lattice,
119    K: ExchangeData + Hashable,
120    V: ExchangeData,
121    R: ExchangeData,
122{
123    fn mz_arrange<Chu, Ba, Bu, Tr>(self, name: &str) -> Arranged<'scope, TraceAgent<Tr>>
124    where
125        Ba: Batcher<Time = T> + 'static,
126        Chu: ContainerBuilder<Container = Ba::Output>
127            + for<'a> PushInto<&'a mut Self::Input>
128            + 'static,
129        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
130        Tr: Trace + TraceReader<Time = T> + 'static,
131        Tr::Batch: Batch,
132        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
133    {
134        let exchange = Exchange::new(move |update: &((K, V), T, R)| (update.0).0.hashed().into());
135        self.mz_arrange_core::<_, Chu, Ba, Bu, _>(exchange, name)
136    }
137}
138
139impl<'scope, T, C> MzArrangeCore<'scope> for Collection<'scope, T, C>
140where
141    T: Timestamp + Lattice,
142    C: Container + Clone + 'static,
143{
144    type Timestamp = T;
145    type Input = C;
146
147    fn mz_arrange_core<P, Chu, Ba, Bu, Tr>(
148        self,
149        pact: P,
150        name: &str,
151    ) -> Arranged<'scope, TraceAgent<Tr>>
152    where
153        P: ParallelizationContract<T, Self::Input>,
154        Ba: Batcher<Time = T> + 'static,
155        Chu: ContainerBuilder<Container = Ba::Output>
156            + for<'a> PushInto<&'a mut Self::Input>
157            + 'static,
158        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
159        Tr: Trace + TraceReader<Time = T> + 'static,
160        Tr::Batch: Batch,
161        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
162    {
163        self.inner.mz_arrange_core::<_, Chu, Ba, Bu, _>(pact, name)
164    }
165}
166
167/// A specialized collection where data only has a key, but no associated value.
168///
169/// Created by calling `collection.into()`.
170pub struct KeyCollection<'scope, T: Timestamp, K: 'static, R: 'static = usize>(
171    VecCollection<'scope, T, K, R>,
172);
173
174impl<'scope, T: Timestamp, K, R: Semigroup> From<VecCollection<'scope, T, K, R>>
175    for KeyCollection<'scope, T, K, R>
176{
177    fn from(value: VecCollection<'scope, T, K, R>) -> Self {
178        KeyCollection(value)
179    }
180}
181
182impl<'scope, T, K, R> MzArrange<'scope> for KeyCollection<'scope, T, K, R>
183where
184    T: Timestamp + Lattice,
185    K: ExchangeData + Hashable,
186    R: ExchangeData,
187{
188    fn mz_arrange<Chu, Ba, Bu, Tr>(self, name: &str) -> Arranged<'scope, TraceAgent<Tr>>
189    where
190        Ba: Batcher<Time = T> + 'static,
191        Chu: ContainerBuilder<Container = Ba::Output>
192            + for<'a> PushInto<&'a mut Self::Input>
193            + 'static,
194        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
195        Tr: Trace + TraceReader<Time = T> + 'static,
196        Tr::Batch: Batch,
197        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
198    {
199        self.0.map(|d| (d, ())).mz_arrange::<Chu, Ba, Bu, _>(name)
200    }
201}
202
203impl<'scope, T, K, R> MzArrangeCore<'scope> for KeyCollection<'scope, T, K, R>
204where
205    T: Timestamp + Lattice,
206    K: Clone + 'static,
207    R: Clone + 'static,
208{
209    type Timestamp = T;
210    type Input = Vec<((K, ()), T, R)>;
211
212    fn mz_arrange_core<P, Chu, Ba, Bu, Tr>(
213        self,
214        pact: P,
215        name: &str,
216    ) -> Arranged<'scope, TraceAgent<Tr>>
217    where
218        P: ParallelizationContract<T, Self::Input>,
219        Ba: Batcher<Time = T> + 'static,
220        Chu: ContainerBuilder<Container = Ba::Output>
221            + for<'a> PushInto<&'a mut Self::Input>
222            + 'static,
223        Bu: Builder<Time = T, Input = Ba::Output, Output = Tr::Batch>,
224        Tr: Trace + TraceReader<Time = T> + 'static,
225        Tr::Batch: Batch,
226        Arranged<'scope, TraceAgent<Tr>>: ArrangementSize,
227    {
228        self.0
229            .map(|d| (d, ()))
230            .mz_arrange_core::<_, Chu, Ba, Bu, _>(pact, name)
231    }
232}
233
234/// A type that can log its heap size.
235pub trait ArrangementSize {
236    /// Install a logger to track the heap size of the target.
237    fn log_arrangement_size(self) -> Self;
238}
239
240/// Helper for [`ArrangementSize`] to install a common operator holding on to a trace.
241///
242/// * `arranged`: The arrangement to inspect.
243/// * `logic`: Closure that calculates the heap size/capacity/allocations for a batch. The return
244///    value are size and capacity in bytes, and number of allocations, all in absolute values.
245///
246/// Batch-size logging identifies each batch by the address of its backing allocation and holds a
247/// weak reference to it, so it needs the `Arc` underlying the spine's [`ArcBatch<B>`] batches;
248/// `batch.0` reaches straight through the newtype to it.
249fn log_arrangement_size_inner<'scope, B, L>(
250    arranged: Arranged<'scope, TraceAgent<Spine<ArcBatch<B>>>>,
251    mut logic: L,
252) -> Arranged<'scope, TraceAgent<Spine<ArcBatch<B>>>>
253where
254    B: Batch + 'static,
255    L: FnMut(&B) -> (usize, usize, usize) + 'static,
256{
257    let scope = arranged.stream.scope();
258    let Some(logger) = scope
259        .worker()
260        .logger_for::<ComputeEventBuilder>("materialize/compute")
261    else {
262        return arranged;
263    };
264    let operator_id = arranged.trace.operator().global_id;
265    let trace = Rc::downgrade(&arranged.trace.trace_box_unstable());
266
267    let (mut old_size, mut old_capacity, mut old_allocations) = (0isize, 0isize, 0isize);
268
269    let stream = arranged
270        .stream
271        .unary(Pipeline, "ArrangementSize", |_cap, info| {
272            let address = info.address;
273            logger.log(&ComputeEvent::ArrangementHeapSizeOperator(
274                ArrangementHeapSizeOperator {
275                    operator_id,
276                    address: address.to_vec(),
277                },
278            ));
279
280            // Weak references to batches, so we can observe batches outside the trace.
281            // Batches are immutable once sealed, so we compute their size exactly
282            // once (when first observed) and cache it alongside the weak reference.
283            // Subsequent activations only sum the cached values for live batches,
284            // avoiding a repeated walk of every batch's backing regions.
285            let mut batches: BTreeMap<*const B, (Weak<B>, (usize, usize, usize))> = BTreeMap::new();
286
287            move |input, output| {
288                input.for_each(|time, data| {
289                    for batch in data.iter() {
290                        batches
291                            .entry(Arc::as_ptr(&batch.0))
292                            .or_insert_with(|| (Arc::downgrade(&batch.0), logic(&batch.0)));
293                    }
294                    output.session(&time).give_container(data);
295                });
296                let Some(trace) = trace.upgrade() else {
297                    // Invariant: `batches` holds no entries once the trace is gone. Each entry's
298                    // `Weak` keeps its batch's `ArcInner` allocation reserved, and the `retain` below
299                    // that would drop it is unreachable on this path, so the entries have to go
300                    // here. The upgrade cannot start succeeding again, hence clearing on every
301                    // activation that takes this path also covers batches that arrive on the
302                    // input afterwards.
303                    batches.clear();
304                    return;
305                };
306
307                trace.borrow().trace().map_batches(|batch| {
308                    batches
309                        .entry(Arc::as_ptr(&batch.0))
310                        .or_insert_with(|| (Arc::downgrade(&batch.0), logic(&batch.0)));
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, R> ArrangementSize for Arranged<'scope, RowAgent<T, R>>
462where
463    T: MzTimestamp,
464    R: Semigroup + Ord + MzArrangeData + 'static,
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}