Skip to main content

commonware_runtime/telemetry/metrics/
histogram.rs

1//! Utilities for working with histograms.
2
3use super::{raw, Histogram, MetricsExt as _};
4use crate::{Clock, Metrics};
5use std::{future::Future, sync::Arc, time::SystemTime};
6
7/// Convenience methods for Prometheus histograms.
8pub trait HistogramExt {
9    /// Observe the duration between two points in time, in seconds.
10    ///
11    /// If the clock goes backwards, the duration is 0.
12    fn observe_between(&self, start: SystemTime, end: SystemTime);
13}
14
15impl HistogramExt for raw::Histogram {
16    fn observe_between(&self, start: SystemTime, end: SystemTime) {
17        let duration = end
18            .duration_since(start)
19            .map_or(0.0, |duration| duration.as_secs_f64());
20        self.observe(duration);
21    }
22}
23
24/// Holds constants for bucket sizes for histograms.
25///
26/// The bucket sizes are in seconds.
27pub struct Buckets;
28
29impl Buckets {
30    /// For resolving items over a network.
31    ///
32    /// These tasks could either be between two peers or require multiple hops, rounds, retries,
33    /// etc.
34    pub const NETWORK: [f64; 13] = [
35        0.010, 0.020, 0.050, 0.100, 0.200, 0.500, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0, 300.0,
36    ];
37
38    /// For resolving items locally.
39    ///
40    /// These tasks are expected to be fast and not require network access, but might require
41    /// expensive computation, disk access, etc.
42    pub const LOCAL: [f64; 12] = [
43        3e-6, 1e-5, 3e-5, 1e-4, 3e-4, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1.0,
44    ];
45
46    /// For cryptographic operations.
47    ///
48    /// These operations are expected to be fast and not require network access, but might
49    /// require expensive computation.
50    pub const CRYPTOGRAPHY: [f64; 16] = [
51        3e-6, 1e-5, 3e-5, 1e-4, 3e-4, 0.001, 0.002, 0.003, 0.005, 0.01, 0.015, 0.02, 0.025, 0.03,
52        0.1, 0.2,
53    ];
54}
55
56/// A wrapper around a histogram that can time operations using a caller-provided clock.
57#[derive(Clone)]
58pub struct Timed {
59    /// The histogram to record durations in.
60    histogram: Histogram,
61}
62
63impl Timed {
64    /// Create a new timed histogram.
65    pub const fn new(histogram: Histogram) -> Self {
66        Self { histogram }
67    }
68
69    /// Register a duration histogram (see [`duration_histogram`]) wrapped for timing.
70    pub fn register<M: Metrics>(context: &M, name: &'static str, help: &'static str) -> Self {
71        Self::new(duration_histogram(context, name, help))
72    }
73
74    /// Create a new timer that can record a duration from the current time.
75    pub fn timer<C: Clock>(&self, clock: &C) -> Timer {
76        let start = clock.current();
77        Timer {
78            histogram: self.histogram.clone(),
79            start,
80        }
81    }
82
83    /// Time an operation, recording a sample only if it resolves to `Some`.
84    pub async fn time_some<C: Clock, T>(
85        &self,
86        clock: &C,
87        op: impl Future<Output = Option<T>>,
88    ) -> Option<T> {
89        let start = clock.current();
90        let result = op.await;
91        if result.is_some() {
92            self.histogram.observe_between(start, clock.current());
93        }
94        result
95    }
96}
97
98/// A timer that records a duration when explicitly observed.
99pub struct Timer {
100    /// The histogram to record durations in.
101    histogram: Histogram,
102
103    /// The time at which the timer was started.
104    start: SystemTime,
105}
106
107impl Timer {
108    /// Record the duration using the given clock.
109    pub fn observe<C: Clock>(self, clock: &C) {
110        self.histogram.observe_between(self.start, clock.current());
111    }
112}
113
114/// A timer guard that observes its duration when dropped.
115///
116/// Built on top of [`Timer`]. Useful for `?`-heavy async code where every early-return path
117/// would otherwise need to remember to call [`Timer::observe`]. Validation failures after the
118/// guard is created are still part of the recorded duration; if a code path should not record
119/// a sample, call [`ScopedTimer::cancel`] before the guard is dropped.
120pub struct ScopedTimer<C: Clock> {
121    timer: Option<Timer>,
122    clock: Arc<C>,
123}
124
125impl<C: Clock> ScopedTimer<C> {
126    /// Cancel the guard so it does not observe a sample on drop.
127    pub fn cancel(mut self) {
128        self.timer = None;
129    }
130}
131
132impl<C: Clock> Drop for ScopedTimer<C> {
133    fn drop(&mut self) {
134        if let Some(timer) = self.timer.take() {
135            timer.observe(self.clock.as_ref());
136        }
137    }
138}
139
140impl Timed {
141    /// Start a timer guard that observes the elapsed duration when dropped.
142    pub fn scoped<C: Clock>(&self, clock: &Arc<C>) -> ScopedTimer<C> {
143        ScopedTimer {
144            timer: Some(self.timer(clock.as_ref())),
145            clock: clock.clone(),
146        }
147    }
148}
149
150/// Register a duration histogram using [`Buckets::LOCAL`] (storage-style work).
151pub fn duration_histogram<M: Metrics>(
152    context: &M,
153    name: &'static str,
154    help: &'static str,
155) -> Histogram {
156    context.histogram(name, help, Buckets::LOCAL)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::{deterministic, Runner as _, Supervisor as _};
163    use std::time::Duration;
164
165    #[test]
166    fn duration_records_all_calls() {
167        deterministic::Runner::default().start(|context| async move {
168            let histogram = duration_histogram(&context, "test_duration", "test duration");
169            let timed = Timed::new(histogram);
170            let clock = Arc::new(context.child("timer"));
171
172            {
173                let _timer = timed.scoped(&clock);
174                context.sleep(Duration::from_millis(1)).await;
175                let result: Result<(), ()> = Ok(());
176                assert!(result.is_ok());
177            }
178
179            {
180                let _timer = timed.scoped(&clock);
181                context.sleep(Duration::from_millis(1)).await;
182                let result: Result<(), ()> = Err(());
183                assert!(result.is_err());
184            }
185
186            {
187                let _timer = timed.scoped(&clock);
188                context.sleep(Duration::from_millis(1)).await;
189            }
190
191            let metrics = context.encode();
192            assert!(
193                metrics.contains("test_duration_count 3"),
194                "unexpected metrics: {metrics}"
195            );
196        });
197    }
198}