Skip to main content

mz_compute/logging/
compute.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
10//! Logging dataflows for events generated by clusterd.
11
12use std::cell::RefCell;
13use std::collections::{BTreeMap, BTreeSet};
14use std::fmt::{Display, Write};
15use std::rc::Rc;
16use std::time::{Duration, Instant};
17
18use chrono::DateTime;
19use columnar::{Columnar, Index, Ref};
20use differential_dataflow::VecCollection;
21use differential_dataflow::collection::AsCollection;
22use differential_dataflow::trace::{BatchReader, Cursor, Navigable};
23use mz_compute_types::plan::LirId;
24use mz_ore::cast::CastFrom;
25use mz_repr::adt::timestamp::CheckedTimestamp;
26use mz_repr::{Datum, Diff, GlobalId, Row, RowRef, Timestamp};
27use mz_timely_util::columnar::batcher;
28use mz_timely_util::columnar::builder::ColumnBuilder;
29use mz_timely_util::columnar::{Col2ValBatcher, Column, columnar_exchange};
30use mz_timely_util::replay::MzReplay;
31use timely::dataflow::channels::pact::{ExchangeCore, Pipeline};
32use timely::dataflow::operators::Operator;
33use timely::dataflow::operators::generic::OutputBuilder;
34use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
35use timely::dataflow::operators::generic::operator::empty;
36use timely::dataflow::{Scope, StreamVec};
37use timely::scheduling::activate::{Activations, Activator};
38use tracing::error;
39use uuid::Uuid;
40
41use crate::extensions::arrange::MzArrangeCore;
42use crate::logging::{
43    ComputeLog, EventQueue, LogCollection, LogVariant, OutputSessionColumnar, PermutedRowPacker,
44    SharedLoggingState, Update,
45};
46use crate::typedefs::RowRowSpine;
47use mz_row_spine::RowRowBuilder;
48
49/// Type alias for a logger of compute events.
50pub type Logger = timely::logging_core::Logger<ComputeEventBuilder>;
51pub type ComputeEventBuilder = ColumnBuilder<(Duration, ComputeEvent)>;
52
53/// A dataflow exports a global ID.
54#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
55pub struct Export {
56    /// Identifier of the export.
57    pub export_id: GlobalId,
58    /// Timely worker index of the exporting dataflow.
59    pub dataflow_index: usize,
60}
61
62/// The export for a global id was dropped.
63#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
64pub struct ExportDropped {
65    /// Identifier of the export.
66    pub export_id: GlobalId,
67}
68
69/// A peek event with a [`PeekType`], and an installation status.
70#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
71pub struct PeekEvent {
72    /// The identifier of the view the peek targets.
73    pub id: GlobalId,
74    /// The logical timestamp requested.
75    pub time: Timestamp,
76    /// The ID of the peek.
77    pub uuid: uuid::Bytes,
78    /// The relevant _type_ of peek: index or persist.
79    // Note that this is not stored on the Peek event for data-packing reasons only.
80    pub peek_type: PeekType,
81    /// True if the peek is being installed; false if it's being removed.
82    pub installed: bool,
83}
84
85/// Frontier change event.
86#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
87pub struct Frontier {
88    pub export_id: GlobalId,
89    pub time: Timestamp,
90    pub diff: i8,
91}
92
93/// An import frontier change.
94#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
95pub struct ImportFrontier {
96    pub import_id: GlobalId,
97    pub export_id: GlobalId,
98    pub time: Timestamp,
99    pub diff: i8,
100}
101
102/// A change in an arrangement's heap size.
103#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
104pub struct ArrangementHeapSize {
105    /// Operator index
106    pub operator_id: usize,
107    /// Delta of the heap size in bytes of the arrangement.
108    pub delta_size: isize,
109}
110
111/// A change in an arrangement's heap capacity.
112#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
113pub struct ArrangementHeapCapacity {
114    /// Operator index
115    pub operator_id: usize,
116    /// Delta of the heap capacity in bytes of the arrangement.
117    pub delta_capacity: isize,
118}
119
120/// A change in an arrangement's heap allocation count.
121#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
122pub struct ArrangementHeapAllocations {
123    /// Operator index
124    pub operator_id: usize,
125    /// Delta of distinct heap allocations backing the arrangement.
126    pub delta_allocations: isize,
127}
128
129/// Announcing an operator that manages an arrangement.
130#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
131pub struct ArrangementHeapSizeOperator {
132    /// Operator index
133    pub operator_id: usize,
134    /// The address of the operator.
135    pub address: Vec<usize>,
136}
137
138/// Drop event for an operator managing an arrangement.
139#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
140pub struct ArrangementHeapSizeOperatorDrop {
141    /// Operator index
142    pub operator_id: usize,
143}
144
145/// Dataflow shutdown event.
146#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
147pub struct DataflowShutdown {
148    /// Timely worker index of the dataflow.
149    pub dataflow_index: usize,
150}
151
152/// Error count update event.
153#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
154pub struct ErrorCount {
155    /// Identifier of the export.
156    pub export_id: GlobalId,
157    /// The change in error count.
158    pub diff: Diff,
159}
160
161/// An export started hydrating.
162#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
163pub struct HydrationStart {
164    /// Identifier of the export.
165    pub export_id: GlobalId,
166}
167
168/// An export is hydrated.
169#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
170pub struct Hydration {
171    /// Identifier of the export.
172    pub export_id: GlobalId,
173}
174
175/// An operator's hydration status changed.
176#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
177pub struct OperatorHydration {
178    /// Identifier of the export.
179    pub export_id: GlobalId,
180    /// Identifier of the operator's LIR node.
181    pub lir_id: LirId,
182    /// Whether the operator is hydrated.
183    pub hydrated: bool,
184}
185
186/// Announce a mapping of an LIR operator to a dataflow operator for a global ID.
187#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
188pub struct LirMapping {
189    /// The `GlobalId` in which the LIR operator is rendered.
190    ///
191    /// NB a single a dataflow may have many `GlobalId`s inside it.
192    /// A separate mapping (using `ComputeEvent::DataflowGlobal`)
193    /// tracks the many-to-one relationship between `GlobalId`s and
194    /// dataflows.
195    pub global_id: GlobalId,
196    /// The actual mapping.
197    /// Represented this way to reduce the size of `ComputeEvent`.
198    pub mapping: Vec<(LirId, LirMetadata)>,
199}
200
201/// Announce that a dataflow supports a specific global ID.
202#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
203pub struct DataflowGlobal {
204    /// The identifier of the dataflow.
205    pub dataflow_index: usize,
206    /// A `GlobalId` that is rendered as part of this dataflow.
207    pub global_id: GlobalId,
208}
209
210/// A logged compute event.
211#[derive(Debug, Clone, PartialOrd, PartialEq, Columnar)]
212pub enum ComputeEvent {
213    /// A dataflow export was created.
214    Export(Export),
215    /// A dataflow export was dropped.
216    ExportDropped(ExportDropped),
217    /// Peek command.
218    Peek(PeekEvent),
219    /// Available frontier information for dataflow exports.
220    Frontier(Frontier),
221    /// Available frontier information for dataflow imports.
222    ImportFrontier(ImportFrontier),
223    /// Arrangement heap size update
224    ArrangementHeapSize(ArrangementHeapSize),
225    /// Arrangement heap size update
226    ArrangementHeapCapacity(ArrangementHeapCapacity),
227    /// Arrangement heap size update
228    ArrangementHeapAllocations(ArrangementHeapAllocations),
229    /// Arrangement size operator address
230    ArrangementHeapSizeOperator(ArrangementHeapSizeOperator),
231    /// Arrangement size operator dropped
232    ArrangementHeapSizeOperatorDrop(ArrangementHeapSizeOperatorDrop),
233    /// All operators of a dataflow have shut down.
234    DataflowShutdown(DataflowShutdown),
235    /// The number of errors in a dataflow export has changed.
236    ErrorCount(ErrorCount),
237    /// A dataflow export started hydrating, i.e. its dataflow was unsuspended.
238    HydrationStart(HydrationStart),
239    /// A dataflow export was hydrated.
240    Hydration(Hydration),
241    /// A dataflow operator's hydration status changed.
242    OperatorHydration(OperatorHydration),
243    /// An LIR operator was mapped to some particular dataflow operator.
244    ///
245    /// Cf. `ComputeLog::LirMaping`
246    LirMapping(LirMapping),
247    DataflowGlobal(DataflowGlobal),
248}
249
250/// A peek type distinguishing between index and persist peeks.
251#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Columnar)]
252pub enum PeekType {
253    /// A peek against an index.
254    Index,
255    /// A peek against persist.
256    Persist,
257}
258
259impl PeekType {
260    /// A human-readable name for a peek type.
261    fn name(self) -> &'static str {
262        match self {
263            PeekType::Index => "index",
264            PeekType::Persist => "persist",
265        }
266    }
267}
268
269/// Metadata for LIR operators.
270#[derive(Clone, Debug, PartialEq, PartialOrd, Columnar)]
271pub struct LirMetadata {
272    /// The LIR operator, as a string (see `FlatPlanNode::humanize`).
273    operator: String,
274    /// The LIR identifier of the parent (if any).
275    parent_lir_id: Option<LirId>,
276    /// How nested the operator is (for nice indentation).
277    nesting: u8,
278    /// The dataflow operator ids, given as start (inclusive) and end (exclusive).
279    /// If `start == end`, then no operators were used.
280    operator_span: (usize, usize),
281}
282
283impl LirMetadata {
284    /// Construct a new LIR metadata object.
285    pub fn new(
286        operator: String,
287        parent_lir_id: Option<LirId>,
288        nesting: u8,
289        operator_span: (usize, usize),
290    ) -> Self {
291        Self {
292            operator,
293            parent_lir_id,
294            nesting,
295            operator_span,
296        }
297    }
298}
299
300/// The return type of the [`construct`] function.
301pub(super) struct Return {
302    /// Collections returned by [`construct`].
303    pub collections: BTreeMap<LogVariant, LogCollection>,
304}
305
306/// Constructs the logging dataflow fragment for compute logs.
307///
308/// Params
309/// * `scope`: The Timely scope hosting the log analysis dataflow.
310/// * `scheduler`: The timely scheduler to obtainer activators.
311/// * `config`: Logging configuration.
312/// * `event_queue`: The source to read compute log events from.
313/// * `compute_event_streams`: Additional compute event streams to absorb.
314/// * `shared_state`: Shared state between logging dataflow fragments.
315pub(super) fn construct<'scope>(
316    scope: Scope<'scope, Timestamp>,
317    activations: Rc<RefCell<Activations>>,
318    config: &mz_compute_client::logging::LoggingConfig,
319    event_queue: EventQueue<Column<(Duration, ComputeEvent)>>,
320    shared_state: Rc<RefCell<SharedLoggingState>>,
321) -> Return {
322    let logging_interval_ms = std::cmp::max(1, config.interval.as_millis());
323
324    scope.scoped("compute logging", move |scope| {
325        let enable_logging = config.enable_logging;
326        let (logs, token) = if enable_logging {
327            event_queue.links.mz_replay(
328                scope,
329                "compute logs",
330                config.interval,
331                event_queue.activator,
332            )
333        } else {
334            let token: Rc<dyn std::any::Any> = Rc::new(Box::new(()));
335            (empty(scope), token)
336        };
337
338        // Build a demux operator that splits the replayed event stream up into the separate
339        // logging streams.
340        let mut demux = OperatorBuilder::new("Compute Logging Demux".to_string(), scope.clone());
341        let mut input = demux.new_input(logs, Pipeline);
342        let (export_out, export) = demux.new_output();
343        let mut export_out = OutputBuilder::from(export_out);
344        let (frontier_out, frontier) = demux.new_output();
345        let mut frontier_out = OutputBuilder::from(frontier_out);
346        let (import_frontier_out, import_frontier) = demux.new_output();
347        let mut import_frontier_out = OutputBuilder::from(import_frontier_out);
348        let (peek_out, peek) = demux.new_output();
349        let mut peek_out = OutputBuilder::from(peek_out);
350        let (peek_duration_out, peek_duration) = demux.new_output();
351        let mut peek_duration_out = OutputBuilder::from(peek_duration_out);
352        let (arrangement_heap_size_out, arrangement_heap_size) = demux.new_output();
353        let mut arrangement_heap_size_out = OutputBuilder::from(arrangement_heap_size_out);
354        let (arrangement_heap_capacity_out, arrangement_heap_capacity) = demux.new_output();
355        let mut arrangement_heap_capacity_out = OutputBuilder::from(arrangement_heap_capacity_out);
356        let (arrangement_heap_allocations_out, arrangement_heap_allocations) = demux.new_output();
357        let mut arrangement_heap_allocations_out =
358            OutputBuilder::from(arrangement_heap_allocations_out);
359        let (error_count_out, error_count) = demux.new_output();
360        let mut error_count_out = OutputBuilder::from(error_count_out);
361        let (hydration_time_out, hydration_time) = demux.new_output();
362        let mut hydration_time_out = OutputBuilder::from(hydration_time_out);
363        let (operator_hydration_status_out, operator_hydration_status) = demux.new_output();
364        let mut operator_hydration_status_out = OutputBuilder::from(operator_hydration_status_out);
365        let (lir_mapping_out, lir_mapping) = demux.new_output();
366        let mut lir_mapping_out = OutputBuilder::from(lir_mapping_out);
367        let (dataflow_global_ids_out, dataflow_global_ids) = demux.new_output();
368        let mut dataflow_global_ids_out = OutputBuilder::from(dataflow_global_ids_out);
369
370        let mut demux_state = DemuxState::new(activations, scope.index());
371        demux.build(move |_capability| {
372            move |_frontiers| {
373                let mut export = export_out.activate();
374                let mut frontier = frontier_out.activate();
375                let mut import_frontier = import_frontier_out.activate();
376                let mut peek = peek_out.activate();
377                let mut peek_duration = peek_duration_out.activate();
378                let mut arrangement_heap_size = arrangement_heap_size_out.activate();
379                let mut arrangement_heap_capacity = arrangement_heap_capacity_out.activate();
380                let mut arrangement_heap_allocations = arrangement_heap_allocations_out.activate();
381                let mut error_count = error_count_out.activate();
382                let mut hydration_time = hydration_time_out.activate();
383                let mut operator_hydration_status = operator_hydration_status_out.activate();
384                let mut lir_mapping = lir_mapping_out.activate();
385                let mut dataflow_global_ids = dataflow_global_ids_out.activate();
386
387                input.for_each(|cap, data| {
388                    let mut output_sessions = DemuxOutput {
389                        export: export.session_with_builder(&cap),
390                        frontier: frontier.session_with_builder(&cap),
391                        import_frontier: import_frontier.session_with_builder(&cap),
392                        peek: peek.session_with_builder(&cap),
393                        peek_duration: peek_duration.session_with_builder(&cap),
394                        arrangement_heap_allocations: arrangement_heap_allocations
395                            .session_with_builder(&cap),
396                        arrangement_heap_capacity: arrangement_heap_capacity
397                            .session_with_builder(&cap),
398                        arrangement_heap_size: arrangement_heap_size.session_with_builder(&cap),
399                        error_count: error_count.session_with_builder(&cap),
400                        hydration_time: hydration_time.session_with_builder(&cap),
401                        operator_hydration_status: operator_hydration_status
402                            .session_with_builder(&cap),
403                        lir_mapping: lir_mapping.session_with_builder(&cap),
404                        dataflow_global_ids: dataflow_global_ids.session_with_builder(&cap),
405                    };
406
407                    let shared_state = &mut shared_state.borrow_mut();
408                    for (time, event) in data.borrow().into_index_iter() {
409                        DemuxHandler {
410                            state: &mut demux_state,
411                            shared_state,
412                            output: &mut output_sessions,
413                            logging_interval_ms,
414                            time,
415                        }
416                        .handle(event);
417                    }
418                });
419            }
420        });
421
422        use ComputeLog::*;
423        let logs = [
424            (ArrangementHeapAllocations, arrangement_heap_allocations),
425            (ArrangementHeapCapacity, arrangement_heap_capacity),
426            (ArrangementHeapSize, arrangement_heap_size),
427            (DataflowCurrent, export),
428            (DataflowGlobal, dataflow_global_ids),
429            (ErrorCount, error_count),
430            (FrontierCurrent, frontier),
431            (HydrationTime, hydration_time),
432            (ImportFrontierCurrent, import_frontier),
433            (LirMapping, lir_mapping),
434            (OperatorHydrationStatus, operator_hydration_status),
435            (PeekCurrent, peek),
436            (PeekDuration, peek_duration),
437        ];
438
439        // Build the output arrangements.
440        let mut collections = BTreeMap::new();
441        for (variant, stream) in logs {
442            let variant = LogVariant::Compute(variant);
443            if config.index_logs.contains_key(&variant) {
444                let exchange = ExchangeCore::<ColumnBuilder<_>, _>::new_core(
445                    columnar_exchange::<Row, Row, Timestamp, Diff>,
446                );
447                let trace = stream
448                    .mz_arrange_core::<
449                        _,
450                        batcher::Chunker<_>,
451                        Col2ValBatcher<_, _, _, _>,
452                        RowRowBuilder<_, _>,
453                        RowRowSpine<_, _>,
454                    >(exchange, &format!("Arrange {variant:?}"))
455                    .trace;
456                let collection = LogCollection {
457                    trace,
458                    token: Rc::clone(&token),
459                };
460                collections.insert(variant, collection);
461            }
462        }
463
464        Return { collections }
465    })
466}
467
468/// Format the given value and pack it into a `Datum::String`.
469///
470/// The `scratch` buffer is used to perform the string conversion without an allocation.
471/// Callers should not assume anything about the contents of this buffer after this function
472/// returns.
473fn make_string_datum<V>(value: V, scratch: &mut String) -> Datum<'_>
474where
475    V: Display,
476{
477    scratch.clear();
478    write!(scratch, "{}", value).expect("writing to a `String` can't fail");
479    Datum::String(scratch)
480}
481
482/// Pack an offset from the Unix epoch into a `Datum::TimestampTz`.
483///
484/// The offset is rounded to microseconds, the maximum resolution of `timestamptz`. Rounding is
485/// monotone, so it cannot reorder two offsets that were ordered before.
486fn epoch_offset_datum(offset: Duration) -> Datum<'static> {
487    let secs = i64::try_from(offset.as_secs()).expect("must fit");
488    let datetime = DateTime::from_timestamp(secs, offset.subsec_nanos())
489        .expect("epoch offset is in range for `DateTime`");
490    let timestamp = CheckedTimestamp::try_from(datetime)
491        .expect("epoch offset is a valid timestamp")
492        .round_to_precision(None)
493        .expect("epoch offset is far from the maximum timestamp");
494    Datum::TimestampTz(timestamp)
495}
496
497/// State maintained by the demux operator.
498struct DemuxState {
499    /// The timely activations handle.
500    activations: Rc<RefCell<Activations>>,
501    /// The index of this worker.
502    worker_id: usize,
503    /// A reusable scratch string for formatting IDs.
504    scratch_string_a: String,
505    /// A reusable scratch string for formatting IDs.
506    scratch_string_b: String,
507    /// State tracked per dataflow export.
508    exports: BTreeMap<GlobalId, ExportState>,
509    /// Maps pending peeks to their installation time.
510    peek_stash: BTreeMap<Uuid, Duration>,
511    /// Arrangement size stash.
512    arrangement_size: BTreeMap<usize, ArrangementSizeState>,
513    /// LIR -> operator span mapping.
514    lir_mapping: BTreeMap<GlobalId, BTreeMap<LirId, LirMetadata>>,
515    /// Dataflow -> `GlobalId` mapping (many-to-one).
516    dataflow_global_ids: BTreeMap<usize, BTreeSet<GlobalId>>,
517    /// A row packer for the arrangement heap allocations output.
518    arrangement_heap_allocations_packer: PermutedRowPacker,
519    /// A row packer for the arrangement heap capacity output.
520    arrangement_heap_capacity_packer: PermutedRowPacker,
521    /// A row packer for the arrangement heap size output.
522    arrangement_heap_size_packer: PermutedRowPacker,
523    /// A row packer for the dataflow global output.
524    dataflow_global_packer: PermutedRowPacker,
525    /// A row packer for the error count output.
526    error_count_packer: PermutedRowPacker,
527    /// A row packer for the exports output.
528    export_packer: PermutedRowPacker,
529    /// A row packer for the frontier output.
530    frontier_packer: PermutedRowPacker,
531    /// A row packer for the exports output.
532    import_frontier_packer: PermutedRowPacker,
533    /// A row packer for the LIR mapping output.
534    lir_mapping_packer: PermutedRowPacker,
535    /// A row packer for the operator hydration status output.
536    operator_hydration_status_packer: PermutedRowPacker,
537    /// A row packer for the peek durations output.
538    peek_duration_packer: PermutedRowPacker,
539    /// A row packer for the peek output.
540    peek_packer: PermutedRowPacker,
541    /// A row packer for the hydration time output.
542    hydration_time_packer: PermutedRowPacker,
543}
544
545impl DemuxState {
546    fn new(activations: Rc<RefCell<Activations>>, worker_id: usize) -> Self {
547        Self {
548            activations,
549            worker_id,
550            scratch_string_a: String::new(),
551            scratch_string_b: String::new(),
552            exports: Default::default(),
553            peek_stash: Default::default(),
554            arrangement_size: Default::default(),
555            lir_mapping: Default::default(),
556            dataflow_global_ids: Default::default(),
557            arrangement_heap_allocations_packer: PermutedRowPacker::new(
558                ComputeLog::ArrangementHeapAllocations,
559            ),
560            arrangement_heap_capacity_packer: PermutedRowPacker::new(
561                ComputeLog::ArrangementHeapCapacity,
562            ),
563            arrangement_heap_size_packer: PermutedRowPacker::new(ComputeLog::ArrangementHeapSize),
564            dataflow_global_packer: PermutedRowPacker::new(ComputeLog::DataflowGlobal),
565            error_count_packer: PermutedRowPacker::new(ComputeLog::ErrorCount),
566            export_packer: PermutedRowPacker::new(ComputeLog::DataflowCurrent),
567            frontier_packer: PermutedRowPacker::new(ComputeLog::FrontierCurrent),
568            hydration_time_packer: PermutedRowPacker::new(ComputeLog::HydrationTime),
569            import_frontier_packer: PermutedRowPacker::new(ComputeLog::ImportFrontierCurrent),
570            lir_mapping_packer: PermutedRowPacker::new(ComputeLog::LirMapping),
571            operator_hydration_status_packer: PermutedRowPacker::new(
572                ComputeLog::OperatorHydrationStatus,
573            ),
574            peek_duration_packer: PermutedRowPacker::new(ComputeLog::PeekDuration),
575            peek_packer: PermutedRowPacker::new(ComputeLog::PeekCurrent),
576        }
577    }
578
579    /// Pack an arrangement heap allocations update key-value for the given operator.
580    fn pack_arrangement_heap_allocations_update(
581        &mut self,
582        operator_id: usize,
583    ) -> (&RowRef, &RowRef) {
584        self.arrangement_heap_allocations_packer.pack_slice(&[
585            Datum::UInt64(operator_id.try_into().expect("operator_id too big")),
586            Datum::UInt64(u64::cast_from(self.worker_id)),
587        ])
588    }
589
590    /// Pack an arrangement heap capacity update key-value for the given operator.
591    fn pack_arrangement_heap_capacity_update(&mut self, operator_id: usize) -> (&RowRef, &RowRef) {
592        self.arrangement_heap_capacity_packer.pack_slice(&[
593            Datum::UInt64(operator_id.try_into().expect("operator_id too big")),
594            Datum::UInt64(u64::cast_from(self.worker_id)),
595        ])
596    }
597
598    /// Pack an arrangement heap size update key-value for the given operator.
599    fn pack_arrangement_heap_size_update(&mut self, operator_id: usize) -> (&RowRef, &RowRef) {
600        self.arrangement_heap_size_packer.pack_slice(&[
601            Datum::UInt64(operator_id.try_into().expect("operator_id too big")),
602            Datum::UInt64(u64::cast_from(self.worker_id)),
603        ])
604    }
605
606    /// Pack a dataflow global update key-value for the given dataflow index and global ID.
607    fn pack_dataflow_global_update(
608        &mut self,
609        dataflow_index: usize,
610        global_id: GlobalId,
611    ) -> (&RowRef, &RowRef) {
612        self.dataflow_global_packer.pack_slice(&[
613            Datum::UInt64(u64::cast_from(dataflow_index)),
614            Datum::UInt64(u64::cast_from(self.worker_id)),
615            make_string_datum(global_id, &mut self.scratch_string_a),
616        ])
617    }
618
619    /// Pack an error count update key-value for the given export ID and count.
620    fn pack_error_count_update(&mut self, export_id: GlobalId, count: Diff) -> (&RowRef, &RowRef) {
621        // Normally we would use DD's diff field to encode counts, but in this case we can't: The total
622        // per-worker error count might be negative and at the SQL level having negative multiplicities
623        // is treated as an error.
624        self.error_count_packer.pack_slice(&[
625            make_string_datum(export_id, &mut self.scratch_string_a),
626            Datum::UInt64(u64::cast_from(self.worker_id)),
627            Datum::Int64(count.into_inner()),
628        ])
629    }
630
631    /// Pack an export update key-value for the given export ID and dataflow index.
632    fn pack_export_update(
633        &mut self,
634        export_id: GlobalId,
635        dataflow_index: usize,
636    ) -> (&RowRef, &RowRef) {
637        self.export_packer.pack_slice(&[
638            make_string_datum(export_id, &mut self.scratch_string_a),
639            Datum::UInt64(u64::cast_from(self.worker_id)),
640            Datum::UInt64(u64::cast_from(dataflow_index)),
641        ])
642    }
643
644    /// Pack a hydration time update key-value for the given export ID, hydration time, and
645    /// hydration lifecycle timestamps.
646    fn pack_hydration_time_update(
647        &mut self,
648        export_id: GlobalId,
649        time_ns: Option<u64>,
650        timestamps: &HydrationTimestamps,
651    ) -> (&RowRef, &RowRef) {
652        self.hydration_time_packer.pack_slice(&[
653            make_string_datum(export_id, &mut self.scratch_string_a),
654            Datum::UInt64(u64::cast_from(self.worker_id)),
655            Datum::from(time_ns),
656            epoch_offset_datum(timestamps.installed_at),
657            timestamps
658                .started_at
659                .map_or(Datum::Null, epoch_offset_datum),
660            timestamps
661                .hydrated_at
662                .map_or(Datum::Null, epoch_offset_datum),
663        ])
664    }
665
666    /// Pack an import frontier update key-value for the given export ID and dataflow index.
667    fn pack_import_frontier_update(
668        &mut self,
669        export_id: GlobalId,
670        import_id: GlobalId,
671        time: Timestamp,
672    ) -> (&RowRef, &RowRef) {
673        self.import_frontier_packer.pack_slice(&[
674            make_string_datum(export_id, &mut self.scratch_string_a),
675            make_string_datum(import_id, &mut self.scratch_string_b),
676            Datum::UInt64(u64::cast_from(self.worker_id)),
677            Datum::MzTimestamp(time),
678        ])
679    }
680
681    /// Pack an LIR mapping update key-value for the given LIR operator metadata.
682    fn pack_lir_mapping_update(
683        &mut self,
684        global_id: GlobalId,
685        lir_id: LirId,
686        operator: String,
687        parent_lir_id: Option<LirId>,
688        nesting: u8,
689        operator_span: (usize, usize),
690    ) -> (&RowRef, &RowRef) {
691        self.lir_mapping_packer.pack_slice(&[
692            make_string_datum(global_id, &mut self.scratch_string_a),
693            Datum::UInt64(lir_id.into()),
694            Datum::UInt64(u64::cast_from(self.worker_id)),
695            make_string_datum(operator, &mut self.scratch_string_b),
696            parent_lir_id.map_or(Datum::Null, |lir_id| Datum::UInt64(lir_id.into())),
697            Datum::UInt16(u16::cast_from(nesting)),
698            Datum::UInt64(u64::cast_from(operator_span.0)),
699            Datum::UInt64(u64::cast_from(operator_span.1)),
700        ])
701    }
702
703    /// Pack an operator hydration status update key-value for the given export ID, LIR ID, and
704    /// hydration status.
705    fn pack_operator_hydration_status_update(
706        &mut self,
707        export_id: GlobalId,
708        lir_id: LirId,
709        hydrated: bool,
710    ) -> (&RowRef, &RowRef) {
711        self.operator_hydration_status_packer.pack_slice(&[
712            make_string_datum(export_id, &mut self.scratch_string_a),
713            Datum::UInt64(lir_id.into()),
714            Datum::UInt64(u64::cast_from(self.worker_id)),
715            Datum::from(hydrated),
716        ])
717    }
718
719    /// Pack a peek duration update key-value for the given peek and peek type.
720    fn pack_peek_duration_update(
721        &mut self,
722        peek_type: PeekType,
723        bucket: u128,
724    ) -> (&RowRef, &RowRef) {
725        self.peek_duration_packer.pack_slice(&[
726            Datum::UInt64(u64::cast_from(self.worker_id)),
727            Datum::String(peek_type.name()),
728            Datum::UInt64(bucket.try_into().expect("bucket too big")),
729        ])
730    }
731
732    /// Pack a peek update key-value for the given peek and peek type.
733    fn pack_peek_update(
734        &mut self,
735        id: GlobalId,
736        time: Timestamp,
737        uuid: Uuid,
738        peek_type: PeekType,
739    ) -> (&RowRef, &RowRef) {
740        self.peek_packer.pack_slice(&[
741            Datum::Uuid(uuid),
742            Datum::UInt64(u64::cast_from(self.worker_id)),
743            make_string_datum(id, &mut self.scratch_string_a),
744            Datum::String(peek_type.name()),
745            Datum::MzTimestamp(time),
746        ])
747    }
748
749    /// Pack a frontier update key-value for the given export ID and time.
750    fn pack_frontier_update(&mut self, export_id: GlobalId, time: Timestamp) -> (&RowRef, &RowRef) {
751        self.frontier_packer.pack_slice(&[
752            make_string_datum(export_id, &mut self.scratch_string_a),
753            Datum::UInt64(u64::cast_from(self.worker_id)),
754            Datum::MzTimestamp(time),
755        ])
756    }
757}
758
759/// Wallclock instants of an export's hydration lifecycle, as durations since the Unix epoch.
760///
761/// These are sampled from compute logging event times, which advance off an `Instant` anchored to
762/// the epoch once per worker when logging is initialized. They are therefore monotone within a
763/// worker and unaffected by system clock steps, but they carry that worker's anchor, so comparing
764/// them across workers absorbs anchor skew.
765///
766/// The invariant `installed_at <= started_at <= hydrated_at` holds over the non-NULL values, and no
767/// later stage is ever set without its predecessors.
768#[derive(Debug, Clone, Copy)]
769struct HydrationTimestamps {
770    /// When this export's dataflow was installed, still suspended. Also the start of queueing.
771    installed_at: Duration,
772    /// When hydration work began, that is, when the dataflow was unsuspended.
773    ///
774    /// Equals `installed_at` for a dataflow that was never suspended, since such a dataflow starts
775    /// running the moment it is built.
776    started_at: Option<Duration>,
777    /// When this export's output frontier passed the dataflow as-of.
778    hydrated_at: Option<Duration>,
779}
780
781/// State tracked for each dataflow export.
782struct ExportState {
783    /// The ID of the dataflow maintaining this export.
784    dataflow_index: usize,
785    /// Number of errors in this export.
786    ///
787    /// This must be a signed integer, since per-worker error counts can be negative, only the
788    /// cross-worker total has to sum up to a non-negative value.
789    error_count: Diff,
790    /// When this export was created.
791    created_at: Instant,
792    /// Whether the exported collection is hydrated.
793    hydration_time_ns: Option<u64>,
794    /// Wallclock instants of this export's hydration lifecycle.
795    ///
796    /// Distinct from `created_at`/`hydration_time_ns`, which remain the authoritative per-worker
797    /// duration at nanosecond precision. These instants exist to identify and bound hydration
798    /// episodes, which a duration cannot do.
799    hydration_timestamps: HydrationTimestamps,
800    /// Hydration status of operators feeding this export.
801    operator_hydration: BTreeMap<LirId, bool>,
802}
803
804impl ExportState {
805    fn new(dataflow_index: usize, installed_at: Duration) -> Self {
806        Self {
807            dataflow_index,
808            error_count: Diff::ZERO,
809            created_at: Instant::now(),
810            hydration_time_ns: None,
811            hydration_timestamps: HydrationTimestamps {
812                installed_at,
813                started_at: None,
814                hydrated_at: None,
815            },
816            operator_hydration: BTreeMap::new(),
817        }
818    }
819}
820
821/// State for tracking arrangement sizes.
822#[derive(Default, Debug)]
823struct ArrangementSizeState {
824    size: isize,
825    capacity: isize,
826    count: isize,
827}
828
829/// Bundled output sessions used by the demux operator.
830struct DemuxOutput<'a, 'b> {
831    export: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
832    frontier: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
833    import_frontier: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
834    peek: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
835    peek_duration: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
836    arrangement_heap_allocations: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
837    arrangement_heap_capacity: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
838    arrangement_heap_size: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
839    hydration_time: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
840    operator_hydration_status: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
841    error_count: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
842    lir_mapping: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
843    dataflow_global_ids: OutputSessionColumnar<'a, 'b, Update<(Row, Row)>>,
844}
845
846/// Event handler of the demux operator.
847struct DemuxHandler<'a, 'b, 'c> {
848    /// State kept by the demux operator.
849    state: &'a mut DemuxState,
850    /// State shared across log receivers.
851    shared_state: &'a mut SharedLoggingState,
852    /// Demux output sessions.
853    output: &'a mut DemuxOutput<'b, 'c>,
854    /// The logging interval specifying the time granularity for the updates.
855    logging_interval_ms: u128,
856    /// The current event time.
857    time: Duration,
858}
859
860impl DemuxHandler<'_, '_, '_> {
861    /// Return the timestamp associated with the current event, based on the event time and the
862    /// logging interval.
863    fn ts(&self) -> Timestamp {
864        let time_ms = self.time.as_millis();
865        let interval = self.logging_interval_ms;
866        let rounded = (time_ms / interval + 1) * interval;
867        rounded.try_into().expect("must fit")
868    }
869
870    /// Handle the given compute event.
871    fn handle(&mut self, event: Ref<'_, ComputeEvent>) {
872        use ComputeEventReference::*;
873        match event {
874            Export(export) => self.handle_export(export),
875            ExportDropped(export_dropped) => self.handle_export_dropped(export_dropped),
876            Peek(peek) if peek.installed => self.handle_peek_install(peek),
877            Peek(peek) => self.handle_peek_retire(peek),
878            Frontier(frontier) => self.handle_frontier(frontier),
879            ImportFrontier(import_frontier) => self.handle_import_frontier(import_frontier),
880            ArrangementHeapSize(inner) => self.handle_arrangement_heap_size(inner),
881            ArrangementHeapCapacity(inner) => self.handle_arrangement_heap_capacity(inner),
882            ArrangementHeapAllocations(inner) => self.handle_arrangement_heap_allocations(inner),
883            ArrangementHeapSizeOperator(inner) => self.handle_arrangement_heap_size_operator(inner),
884            ArrangementHeapSizeOperatorDrop(inner) => {
885                self.handle_arrangement_heap_size_operator_dropped(inner)
886            }
887            DataflowShutdown(shutdown) => self.handle_dataflow_shutdown(shutdown),
888            ErrorCount(error_count) => self.handle_error_count(error_count),
889            HydrationStart(hydration) => self.handle_hydration_start(hydration),
890            Hydration(hydration) => self.handle_hydration(hydration),
891            OperatorHydration(hydration) => self.handle_operator_hydration(hydration),
892            LirMapping(mapping) => self.handle_lir_mapping(mapping),
893            DataflowGlobal(global) => self.handle_dataflow_global(global),
894        }
895    }
896
897    fn handle_export(
898        &mut self,
899        ExportReference {
900            export_id,
901            dataflow_index,
902        }: Ref<'_, Export>,
903    ) {
904        let export_id = Columnar::into_owned(export_id);
905        let ts = self.ts();
906        let datum = self.state.pack_export_update(export_id, dataflow_index);
907        self.output.export.give((datum, ts, Diff::ONE));
908
909        // Stamp the event time, not `ts`, which is rounded up to the logging interval. The rounding
910        // then only delays when an update becomes visible, rather than skewing recorded instants.
911        let installed_at = self.time;
912
913        let existing = self
914            .state
915            .exports
916            .insert(export_id, ExportState::new(dataflow_index, installed_at));
917        if existing.is_some() {
918            error!(%export_id, "export already registered");
919        }
920
921        // Insert hydration time logging for this export.
922        let timestamps = HydrationTimestamps {
923            installed_at,
924            started_at: None,
925            hydrated_at: None,
926        };
927        let datum = self
928            .state
929            .pack_hydration_time_update(export_id, None, &timestamps);
930        self.output.hydration_time.give((datum, ts, Diff::ONE));
931    }
932
933    fn handle_export_dropped(
934        &mut self,
935        ExportDroppedReference { export_id }: Ref<'_, ExportDropped>,
936    ) {
937        let export_id = Columnar::into_owned(export_id);
938        let Some(export) = self.state.exports.remove(&export_id) else {
939            error!(%export_id, "missing exports entry at time of export drop");
940            return;
941        };
942
943        let ts = self.ts();
944        let dataflow_index = export.dataflow_index;
945
946        let datum = self.state.pack_export_update(export_id, dataflow_index);
947        self.output.export.give((datum, ts, Diff::MINUS_ONE));
948
949        // Remove error count logging for this export.
950        if export.error_count != Diff::ZERO {
951            let datum = self
952                .state
953                .pack_error_count_update(export_id, export.error_count);
954            self.output.error_count.give((datum, ts, Diff::MINUS_ONE));
955        }
956
957        // Remove hydration time logging for this export.
958        let datum = self.state.pack_hydration_time_update(
959            export_id,
960            export.hydration_time_ns,
961            &export.hydration_timestamps,
962        );
963        self.output
964            .hydration_time
965            .give((datum, ts, Diff::MINUS_ONE));
966
967        // Remove operator hydration logging for this export.
968        for (lir_id, hydrated) in export.operator_hydration {
969            let datum = self
970                .state
971                .pack_operator_hydration_status_update(export_id, lir_id, hydrated);
972            self.output
973                .operator_hydration_status
974                .give((datum, ts, Diff::MINUS_ONE));
975        }
976    }
977
978    fn handle_dataflow_shutdown(
979        &mut self,
980        DataflowShutdownReference { dataflow_index }: Ref<'_, DataflowShutdown>,
981    ) {
982        let ts = self.ts();
983
984        // We deal with any `GlobalId` based mappings in this event.
985        if let Some(global_ids) = self.state.dataflow_global_ids.remove(&dataflow_index) {
986            for global_id in global_ids {
987                // Remove dataflow/`GlobalID` mapping.
988                let datum = self
989                    .state
990                    .pack_dataflow_global_update(dataflow_index, global_id);
991                self.output
992                    .dataflow_global_ids
993                    .give((datum, ts, Diff::MINUS_ONE));
994
995                // Remove LIR mapping.
996                if let Some(mappings) = self.state.lir_mapping.remove(&global_id) {
997                    for (
998                        lir_id,
999                        LirMetadata {
1000                            operator,
1001                            parent_lir_id,
1002                            nesting,
1003                            operator_span,
1004                        },
1005                    ) in mappings
1006                    {
1007                        let datum = self.state.pack_lir_mapping_update(
1008                            global_id,
1009                            lir_id,
1010                            operator,
1011                            parent_lir_id,
1012                            nesting,
1013                            operator_span,
1014                        );
1015                        self.output.lir_mapping.give((datum, ts, Diff::MINUS_ONE));
1016                    }
1017                }
1018            }
1019        }
1020    }
1021
1022    fn handle_error_count(&mut self, ErrorCountReference { export_id, diff }: Ref<'_, ErrorCount>) {
1023        let ts = self.ts();
1024        let export_id = Columnar::into_owned(export_id);
1025
1026        let Some(export) = self.state.exports.get_mut(&export_id) else {
1027            // The export might have already been dropped, in which case we are no longer
1028            // interested in its errors.
1029            return;
1030        };
1031
1032        let old_count = export.error_count;
1033        let new_count = old_count + diff;
1034        export.error_count = new_count;
1035
1036        if old_count != Diff::ZERO {
1037            let datum = self.state.pack_error_count_update(export_id, old_count);
1038            self.output.error_count.give((datum, ts, Diff::MINUS_ONE));
1039        }
1040        if new_count != Diff::ZERO {
1041            let datum = self.state.pack_error_count_update(export_id, new_count);
1042            self.output.error_count.give((datum, ts, Diff::ONE));
1043        }
1044    }
1045
1046    fn handle_hydration_start(
1047        &mut self,
1048        HydrationStartReference { export_id }: Ref<'_, HydrationStart>,
1049    ) {
1050        let ts = self.ts();
1051        // Stamp the event time rather than `ts`, as in `handle_export`.
1052        let started_at = self.time;
1053        let export_id = Columnar::into_owned(export_id);
1054
1055        let Some(export) = self.state.exports.get_mut(&export_id) else {
1056            error!(%export_id, "hydration start event for unknown export");
1057            return;
1058        };
1059        if export.hydration_timestamps.started_at.is_some() {
1060            // A `Schedule` command is re-sent for dataflows retained across reconciliation, and is
1061            // sent even for dataflows that were never suspended and have hydrated already. Ignore
1062            // the repeats, mirroring the guard in `handle_hydration`.
1063            return;
1064        }
1065
1066        let old_timestamps = export.hydration_timestamps;
1067        export.hydration_timestamps.started_at = Some(started_at);
1068        let new_timestamps = export.hydration_timestamps;
1069        let time_ns = export.hydration_time_ns;
1070
1071        let retraction = self
1072            .state
1073            .pack_hydration_time_update(export_id, time_ns, &old_timestamps);
1074        self.output
1075            .hydration_time
1076            .give((retraction, ts, Diff::MINUS_ONE));
1077        let insertion = self
1078            .state
1079            .pack_hydration_time_update(export_id, time_ns, &new_timestamps);
1080        self.output.hydration_time.give((insertion, ts, Diff::ONE));
1081    }
1082
1083    fn handle_hydration(&mut self, HydrationReference { export_id }: Ref<'_, Hydration>) {
1084        let ts = self.ts();
1085        // Stamp the event time rather than `ts`, as in `handle_export`.
1086        let hydrated_at = self.time;
1087        let export_id = Columnar::into_owned(export_id);
1088
1089        let Some(export) = self.state.exports.get_mut(&export_id) else {
1090            error!(%export_id, "hydration event for unknown export");
1091            return;
1092        };
1093        if export.hydration_time_ns.is_some() {
1094            // Hydration events for already hydrated dataflows can occur when a dataflow is reused
1095            // after reconciliation. We can simply ignore these.
1096            return;
1097        }
1098
1099        let duration = export.created_at.elapsed();
1100        let nanos = u64::try_from(duration.as_nanos()).expect("must fit");
1101        export.hydration_time_ns = Some(nanos);
1102
1103        let old_timestamps = export.hydration_timestamps;
1104        export.hydration_timestamps.hydrated_at = Some(hydrated_at);
1105        // A dataflow can reach hydration before its `Schedule` arrives, and not only when it has
1106        // no imports to suspend: an index over an already-hydrated arrangement reports hydration
1107        // while still suspended, which happens for a handful of `mz_catalog_server` indexes on
1108        // every bootstrap. So this is a normal path, not a repair for an exotic one.
1109        //
1110        // Stamp `started_at` from `installed_at`, which keeps `installed_at <= started_at <=
1111        // hydrated_at` total and reports the queueing interval as zero. Stamping `hydrated_at`
1112        // instead would invert it, charging the whole life to queueing and reporting zero
1113        // hydration time for a dataflow that only ever hydrated.
1114        if export.hydration_timestamps.started_at.is_none() {
1115            export.hydration_timestamps.started_at = Some(export.hydration_timestamps.installed_at);
1116        }
1117        let new_timestamps = export.hydration_timestamps;
1118
1119        let retraction = self
1120            .state
1121            .pack_hydration_time_update(export_id, None, &old_timestamps);
1122        self.output
1123            .hydration_time
1124            .give((retraction, ts, Diff::MINUS_ONE));
1125        let insertion =
1126            self.state
1127                .pack_hydration_time_update(export_id, Some(nanos), &new_timestamps);
1128        self.output.hydration_time.give((insertion, ts, Diff::ONE));
1129    }
1130
1131    fn handle_operator_hydration(
1132        &mut self,
1133        OperatorHydrationReference {
1134            export_id,
1135            lir_id,
1136            hydrated,
1137        }: Ref<'_, OperatorHydration>,
1138    ) {
1139        let ts = self.ts();
1140        let export_id = Columnar::into_owned(export_id);
1141        let lir_id = Columnar::into_owned(lir_id);
1142        let hydrated = Columnar::into_owned(hydrated);
1143
1144        let Some(export) = self.state.exports.get_mut(&export_id) else {
1145            // The export might have already been dropped, in which case we are no longer
1146            // interested in its operator hydration events.
1147            return;
1148        };
1149
1150        let old_status = export.operator_hydration.get(&lir_id).copied();
1151        export.operator_hydration.insert(lir_id, hydrated);
1152
1153        if let Some(hydrated) = old_status {
1154            let retraction = self
1155                .state
1156                .pack_operator_hydration_status_update(export_id, lir_id, hydrated);
1157            self.output
1158                .operator_hydration_status
1159                .give((retraction, ts, Diff::MINUS_ONE));
1160        }
1161
1162        let insertion = self
1163            .state
1164            .pack_operator_hydration_status_update(export_id, lir_id, hydrated);
1165        self.output
1166            .operator_hydration_status
1167            .give((insertion, ts, Diff::ONE));
1168    }
1169
1170    fn handle_peek_install(
1171        &mut self,
1172        PeekEventReference {
1173            id,
1174            time,
1175            uuid,
1176            peek_type,
1177            installed: _,
1178        }: Ref<'_, PeekEvent>,
1179    ) {
1180        let id = Columnar::into_owned(id);
1181        let uuid = Uuid::from_bytes(uuid::Bytes::into_owned(uuid));
1182        let ts = self.ts();
1183        let datum = self.state.pack_peek_update(id, time, uuid, peek_type);
1184        self.output.peek.give((datum, ts, Diff::ONE));
1185
1186        let existing = self.state.peek_stash.insert(uuid, self.time);
1187        if existing.is_some() {
1188            error!(%uuid, "peek already registered");
1189        }
1190    }
1191
1192    fn handle_peek_retire(
1193        &mut self,
1194        PeekEventReference {
1195            id,
1196            time,
1197            uuid,
1198            peek_type,
1199            installed: _,
1200        }: Ref<'_, PeekEvent>,
1201    ) {
1202        let id = Columnar::into_owned(id);
1203        let uuid = Uuid::from_bytes(uuid::Bytes::into_owned(uuid));
1204        let ts = self.ts();
1205        let datum = self.state.pack_peek_update(id, time, uuid, peek_type);
1206        self.output.peek.give((datum, ts, Diff::MINUS_ONE));
1207
1208        if let Some(start) = self.state.peek_stash.remove(&uuid) {
1209            let elapsed_ns = self.time.saturating_sub(start).as_nanos();
1210            let bucket = elapsed_ns.next_power_of_two();
1211            let datum = self.state.pack_peek_duration_update(peek_type, bucket);
1212            self.output.peek_duration.give((datum, ts, Diff::ONE));
1213        } else {
1214            error!(%uuid, "peek not yet registered");
1215        }
1216    }
1217
1218    fn handle_frontier(
1219        &mut self,
1220        FrontierReference {
1221            export_id,
1222            time,
1223            diff,
1224        }: Ref<'_, Frontier>,
1225    ) {
1226        let export_id = Columnar::into_owned(export_id);
1227        let diff = Diff::from(*diff);
1228        let ts = self.ts();
1229        let time = Columnar::into_owned(time);
1230        let datum = self.state.pack_frontier_update(export_id, time);
1231        self.output.frontier.give((datum, ts, diff));
1232    }
1233
1234    fn handle_import_frontier(
1235        &mut self,
1236        ImportFrontierReference {
1237            import_id,
1238            export_id,
1239            time,
1240            diff,
1241        }: Ref<'_, ImportFrontier>,
1242    ) {
1243        let import_id = Columnar::into_owned(import_id);
1244        let export_id = Columnar::into_owned(export_id);
1245        let diff = Diff::from(*diff);
1246        let ts = self.ts();
1247        let time = Columnar::into_owned(time);
1248        let datum = self
1249            .state
1250            .pack_import_frontier_update(export_id, import_id, time);
1251        self.output.import_frontier.give((datum, ts, diff));
1252    }
1253
1254    /// Update the allocation size for an arrangement.
1255    fn handle_arrangement_heap_size(
1256        &mut self,
1257        ArrangementHeapSizeReference {
1258            operator_id,
1259            delta_size,
1260        }: Ref<'_, ArrangementHeapSize>,
1261    ) {
1262        let ts = self.ts();
1263        let Some(state) = self.state.arrangement_size.get_mut(&operator_id) else {
1264            return;
1265        };
1266
1267        state.size += delta_size;
1268
1269        let datum = self.state.pack_arrangement_heap_size_update(operator_id);
1270        let diff = Diff::cast_from(delta_size);
1271        self.output.arrangement_heap_size.give((datum, ts, diff));
1272    }
1273
1274    /// Update the allocation capacity for an arrangement.
1275    fn handle_arrangement_heap_capacity(
1276        &mut self,
1277        ArrangementHeapCapacityReference {
1278            operator_id,
1279            delta_capacity,
1280        }: Ref<'_, ArrangementHeapCapacity>,
1281    ) {
1282        let ts = self.ts();
1283        let Some(state) = self.state.arrangement_size.get_mut(&operator_id) else {
1284            return;
1285        };
1286
1287        state.capacity += delta_capacity;
1288
1289        let datum = self
1290            .state
1291            .pack_arrangement_heap_capacity_update(operator_id);
1292        let diff = Diff::cast_from(delta_capacity);
1293        self.output
1294            .arrangement_heap_capacity
1295            .give((datum, ts, diff));
1296    }
1297
1298    /// Update the allocation count for an arrangement.
1299    fn handle_arrangement_heap_allocations(
1300        &mut self,
1301        ArrangementHeapAllocationsReference {
1302            operator_id,
1303            delta_allocations,
1304        }: Ref<'_, ArrangementHeapAllocations>,
1305    ) {
1306        let ts = self.ts();
1307        let Some(state) = self.state.arrangement_size.get_mut(&operator_id) else {
1308            return;
1309        };
1310
1311        state.count += delta_allocations;
1312
1313        let datum = self
1314            .state
1315            .pack_arrangement_heap_allocations_update(operator_id);
1316        let diff = Diff::cast_from(delta_allocations);
1317        self.output
1318            .arrangement_heap_allocations
1319            .give((datum, ts, diff));
1320    }
1321
1322    /// Indicate that a new arrangement exists, start maintaining the heap size state.
1323    fn handle_arrangement_heap_size_operator(
1324        &mut self,
1325        ArrangementHeapSizeOperatorReference {
1326            operator_id,
1327            address,
1328        }: Ref<'_, ArrangementHeapSizeOperator>,
1329    ) {
1330        let activator = Activator::new(
1331            address.into_iter().collect(),
1332            Rc::clone(&self.state.activations),
1333        );
1334        let existing = self
1335            .state
1336            .arrangement_size
1337            .insert(operator_id, Default::default());
1338        if existing.is_some() {
1339            error!(%operator_id, "arrangement size operator already registered");
1340        }
1341        let existing = self
1342            .shared_state
1343            .arrangement_size_activators
1344            .insert(operator_id, activator);
1345        if existing.is_some() {
1346            error!(%operator_id, "arrangement size activator already registered");
1347        }
1348    }
1349
1350    /// Indicate that an arrangement has been dropped and we can cleanup the heap size state.
1351    fn handle_arrangement_heap_size_operator_dropped(
1352        &mut self,
1353        event: Ref<'_, ArrangementHeapSizeOperatorDrop>,
1354    ) {
1355        let operator_id = event.operator_id;
1356        if let Some(state) = self.state.arrangement_size.remove(&operator_id) {
1357            let ts = self.ts();
1358            let allocations = self
1359                .state
1360                .pack_arrangement_heap_allocations_update(operator_id);
1361            let diff = -Diff::cast_from(state.count);
1362            self.output
1363                .arrangement_heap_allocations
1364                .give((allocations, ts, diff));
1365
1366            let capacity = self
1367                .state
1368                .pack_arrangement_heap_capacity_update(operator_id);
1369            let diff = -Diff::cast_from(state.capacity);
1370            self.output
1371                .arrangement_heap_capacity
1372                .give((capacity, ts, diff));
1373
1374            let size = self.state.pack_arrangement_heap_size_update(operator_id);
1375            let diff = -Diff::cast_from(state.size);
1376            self.output.arrangement_heap_size.give((size, ts, diff));
1377        }
1378        self.shared_state
1379            .arrangement_size_activators
1380            .remove(&operator_id);
1381    }
1382
1383    /// Indicate that a new LIR operator exists; record the dataflow address it maps to.
1384    fn handle_lir_mapping(
1385        &mut self,
1386        LirMappingReference { global_id, mapping }: Ref<'_, LirMapping>,
1387    ) {
1388        let global_id = Columnar::into_owned(global_id);
1389        // record the state (for the later drop)
1390        let mappings = || mapping.into_iter().map(Columnar::into_owned);
1391        self.state
1392            .lir_mapping
1393            .entry(global_id)
1394            .and_modify(|existing_mapping| existing_mapping.extend(mappings()))
1395            .or_insert_with(|| mappings().collect());
1396
1397        // send the datum out
1398        let ts = self.ts();
1399        for (lir_id, meta) in mapping.into_iter() {
1400            let datum = self.state.pack_lir_mapping_update(
1401                global_id,
1402                Columnar::into_owned(lir_id),
1403                Columnar::into_owned(meta.operator),
1404                Columnar::into_owned(meta.parent_lir_id),
1405                Columnar::into_owned(meta.nesting),
1406                Columnar::into_owned(meta.operator_span),
1407            );
1408            self.output.lir_mapping.give((datum, ts, Diff::ONE));
1409        }
1410    }
1411
1412    fn handle_dataflow_global(
1413        &mut self,
1414        DataflowGlobalReference {
1415            dataflow_index,
1416            global_id,
1417        }: Ref<'_, DataflowGlobal>,
1418    ) {
1419        let global_id = Columnar::into_owned(global_id);
1420        self.state
1421            .dataflow_global_ids
1422            .entry(dataflow_index)
1423            .and_modify(|globals| {
1424                // NB BTreeSet::insert() returns `false` when the element was already in the set
1425                if !globals.insert(global_id) {
1426                    error!(%dataflow_index, %global_id, "dataflow mapping already knew about this GlobalId");
1427                }
1428            })
1429            .or_insert_with(|| BTreeSet::from([global_id]));
1430
1431        let ts = self.ts();
1432        let datum = self
1433            .state
1434            .pack_dataflow_global_update(dataflow_index, global_id);
1435        self.output.dataflow_global_ids.give((datum, ts, Diff::ONE));
1436    }
1437}
1438
1439/// Logging state maintained for a compute collection.
1440///
1441/// This type is used to produce appropriate log events in response to changes of logged collection
1442/// state, e.g. frontiers, and to produce cleanup events when a collection is dropped.
1443pub struct CollectionLogging {
1444    export_id: GlobalId,
1445    logger: Logger,
1446
1447    logged_frontier: Option<Timestamp>,
1448    logged_import_frontiers: BTreeMap<GlobalId, Timestamp>,
1449}
1450
1451impl CollectionLogging {
1452    /// Create new logging state for the identified collection and emit initial logging events.
1453    pub fn new(
1454        export_id: GlobalId,
1455        logger: Logger,
1456        dataflow_index: usize,
1457        import_ids: impl Iterator<Item = GlobalId>,
1458    ) -> Self {
1459        logger.log(&ComputeEvent::Export(Export {
1460            export_id,
1461            dataflow_index,
1462        }));
1463
1464        let mut self_ = Self {
1465            export_id,
1466            logger,
1467            logged_frontier: None,
1468            logged_import_frontiers: Default::default(),
1469        };
1470
1471        // Initialize frontier logging.
1472        let initial_frontier = Some(Timestamp::MIN);
1473        self_.set_frontier(initial_frontier);
1474        import_ids.for_each(|id| self_.set_import_frontier(id, initial_frontier));
1475
1476        self_
1477    }
1478
1479    /// Set the collection frontier to the given new time and emit corresponding logging events.
1480    pub fn set_frontier(&mut self, new_time: Option<Timestamp>) {
1481        let old_time = self.logged_frontier;
1482        self.logged_frontier = new_time;
1483
1484        if old_time != new_time {
1485            let export_id = self.export_id;
1486            let retraction = old_time.map(|time| {
1487                ComputeEvent::Frontier(Frontier {
1488                    export_id,
1489                    time,
1490                    diff: -1,
1491                })
1492            });
1493            let insertion = new_time.map(|time| {
1494                ComputeEvent::Frontier(Frontier {
1495                    export_id,
1496                    time,
1497                    diff: 1,
1498                })
1499            });
1500            let events = retraction.as_ref().into_iter().chain(insertion.as_ref());
1501            self.logger.log_many(events);
1502        }
1503    }
1504
1505    /// Set the frontier of the given import to the given new time and emit corresponding logging
1506    /// events.
1507    pub fn set_import_frontier(&mut self, import_id: GlobalId, new_time: Option<Timestamp>) {
1508        let old_time = self.logged_import_frontiers.remove(&import_id);
1509        if let Some(time) = new_time {
1510            self.logged_import_frontiers.insert(import_id, time);
1511        }
1512
1513        if old_time != new_time {
1514            let export_id = self.export_id;
1515            let retraction = old_time.map(|time| {
1516                ComputeEvent::ImportFrontier(ImportFrontier {
1517                    import_id,
1518                    export_id,
1519                    time,
1520                    diff: -1,
1521                })
1522            });
1523            let insertion = new_time.map(|time| {
1524                ComputeEvent::ImportFrontier(ImportFrontier {
1525                    import_id,
1526                    export_id,
1527                    time,
1528                    diff: 1,
1529                })
1530            });
1531            let events = retraction.as_ref().into_iter().chain(insertion.as_ref());
1532            self.logger.log_many(events);
1533        }
1534    }
1535
1536    /// Record that the collection's dataflow was unsuspended, so hydration work has begun.
1537    ///
1538    /// Repeated calls are ignored by the demux, so callers need not track whether the dataflow was
1539    /// already unsuspended.
1540    pub fn set_hydration_start(&self) {
1541        self.logger
1542            .log(&ComputeEvent::HydrationStart(HydrationStart {
1543                export_id: self.export_id,
1544            }));
1545    }
1546
1547    /// Set the collection as hydrated.
1548    pub fn set_hydrated(&self) {
1549        self.logger.log(&ComputeEvent::Hydration(Hydration {
1550            export_id: self.export_id,
1551        }));
1552    }
1553
1554    /// The export global ID of this collection.
1555    pub fn export_id(&self) -> GlobalId {
1556        self.export_id
1557    }
1558}
1559
1560impl Drop for CollectionLogging {
1561    fn drop(&mut self) {
1562        // Emit retraction events to clean up events previously logged.
1563        self.set_frontier(None);
1564
1565        let import_ids: Vec<_> = self.logged_import_frontiers.keys().copied().collect();
1566        for import_id in import_ids {
1567            self.set_import_frontier(import_id, None);
1568        }
1569
1570        self.logger.log(&ComputeEvent::ExportDropped(ExportDropped {
1571            export_id: self.export_id,
1572        }));
1573    }
1574}
1575
1576/// Extension trait to attach `ComputeEvent::DataflowError` logging operators to collections and
1577/// batch streams.
1578pub(crate) trait LogDataflowErrors {
1579    fn log_dataflow_errors(self, logger: Logger, export_id: GlobalId) -> Self;
1580}
1581
1582impl<'scope, T, D> LogDataflowErrors for VecCollection<'scope, T, D, Diff>
1583where
1584    T: timely::progress::Timestamp,
1585    D: Clone + 'static,
1586{
1587    fn log_dataflow_errors(self, logger: Logger, export_id: GlobalId) -> Self {
1588        self.inner
1589            .unary(Pipeline, "LogDataflowErrorsCollection", |_cap, _info| {
1590                move |input, output| {
1591                    input.for_each(|cap, data| {
1592                        let diff = data.iter().map(|(_d, _t, r)| *r).sum::<Diff>();
1593                        logger.log(&ComputeEvent::ErrorCount(ErrorCount { export_id, diff }));
1594
1595                        output.session(&cap).give_container(data);
1596                    });
1597                }
1598            })
1599            .as_collection()
1600    }
1601}
1602
1603impl<'scope, T, B> LogDataflowErrors for StreamVec<'scope, T, B>
1604where
1605    T: timely::progress::Timestamp,
1606    B: BatchReader + Navigable + Clone + 'static,
1607    for<'a> B::Cursor: Cursor<DiffGat<'a> = &'a Diff>,
1608{
1609    fn log_dataflow_errors(self, logger: Logger, export_id: GlobalId) -> Self {
1610        self.unary(Pipeline, "LogDataflowErrorsStream", |_cap, _info| {
1611            move |input, output| {
1612                input.for_each(|cap, data| {
1613                    let diff = data.iter().map(sum_batch_diffs).sum::<Diff>();
1614                    logger.log(&ComputeEvent::ErrorCount(ErrorCount { export_id, diff }));
1615
1616                    output.session(&cap).give_container(data);
1617                });
1618            }
1619        })
1620    }
1621}
1622
1623/// Return the sum of all diffs within the given batch.
1624///
1625/// Note that this operation can be expensive: Its runtime is O(N) with N being the number of
1626/// unique (key, value, time) tuples. We only use it on error streams, which are expected to
1627/// contain only a small number of records, so this doesn't matter much. But avoid using it when
1628/// batches might become large.
1629fn sum_batch_diffs<B>(batch: &B) -> Diff
1630where
1631    B: BatchReader + Navigable,
1632    for<'a> B::Cursor: Cursor<DiffGat<'a> = &'a Diff>,
1633{
1634    let mut sum = Diff::ZERO;
1635    let mut cursor = batch.cursor();
1636
1637    while cursor.key_valid(batch) {
1638        while cursor.val_valid(batch) {
1639            cursor.map_times(batch, |_t, r| sum += r);
1640            cursor.step_val(batch);
1641        }
1642        cursor.step_key(batch);
1643    }
1644
1645    sum
1646}
1647
1648#[cfg(test)]
1649mod tests {
1650    use super::*;
1651
1652    #[mz_ore::test]
1653    fn test_compute_event_size() {
1654        // This could be a static assertion, but we don't use those yet in this crate.
1655        assert_eq!(56, std::mem::size_of::<ComputeEvent>())
1656    }
1657}