1use 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
27pub const BACKEND_METRIC_DEVICE_CAPACITY: usize = 64;
33
34pub const BACKEND_ATTEMPT_DURATION_BUCKET_COUNT: usize = BACKEND_ATTEMPT_DURATION_BUCKETS_NS.len() + 1;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40#[repr(usize)]
41pub enum BackendAttemptDisposition {
42 Unplanned = 0,
44 CpuFallback = 1,
46 PartialNative = 2,
48 Native = 3,
50}
51
52impl BackendAttemptDisposition {
53 pub const COUNT: usize = 4;
55
56 pub const ALL: [Self; Self::COUNT] = [Self::Unplanned, Self::CpuFallback, Self::PartialNative, Self::Native];
58
59 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
85#[repr(usize)]
86pub enum BackendAttemptOutcome {
87 Succeeded = 0,
89 Failed = 1,
91 Cancelled = 2,
93 Dropped = 3,
95 Panicked = 4,
97}
98
99impl BackendAttemptOutcome {
100 pub const COUNT: usize = 5;
102
103 pub const ALL: [Self; Self::COUNT] =
105 [Self::Succeeded, Self::Failed, Self::Cancelled, Self::Dropped, Self::Panicked];
106
107 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
135#[repr(usize)]
136pub enum BackendAttemptDuration {
137 Total = 0,
139 Planning = 1,
141 QueueWait = 2,
143 PhysicalExecution = 3,
145}
146
147impl BackendAttemptDuration {
148 pub const COUNT: usize = 4;
150
151 pub const ALL: [Self; Self::COUNT] = [Self::Total, Self::Planning, Self::QueueWait, Self::PhysicalExecution];
153
154 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
195pub enum BackendAdmissionHealth {
196 Open,
198 Closed,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
205pub struct BackendHistogramSnapshot {
206 pub bucket_counts: [u64; BACKEND_ATTEMPT_DURATION_BUCKET_COUNT],
208 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#[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 pub const fn completed(self, disposition: BackendAttemptDisposition, outcome: BackendAttemptOutcome) -> u64 {
229 self.completed[disposition.index()][outcome.index()]
230 }
231
232 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
246pub struct BackendAdmissionMetricsSnapshot {
247 pub active_attempts: u64,
249 pub live_grant_bytes: u64,
251 pub resident_owner_bytes: u64,
253 pub max_resident_owner_bytes: u64,
255 pub cache_evictable_resident_bytes: u64,
257 pub cache_pinned_in_use_bytes: u64,
259 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
278pub struct BackendDeviceMetricsSnapshot {
279 pub device_ordinal: usize,
281 pub ledger_sequence: u64,
283 pub admission: BackendAdmissionMetricsSnapshot,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
295pub struct BackendMetricsSnapshot {
296 pub backend_generation: u64,
298 pub generation: u64,
300 #[serde(serialize_with = "serialize_backend_metric_devices")]
301 pub devices: [Option<BackendDeviceMetricsSnapshot>; BACKEND_METRIC_DEVICE_CAPACITY],
303 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 #[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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
440pub enum BackendMetricAttributeValue {
441 I64(i64),
443 Str(&'static str),
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub struct BackendMetricAttribute {
450 pub key: &'static str,
452 pub value: BackendMetricAttributeValue,
454}
455
456#[derive(Debug, Clone, PartialEq)]
460pub struct BackendHistogramProjection {
461 pub explicit_bounds: Vec<f64>,
463 pub bucket_counts: [u64; BACKEND_ATTEMPT_DURATION_BUCKET_COUNT],
465 pub count: u64,
467 pub sum: f64,
469}
470
471#[derive(Debug, Clone, PartialEq)]
473pub enum BackendMetricSampleValue {
474 Scalar(u64),
476 Histogram(BackendHistogramProjection),
478}
479
480#[derive(Debug, Clone, PartialEq)]
482pub struct BackendMetricSample {
483 pub attributes: Vec<BackendMetricAttribute>,
485 pub value: BackendMetricSampleValue,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
492pub enum BackendMetricProjectionError {
493 NonCanonicalDescriptor {
495 metric_name: &'static str,
497 },
498 Duration(BackendDurationProjectionError),
500 CumulativeHistogramDecreases {
502 metric_name: &'static str,
504 bucket_index: usize,
506 },
507 CumulativeHistogramCountMismatch {
509 metric_name: &'static str,
511 cumulative_count: u64,
513 count: u64,
515 },
516 DisjointHistogramCountMismatch {
518 metric_name: &'static str,
520 bucket_count_sum: u64,
522 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
615pub(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 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#[derive(Clone, Debug)]
797pub(crate) struct BackendMetricsStore {
798 backend_generation: u64,
799 state: Arc<Mutex<BackendMetricsState>>,
800}
801
802impl BackendMetricsStore {
803 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 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 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 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 #[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}