Skip to main content

ic_backup/ops/persistence/download_journal/metrics/
mod.rs

1//! Caller-owned host diagnostics using shared arithmetic; no retained authority.
2
3use super::DownloadJournalGuard;
4/// Canonical shared arithmetic for local diagnostic summaries and distributions.
5///
6/// Re-exported so callers can name returned summaries without selecting a separate
7/// Metrics dependency. This is the shared type, with no local wrapper or arithmetic.
8pub use ic_metrics::{MeasurementAggregate, MeasurementHistogram, MeasurementSummary};
9use std::{
10    sync::PoisonError,
11    time::{Duration, Instant},
12};
13
14const EMPTY_CHUNK_HISTOGRAM: MeasurementHistogram<4> =
15    match MeasurementHistogram::new([0, 32 * 1024, 256 * 1024, 1024 * 1024]) {
16        Ok(histogram) => histogram,
17        Err(_) => panic!("prepared chunk bounds must be strictly increasing"),
18    };
19
20/// Read-only diagnostic snapshot for one opened download-journal guard.
21///
22/// Successful and failed returned calls have separate duration summaries in
23/// nanoseconds. Verification includes internal upload-preparation checks; preparation
24/// durations include those checks and must not be summed with them as exclusive work.
25/// Prepared bytes count successful data payloads only, including empty known chunks;
26/// repeated preparation records another sample, not unique or transferred bytes.
27///
28/// Sampling starts empty on create/open and is never persisted or serialized
29/// or reconstructed from journal evidence. Samples outside the `u64` unit range are excluded independently.
30/// Counts/totals clip independently with sticky actual-overflow flags; exactly
31/// reached caps remain exact. Inspect the shared aggregate before exact arithmetic. These values establish
32/// no IC cost, complete transfer, spending, receipt, freshness or outcome authority.
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub struct IcSnapshotLocalMetrics {
35    verification_success_ns: MeasurementSummary,
36    verification_failure_ns: MeasurementSummary,
37    upload_metadata_success_ns: MeasurementSummary,
38    upload_metadata_failure_ns: MeasurementSummary,
39    upload_data_success_ns: MeasurementSummary,
40    upload_data_failure_ns: MeasurementSummary,
41    prepared_chunk_bytes: MeasurementHistogram<4>,
42}
43
44impl Default for IcSnapshotLocalMetrics {
45    fn default() -> Self {
46        Self {
47            verification_success_ns: MeasurementSummary::EMPTY,
48            verification_failure_ns: MeasurementSummary::EMPTY,
49            upload_metadata_success_ns: MeasurementSummary::EMPTY,
50            upload_metadata_failure_ns: MeasurementSummary::EMPTY,
51            upload_data_success_ns: MeasurementSummary::EMPTY,
52            upload_data_failure_ns: MeasurementSummary::EMPTY,
53            prepared_chunk_bytes: EMPTY_CHUNK_HISTOGRAM,
54        }
55    }
56}
57
58impl IcSnapshotLocalMetrics {
59    /// Read successful local IC-tree verification durations in nanoseconds.
60    #[must_use]
61    pub const fn verification_success_ns(self) -> MeasurementSummary {
62        self.verification_success_ns
63    }
64    /// Read rejected local IC-tree verification durations in nanoseconds.
65    #[must_use]
66    pub const fn verification_failure_ns(self) -> MeasurementSummary {
67        self.verification_failure_ns
68    }
69    /// Read successful local upload-metadata preparation durations in nanoseconds.
70    #[must_use]
71    pub const fn upload_metadata_success_ns(self) -> MeasurementSummary {
72        self.upload_metadata_success_ns
73    }
74    /// Read rejected local upload-metadata preparation durations in nanoseconds.
75    #[must_use]
76    pub const fn upload_metadata_failure_ns(self) -> MeasurementSummary {
77        self.upload_metadata_failure_ns
78    }
79    /// Read successful local upload-data preparation durations in nanoseconds.
80    #[must_use]
81    pub const fn upload_data_success_ns(self) -> MeasurementSummary {
82        self.upload_data_success_ns
83    }
84    /// Read rejected local upload-data preparation durations in nanoseconds.
85    #[must_use]
86    pub const fn upload_data_failure_ns(self) -> MeasurementSummary {
87        self.upload_data_failure_ns
88    }
89    /// Read successful prepared chunk sizes in bytes, including zero and repeated work.
90    ///
91    /// This mergeable aggregate retains count, total and maximum, without ordering
92    /// or a latest value. The histogram is its sole owner.
93    #[must_use]
94    pub const fn prepared_chunk_bytes(self) -> MeasurementAggregate {
95        self.prepared_chunk_bytes.aggregate()
96    }
97    /// Read the distribution of successful prepared chunk sizes in bytes.
98    ///
99    /// Inclusive upper bounds are zero, 32 KiB, 256 KiB and 1 MiB. Disjoint
100    /// buckets distinguish empty chunks, small extents, larger extents and the
101    /// supported maximum. Overflow is separate; admitted payloads cannot exceed
102    /// 1 MiB. Repeated preparation is another sample, not unique progress.
103    /// Counts saturate independently; ranges are not exact percentiles.
104    /// The histogram owns the aggregate returned by [`Self::prepared_chunk_bytes`].
105    #[must_use]
106    pub const fn prepared_chunk_bytes_histogram(self) -> MeasurementHistogram<4> {
107        self.prepared_chunk_bytes
108    }
109
110    fn record(
111        &mut self,
112        operation: LocalOperation,
113        elapsed: Duration,
114        succeeded: bool,
115        chunk_bytes: Option<usize>,
116    ) {
117        let duration = u64::try_from(elapsed.as_nanos());
118        let summary = match (operation, succeeded) {
119            (LocalOperation::Verification, true) => &mut self.verification_success_ns,
120            (LocalOperation::Verification, false) => &mut self.verification_failure_ns,
121            (LocalOperation::UploadMetadata, true) => &mut self.upload_metadata_success_ns,
122            (LocalOperation::UploadMetadata, false) => &mut self.upload_metadata_failure_ns,
123            (LocalOperation::UploadData, true) => &mut self.upload_data_success_ns,
124            (LocalOperation::UploadData, false) => &mut self.upload_data_failure_ns,
125        };
126        // Never turn an unrepresentable observation into an exact cap sample.
127        if let Ok(duration) = duration {
128            summary.record(duration);
129        }
130        if let (LocalOperation::UploadData, true, Some(bytes)) = (operation, succeeded, chunk_bytes)
131            && let Ok(bytes) = u64::try_from(bytes)
132        {
133            self.prepared_chunk_bytes.record(bytes);
134        }
135    }
136}
137
138#[derive(Clone, Copy)]
139pub(super) enum LocalOperation {
140    Verification,
141    UploadMetadata,
142    UploadData,
143}
144
145impl DownloadJournalGuard<'_> {
146    /// Read a copied local diagnostic snapshot without filesystem IO or fresh checks.
147    ///
148    /// Sampling is per guard lifetime, including rejected calls; ordinary journal
149    /// replay never supplies samples. Poison recovery is diagnostic only and cannot
150    /// change an operation result. No labels, IDs, byte contents or global registry
151    /// are retained. Host monotonic durations measure inclusive local work, not IC
152    /// instructions/cycles, unique transfer progress or an authoritative receipt.
153    #[must_use]
154    pub fn ic_snapshot_metrics(&self) -> IcSnapshotLocalMetrics {
155        *self
156            .ic_snapshot_metrics
157            .lock()
158            .unwrap_or_else(PoisonError::into_inner)
159    }
160
161    pub(super) fn record_ic_snapshot_metrics(
162        &self,
163        operation: LocalOperation,
164        started: Instant,
165        succeeded: bool,
166        chunk_bytes: Option<usize>,
167    ) {
168        let elapsed = started.elapsed();
169        self.ic_snapshot_metrics
170            .lock()
171            .unwrap_or_else(PoisonError::into_inner)
172            .record(operation, elapsed, succeeded, chunk_bytes);
173    }
174}
175
176#[cfg(test)]
177mod tests;