Skip to main content

commonware_runtime/telemetry/metrics/
histogram.rs

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