Skip to main content

query_runtime/runtime/
backend_metrics.rs

1//! Fixed-cardinality backend metrics state.
2//!
3//! This module owns one bounded, backend-generation-scoped projection of
4//! ledger and finalization transitions. It deliberately stores no
5//! query correlations, report payloads, or application-provided labels.
6
7use std::{
8    error::Error,
9    fmt, sync,
10    sync::{Arc, Mutex},
11};
12
13use query_planning::error::{QueryError, QueryErrorCode, Result};
14use serde::Serialize;
15
16use crate::{
17    metrics::{
18        BACKEND_ATTEMPT_DURATION_BUCKETS_NS, BackendDurationProjectionError, BackendMetric, BackendMetricDescriptor,
19        project_backend_duration_histogram,
20    },
21    observability::{AttemptDisposition, RuntimeTerminalOutcome, SealedAttemptProjection},
22    runtime::gpu::{DeviceAdmissionHealth, LedgerTransition},
23};
24
25const OPERATION: &str = "query_engine.backend_metrics";
26
27/// Maximum number of ordinal-addressable devices in one backend metrics store.
28///
29/// This is the same closed device-label cardinality frozen by the backend
30/// descriptor registry. A device ordinal indexes its fixed slot directly, so
31/// update paths need neither a map lookup nor an allocation.
32pub const BACKEND_METRIC_DEVICE_CAPACITY: usize = 64;
33
34/// Number of cumulative histogram buckets, including the final `+Inf`
35/// bucket.
36pub const BACKEND_ATTEMPT_DURATION_BUCKET_COUNT: usize = BACKEND_ATTEMPT_DURATION_BUCKETS_NS.len() + 1;
37
38/// Closed disposition dimension of terminal backend-attempt metrics.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40#[repr(usize)]
41pub enum BackendAttemptDisposition {
42    /// No classified physical plan was obtained.
43    Unplanned = 0,
44    /// The plan used DataFusion execution without native operators.
45    CpuFallback = 1,
46    /// The plan combined native and DataFusion execution.
47    PartialNative = 2,
48    /// The plan had native execution coverage.
49    Native = 3,
50}
51
52impl BackendAttemptDisposition {
53    /// Number of closed disposition values in the backend metric store.
54    pub const COUNT: usize = 4;
55
56    /// Every closed disposition value in descriptor order.
57    pub const ALL: [Self; Self::COUNT] = [Self::Unplanned, Self::CpuFallback, Self::PartialNative, Self::Native];
58
59    /// Stable closed label value for the backend descriptor table.
60    pub const fn as_str(self) -> &'static str {
61        match self {
62            Self::Unplanned => "unplanned",
63            Self::CpuFallback => "cpu_fallback",
64            Self::PartialNative => "partial_native",
65            Self::Native => "native",
66        }
67    }
68
69    const fn from_projection(disposition: AttemptDisposition) -> Self {
70        match disposition {
71            AttemptDisposition::Unplanned => Self::Unplanned,
72            AttemptDisposition::CpuFallback => Self::CpuFallback,
73            AttemptDisposition::PartialNative => Self::PartialNative,
74            AttemptDisposition::Native => Self::Native,
75        }
76    }
77
78    const fn index(self) -> usize {
79        self as usize
80    }
81}
82
83/// Closed outcome dimension of terminal backend-attempt metrics.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
85#[repr(usize)]
86pub enum BackendAttemptOutcome {
87    /// Execution completed successfully.
88    Succeeded = 0,
89    /// A classified error ended execution.
90    Failed = 1,
91    /// Caller cancellation or deadline expiry ended execution.
92    Cancelled = 2,
93    /// The attempt or stream was dropped before another terminal outcome.
94    Dropped = 3,
95    /// An unwind ended execution.
96    Panicked = 4,
97}
98
99impl BackendAttemptOutcome {
100    /// Number of closed terminal outcome values in the backend metric store.
101    pub const COUNT: usize = 5;
102
103    /// Every closed terminal outcome value in descriptor order.
104    pub const ALL: [Self; Self::COUNT] =
105        [Self::Succeeded, Self::Failed, Self::Cancelled, Self::Dropped, Self::Panicked];
106
107    /// Stable closed label value for the backend descriptor table.
108    pub const fn as_str(self) -> &'static str {
109        match self {
110            Self::Succeeded => "succeeded",
111            Self::Failed => "failed",
112            Self::Cancelled => "cancelled",
113            Self::Dropped => "dropped",
114            Self::Panicked => "panicked",
115        }
116    }
117
118    const fn from_projection(outcome: &RuntimeTerminalOutcome) -> Self {
119        match outcome {
120            RuntimeTerminalOutcome::Succeeded => Self::Succeeded,
121            RuntimeTerminalOutcome::Failed { .. } => Self::Failed,
122            RuntimeTerminalOutcome::Cancelled => Self::Cancelled,
123            RuntimeTerminalOutcome::Dropped => Self::Dropped,
124            RuntimeTerminalOutcome::Panicked => Self::Panicked,
125        }
126    }
127
128    const fn index(self) -> usize {
129        self as usize
130    }
131}
132
133/// Closed duration dimension for the attempt-duration histogram.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
135#[repr(usize)]
136pub enum BackendAttemptDuration {
137    /// Submission to report readiness, including cleanup.
138    Total = 0,
139    /// Planning start to planning finish.
140    Planning = 1,
141    /// Admission request to immutable grant publication.
142    QueueWait = 2,
143    /// Physical execution start to terminal outcome.
144    PhysicalExecution = 3,
145}
146
147impl BackendAttemptDuration {
148    /// Number of closed attempt-duration values in the backend metric store.
149    pub const COUNT: usize = 4;
150
151    /// Every closed attempt-duration value in descriptor order.
152    pub const ALL: [Self; Self::COUNT] = [Self::Total, Self::Planning, Self::QueueWait, Self::PhysicalExecution];
153
154    /// Stable closed label value for the backend descriptor table.
155    pub const fn as_str(self) -> &'static str {
156        match self {
157            Self::Total => "total",
158            Self::Planning => "planning",
159            Self::QueueWait => "queue_wait",
160            Self::PhysicalExecution => "physical_execution",
161        }
162    }
163
164    const fn index(self) -> usize {
165        self as usize
166    }
167}
168
169/// Explicit duration values already derived from the sealed attempt report.
170///
171/// The store does not inspect report timestamps or rebuild durations. A
172/// missing duration is absent from its histogram, and the store does not record
173/// it as a zero-duration sample.
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
175pub(crate) struct BackendAttemptDurations {
176    pub(crate) total_ns: Option<u64>,
177    pub(crate) planning_ns: Option<u64>,
178    pub(crate) queue_wait_ns: Option<u64>,
179    pub(crate) physical_execution_ns: Option<u64>,
180}
181
182impl BackendAttemptDurations {
183    const fn value(self, duration: BackendAttemptDuration) -> Option<u64> {
184        match duration {
185            BackendAttemptDuration::Total => self.total_ns,
186            BackendAttemptDuration::Planning => self.planning_ns,
187            BackendAttemptDuration::QueueWait => self.queue_wait_ns,
188            BackendAttemptDuration::PhysicalExecution => self.physical_execution_ns,
189        }
190    }
191}
192
193/// Closed admission health projected from the owning ledger.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
195pub enum BackendAdmissionHealth {
196    /// The device ledger accepts new admission.
197    Open,
198    /// The device ledger rejects new admission after close or a health transition.
199    Closed,
200}
201
202/// A fixed cumulative histogram snapshot. `bucket_counts` follows
203/// [`BACKEND_ATTEMPT_DURATION_BUCKETS_NS`] and ends with its `+Inf` bucket.
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
205pub struct BackendHistogramSnapshot {
206    /// Cumulative counts at each finite nanosecond bound and the final `+Inf` bound.
207    pub bucket_counts: [u64; BACKEND_ATTEMPT_DURATION_BUCKET_COUNT],
208    /// Total duration samples represented by the final cumulative bucket.
209    pub count: u64,
210    pub(crate) sum: u64,
211}
212
213impl BackendHistogramSnapshot {
214    const EMPTY: Self = Self { bucket_counts: [0; BACKEND_ATTEMPT_DURATION_BUCKET_COUNT], count: 0, sum: 0 };
215}
216
217/// Terminal counters and duration histograms for the complete backend.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
219pub struct BackendAttemptMetricsSnapshot {
220    completed: [[u64; BackendAttemptOutcome::COUNT]; BackendAttemptDisposition::COUNT],
221    durations: [[[BackendHistogramSnapshot; BackendAttemptDuration::COUNT]; BackendAttemptOutcome::COUNT];
222        BackendAttemptDisposition::COUNT],
223}
224
225impl BackendAttemptMetricsSnapshot {
226    /// Returns the exact terminal counter for one closed disposition/outcome
227    /// pair.
228    pub const fn completed(self, disposition: BackendAttemptDisposition, outcome: BackendAttemptOutcome) -> u64 {
229        self.completed[disposition.index()][outcome.index()]
230    }
231
232    /// Returns the fixed cumulative histogram for one closed terminal series
233    /// and duration kind.
234    pub const fn duration(
235        self,
236        disposition: BackendAttemptDisposition,
237        outcome: BackendAttemptOutcome,
238        duration: BackendAttemptDuration,
239    ) -> BackendHistogramSnapshot {
240        self.durations[disposition.index()][outcome.index()][duration.index()]
241    }
242}
243
244/// Ledger-class backend metrics for one fixed device slot.
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
246pub struct BackendAdmissionMetricsSnapshot {
247    /// Attempts with an active or cleaning admission grant.
248    pub active_attempts: u64,
249    /// Bytes committed to immutable active and cleaning query grants.
250    pub live_grant_bytes: u64,
251    /// Bytes retained by resident owners at the ledger transition.
252    pub resident_owner_bytes: u64,
253    /// Largest resident-owner charge observed during this backend generation.
254    pub max_resident_owner_bytes: u64,
255    /// Evictable cache residency from the device ledger's current snapshot.
256    pub cache_evictable_resident_bytes: u64,
257    /// Pinned cache residency from the device ledger's current snapshot.
258    pub cache_pinned_in_use_bytes: u64,
259    /// Ledger admission health at the recorded transition.
260    pub health: BackendAdmissionHealth,
261}
262
263impl BackendAdmissionMetricsSnapshot {
264    const EMPTY: Self = Self {
265        active_attempts: 0,
266        live_grant_bytes: 0,
267        resident_owner_bytes: 0,
268        max_resident_owner_bytes: 0,
269        cache_evictable_resident_bytes: 0,
270        cache_pinned_in_use_bytes: 0,
271        health: BackendAdmissionHealth::Open,
272    };
273}
274
275/// Metrics for one configured device. The ordinal is the bounded device-label
276/// value fixed during backend construction, never an attempt or query id.
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
278pub struct BackendDeviceMetricsSnapshot {
279    /// Configured CUDA ordinal used as the closed device label.
280    pub device_ordinal: usize,
281    /// Authoritative ledger sequence associated with the admission metrics.
282    pub ledger_sequence: u64,
283    /// Current and maximum values recorded from device ledger transitions.
284    pub admission: BackendAdmissionMetricsSnapshot,
285}
286
287/// Coherent fixed-cardinality snapshot of one backend metrics generation.
288///
289/// `generation` advances after each accepted store update. Consumers that
290/// combine this snapshot with another read can compare the two generations;
291/// unequal backend or store generations make a torn combination detectable.
292/// All device slots are fixed. `None` means that ordinal is not part of this
293/// backend generation.
294#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
295pub struct BackendMetricsSnapshot {
296    /// Backend resource generation that owns these metrics.
297    pub backend_generation: u64,
298    /// Store update sequence advanced after each accepted metric update.
299    pub generation: u64,
300    #[serde(serialize_with = "serialize_backend_metric_devices")]
301    /// Fixed ordinal-indexed device slots; `None` for an unconfigured ordinal.
302    pub devices: [Option<BackendDeviceMetricsSnapshot>; BACKEND_METRIC_DEVICE_CAPACITY],
303    /// Terminal counters and duration histograms accumulated at report readiness.
304    pub attempts: BackendAttemptMetricsSnapshot,
305}
306
307fn serialize_backend_metric_devices<S>(
308    devices: &[Option<BackendDeviceMetricsSnapshot>; BACKEND_METRIC_DEVICE_CAPACITY],
309    serializer: S,
310) -> std::result::Result<S::Ok, S::Error>
311where
312    S: serde::Serializer,
313{
314    devices.as_slice().serialize(serializer)
315}
316
317impl BackendMetricsSnapshot {
318    /// Returns whether two snapshots cannot be combined as one coherent read.
319    #[cfg(test)]
320    pub(crate) const fn is_torn_against(&self, other: &Self) -> bool {
321        self.backend_generation != other.backend_generation || self.generation != other.generation
322    }
323
324    /// Returns the configured device record for one bounded device ordinal.
325    pub const fn device(&self, device_ordinal: usize) -> Option<BackendDeviceMetricsSnapshot> {
326        if device_ordinal >= BACKEND_METRIC_DEVICE_CAPACITY { None } else { self.devices[device_ordinal] }
327    }
328
329    /// Projects one canonical descriptor from this coherent snapshot.
330    ///
331    /// This is the only control-plane boundary that derives backend metric
332    /// samples, labels, duration units, or histogram bucket semantics. An
333    /// exporter therefore only iterates the immutable descriptor registry and
334    /// selects its ingress by [`crate::metrics::BackendMetricKind`].
335    pub fn project_backend_metric(
336        &self,
337        descriptor: &'static BackendMetricDescriptor,
338    ) -> std::result::Result<Vec<BackendMetricSample>, BackendMetricProjectionError> {
339        if descriptor != descriptor.metric.descriptor() {
340            return Err(BackendMetricProjectionError::NonCanonicalDescriptor { metric_name: descriptor.name });
341        }
342
343        let mut samples = Vec::new();
344        match descriptor.metric {
345            BackendMetric::admission_active_attempts => {
346                for device in self.devices.iter().flatten() {
347                    push_scalar_sample(
348                        &mut samples,
349                        descriptor,
350                        device_attributes(descriptor, device.device_ordinal),
351                        device.admission.active_attempts,
352                    );
353                }
354            },
355            BackendMetric::admission_live_grant_bytes => {
356                for device in self.devices.iter().flatten() {
357                    push_scalar_sample(
358                        &mut samples,
359                        descriptor,
360                        device_attributes(descriptor, device.device_ordinal),
361                        device.admission.live_grant_bytes,
362                    );
363                }
364            },
365            BackendMetric::admission_resident_owner_bytes => {
366                for device in self.devices.iter().flatten() {
367                    push_scalar_sample(
368                        &mut samples,
369                        descriptor,
370                        device_attributes(descriptor, device.device_ordinal),
371                        device.admission.resident_owner_bytes,
372                    );
373                }
374            },
375            BackendMetric::admission_max_resident_owner_bytes => {
376                for device in self.devices.iter().flatten() {
377                    push_scalar_sample(
378                        &mut samples,
379                        descriptor,
380                        device_attributes(descriptor, device.device_ordinal),
381                        device.admission.max_resident_owner_bytes,
382                    );
383                }
384            },
385            BackendMetric::attempts_completed => {
386                for disposition in BackendAttemptDisposition::ALL {
387                    for outcome in BackendAttemptOutcome::ALL {
388                        push_scalar_sample(
389                            &mut samples,
390                            descriptor,
391                            attempt_attributes(descriptor, disposition, outcome),
392                            self.attempts.completed(disposition, outcome),
393                        );
394                    }
395                }
396            },
397            BackendMetric::attempts_duration => {
398                for disposition in BackendAttemptDisposition::ALL {
399                    for outcome in BackendAttemptOutcome::ALL {
400                        for duration in BackendAttemptDuration::ALL {
401                            let histogram = project_backend_histogram(
402                                descriptor,
403                                self.attempts.duration(disposition, outcome, duration),
404                            )?;
405                            samples.push(BackendMetricSample {
406                                attributes: duration_attributes(descriptor, disposition, outcome, duration),
407                                value: BackendMetricSampleValue::Histogram(histogram),
408                            });
409                        }
410                    }
411                }
412            },
413            BackendMetric::cache_evictable_resident_bytes => {
414                for device in self.devices.iter().flatten() {
415                    push_scalar_sample(
416                        &mut samples,
417                        descriptor,
418                        device_attributes(descriptor, device.device_ordinal),
419                        device.admission.cache_evictable_resident_bytes,
420                    );
421                }
422            },
423            BackendMetric::cache_pinned_in_use_bytes => {
424                for device in self.devices.iter().flatten() {
425                    push_scalar_sample(
426                        &mut samples,
427                        descriptor,
428                        device_attributes(descriptor, device.device_ordinal),
429                        device.admission.cache_pinned_in_use_bytes,
430                    );
431                }
432            },
433        }
434        Ok(samples)
435    }
436}
437
438/// A typed, closed backend-metric attribute value.
439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
440pub enum BackendMetricAttributeValue {
441    /// Bounded integer identity, such as a configured device ordinal.
442    I64(i64),
443    /// Value from a closed stable label domain.
444    Str(&'static str),
445}
446
447/// One descriptor-ordered attribute of a projected backend metric sample.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub struct BackendMetricAttribute {
450    /// Canonical label name from the descriptor, in descriptor order.
451    pub key: &'static str,
452    /// Value in that descriptor label's closed domain.
453    pub value: BackendMetricAttributeValue,
454}
455
456/// An OTLP-compatible histogram projection. Bounds and `sum` use the
457/// descriptor-declared unit, while bucket counts are disjoint (including the
458/// final implicit `+Inf` bucket).
459#[derive(Debug, Clone, PartialEq)]
460pub struct BackendHistogramProjection {
461    /// Finite increasing bounds in the descriptor's declared unit.
462    pub explicit_bounds: Vec<f64>,
463    /// Disjoint counts for each finite interval and the final `+Inf` interval.
464    pub bucket_counts: [u64; BACKEND_ATTEMPT_DURATION_BUCKET_COUNT],
465    /// Total samples represented by the disjoint bucket counts.
466    pub count: u64,
467    /// Total observed duration in the descriptor's declared unit.
468    pub sum: f64,
469}
470
471/// One value produced by a backend descriptor projection.
472#[derive(Debug, Clone, PartialEq)]
473pub enum BackendMetricSampleValue {
474    /// Unsigned counter, gauge or maximum in the descriptor unit.
475    Scalar(u64),
476    /// Duration distribution converted to the descriptor unit.
477    Histogram(BackendHistogramProjection),
478}
479
480/// One closed-label sample for a canonical backend descriptor.
481#[derive(Debug, Clone, PartialEq)]
482pub struct BackendMetricSample {
483    /// Closed label attributes ordered by the canonical descriptor.
484    pub attributes: Vec<BackendMetricAttribute>,
485    /// Scalar or histogram value in the descriptor's declared unit.
486    pub value: BackendMetricSampleValue,
487}
488
489/// Projection failures that prevent an exporter from emitting a valid
490/// descriptor-owned backend metric.
491#[derive(Debug, Clone, PartialEq, Eq)]
492pub enum BackendMetricProjectionError {
493    /// The supplied descriptor differs from the registered descriptor for its identity.
494    NonCanonicalDescriptor {
495        /// Name of the rejected descriptor.
496        metric_name: &'static str,
497    },
498    /// A duration cannot be converted to the descriptor's declared unit.
499    Duration(BackendDurationProjectionError),
500    /// Cumulative counts are not monotonically increasing.
501    CumulativeHistogramDecreases {
502        /// Metric with invalid bucket counts.
503        metric_name: &'static str,
504        /// Zero-based index of the first decreasing cumulative bucket.
505        bucket_index: usize,
506    },
507    /// The final cumulative bucket differs from the histogram sample count.
508    CumulativeHistogramCountMismatch {
509        /// Metric with inconsistent counts.
510        metric_name: &'static str,
511        /// Count in the final cumulative `+Inf` bucket.
512        cumulative_count: u64,
513        /// Total histogram sample count.
514        count: u64,
515    },
516    /// Disjoint bucket counts do not add up to the histogram sample count.
517    DisjointHistogramCountMismatch {
518        /// Metric with inconsistent counts.
519        metric_name: &'static str,
520        /// Sum of disjoint bucket counts.
521        bucket_count_sum: u64,
522        /// Total histogram sample count.
523        count: u64,
524    },
525}
526
527impl fmt::Display for BackendMetricProjectionError {
528    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
529        match self {
530            Self::NonCanonicalDescriptor { metric_name } => {
531                write!(formatter, "{metric_name} is not the canonical backend descriptor")
532            },
533            Self::Duration(error) => error.fmt(formatter),
534            Self::CumulativeHistogramDecreases { metric_name, bucket_index } => {
535                write!(formatter, "{metric_name} cumulative histogram decreases at bucket {bucket_index}")
536            },
537            Self::CumulativeHistogramCountMismatch { metric_name, cumulative_count, count } => write!(
538                formatter,
539                "{metric_name} cumulative +Inf bucket {cumulative_count} does not match count {count}"
540            ),
541            Self::DisjointHistogramCountMismatch { metric_name, bucket_count_sum, count } => {
542                write!(formatter, "{metric_name} disjoint bucket sum {bucket_count_sum} does not match count {count}")
543            },
544        }
545    }
546}
547
548impl Error for BackendMetricProjectionError {}
549
550impl From<BackendDurationProjectionError> for BackendMetricProjectionError {
551    fn from(error: BackendDurationProjectionError) -> Self {
552        Self::Duration(error)
553    }
554}
555
556fn push_scalar_sample(
557    samples: &mut Vec<BackendMetricSample>,
558    descriptor: &BackendMetricDescriptor,
559    attributes: Vec<BackendMetricAttribute>,
560    value: u64,
561) {
562    debug_assert_eq!(descriptor.labels.len(), attributes.len());
563    samples.push(BackendMetricSample { attributes, value: BackendMetricSampleValue::Scalar(value) });
564}
565
566fn device_attributes(descriptor: &BackendMetricDescriptor, device_ordinal: usize) -> Vec<BackendMetricAttribute> {
567    descriptor_attributes(descriptor, &[BackendMetricAttributeValue::I64(device_ordinal as i64)])
568}
569
570fn attempt_attributes(
571    descriptor: &BackendMetricDescriptor,
572    disposition: BackendAttemptDisposition,
573    outcome: BackendAttemptOutcome,
574) -> Vec<BackendMetricAttribute> {
575    descriptor_attributes(
576        descriptor,
577        &[BackendMetricAttributeValue::Str(disposition.as_str()), BackendMetricAttributeValue::Str(outcome.as_str())],
578    )
579}
580
581fn duration_attributes(
582    descriptor: &BackendMetricDescriptor,
583    disposition: BackendAttemptDisposition,
584    outcome: BackendAttemptOutcome,
585    duration: BackendAttemptDuration,
586) -> Vec<BackendMetricAttribute> {
587    descriptor_attributes(
588        descriptor,
589        &[
590            BackendMetricAttributeValue::Str(disposition.as_str()),
591            BackendMetricAttributeValue::Str(outcome.as_str()),
592            BackendMetricAttributeValue::Str(duration.as_str()),
593        ],
594    )
595}
596
597fn descriptor_attributes(
598    descriptor: &BackendMetricDescriptor,
599    values: &[BackendMetricAttributeValue],
600) -> Vec<BackendMetricAttribute> {
601    assert_eq!(
602        descriptor.labels.len(),
603        values.len(),
604        "{} values must match the descriptor's closed label table",
605        descriptor.name
606    );
607    descriptor
608        .labels
609        .iter()
610        .zip(values)
611        .map(|(label, value)| BackendMetricAttribute { key: label.name, value: *value })
612        .collect()
613}
614
615/// Converts the fixed backend cumulative-by-upper-bound histogram snapshot
616/// into the descriptor-declared, OTLP-compatible disjoint representation.
617pub(crate) fn project_backend_histogram(
618    descriptor: &BackendMetricDescriptor,
619    snapshot: BackendHistogramSnapshot,
620) -> std::result::Result<BackendHistogramProjection, BackendMetricProjectionError> {
621    let duration = project_backend_duration_histogram(descriptor, snapshot.sum)?;
622    let mut bucket_counts = [0; BACKEND_ATTEMPT_DURATION_BUCKET_COUNT];
623    let mut previous = 0;
624    for (bucket_index, cumulative) in snapshot.bucket_counts.iter().copied().enumerate() {
625        if cumulative < previous {
626            return Err(BackendMetricProjectionError::CumulativeHistogramDecreases {
627                metric_name: descriptor.name,
628                bucket_index,
629            });
630        }
631        bucket_counts[bucket_index] = cumulative - previous;
632        previous = cumulative;
633    }
634    if previous != snapshot.count {
635        return Err(BackendMetricProjectionError::CumulativeHistogramCountMismatch {
636            metric_name: descriptor.name,
637            cumulative_count: previous,
638            count: snapshot.count,
639        });
640    }
641    let bucket_count_sum =
642        bucket_counts.iter().try_fold(0_u64, |total, bucket| total.checked_add(*bucket)).unwrap_or(u64::MAX);
643    if bucket_count_sum != snapshot.count {
644        return Err(BackendMetricProjectionError::DisjointHistogramCountMismatch {
645            metric_name: descriptor.name,
646            bucket_count_sum,
647            count: snapshot.count,
648        });
649    }
650    Ok(BackendHistogramProjection {
651        explicit_bounds: duration.bucket_bounds,
652        bucket_counts,
653        count: snapshot.count,
654        sum: duration.sum,
655    })
656}
657
658#[derive(Debug, Clone, Copy)]
659struct HistogramState {
660    snapshot: BackendHistogramSnapshot,
661}
662
663impl HistogramState {
664    const EMPTY: Self = Self { snapshot: BackendHistogramSnapshot::EMPTY };
665
666    fn record(&mut self, value: u64) {
667        for (index, upper_bound) in BACKEND_ATTEMPT_DURATION_BUCKETS_NS.iter().enumerate() {
668            if value <= *upper_bound {
669                self.snapshot.bucket_counts[index] = self.snapshot.bucket_counts[index].saturating_add(1);
670            }
671        }
672        let infinity_bucket = BACKEND_ATTEMPT_DURATION_BUCKET_COUNT - 1;
673        self.snapshot.bucket_counts[infinity_bucket] = self.snapshot.bucket_counts[infinity_bucket].saturating_add(1);
674        self.snapshot.count = self.snapshot.count.saturating_add(1);
675        self.snapshot.sum = self.snapshot.sum.saturating_add(value);
676    }
677}
678
679#[derive(Debug, Clone, Copy)]
680struct AttemptMetricsState {
681    completed: [[u64; BackendAttemptOutcome::COUNT]; BackendAttemptDisposition::COUNT],
682    durations: [[[HistogramState; BackendAttemptDuration::COUNT]; BackendAttemptOutcome::COUNT];
683        BackendAttemptDisposition::COUNT],
684}
685
686impl AttemptMetricsState {
687    const EMPTY: Self = Self {
688        completed: [[0; BackendAttemptOutcome::COUNT]; BackendAttemptDisposition::COUNT],
689        durations: [[[HistogramState::EMPTY; BackendAttemptDuration::COUNT]; BackendAttemptOutcome::COUNT];
690            BackendAttemptDisposition::COUNT],
691    };
692
693    fn record(&mut self, projection: &SealedAttemptProjection, durations: BackendAttemptDurations) {
694        let disposition = BackendAttemptDisposition::from_projection(projection.disposition);
695        let outcome = BackendAttemptOutcome::from_projection(&projection.outcome);
696        self.completed[disposition.index()][outcome.index()] =
697            self.completed[disposition.index()][outcome.index()].saturating_add(1);
698        for duration in [
699            BackendAttemptDuration::Total,
700            BackendAttemptDuration::Planning,
701            BackendAttemptDuration::QueueWait,
702            BackendAttemptDuration::PhysicalExecution,
703        ] {
704            if let Some(value) = durations.value(duration) {
705                self.durations[disposition.index()][outcome.index()][duration.index()].record(value);
706            }
707        }
708    }
709
710    const fn snapshot(self) -> BackendAttemptMetricsSnapshot {
711        let mut durations = [[[BackendHistogramSnapshot::EMPTY; BackendAttemptDuration::COUNT];
712            BackendAttemptOutcome::COUNT]; BackendAttemptDisposition::COUNT];
713        let mut disposition = 0;
714        while disposition < BackendAttemptDisposition::COUNT {
715            let mut outcome = 0;
716            while outcome < BackendAttemptOutcome::COUNT {
717                let mut duration = 0;
718                while duration < BackendAttemptDuration::COUNT {
719                    durations[disposition][outcome][duration] = self.durations[disposition][outcome][duration].snapshot;
720                    duration += 1;
721                }
722                outcome += 1;
723            }
724            disposition += 1;
725        }
726        BackendAttemptMetricsSnapshot { completed: self.completed, durations }
727    }
728}
729
730#[derive(Debug, Clone, Copy)]
731struct DeviceMetricsState {
732    configured: bool,
733    device_ordinal: usize,
734    ledger_sequence: u64,
735    admission: BackendAdmissionMetricsSnapshot,
736}
737
738impl DeviceMetricsState {
739    const EMPTY: Self = Self {
740        configured: false,
741        device_ordinal: 0,
742        ledger_sequence: 0,
743        admission: BackendAdmissionMetricsSnapshot::EMPTY,
744    };
745
746    const fn configured(device_ordinal: usize) -> Self {
747        Self { configured: true, device_ordinal, ..Self::EMPTY }
748    }
749
750    fn record_ledger_transition(&mut self, transition: &LedgerTransition) {
751        let snapshot = &transition.snapshot;
752        self.ledger_sequence = snapshot.sequence;
753        self.admission.active_attempts = u64::try_from(snapshot.active_attempts).unwrap_or(u64::MAX);
754        self.admission.live_grant_bytes = snapshot.live_grants_bytes;
755        self.admission.resident_owner_bytes = snapshot.resident_owner_bytes;
756        self.admission.max_resident_owner_bytes =
757            self.admission.max_resident_owner_bytes.max(snapshot.resident_owner_bytes);
758        // Sealed reports, admission TSV, and backend descriptors all project
759        // this exact ledger transition; resident caches do not own a second
760        // gauge.
761        self.admission.cache_evictable_resident_bytes = snapshot.cache_evictable_resident_bytes;
762        self.admission.cache_pinned_in_use_bytes = snapshot.cache_pinned_in_use_bytes;
763        self.admission.health = match snapshot.health {
764            DeviceAdmissionHealth::Open => BackendAdmissionHealth::Open,
765            DeviceAdmissionHealth::Closed { .. } => BackendAdmissionHealth::Closed,
766        };
767    }
768
769    const fn snapshot(self) -> BackendDeviceMetricsSnapshot {
770        BackendDeviceMetricsSnapshot {
771            device_ordinal: self.device_ordinal,
772            ledger_sequence: self.ledger_sequence,
773            admission: self.admission,
774        }
775    }
776}
777
778#[derive(Debug)]
779struct BackendMetricsState {
780    generation: u64,
781    devices: [DeviceMetricsState; BACKEND_METRIC_DEVICE_CAPACITY],
782    attempts: AttemptMetricsState,
783}
784
785impl BackendMetricsState {
786    fn advance_generation(&mut self) {
787        self.generation = self.generation.saturating_add(1);
788    }
789}
790
791/// One backend-owned fixed-cardinality metrics store.
792///
793/// The store is created once for the backend's resource generation. All update
794/// methods are crate-private so only the owning ledger observer and the
795/// sealed-report sink can feed it.
796#[derive(Clone, Debug)]
797pub(crate) struct BackendMetricsStore {
798    backend_generation: u64,
799    state: Arc<Mutex<BackendMetricsState>>,
800}
801
802impl BackendMetricsStore {
803    /// Creates one fixed-slot store for a nonzero backend resource generation.
804    pub(crate) fn new(backend_generation: u64, device_ordinals: &[usize]) -> Result<Self> {
805        if backend_generation == 0 {
806            return Err(QueryError::new(
807                QueryErrorCode::Internal,
808                OPERATION,
809                "backend metrics store requires a nonzero backend generation",
810            )
811            .with_fact("reason", "zero_backend_generation"));
812        }
813        let mut devices = [DeviceMetricsState::EMPTY; BACKEND_METRIC_DEVICE_CAPACITY];
814        for &device_ordinal in device_ordinals {
815            let Some(slot) = devices.get_mut(device_ordinal) else {
816                return Err(QueryError::new(
817                    QueryErrorCode::InvalidConfiguration,
818                    OPERATION,
819                    "backend device ordinal exceeds fixed metrics capacity",
820                )
821                .with_fact("device_ordinal", device_ordinal)
822                .with_fact("device_capacity", BACKEND_METRIC_DEVICE_CAPACITY)
823                .with_fact("reason", "backend_metrics_device_ordinal_out_of_range"));
824            };
825            if slot.configured {
826                return Err(QueryError::new(
827                    QueryErrorCode::InvalidConfiguration,
828                    OPERATION,
829                    "backend metrics device ordinal is duplicated",
830                )
831                .with_fact("device_ordinal", device_ordinal)
832                .with_fact("reason", "duplicate_backend_metrics_device_ordinal"));
833            }
834            *slot = DeviceMetricsState::configured(device_ordinal);
835        }
836        Ok(Self {
837            backend_generation,
838            state: Arc::new(Mutex::new(BackendMetricsState {
839                generation: 0,
840                devices,
841                attempts: AttemptMetricsState::EMPTY,
842            })),
843        })
844    }
845
846    /// Records one exact in-lock ledger transition. Unknown ordinals are not
847    /// part of this backend generation and are ignored defensively.
848    pub(crate) fn record_ledger_transition(&self, transition: &LedgerTransition) {
849        let device_ordinal = transition.snapshot.device_ordinal;
850        let mut state = self.state.lock().unwrap_or_else(sync::PoisonError::into_inner);
851        let Some(device) = state.devices.get_mut(device_ordinal) else {
852            return;
853        };
854        if !device.configured {
855            return;
856        }
857        device.record_ledger_transition(transition);
858        state.advance_generation();
859    }
860
861    /// Records one report-ready sealed projection and its explicit durations.
862    ///
863    /// The caller owns exactly-once delivery through the attempt completion
864    /// sink; this method intentionally does not retain an attempt identity.
865    pub(crate) fn record_sealed_attempt(
866        &self,
867        projection: &SealedAttemptProjection,
868        durations: BackendAttemptDurations,
869    ) {
870        let mut state = self.state.lock().unwrap_or_else(sync::PoisonError::into_inner);
871        state.attempts.record(projection, durations);
872        state.advance_generation();
873    }
874
875    /// Returns one coherent pull snapshot without exposing the mutable store.
876    pub(crate) fn snapshot(&self) -> BackendMetricsSnapshot {
877        let state = self.state.lock().unwrap_or_else(sync::PoisonError::into_inner);
878        BackendMetricsSnapshot {
879            backend_generation: self.backend_generation,
880            generation: state.generation,
881            devices: state.devices.map(|device| device.configured.then(|| device.snapshot())),
882            attempts: state.attempts.snapshot(),
883        }
884    }
885
886    /// Returns whether a previously pulled snapshot still names the current
887    /// store generation. This is useful to detect a torn multi-read without
888    /// exposing a second mutable observation surface.
889    #[cfg(test)]
890    pub(crate) fn is_snapshot_current(&self, snapshot: &BackendMetricsSnapshot) -> bool {
891        if snapshot.backend_generation != self.backend_generation {
892            return false;
893        }
894        let state = self.state.lock().unwrap_or_else(sync::PoisonError::into_inner);
895        snapshot.generation == state.generation
896    }
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902    use crate::{
903        metrics::{BackendMetric, backend_metric_descriptors},
904        observability::{
905            AttemptCapacityDisposition, AttemptCleanupDisposition, AttemptId, AttemptRetryAdvice,
906            SEALED_ATTEMPT_PROJECTION_SCHEMA,
907        },
908        runtime::gpu::{LedgerTransitionKind, ledger::LedgerSnapshot},
909    };
910
911    fn ledger_transition(
912        sequence: u64,
913        active_attempts: usize,
914        live_grants_bytes: u64,
915        resident_owner_bytes: u64,
916        cache_resident_bytes: u64,
917    ) -> LedgerTransition {
918        LedgerTransition {
919            kind: LedgerTransitionKind::GrantAdmitted,
920            snapshot: LedgerSnapshot {
921                device_ordinal: 3,
922                sequence,
923                managed_capacity_bytes: 10_000,
924                cache_cap_bytes: 0,
925                query_share_bytes: 10_000,
926                default_query_limit_bytes: 10,
927                max_active_attempts: 8,
928                active_attempts,
929                live_grants_bytes,
930                frozen_resident_bytes: 0,
931                resident_owner_bytes,
932                root_streams_available: 4,
933                auxiliary_streams_available: 4,
934                root_worker_slots_available: 4,
935                auxiliary_worker_slots_available: 4,
936                worker_depth: active_attempts,
937                cache_evictable_resident_bytes: cache_resident_bytes,
938                cache_pinned_in_use_bytes: cache_resident_bytes,
939                health: DeviceAdmissionHealth::Open,
940            },
941        }
942    }
943
944    fn sealed_projection(disposition: AttemptDisposition, outcome: RuntimeTerminalOutcome) -> SealedAttemptProjection {
945        SealedAttemptProjection {
946            schema: SEALED_ATTEMPT_PROJECTION_SCHEMA,
947            attempt_id: AttemptId::mint(),
948            disposition,
949            outcome,
950            cleanup_disposition: AttemptCleanupDisposition::NotRequired,
951            cleanup_failure_count: 0,
952            cleanup_failure_count_complete: true,
953            capacity_disposition: AttemptCapacityDisposition::NotAdmitted,
954            health_transition: None,
955            retry_advice: AttemptRetryAdvice::no_retry_needed(),
956            observation_truncated: false,
957            observation_degraded: false,
958            native_fault_evidence_present: false,
959            resource_evidence_present: false,
960        }
961    }
962
963    #[test]
964    fn store_uses_fixed_ordinal_slots_and_rejects_invalid_configuration() {
965        let store = BackendMetricsStore::new(41, &[3, 17]).unwrap();
966        let snapshot = store.snapshot();
967
968        assert_eq!(snapshot.backend_generation, 41);
969        assert_eq!(snapshot.generation, 0);
970        assert_eq!(snapshot.devices.len(), BACKEND_METRIC_DEVICE_CAPACITY);
971        assert_eq!(snapshot.device(3).unwrap().device_ordinal, 3);
972        assert_eq!(snapshot.device(17).unwrap().device_ordinal, 17);
973        assert_eq!(snapshot.device(2), None);
974        assert!(!std::mem::needs_drop::<BackendMetricsSnapshot>());
975        assert!(!std::mem::needs_drop::<BackendDeviceMetricsSnapshot>());
976        assert!(!std::mem::needs_drop::<BackendAttemptMetricsSnapshot>());
977        assert!(BackendMetricsStore::new(41, &[BACKEND_METRIC_DEVICE_CAPACITY]).is_err());
978        assert!(BackendMetricsStore::new(41, &[3, 3]).is_err());
979        assert!(BackendMetricsStore::new(0, &[3]).is_err());
980    }
981
982    #[test]
983    fn ledger_transitions_update_exact_gauges_and_project_cache_residency() {
984        let store = BackendMetricsStore::new(42, &[3]).unwrap();
985        store.record_ledger_transition(&ledger_transition(9, 2, 500, 400, 333));
986        store.record_ledger_transition(&ledger_transition(10, 1, 200, 100, 999));
987        store.record_ledger_transition(&ledger_transition(11, 1, 200, 450, 777));
988
989        let device = store.snapshot().device(3).unwrap();
990        assert_eq!(device.ledger_sequence, 11);
991        assert_eq!(device.admission.active_attempts, 1);
992        assert_eq!(device.admission.live_grant_bytes, 200);
993        assert_eq!(device.admission.resident_owner_bytes, 450);
994        assert_eq!(device.admission.max_resident_owner_bytes, 450);
995        assert_eq!(device.admission.cache_evictable_resident_bytes, 777);
996        assert_eq!(device.admission.cache_pinned_in_use_bytes, 777);
997    }
998
999    #[test]
1000    fn sealed_projection_updates_one_counter_and_the_exact_fixed_histogram_buckets() {
1001        let store = BackendMetricsStore::new(43, &[3]).unwrap();
1002        let projection = sealed_projection(AttemptDisposition::Native, RuntimeTerminalOutcome::Succeeded);
1003        store.record_sealed_attempt(
1004            &projection,
1005            BackendAttemptDurations {
1006                total_ns: Some(1_000),
1007                planning_ns: Some(1_001),
1008                queue_wait_ns: None,
1009                physical_execution_ns: Some(2_000_000_000),
1010            },
1011        );
1012
1013        let attempts = store.snapshot().attempts;
1014        assert_eq!(attempts.completed(BackendAttemptDisposition::Native, BackendAttemptOutcome::Succeeded), 1);
1015        let total = attempts.duration(
1016            BackendAttemptDisposition::Native,
1017            BackendAttemptOutcome::Succeeded,
1018            BackendAttemptDuration::Total,
1019        );
1020        assert_eq!(total.count, 1);
1021        assert_eq!(total.sum, 1_000);
1022        assert_eq!(total.bucket_counts, [1; BACKEND_ATTEMPT_DURATION_BUCKET_COUNT]);
1023        let planning = attempts.duration(
1024            BackendAttemptDisposition::Native,
1025            BackendAttemptOutcome::Succeeded,
1026            BackendAttemptDuration::Planning,
1027        );
1028        assert_eq!(planning.bucket_counts, [0, 1, 1, 1, 1, 1, 1, 1]);
1029        let queue = attempts.duration(
1030            BackendAttemptDisposition::Native,
1031            BackendAttemptOutcome::Succeeded,
1032            BackendAttemptDuration::QueueWait,
1033        );
1034        assert_eq!(queue, BackendHistogramSnapshot::EMPTY);
1035        let physical = attempts.duration(
1036            BackendAttemptDisposition::Native,
1037            BackendAttemptOutcome::Succeeded,
1038            BackendAttemptDuration::PhysicalExecution,
1039        );
1040        assert_eq!(physical.bucket_counts, [0, 0, 0, 0, 0, 0, 0, 1]);
1041    }
1042
1043    #[test]
1044    fn snapshot_generation_is_monotonic_and_detects_torn_multi_reads() {
1045        let store = BackendMetricsStore::new(44, &[3]).unwrap();
1046        let before = store.snapshot();
1047        assert!(store.is_snapshot_current(&before));
1048
1049        store.record_ledger_transition(&ledger_transition(1, 1, 100, 50, 0));
1050        let after_ledger = store.snapshot();
1051        assert!(after_ledger.generation > before.generation);
1052        assert!(before.is_torn_against(&after_ledger));
1053        assert!(!store.is_snapshot_current(&before));
1054        assert!(store.is_snapshot_current(&after_ledger));
1055
1056        store.record_ledger_transition(&ledger_transition(2, 0, 0, 0, 0));
1057        let after_release = store.snapshot();
1058        assert!(after_release.generation > after_ledger.generation);
1059        assert!(after_ledger.is_torn_against(&after_release));
1060        assert!(store.is_snapshot_current(&after_release));
1061    }
1062
1063    #[test]
1064    fn histogram_projection_converts_units_and_cumulative_buckets_at_the_owning_boundary() {
1065        let descriptor = BackendMetric::attempts_duration.descriptor();
1066        let projected = project_backend_histogram(
1067            descriptor,
1068            BackendHistogramSnapshot { bucket_counts: [1, 3, 3, 4, 4, 4, 5, 5], count: 5, sum: 1_000_000_123 },
1069        )
1070        .expect("canonical duration histogram projects");
1071
1072        assert_eq!(projected.explicit_bounds, vec![0.000_001, 0.000_01, 0.000_1, 0.001, 0.01, 0.1, 1.0]);
1073        assert_eq!(projected.sum, 1.000_000_123);
1074        assert_eq!(projected.bucket_counts, [1, 2, 0, 1, 0, 0, 1, 0]);
1075        assert_eq!(projected.bucket_counts.iter().sum::<u64>(), projected.count);
1076        assert_eq!(projected.explicit_bounds.len(), projected.bucket_counts.len() - 1);
1077    }
1078
1079    #[test]
1080    fn histogram_projection_rejects_invalid_cumulative_snapshots() {
1081        let descriptor = BackendMetric::attempts_duration.descriptor();
1082        let decreasing = project_backend_histogram(
1083            descriptor,
1084            BackendHistogramSnapshot { bucket_counts: [1, 0, 0, 0, 0, 0, 0, 0], count: 0, sum: 0 },
1085        );
1086        assert_eq!(
1087            decreasing,
1088            Err(BackendMetricProjectionError::CumulativeHistogramDecreases {
1089                metric_name: descriptor.name,
1090                bucket_index: 1,
1091            })
1092        );
1093
1094        let mismatched_count = project_backend_histogram(
1095            descriptor,
1096            BackendHistogramSnapshot { bucket_counts: [0; BACKEND_ATTEMPT_DURATION_BUCKET_COUNT], count: 1, sum: 0 },
1097        );
1098        assert_eq!(
1099            mismatched_count,
1100            Err(BackendMetricProjectionError::CumulativeHistogramCountMismatch {
1101                metric_name: descriptor.name,
1102                cumulative_count: 0,
1103                count: 1,
1104            })
1105        );
1106    }
1107
1108    #[test]
1109    fn initial_snapshot_projection_covers_the_descriptor_table_and_keeps_cache_metrics_zero() {
1110        let snapshot = BackendMetricsStore::new(49, &[3]).unwrap().snapshot();
1111        for descriptor in backend_metric_descriptors() {
1112            let samples = snapshot.project_backend_metric(descriptor).expect("canonical descriptor projects");
1113            assert!(!samples.is_empty(), "{} has a dashboard projection", descriptor.name);
1114            for sample in &samples {
1115                assert_eq!(sample.attributes.len(), descriptor.labels.len());
1116                for (attribute, label) in sample.attributes.iter().zip(descriptor.labels) {
1117                    assert_eq!(attribute.key, label.name);
1118                }
1119            }
1120            if matches!(
1121                descriptor.metric,
1122                BackendMetric::cache_evictable_resident_bytes | BackendMetric::cache_pinned_in_use_bytes
1123            ) {
1124                assert!(samples.iter().all(|sample| sample.value == BackendMetricSampleValue::Scalar(0)));
1125            }
1126        }
1127    }
1128}