commonware_runtime/telemetry/metrics/
histogram.rs1use super::{Histogram, MetricsExt as _, raw};
4use crate::{Clock, Metrics};
5use std::{future::Future, sync::Arc, time::SystemTime};
6
7pub trait HistogramExt {
9 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
24pub struct Buckets;
28
29impl Buckets {
30 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 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 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#[derive(Clone)]
58pub struct Timed {
59 histogram: Histogram,
61}
62
63impl Timed {
64 pub const fn new(histogram: Histogram) -> Self {
66 Self { histogram }
67 }
68
69 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 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 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 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
106pub struct Timer {
108 histogram: Histogram,
110
111 start: SystemTime,
113}
114
115impl Timer {
116 pub fn observe<C: Clock>(self, clock: &C) {
118 self.histogram.observe_between(self.start, clock.current());
119 }
120}
121
122pub struct ScopedTimer<C: Clock> {
129 timer: Option<Timer>,
130 clock: Arc<C>,
131}
132
133impl<C: Clock> ScopedTimer<C> {
134 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
148pub 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}