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::{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    /// Create a new timer that can record a duration from the current time.
70    pub fn timer<C: Clock>(&self, clock: &C) -> Timer {
71        let start = clock.current();
72        Timer {
73            histogram: self.histogram.clone(),
74            start,
75        }
76    }
77
78    /// Time an operation, recording only if it returns `Some`.
79    pub fn time_some<C: Clock, T, F: FnOnce() -> Option<T>>(&self, clock: &C, f: F) -> Option<T> {
80        let start = clock.current();
81        let result = f();
82        if result.is_some() {
83            self.histogram.observe_between(start, clock.current());
84        }
85        result
86    }
87}
88
89/// A timer that records a duration when explicitly observed.
90pub struct Timer {
91    /// The histogram to record durations in.
92    histogram: Histogram,
93
94    /// The time at which the timer was started.
95    start: SystemTime,
96}
97
98impl Timer {
99    /// Record the duration using the given clock.
100    pub fn observe<C: Clock>(self, clock: &C) {
101        self.histogram.observe_between(self.start, clock.current());
102    }
103}
104
105/// A timer guard that observes its duration when dropped.
106///
107/// Built on top of [`Timer`]. Useful for `?`-heavy async code where every early-return path
108/// would otherwise need to remember to call [`Timer::observe`]. Validation failures after the
109/// guard is created are still part of the recorded duration; if a code path should not record
110/// a sample, call [`ScopedTimer::cancel`] before the guard is dropped.
111pub struct ScopedTimer<C: Clock> {
112    timer: Option<Timer>,
113    clock: Arc<C>,
114}
115
116impl<C: Clock> ScopedTimer<C> {
117    /// Cancel the guard so it does not observe a sample on drop.
118    pub fn cancel(mut self) {
119        self.timer = None;
120    }
121}
122
123impl<C: Clock> Drop for ScopedTimer<C> {
124    fn drop(&mut self) {
125        if let Some(timer) = self.timer.take() {
126            timer.observe(self.clock.as_ref());
127        }
128    }
129}
130
131impl Timed {
132    /// Start a timer guard that observes the elapsed duration when dropped.
133    pub fn scoped<C: Clock>(&self, clock: &Arc<C>) -> ScopedTimer<C> {
134        ScopedTimer {
135            timer: Some(self.timer(clock.as_ref())),
136            clock: clock.clone(),
137        }
138    }
139}
140
141/// Register a duration histogram using [`Buckets::LOCAL`] (storage-style work).
142pub fn duration_histogram<M: Metrics>(
143    context: &M,
144    name: &'static str,
145    help: &'static str,
146) -> Histogram {
147    context.histogram(name, help, Buckets::LOCAL)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::{deterministic, Runner as _, Supervisor as _};
154    use std::time::Duration;
155
156    #[test]
157    fn duration_records_all_calls() {
158        deterministic::Runner::default().start(|context| async move {
159            let histogram = duration_histogram(&context, "test_duration", "test duration");
160            let timed = Timed::new(histogram);
161            let clock = Arc::new(context.child("timer"));
162
163            {
164                let _timer = timed.scoped(&clock);
165                context.sleep(Duration::from_millis(1)).await;
166                let result: Result<(), ()> = Ok(());
167                assert!(result.is_ok());
168            }
169
170            {
171                let _timer = timed.scoped(&clock);
172                context.sleep(Duration::from_millis(1)).await;
173                let result: Result<(), ()> = Err(());
174                assert!(result.is_err());
175            }
176
177            {
178                let _timer = timed.scoped(&clock);
179                context.sleep(Duration::from_millis(1)).await;
180            }
181
182            let metrics = context.encode();
183            assert!(
184                metrics.contains("test_duration_count 3"),
185                "unexpected metrics: {metrics}"
186            );
187        });
188    }
189}