1#![deny(missing_docs)]
26#![cfg_attr(test, deny(warnings))]
27
28use std::fmt::{self, Debug};
29use std::sync::atomic::{AtomicUsize, Ordering};
30
31const PRECISION: f64 = 100.;
32const BUCKETS: usize = 1 << 16;
33
34pub struct Histo {
36 vals: Vec<AtomicUsize>,
37 sum: AtomicUsize,
38 count: AtomicUsize,
39}
40
41impl Default for Histo {
42 fn default() -> Histo {
43 let mut vals = Vec::with_capacity(BUCKETS);
44 vals.resize_with(BUCKETS, Default::default);
45
46 Histo {
47 vals,
48 sum: AtomicUsize::new(0),
49 count: AtomicUsize::new(0),
50 }
51 }
52}
53
54unsafe impl Send for Histo {}
55
56impl Debug for Histo {
57 fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
58 const PS: [f64; 10] = [0., 50., 75., 90., 95., 97.5, 99., 99.9, 99.99, 100.];
59 f.write_str("Histogram[")?;
60
61 for p in &PS {
62 let res = self.percentile(*p).round();
63 let line = format!("({} -> {}) ", p, res);
64 f.write_str(&*line)?;
65 }
66
67 f.write_str("]")
68 }
69}
70
71impl Histo {
72 #[inline]
74 pub fn measure<T: Into<f64>>(&self, raw_value: T) -> usize {
75 #[cfg(not(feature = "disable"))]
76 {
77 let value_float: f64 = raw_value.into();
78 self.sum
79 .fetch_add(value_float.round() as usize, Ordering::Relaxed);
80
81 self.count.fetch_add(1, Ordering::Relaxed);
82
83 let compressed: u16 = compress(value_float);
86
87 self.vals[compressed as usize].fetch_add(1, Ordering::Relaxed) + 1
89 }
90
91 #[cfg(feature = "disable")]
92 {
93 0
94 }
95 }
96
97 pub fn percentile(&self, p: f64) -> f64 {
100 #[cfg(not(feature = "disable"))]
101 {
102 assert!(p <= 100., "percentiles must not exceed 100.0");
103
104 let count = self.count.load(Ordering::Acquire);
105
106 if count == 0 {
107 return std::f64::NAN;
108 }
109
110 let mut target = count as f64 * (p / 100.);
111 if target == 0. {
112 target = 1.;
113 }
114
115 let mut sum = 0.;
116
117 for (idx, val) in self.vals.iter().enumerate() {
118 let count = val.load(Ordering::Acquire);
119 sum += count as f64;
120
121 if sum >= target {
122 return decompress(idx as u16);
123 }
124 }
125 }
126
127 std::f64::NAN
128 }
129
130 pub fn print_percentiles(&self) {
132 println!("{:?}", self);
133 }
134
135 pub fn sum(&self) -> usize {
137 self.sum.load(Ordering::Acquire)
138 }
139
140 pub fn count(&self) -> usize {
142 self.count.load(Ordering::Acquire)
143 }
144}
145
146#[inline]
151fn compress<T: Into<f64>>(value: T) -> u16 {
152 let value: f64 = value.into();
153 let abs = value.abs();
154 let boosted = 1. + abs;
155 let ln = boosted.ln();
156 let compressed = PRECISION * ln + 0.5;
157 assert!(compressed <= std::u16::MAX as f64);
158 compressed as u16
159}
160
161#[inline]
164fn decompress(compressed: u16) -> f64 {
165 let unboosted = compressed as f64 / PRECISION;
166 (unboosted.exp() - 1.)
167}
168
169#[test]
170fn it_works() {
171 let c = Histo::default();
172 assert_eq!(c.measure(2), 1);
173 assert_eq!(c.measure(2), 2);
174 assert_eq!(c.measure(3), 1);
175 assert_eq!(c.measure(3), 2);
176 assert_eq!(c.measure(4), 1);
177 assert_eq!(c.percentile(0.).round() as usize, 2);
178 assert_eq!(c.percentile(40.).round() as usize, 2);
179 assert_eq!(c.percentile(40.1).round() as usize, 3);
180 assert_eq!(c.percentile(80.).round() as usize, 3);
181 assert_eq!(c.percentile(80.1).round() as usize, 4);
182 assert_eq!(c.percentile(100.).round() as usize, 4);
183 c.print_percentiles();
184}
185
186#[test]
187fn high_percentiles() {
188 let c = Histo::default();
189 for _ in 0..9000 {
190 c.measure(10);
191 }
192 for _ in 0..900 {
193 c.measure(25);
194 }
195 for _ in 0..90 {
196 c.measure(33);
197 }
198 for _ in 0..9 {
199 c.measure(47);
200 }
201 c.measure(500);
202 assert_eq!(c.percentile(0.).round() as usize, 10);
203 assert_eq!(c.percentile(99.).round() as usize, 25);
204 assert_eq!(c.percentile(99.89).round() as usize, 33);
205 assert_eq!(c.percentile(99.91).round() as usize, 47);
206 assert_eq!(c.percentile(99.99).round() as usize, 47);
207 assert_eq!(c.percentile(100.).round() as usize, 502);
208}
209
210#[test]
211fn multithreaded() {
212 use std::sync::Arc;
213 use std::thread;
214
215 let h = Arc::new(Histo::default());
216 let mut threads = vec![];
217
218 for _ in 0..10 {
219 let h = h.clone();
220 threads.push(thread::spawn(move || {
221 h.measure(20);
222 }));
223 }
224
225 for t in threads.into_iter() {
226 t.join().unwrap();
227 }
228
229 assert_eq!(h.percentile(50.).round() as usize, 20);
230}