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/// Individual observations and counts use `u64`; accumulated totals and integer
31/// means use `u128`. Counts/totals clip independently with sticky actual-overflow
32/// flags; every exactly counted batch of admitted observations fits its total.
33/// Inspect the shared aggregate before exact arithmetic. These values establish
34/// no IC cost, complete transfer, spending, receipt, freshness or outcome authority.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct IcSnapshotLocalMetrics {
37    verification_success_ns: MeasurementSummary,
38    verification_failure_ns: MeasurementSummary,
39    upload_metadata_success_ns: MeasurementSummary,
40    upload_metadata_failure_ns: MeasurementSummary,
41    upload_data_success_ns: MeasurementSummary,
42    upload_data_failure_ns: MeasurementSummary,
43    prepared_chunk_bytes: MeasurementHistogram<4>,
44}
45
46impl Default for IcSnapshotLocalMetrics {
47    fn default() -> Self {
48        Self {
49            verification_success_ns: MeasurementSummary::EMPTY,
50            verification_failure_ns: MeasurementSummary::EMPTY,
51            upload_metadata_success_ns: MeasurementSummary::EMPTY,
52            upload_metadata_failure_ns: MeasurementSummary::EMPTY,
53            upload_data_success_ns: MeasurementSummary::EMPTY,
54            upload_data_failure_ns: MeasurementSummary::EMPTY,
55            prepared_chunk_bytes: EMPTY_CHUNK_HISTOGRAM,
56        }
57    }
58}
59
60impl IcSnapshotLocalMetrics {
61    /// Read successful local IC-tree verification durations in nanoseconds.
62    #[must_use]
63    pub const fn verification_success_ns(self) -> MeasurementSummary {
64        self.verification_success_ns
65    }
66    /// Read rejected local IC-tree verification durations in nanoseconds.
67    #[must_use]
68    pub const fn verification_failure_ns(self) -> MeasurementSummary {
69        self.verification_failure_ns
70    }
71    /// Read successful local upload-metadata preparation durations in nanoseconds.
72    #[must_use]
73    pub const fn upload_metadata_success_ns(self) -> MeasurementSummary {
74        self.upload_metadata_success_ns
75    }
76    /// Read rejected local upload-metadata preparation durations in nanoseconds.
77    #[must_use]
78    pub const fn upload_metadata_failure_ns(self) -> MeasurementSummary {
79        self.upload_metadata_failure_ns
80    }
81    /// Read successful local upload-data preparation durations in nanoseconds.
82    #[must_use]
83    pub const fn upload_data_success_ns(self) -> MeasurementSummary {
84        self.upload_data_success_ns
85    }
86    /// Read rejected local upload-data preparation durations in nanoseconds.
87    #[must_use]
88    pub const fn upload_data_failure_ns(self) -> MeasurementSummary {
89        self.upload_data_failure_ns
90    }
91    /// Read successful prepared chunk sizes in bytes, including zero and repeated work.
92    ///
93    /// This mergeable aggregate retains count, total and maximum, without ordering
94    /// or a latest value. The histogram is its sole owner.
95    #[must_use]
96    pub const fn prepared_chunk_bytes(self) -> MeasurementAggregate {
97        self.prepared_chunk_bytes.aggregate()
98    }
99    /// Read the distribution of successful prepared chunk sizes in bytes.
100    ///
101    /// Inclusive upper bounds are zero, 32 KiB, 256 KiB and 1 MiB. Disjoint
102    /// buckets distinguish empty chunks, small extents, larger extents and the
103    /// supported maximum. Overflow is separate; admitted payloads cannot exceed
104    /// 1 MiB. Repeated preparation is another sample, not unique progress.
105    /// Counts saturate independently; ranges are not exact percentiles.
106    /// The histogram owns the aggregate returned by [`Self::prepared_chunk_bytes`].
107    #[must_use]
108    pub const fn prepared_chunk_bytes_histogram(self) -> MeasurementHistogram<4> {
109        self.prepared_chunk_bytes
110    }
111
112    fn record(
113        &mut self,
114        operation: LocalOperation,
115        elapsed: Duration,
116        succeeded: bool,
117        chunk_bytes: Option<usize>,
118    ) {
119        let duration = u64::try_from(elapsed.as_nanos());
120        let summary = match (operation, succeeded) {
121            (LocalOperation::Verification, true) => &mut self.verification_success_ns,
122            (LocalOperation::Verification, false) => &mut self.verification_failure_ns,
123            (LocalOperation::UploadMetadata, true) => &mut self.upload_metadata_success_ns,
124            (LocalOperation::UploadMetadata, false) => &mut self.upload_metadata_failure_ns,
125            (LocalOperation::UploadData, true) => &mut self.upload_data_success_ns,
126            (LocalOperation::UploadData, false) => &mut self.upload_data_failure_ns,
127        };
128        // Never turn an unrepresentable observation into an exact cap sample.
129        if let Ok(duration) = duration {
130            summary.record(duration);
131        }
132        if let (LocalOperation::UploadData, true, Some(bytes)) = (operation, succeeded, chunk_bytes)
133            && let Ok(bytes) = u64::try_from(bytes)
134        {
135            self.prepared_chunk_bytes.record(bytes);
136        }
137    }
138}
139
140#[derive(Clone, Copy)]
141pub(super) enum LocalOperation {
142    Verification,
143    UploadMetadata,
144    UploadData,
145}
146
147impl DownloadJournalGuard<'_> {
148    /// Read a copied local diagnostic snapshot without filesystem IO or fresh checks.
149    ///
150    /// Sampling is per guard lifetime, including rejected calls; ordinary journal
151    /// replay never supplies samples. Poison recovery is diagnostic only and cannot
152    /// change an operation result. No labels, IDs, byte contents or global registry
153    /// are retained. Host monotonic durations measure inclusive local work, not IC
154    /// instructions/cycles, unique transfer progress or an authoritative receipt.
155    #[must_use]
156    pub fn ic_snapshot_metrics(&self) -> IcSnapshotLocalMetrics {
157        *self
158            .ic_snapshot_metrics
159            .lock()
160            .unwrap_or_else(PoisonError::into_inner)
161    }
162
163    pub(super) fn record_ic_snapshot_metrics(
164        &self,
165        operation: LocalOperation,
166        started: Instant,
167        succeeded: bool,
168        chunk_bytes: Option<usize>,
169    ) {
170        let elapsed = started.elapsed();
171        self.ic_snapshot_metrics
172            .lock()
173            .unwrap_or_else(PoisonError::into_inner)
174            .record(operation, elapsed, succeeded, chunk_bytes);
175    }
176}
177
178#[cfg(test)]
179mod tests;