1use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::RwLock;
5
6#[derive(Debug, Clone)]
8pub struct BucketConfig {
9 pub boundaries: Vec<f64>,
11}
12
13impl Default for BucketConfig {
14 fn default() -> Self {
15 Self {
17 boundaries: vec![
18 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, ],
31 }
32 }
33}
34
35impl BucketConfig {
36 pub fn new(boundaries: Vec<f64>) -> Self {
38 Self { boundaries }
39 }
40}
41
42#[derive(Debug)]
44pub struct Histogram {
45 buckets: Vec<AtomicU64>,
47 boundaries: Vec<f64>,
49 sum: RwLock<f64>,
51 count: AtomicU64,
53 name: String,
55 help: String,
57}
58
59impl Histogram {
60 pub fn new(name: impl Into<String>, help: impl Into<String>) -> Self {
62 Self::with_buckets(name, help, BucketConfig::default())
63 }
64
65 pub fn with_buckets(
67 name: impl Into<String>,
68 help: impl Into<String>,
69 config: BucketConfig,
70 ) -> Self {
71 let num_buckets = config.boundaries.len() + 1; Self {
73 buckets: (0..num_buckets).map(|_| AtomicU64::new(0)).collect(),
74 boundaries: config.boundaries,
75 sum: RwLock::new(0.0),
76 count: AtomicU64::new(0),
77 name: name.into(),
78 help: help.into(),
79 }
80 }
81
82 pub fn observe(&self, value: f64) {
84 let bucket_idx = self.find_bucket(value);
86 self.buckets[bucket_idx].fetch_add(1, Ordering::Relaxed);
87
88 self.count.fetch_add(1, Ordering::Relaxed);
90 if let Ok(mut sum) = self.sum.write() {
91 *sum += value;
92 }
93 }
94
95 fn find_bucket(&self, value: f64) -> usize {
97 for (idx, &boundary) in self.boundaries.iter().enumerate() {
98 if value <= boundary {
99 return idx;
100 }
101 }
102 self.boundaries.len()
104 }
105
106 pub fn bucket_counts(&self) -> Vec<u64> {
108 self.buckets.iter().map(|b| b.load(Ordering::Relaxed)).collect()
109 }
110
111 pub fn boundaries(&self) -> &[f64] {
113 &self.boundaries
114 }
115
116 pub fn sum(&self) -> f64 {
118 self.sum.read().map(|s| *s).unwrap_or(0.0)
119 }
120
121 pub fn count(&self) -> u64 {
123 self.count.load(Ordering::Relaxed)
124 }
125
126 pub fn name(&self) -> &str {
128 &self.name
129 }
130
131 pub fn help(&self) -> &str {
133 &self.help
134 }
135
136 pub fn percentile(&self, p: f64) -> Option<f64> {
138 if p < 0.0 || p > 100.0 {
139 return None;
140 }
141
142 let total = self.count();
143 if total == 0 {
144 return None;
145 }
146
147 let target = (p / 100.0) * total as f64;
148 let mut cumulative = 0u64;
149
150 for (idx, count) in self.bucket_counts().iter().enumerate() {
151 cumulative += *count;
152 if cumulative as f64 >= target {
153 if idx < self.boundaries.len() {
154 return Some(self.boundaries[idx]);
155 } else {
156 return Some(f64::INFINITY);
158 }
159 }
160 }
161
162 None
163 }
164
165 pub fn reset(&self) {
167 for bucket in &self.buckets {
168 bucket.store(0, Ordering::Relaxed);
169 }
170 self.count.store(0, Ordering::Relaxed);
171 if let Ok(mut sum) = self.sum.write() {
172 *sum = 0.0;
173 }
174 }
175}
176
177impl Clone for Histogram {
178 fn clone(&self) -> Self {
179 Self {
180 buckets: self.buckets.iter().map(|b| AtomicU64::new(b.load(Ordering::Relaxed))).collect(),
181 boundaries: self.boundaries.clone(),
182 sum: RwLock::new(*self.sum.read().unwrap()),
183 count: AtomicU64::new(self.count.load(Ordering::Relaxed)),
184 name: self.name.clone(),
185 help: self.help.clone(),
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use std::f64::EPSILON;
194
195 #[test]
196 fn test_histogram_creation() {
197 let hist = Histogram::new("request_duration", "Request duration in seconds");
198 assert_eq!(hist.count(), 0);
199 assert_eq!(hist.sum(), 0.0);
200 assert_eq!(hist.name(), "request_duration");
201 assert!(!hist.boundaries().is_empty());
202 }
203
204 #[test]
205 fn test_histogram_observe() {
206 let hist = Histogram::new("test", "test histogram");
207 hist.observe(0.1);
208 hist.observe(0.2);
209
210 assert_eq!(hist.count(), 2);
211 assert!((hist.sum() - 0.3).abs() < EPSILON);
212 }
213
214 #[test]
215 fn test_histogram_bucket_distribution() {
216 let config = BucketConfig::new(vec![0.1, 0.5, 1.0]);
217 let hist = Histogram::with_buckets("test", "test", config);
218
219 hist.observe(0.05); hist.observe(0.3); hist.observe(0.7); hist.observe(2.0); let counts = hist.bucket_counts();
226 assert_eq!(counts[0], 1); assert_eq!(counts[1], 1); assert_eq!(counts[2], 1); assert_eq!(counts[3], 1); }
231
232 #[test]
233 fn test_histogram_percentile() {
234 let config = BucketConfig::new(vec![0.1, 0.5, 1.0]);
235 let hist = Histogram::with_buckets("test", "test", config);
236
237 for _ in 0..10 {
239 hist.observe(0.05);
240 }
241 for _ in 0..10 {
242 hist.observe(0.3);
243 }
244 for _ in 0..10 {
245 hist.observe(0.8);
246 }
247
248 let p50 = hist.percentile(50.0).unwrap();
250 assert!((p50 - 0.3).abs() < EPSILON || (p50 - 0.5).abs() < EPSILON);
251
252 let p99 = hist.percentile(99.0).unwrap();
254 assert!(p99 <= 1.0 || p99.is_infinite());
255 }
256
257 #[test]
258 fn test_histogram_percentile_empty() {
259 let hist = Histogram::new("test", "test");
260 assert!(hist.percentile(50.0).is_none());
261 }
262
263 #[test]
264 fn test_histogram_percentile_invalid() {
265 let hist = Histogram::new("test", "test");
266 hist.observe(0.1);
267
268 assert!(hist.percentile(-1.0).is_none());
269 assert!(hist.percentile(101.0).is_none());
270 }
271
272 #[test]
273 fn test_histogram_reset() {
274 let hist = Histogram::new("test", "test");
275 hist.observe(0.1);
276 hist.observe(0.2);
277
278 assert_eq!(hist.count(), 2);
279
280 hist.reset();
281
282 assert_eq!(hist.count(), 0);
283 assert_eq!(hist.sum(), 0.0);
284 assert!(hist.bucket_counts().iter().all(|&c| c == 0));
285 }
286
287 #[test]
288 fn test_histogram_clone() {
289 let hist = Histogram::new("test", "test");
290 hist.observe(0.5);
291
292 let cloned = hist.clone();
293 assert_eq!(cloned.count(), 1);
294 assert!((cloned.sum() - 0.5).abs() < EPSILON);
295 assert_eq!(cloned.name(), "test");
296 }
297
298 #[test]
299 fn test_histogram_concurrent_observe() {
300 use std::sync::Arc;
301 use std::thread;
302
303 let hist = Arc::new(Histogram::new("test", "test"));
304 let mut handles = vec![];
305
306 for _ in 0..10 {
307 let hist_clone = Arc::clone(&hist);
308 handles.push(thread::spawn(move || {
309 hist_clone.observe(0.1);
310 }));
311 }
312
313 for handle in handles {
314 handle.join().unwrap();
315 }
316
317 assert_eq!(hist.count(), 10);
318 assert!((hist.sum() - 1.0).abs() < EPSILON);
319 }
320
321 #[test]
322 fn test_custom_bucket_config() {
323 let config = BucketConfig::new(vec![0.01, 0.1, 1.0]);
324 let hist = Histogram::with_buckets("test", "test", config);
325
326 assert_eq!(hist.boundaries().len(), 3);
327 assert_eq!(hist.bucket_counts().len(), 4); }
329
330 #[test]
331 fn test_histogram_large_values() {
332 let hist = Histogram::new("test", "test");
333 hist.observe(1000.0);
334 hist.observe(10000.0);
335
336 assert_eq!(hist.count(), 2);
337 assert!((hist.sum() - 11000.0).abs() < EPSILON);
338 }
339
340 #[test]
341 fn test_histogram_zero_value() {
342 let hist = Histogram::new("test", "test");
343 hist.observe(0.0);
344
345 assert_eq!(hist.count(), 1);
346 assert_eq!(hist.sum(), 0.0);
347 }
348}