tokio_metrics/runtime/
poll_time_histogram.rs1use std::time::Duration;
2
3#[derive(Debug, Clone, Default)]
14#[non_exhaustive]
15pub struct PollTimeHistogram {
16 buckets: Vec<HistogramBucket>,
17}
18
19impl PollTimeHistogram {
20 #[cfg_attr(not(tokio_unstable), allow(dead_code))]
22 pub(crate) fn new(buckets: Vec<HistogramBucket>) -> Self {
23 Self { buckets }
24 }
25
26 pub fn buckets(&self) -> &[HistogramBucket] {
28 &self.buckets
29 }
30
31 #[cfg_attr(not(tokio_unstable), allow(dead_code))]
33 pub(crate) fn buckets_mut(&mut self) -> &mut [HistogramBucket] {
34 &mut self.buckets
35 }
36
37 pub fn as_counts(&self) -> Vec<u64> {
39 self.buckets.iter().map(|b| b.count).collect()
40 }
41}
42
43#[derive(Debug, Clone, Copy, Default)]
45#[non_exhaustive]
46pub struct HistogramBucket {
47 range_start: Duration,
48 range_end: Duration,
49 count: u64,
50}
51
52impl HistogramBucket {
53 #[cfg_attr(not(tokio_unstable), allow(dead_code))]
55 pub(crate) fn new(range_start: Duration, range_end: Duration, count: u64) -> Self {
56 Self { range_start, range_end, count }
57 }
58
59 pub fn range_start(&self) -> Duration {
61 self.range_start
62 }
63
64 pub fn range_end(&self) -> Duration {
66 self.range_end
67 }
68
69 pub fn count(&self) -> u64 {
71 self.count
72 }
73
74 #[cfg_attr(not(tokio_unstable), allow(dead_code))]
77 pub(crate) fn add_count(&mut self, delta: u64) {
78 self.count = self.count.saturating_add(delta);
79 }
80}
81
82#[cfg(feature = "metrique-integration")]
83impl metrique::writer::Value for PollTimeHistogram {
84 fn write(&self, writer: impl metrique::writer::ValueWriter) {
85 use metrique::writer::unit::NegativeScale;
86 use metrique::writer::{MetricFlags, Observation, Unit};
87
88 const LAST_BUCKET_END: Duration = Duration::from_nanos(u64::MAX);
92 writer.metric(
93 self.buckets.iter().filter(|b| b.count > 0).map(|b| {
94 let value_us = if b.range_end == LAST_BUCKET_END {
95 b.range_start.as_micros() as f64
96 } else {
97 #[allow(clippy::incompatible_msrv)] f64::midpoint(
99 b.range_start.as_micros() as f64,
100 b.range_end.as_micros() as f64,
101 )
102 };
103 Observation::Repeated {
104 total: value_us * b.count as f64,
105 occurrences: b.count,
106 }
107 }),
108 Unit::Second(NegativeScale::Micro),
109 [],
110 MetricFlags::empty(),
111 );
112 }
113}
114
115#[cfg(feature = "metrique-integration")]
116impl metrique::CloseValue for PollTimeHistogram {
117 type Closed = Self;
118
119 fn close(self) -> Self {
120 self
121 }
122}
123
124#[cfg(all(test, tokio_unstable, feature = "metrique-integration"))]
128mod tests {
129 use super::*;
130 use crate::runtime::RuntimeMetrics;
131 use metrique::CloseValue;
132 use metrique::test_util::test_metric;
133
134 #[test]
135 fn poll_time_histogram_close_value() {
136 let hist = PollTimeHistogram::new(vec![
137 HistogramBucket::new(Duration::from_micros(0), Duration::from_micros(100), 5),
138 HistogramBucket::new(Duration::from_micros(100), Duration::from_micros(200), 0),
139 HistogramBucket::new(Duration::from_micros(200), Duration::from_micros(500), 3),
140 ]);
141
142 let closed = hist.close();
143 let buckets = closed.buckets();
144 assert_eq!(buckets.len(), 3);
145 assert_eq!(buckets[0].count(), 5);
146 assert_eq!(buckets[0].range_start(), Duration::from_micros(0));
147 assert_eq!(buckets[0].range_end(), Duration::from_micros(100));
148 assert_eq!(buckets[1].count(), 0);
149 assert_eq!(buckets[2].count(), 3);
150 assert_eq!(buckets[2].range_start(), Duration::from_micros(200));
151 assert_eq!(buckets[2].range_end(), Duration::from_micros(500));
152 }
153
154 #[test]
155 fn poll_time_histogram_last_bucket_uses_range_start() {
156 let last_bucket_start = Duration::from_millis(500);
157 let metrics = RuntimeMetrics {
158 poll_time_histogram: PollTimeHistogram::new(vec![
159 HistogramBucket::new(Duration::from_micros(0), Duration::from_micros(100), 0),
160 HistogramBucket::new(last_bucket_start, Duration::from_nanos(u64::MAX), 2),
161 ]),
162 ..Default::default()
163 };
164
165 let entry = test_metric(metrics);
166 let hist = &entry.metrics["poll_time_histogram"];
167 assert_eq!(hist.distribution.len(), 1);
168
169 match hist.distribution[0] {
170 metrique::writer::Observation::Repeated { total, occurrences } => {
171 assert_eq!(occurrences, 2);
172 let expected = last_bucket_start.as_micros() as f64 * 2.0;
173 assert!((total - expected).abs() < 0.01);
174 }
175 other => panic!("expected Repeated, got {other:?}"),
176 }
177 }
178}