Skip to main content

mz_compute/
metrics.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::sync::{Arc, Mutex};
11
12use mz_compute_client::metrics::{CommandMetrics, HistoryMetrics};
13use mz_ore::cast::CastFrom;
14use mz_ore::metric;
15use mz_ore::metrics::{
16    MakeCollectorOpts, MetricTag, MetricVisibility, MetricsRegistry, UIntGauge, raw,
17};
18use mz_repr::{GlobalId, SharedRow};
19use prometheus::core::{AtomicF64, GenericCounter};
20use prometheus::proto::LabelPair;
21use prometheus::{Histogram, HistogramVec, IntCounter};
22
23/// Metrics exposed by compute replicas.
24//
25// Most of the metrics here use the `raw` implementations, rather than the `DeleteOnDrop` wrappers
26// because their labels are fixed throughout the lifetime of the replica process. For example, any
27// metric labeled only by `worker_id` can be `raw` since the number of workers cannot change.
28//
29// Metrics that are labelled by a dimension that can change throughout the lifetime of the process
30// (such as `collection_id`) MUST NOT use the `raw` metric types and must use the `DeleteOnDrop`
31// types instead, to avoid memory leaks.
32#[derive(Clone, Debug)]
33pub struct ComputeMetrics {
34    // Optional workload class label to apply to all metrics in registry.
35    workload_class: Arc<Mutex<Option<String>>>,
36
37    // command history
38    history_command_count: raw::UIntGaugeVec,
39    history_dataflow_count: raw::UIntGaugeVec,
40
41    // reconciliation
42    reconciliation_reused_dataflows_count_total: raw::IntCounterVec,
43    reconciliation_replaced_dataflows_count_total: raw::IntCounterVec,
44
45    // arrangements
46    arrangement_maintenance_seconds_total: raw::CounterVec,
47    arrangement_maintenance_active_info: raw::UIntGaugeVec,
48
49    // timings
50    //
51    // Note that this particular metric unfortunately takes some care to
52    // interpret. It measures the duration of step_or_park calls, which
53    // undesirably includes the parking. This is probably fine because we
54    // regularly send progress information through persist sources, which likely
55    // means the parking is capped at a second or two in practice. It also
56    // doesn't do anything to let you pinpoint _which_ operator or worker isn't
57    // yielding, but it should hopefully alert us when there is something to
58    // look at.
59    timely_step_duration_seconds: HistogramVec,
60    persist_peek_seconds: HistogramVec,
61    handle_command_duration_seconds: HistogramVec,
62
63    // Index peek timing phases (per-cluster, no worker label)
64    index_peek_total_seconds: Histogram,
65    index_peek_seek_fulfillment_seconds: Histogram,
66    index_peek_error_scan_seconds: Histogram,
67    index_peek_cursor_setup_seconds: Histogram,
68    index_peek_row_iteration_seconds: Histogram,
69    index_peek_row_iteration_rows: Histogram,
70    index_peek_result_sort_seconds: Histogram,
71    index_peek_result_sort_rows: Histogram,
72    index_peek_frontier_check_seconds: Histogram,
73    index_peek_row_collection_seconds: Histogram,
74    index_peek_walks_total: raw::IntCounterVec,
75    index_peek_stashed_total: IntCounter,
76    index_peek_permit_queue_depth: UIntGauge,
77    index_peek_permit_wait_seconds: Histogram,
78    index_peek_offload_seconds: Histogram,
79
80    // memory usage
81    shared_row_heap_capacity_bytes: raw::UIntGaugeVec,
82
83    // replica expiration
84    replica_expiration_timestamp_seconds: raw::UIntGaugeVec,
85    replica_expiration_remaining_seconds: raw::GaugeVec,
86
87    // collections
88    collection_count: raw::UIntGaugeVec,
89
90    // subscribes
91    subscribe_snapshots_skipped_total: IntCounter,
92}
93
94/// Applies the per-role const label to `opts`, unless `role` is `Solo`.
95///
96/// The two named roles (maintenance, interactive) each get a distinct `role` label so a second
97/// compute runtime in the same process registers a distinct series rather than colliding with the
98/// first. `Solo` omits the label so a single-runtime deployment registers exactly as it did before
99/// a second runtime existed.
100fn with_role(
101    mut opts: MakeCollectorOpts,
102    role: crate::server::ComputeRuntimeRole,
103) -> MakeCollectorOpts {
104    if let Some(label) = role.label() {
105        opts.opts = opts.opts.const_label("role", label);
106    }
107    opts
108}
109
110impl ComputeMetrics {
111    /// Registers the compute metrics for `role` into `registry`.
112    ///
113    /// The two named roles carry a `role` const label so that a second compute runtime in the same
114    /// process registers a distinct series rather than colliding with the first. `Solo` carries no
115    /// such label.
116    pub fn register_with(
117        registry: &MetricsRegistry,
118        role: crate::server::ComputeRuntimeRole,
119    ) -> Self {
120        let workload_class = Arc::new(Mutex::new(None));
121        let mut index_peek_row_buckets =
122            prometheus::exponential_buckets(1.0, 2.0, 25).expect("valid parameters");
123        index_peek_row_buckets.insert(0, 0.0);
124
125        // Apply a `workload_class` label to all metrics in the registry when we
126        // have a known workload class.
127        //
128        // The postprocessor rewrites every metric in the whole registry, so only the maintenance
129        // runtime registers it. A second registration from the interactive runtime would push the
130        // label twice onto each metric and produce a duplicate-label scrape error.
131        if role.owns_process_globals() {
132            registry.register_postprocessor({
133                let workload_class = Arc::clone(&workload_class);
134                move |metrics| {
135                    let workload_class: Option<String> =
136                        workload_class.lock().expect("lock poisoned").clone();
137                    let Some(workload_class) = workload_class else {
138                        return;
139                    };
140                    for metric in metrics {
141                        for metric in metric.mut_metric() {
142                            let mut label = LabelPair::default();
143                            label.set_name("workload_class".into());
144                            label.set_value(workload_class.clone());
145
146                            let mut labels = metric.take_label();
147                            labels.push(label);
148                            metric.set_label(labels);
149                        }
150                    }
151                }
152            });
153        }
154
155        Self {
156            workload_class,
157            history_command_count: registry.register(with_role(metric!(
158                name: "mz_compute_replica_history_command_count",
159                help: "The number of commands in the replica's command history.",
160                var_labels: ["worker_id", "command_type"],
161            ), role)),
162            history_dataflow_count: registry.register(with_role(metric!(
163                name: "mz_compute_replica_history_dataflow_count",
164                help: "The number of dataflows in the replica's command history.",
165                var_labels: ["worker_id"],
166                visibility: MetricVisibility::Public,
167                tags: [MetricTag::Compute],
168            ), role)),
169            reconciliation_reused_dataflows_count_total: registry.register(with_role(metric!(
170                name: "mz_compute_reconciliation_reused_dataflows_count_total",
171                help: "The total number of dataflows that were reused during compute reconciliation.",
172                var_labels: ["worker_id"],
173            ), role)),
174            reconciliation_replaced_dataflows_count_total: registry.register(with_role(metric!(
175                name: "mz_compute_reconciliation_replaced_dataflows_count_total",
176                help: "The total number of dataflows that were replaced during compute reconciliation.",
177                var_labels: ["worker_id", "reason"],
178            ), role)),
179            arrangement_maintenance_seconds_total: registry.register(with_role(metric!(
180                name: "mz_arrangement_maintenance_seconds_total",
181                help: "The total time spent maintaining arrangements.",
182                var_labels: ["worker_id"],
183                visibility: MetricVisibility::Public,
184                tags: [MetricTag::Compute],
185            ), role)),
186            arrangement_maintenance_active_info: registry.register(with_role(metric!(
187                name: "mz_arrangement_maintenance_active_info",
188                help: "Whether maintenance is currently occuring.",
189                var_labels: ["worker_id"],
190            ), role)),
191            timely_step_duration_seconds: registry.register(with_role(metric!(
192                name: "mz_timely_step_duration_seconds",
193                help: "The time spent in each compute step_or_park call",
194                const_labels: {"cluster" => "compute"},
195                var_labels: ["worker_id"],
196                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 32.0),
197            ), role)),
198            shared_row_heap_capacity_bytes: registry.register(with_role(metric!(
199                name: "mz_dataflow_shared_row_heap_capacity_bytes",
200                help: "The heap capacity of the shared row.",
201                var_labels: ["worker_id"],
202            ), role)),
203            persist_peek_seconds: registry.register(with_role(metric!(
204                name: "mz_persist_peek_seconds",
205                help: "Time spent in (experimental) Persist fast-path peeks.",
206                var_labels: ["worker_id"],
207                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
208            ), role)),
209            handle_command_duration_seconds: registry.register(with_role(metric!(
210                name: "mz_cluster_handle_command_duration_seconds",
211                help: "Time spent in handling commands.",
212                const_labels: {"cluster" => "compute"},
213                var_labels: ["worker_id", "command_type"],
214                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
215            ), role)),
216            index_peek_total_seconds: registry.register(with_role(metric!(
217                name: "mz_index_peek_total_seconds",
218                help: "Time one visit to an index peek spent on the timely worker. A peek whose walk was offloaded contributes only the inline slice that offloaded it, and its time away from the worker is `mz_index_peek_offload_seconds`.",
219                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
220            ), role)),
221            index_peek_seek_fulfillment_seconds: registry.register(with_role(metric!(
222                name: "mz_index_peek_seek_fulfillment_seconds",
223                help: "Time in seek_fulfillment method including frontier checks and data collection.",
224                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
225            ), role)),
226            index_peek_error_scan_seconds: registry.register(with_role(metric!(
227                name: "mz_index_peek_error_scan_seconds",
228                help: "Time scanning the error trace for errors, summed over the slices the scan was cut into and observed only for scans that find no error.",
229                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
230            ), role)),
231            index_peek_cursor_setup_seconds: registry.register(with_role(metric!(
232                name: "mz_index_peek_cursor_setup_seconds",
233                help: "Time opening the trace cursor and sorting the literal constraints, excluding the seek to those literals.",
234                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
235            ), role)),
236            index_peek_row_iteration_seconds: registry.register(with_role(metric!(
237                name: "mz_index_peek_row_iteration_seconds",
238                help: "Time iterating rows, seeking the cursor to the literal constraints, and evaluating MFP, summed over the slices the walk was cut into.",
239                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
240            ), role)),
241            index_peek_row_iteration_rows: registry.register(with_role(metric!(
242                name: "mz_index_peek_row_iteration_rows",
243                help: "Number of arrangement rows evaluated by the index peek result iterator.",
244                buckets: index_peek_row_buckets.clone(),
245            ), role)),
246            index_peek_result_sort_seconds: registry.register(with_role(metric!(
247                name: "mz_index_peek_result_sort_seconds",
248                help: "Time thinning intermediate results down to the rows a peek's finishing needs.",
249                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
250            ), role)),
251            index_peek_result_sort_rows: registry.register(with_role(metric!(
252                name: "mz_index_peek_result_sort_rows",
253                help: "Number of intermediate result rows handed to thinning during peek collection, summed across the times it ran.",
254                buckets: index_peek_row_buckets,
255            ), role)),
256            index_peek_frontier_check_seconds: registry.register(with_role(metric!(
257                name: "mz_index_peek_frontier_check_seconds",
258                help: "Time checking trace frontiers.",
259                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
260            ), role)),
261            index_peek_row_collection_seconds: registry.register(with_role(metric!(
262                name: "mz_index_peek_row_collection_seconds",
263                help: "Time constructing RowCollection from peek results, including converting the row counts the scan produced.",
264                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
265            ), role)),
266            index_peek_walks_total: registry.register(with_role(metric!(
267                name: "mz_index_peek_walks_total",
268                help: "The number of index peek walks that reached an outcome, by the substrate they ended on: `inline` on the timely worker, `offloaded` away from it.",
269                var_labels: ["substrate"],
270            ), role)),
271            index_peek_stashed_total: registry.register(with_role(metric!(
272                name: "mz_index_peek_stashed_total",
273                help: "The number of index peek walks that answered with a handle to the peek response stash, always a subset of the `offloaded` substrate of `mz_index_peek_walks_total`.",
274            ), role)),
275            index_peek_permit_queue_depth: registry.register(with_role(metric!(
276                name: "mz_index_peek_permit_queue_depth",
277                help: "The number of offloaded index peek walks waiting for a permit to run.",
278            ), role)),
279            index_peek_permit_wait_seconds: registry.register(with_role(metric!(
280                name: "mz_index_peek_permit_wait_seconds",
281                help: "Time an offloaded index peek walk waited for a permit, observed only for walks that were admitted.",
282                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
283            ), role)),
284            index_peek_offload_seconds: registry.register(with_role(metric!(
285                name: "mz_index_peek_offload_seconds",
286                help: "Wall-clock time an offloaded index peek walk spent away from the timely worker, including the wait for a permit.",
287                buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
288            ), role)),
289            replica_expiration_timestamp_seconds: registry.register(with_role(metric!(
290                name: "mz_dataflow_replica_expiration_timestamp_seconds",
291                help: "The replica expiration timestamp in seconds since epoch.",
292                var_labels: ["worker_id"],
293            ), role)),
294            replica_expiration_remaining_seconds: registry.register(with_role(metric!(
295                name: "mz_dataflow_replica_expiration_remaining_seconds",
296                help: "The remaining seconds until replica expiration. Can go negative, can lag behind.",
297                var_labels: ["worker_id"],
298            ), role)),
299            collection_count: registry.register(with_role(metric!(
300                name: "mz_compute_collection_count",
301                help: "The number and hydration status of maintained compute collections.",
302                var_labels: ["worker_id", "type", "hydrated"],
303            ), role)),
304            subscribe_snapshots_skipped_total: registry.register(with_role(metric!(
305                name: "mz_subscribe_snapshots_skipped_total",
306                help: "The number of collection snapshots that were skipped by the subscribe snapshot optimization.",
307            ), role)),
308        }
309    }
310
311    /// Sets the workload class for the compute metrics.
312    pub fn set_workload_class(&self, workload_class: Option<String>) {
313        let mut guard = self.workload_class.lock().expect("lock poisoned");
314        *guard = workload_class
315    }
316
317    pub fn for_worker(&self, worker_id: usize) -> WorkerMetrics {
318        let worker = worker_id.to_string();
319        let arrangement_maintenance_seconds_total = self
320            .arrangement_maintenance_seconds_total
321            .with_label_values(&[&worker]);
322        let arrangement_maintenance_active_info = self
323            .arrangement_maintenance_active_info
324            .with_label_values(&[&worker]);
325        let timely_step_duration_seconds = self
326            .timely_step_duration_seconds
327            .with_label_values(&[&worker]);
328        let persist_peek_seconds = self.persist_peek_seconds.with_label_values(&[&worker]);
329        let handle_command_duration_seconds = CommandMetrics::build(|typ| {
330            self.handle_command_duration_seconds
331                .with_label_values(&[worker.as_ref(), typ])
332        });
333        let index_peek_total_seconds = self.index_peek_total_seconds.clone();
334        let index_peek_seek_fulfillment_seconds = self.index_peek_seek_fulfillment_seconds.clone();
335        let index_peek_error_scan_seconds = self.index_peek_error_scan_seconds.clone();
336        let index_peek_cursor_setup_seconds = self.index_peek_cursor_setup_seconds.clone();
337        let index_peek_row_iteration_seconds = self.index_peek_row_iteration_seconds.clone();
338        let index_peek_row_iteration_rows = self.index_peek_row_iteration_rows.clone();
339        let index_peek_result_sort_seconds = self.index_peek_result_sort_seconds.clone();
340        let index_peek_result_sort_rows = self.index_peek_result_sort_rows.clone();
341        let index_peek_frontier_check_seconds = self.index_peek_frontier_check_seconds.clone();
342        let index_peek_row_collection_seconds = self.index_peek_row_collection_seconds.clone();
343        let index_peek_walks_inline = self.index_peek_walks_total.with_label_values(&["inline"]);
344        let index_peek_walks_offloaded = self
345            .index_peek_walks_total
346            .with_label_values(&["offloaded"]);
347        let index_peek_stashed_total = self.index_peek_stashed_total.clone();
348        let index_peek_permit_queue_depth = self.index_peek_permit_queue_depth.clone();
349        let index_peek_permit_wait_seconds = self.index_peek_permit_wait_seconds.clone();
350        let index_peek_offload_seconds = self.index_peek_offload_seconds.clone();
351        let replica_expiration_timestamp_seconds = self
352            .replica_expiration_timestamp_seconds
353            .with_label_values(&[&worker]);
354        let replica_expiration_remaining_seconds = self
355            .replica_expiration_remaining_seconds
356            .with_label_values(&[&worker]);
357        let shared_row_heap_capacity_bytes = self
358            .shared_row_heap_capacity_bytes
359            .with_label_values(&[&worker]);
360
361        WorkerMetrics {
362            worker_label: worker,
363            metrics: self.clone(),
364            arrangement_maintenance_seconds_total,
365            arrangement_maintenance_active_info,
366            timely_step_duration_seconds,
367            persist_peek_seconds,
368            handle_command_duration_seconds,
369            index_peek_total_seconds,
370            index_peek_seek_fulfillment_seconds,
371            index_peek_error_scan_seconds,
372            index_peek_cursor_setup_seconds,
373            index_peek_row_iteration_seconds,
374            index_peek_row_iteration_rows,
375            index_peek_result_sort_seconds,
376            index_peek_result_sort_rows,
377            index_peek_frontier_check_seconds,
378            index_peek_row_collection_seconds,
379            index_peek_walks_inline,
380            index_peek_walks_offloaded,
381            index_peek_stashed_total,
382            index_peek_permit_queue_depth,
383            index_peek_permit_wait_seconds,
384            index_peek_offload_seconds,
385            replica_expiration_timestamp_seconds,
386            replica_expiration_remaining_seconds,
387            shared_row_heap_capacity_bytes,
388        }
389    }
390}
391
392/// Per-worker metrics.
393#[derive(Clone, Debug)]
394pub struct WorkerMetrics {
395    worker_label: String,
396    metrics: ComputeMetrics,
397
398    /// The amount of time spent in arrangement maintenance.
399    pub(crate) arrangement_maintenance_seconds_total: GenericCounter<AtomicF64>,
400    /// 1 if this worker is currently doing maintenance.
401    ///
402    /// If maintenance turns out to take a very long time, this will allow us
403    /// to gain a sense that Materialize is stuck on maintenance before the
404    /// maintenance completes
405    pub(crate) arrangement_maintenance_active_info: UIntGauge,
406    /// Histogram of Timely step timings.
407    pub(crate) timely_step_duration_seconds: Histogram,
408    /// Histogram of persist peek durations.
409    pub(crate) persist_peek_seconds: Histogram,
410    /// Histogram of command handling durations.
411    pub(crate) handle_command_duration_seconds: CommandMetrics<Histogram>,
412    /// Histogram of total index peek durations.
413    pub(crate) index_peek_total_seconds: Histogram,
414    /// Histogram of index peek seek_fulfillment durations.
415    pub(crate) index_peek_seek_fulfillment_seconds: Histogram,
416    /// Histogram of index peek error scan durations.
417    pub(crate) index_peek_error_scan_seconds: Histogram,
418    /// Histogram of index peek cursor setup durations.
419    pub(crate) index_peek_cursor_setup_seconds: Histogram,
420    /// Histogram of index peek row iteration durations.
421    pub(crate) index_peek_row_iteration_seconds: Histogram,
422    /// Histogram of index peek rows processed by the result iterator.
423    pub(crate) index_peek_row_iteration_rows: Histogram,
424    /// Histogram of index peek result sort durations.
425    pub(crate) index_peek_result_sort_seconds: Histogram,
426    /// Histogram of index peek rows sorted across all result sort operations.
427    pub(crate) index_peek_result_sort_rows: Histogram,
428    /// Histogram of index peek frontier check durations.
429    pub(crate) index_peek_frontier_check_seconds: Histogram,
430    /// Histogram of index peek row collection construction durations.
431    pub(crate) index_peek_row_collection_seconds: Histogram,
432    /// Counts index peek walks that ran on the timely worker.
433    ///
434    /// Both substrate series are resolved when the worker's metrics are built, so each exists at
435    /// zero before its first walk. A series at zero says the offload never engaged, where an absent
436    /// series says nothing.
437    pub(crate) index_peek_walks_inline: IntCounter,
438    /// Counts index peek walks that ran away from the timely worker.
439    pub(crate) index_peek_walks_offloaded: IntCounter,
440    /// Counts index peek walks that answered from the peek response stash.
441    ///
442    /// Resolved when the worker's metrics are built, so it reports zero before the first stashed
443    /// answer rather than being absent. Whether a peek reached the stash has no other signal.
444    pub(crate) index_peek_stashed_total: IntCounter,
445    /// How many offloaded index peek walks are waiting for a permit.
446    pub(crate) index_peek_permit_queue_depth: UIntGauge,
447    /// Histogram of how long an offloaded index peek walk waited for its permit.
448    pub(crate) index_peek_permit_wait_seconds: Histogram,
449    /// Histogram of how long an offloaded index peek walk was away from the worker.
450    pub(crate) index_peek_offload_seconds: Histogram,
451    /// The timestamp of replica expiration.
452    pub(crate) replica_expiration_timestamp_seconds: UIntGauge,
453    /// Remaining seconds until replica expiration.
454    pub(crate) replica_expiration_remaining_seconds: raw::Gauge,
455    /// Heap capacity of the shared row.
456    shared_row_heap_capacity_bytes: UIntGauge,
457}
458
459impl WorkerMetrics {
460    pub fn for_history(&self) -> HistoryMetrics<UIntGauge> {
461        let command_counts = CommandMetrics::build(|typ| {
462            self.metrics
463                .history_command_count
464                .with_label_values(&[self.worker_label.as_ref(), typ])
465        });
466        let dataflow_count = self
467            .metrics
468            .history_dataflow_count
469            .with_label_values(&[&self.worker_label]);
470
471        HistoryMetrics {
472            command_counts,
473            dataflow_count,
474        }
475    }
476
477    /// Record the reconciliation result for a single dataflow.
478    ///
479    /// Reconciliation is recorded as successful if the given properties all hold. Otherwise it is
480    /// recorded as unsuccessful, with a reason based on the first property that does not hold.
481    ///
482    /// The properties are:
483    ///  * compatible: The old and new dataflow descriptions are compatible.
484    ///  * uncompacted: Collections currently installed for the dataflow exports have not been
485    ///                 allowed to compact beyond that new dataflow as-of.
486    ///  * subscribe_free: The dataflow does not export a subscribe sink.
487    ///  * copy_to_free: The dataflow does not export a copy-to sink.
488    ///  * dependencies_retained: All local inputs to the dataflow were retained by compute
489    ///                           reconciliation.
490    pub fn record_dataflow_reconciliation(
491        &self,
492        compatible: bool,
493        uncompacted: bool,
494        subscribe_free: bool,
495        copy_to_free: bool,
496        dependencies_retained: bool,
497    ) {
498        if !compatible {
499            self.metrics
500                .reconciliation_replaced_dataflows_count_total
501                .with_label_values(&[self.worker_label.as_ref(), "incompatible"])
502                .inc();
503        } else if !uncompacted {
504            self.metrics
505                .reconciliation_replaced_dataflows_count_total
506                .with_label_values(&[self.worker_label.as_ref(), "compacted"])
507                .inc();
508        } else if !subscribe_free {
509            self.metrics
510                .reconciliation_replaced_dataflows_count_total
511                .with_label_values(&[self.worker_label.as_ref(), "subscribe"])
512                .inc();
513        } else if !copy_to_free {
514            self.metrics
515                .reconciliation_replaced_dataflows_count_total
516                .with_label_values(&[self.worker_label.as_ref(), "copy-to"])
517                .inc();
518        } else if !dependencies_retained {
519            self.metrics
520                .reconciliation_replaced_dataflows_count_total
521                .with_label_values(&[self.worker_label.as_ref(), "dependency"])
522                .inc();
523        } else {
524            self.metrics
525                .reconciliation_reused_dataflows_count_total
526                .with_label_values(&[&self.worker_label])
527                .inc();
528        }
529    }
530
531    /// Record the heap capacity of the shared row.
532    pub fn record_shared_row_metrics(&self) {
533        let binding = SharedRow::get();
534        self.shared_row_heap_capacity_bytes
535            .set(u64::cast_from(binding.byte_capacity()));
536    }
537
538    /// Increase the count of maintained collections.
539    fn inc_collection_count(&self, collection_type: &str, hydrated: bool) {
540        let hydrated = if hydrated { "1" } else { "0" };
541        self.metrics
542            .collection_count
543            .with_label_values(&[self.worker_label.as_ref(), collection_type, hydrated])
544            .inc();
545    }
546
547    /// Decrease the count of maintained collections.
548    fn dec_collection_count(&self, collection_type: &str, hydrated: bool) {
549        let hydrated = if hydrated { "1" } else { "0" };
550        self.metrics
551            .collection_count
552            .with_label_values(&[self.worker_label.as_ref(), collection_type, hydrated])
553            .dec();
554    }
555
556    pub fn inc_subscribe_snapshot_optimization(&self) {
557        self.metrics.subscribe_snapshots_skipped_total.inc()
558    }
559
560    /// Sets the workload class for the compute metrics.
561    pub fn set_workload_class(&self, workload_class: Option<String>) {
562        self.metrics.set_workload_class(workload_class);
563    }
564
565    pub fn for_collection(&self, id: GlobalId) -> CollectionMetrics {
566        CollectionMetrics::new(id, self.clone())
567    }
568}
569
570/// Collection metrics.
571///
572/// Note that these metrics do _not_ have a `collection_id` label. We avoid introducing
573/// per-collection, per-worker metrics because the number of resulting time series would
574/// potentially be huge. Instead we count classes of collections, such as hydrated collections.
575#[derive(Clone, Debug)]
576pub struct CollectionMetrics {
577    metrics: WorkerMetrics,
578    collection_type: &'static str,
579    collection_hydrated: bool,
580}
581
582impl CollectionMetrics {
583    pub fn new(collection_id: GlobalId, metrics: WorkerMetrics) -> Self {
584        let collection_type = match collection_id {
585            GlobalId::System(_) => "system",
586            GlobalId::IntrospectionSourceIndex(_) => "log",
587            GlobalId::User(_) => "user",
588            GlobalId::Transient(_) => "transient",
589            GlobalId::Explain => "explain",
590        };
591        let collection_hydrated = false;
592
593        metrics.inc_collection_count(collection_type, collection_hydrated);
594
595        Self {
596            metrics,
597            collection_type,
598            collection_hydrated,
599        }
600    }
601
602    /// Record this collection as hydration.
603    pub fn record_collection_hydrated(&mut self) {
604        if self.collection_hydrated {
605            return;
606        }
607
608        self.metrics
609            .dec_collection_count(self.collection_type, false);
610        self.metrics
611            .inc_collection_count(self.collection_type, true);
612        self.collection_hydrated = true;
613    }
614}
615
616impl Drop for CollectionMetrics {
617    fn drop(&mut self) {
618        self.metrics
619            .dec_collection_count(self.collection_type, self.collection_hydrated);
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use std::collections::BTreeSet;
626
627    use mz_ore::metrics::MetricsRegistry;
628
629    use super::ComputeMetrics;
630    use crate::server::ComputeRuntimeRole;
631
632    /// The `Solo` (single-runtime) role registers exactly as compute did before a second runtime
633    /// existed: no metric carries a `role` label, so single-runtime dashboards and exact-match
634    /// alerts are byte-unchanged.
635    #[mz_ore::test]
636    fn solo_runtime_omits_role_label() {
637        let registry = MetricsRegistry::new();
638        let metrics = ComputeMetrics::register_with(&registry, ComputeRuntimeRole::Solo);
639        // Instantiate the per-worker children so the `*Vec` families emit rows to inspect.
640        let _worker = metrics.for_worker(0);
641
642        for family in registry.gather() {
643            for metric in family.get_metric() {
644                for label in metric.get_label() {
645                    assert_ne!(
646                        label.name(),
647                        "role",
648                        "solo metric {} unexpectedly carries a role label",
649                        family.name(),
650                    );
651                }
652            }
653        }
654    }
655
656    /// The two named roles each carry their own `role` label, so two runtimes in one process
657    /// register distinct series rather than colliding. Registering both on one registry also
658    /// exercises the non-collision that lets them coexist.
659    #[mz_ore::test]
660    fn named_roles_carry_distinct_role_label() {
661        let registry = MetricsRegistry::new();
662        let maintenance = ComputeMetrics::register_with(&registry, ComputeRuntimeRole::Maintenance);
663        let interactive = ComputeMetrics::register_with(&registry, ComputeRuntimeRole::Interactive);
664        let _maintenance_worker = maintenance.for_worker(0);
665        let _interactive_worker = interactive.for_worker(0);
666
667        let mut roles = BTreeSet::new();
668        for family in registry.gather() {
669            for metric in family.get_metric() {
670                let role = metric
671                    .get_label()
672                    .iter()
673                    .find(|label| label.name() == "role")
674                    .unwrap_or_else(|| panic!("metric {} missing a role label", family.name()));
675                roles.insert(role.value().to_string());
676            }
677        }
678
679        assert!(roles.contains("maintenance"), "roles seen: {roles:?}");
680        assert!(roles.contains("interactive"), "roles seen: {roles:?}");
681    }
682}