1use core::time::Duration;
4
5use crate::time::Time;
6
7#[derive(Copy, Clone, Debug, Default)]
9pub struct Ewma {
10 value: f64,
11 init: bool,
12}
13
14impl Ewma {
15 #[inline]
17 pub fn update(&mut self, x: f64, alpha: f64) {
18 if self.init {
19 self.value += alpha * (x - self.value);
20 } else {
21 self.value = x;
22 self.init = true;
23 }
24 }
25 #[inline]
27 pub fn get(&self) -> Option<f64> {
28 self.init.then_some(self.value)
29 }
30}
31
32#[derive(Copy, Clone, Debug)]
34pub struct QuantileEst {
35 q: f64,
36 p: f64,
37 n: u64,
38}
39
40impl QuantileEst {
41 pub fn new(p: f64) -> Self {
43 QuantileEst { q: 0.0, p, n: 0 }
44 }
45 #[inline]
47 pub fn update(&mut self, x: f64, scale: f64) {
48 self.n += 1;
49 if self.n == 1 {
50 self.q = x;
51 return;
52 }
53 let decay = if self.n > 2500 {
55 0.02
56 } else {
57 (1.0 / (self.n as f64).sqrt()).max(0.02)
58 };
59 let step = scale.max(1e-9) * decay;
60 if x > self.q {
61 self.q += step * self.p;
62 } else if x < self.q {
63 self.q -= step * (1.0 - self.p);
64 }
65 if self.q < 0.0 {
66 self.q = 0.0;
67 }
68 }
69 #[inline]
71 pub fn get(&self) -> Option<f64> {
72 (self.n > 0).then_some(self.q)
73 }
74}
75
76#[derive(Copy, Clone, Debug)]
78pub struct DurStats {
79 pub mean: Ewma,
81 pub p50: QuantileEst,
83 pub p90: QuantileEst,
85 pub count: u64,
87}
88
89impl Default for DurStats {
90 fn default() -> Self {
91 DurStats {
92 mean: Ewma::default(),
93 p50: QuantileEst::new(0.5),
94 p90: QuantileEst::new(0.9),
95 count: 0,
96 }
97 }
98}
99
100impl DurStats {
101 pub fn observe(&mut self, secs: f64) {
103 self.count += 1;
104 self.mean.update(secs, 0.1);
105 let scale = self.mean.get().unwrap_or(secs);
106 self.p50.update(secs, scale);
107 self.p90.update(secs, scale);
108 }
109 pub fn estimate(&self) -> Option<Duration> {
111 if self.count < 3 {
112 return self.mean.get().map(Duration::from_secs_f64);
113 }
114 self.p50.get().map(Duration::from_secs_f64)
115 }
116 pub fn p90(&self) -> Option<Duration> {
118 if self.count < 3 {
119 return self.mean.get().map(Duration::from_secs_f64);
120 }
121 self.p90.get().map(Duration::from_secs_f64)
122 }
123}
124
125#[derive(Copy, Clone, Debug)]
127pub(crate) struct P90Stats {
128 mean: Ewma,
129 p90: QuantileEst,
130 count: u64,
131}
132
133impl Default for P90Stats {
134 fn default() -> Self {
135 P90Stats {
136 mean: Ewma::default(),
137 p90: QuantileEst::new(0.9),
138 count: 0,
139 }
140 }
141}
142
143impl P90Stats {
144 #[inline]
145 pub(crate) fn observe(&mut self, secs: f64) {
146 self.count += 1;
147 self.mean.update(secs, 0.1);
148 let scale = self.mean.get().unwrap_or(secs);
149 self.p90.update(secs, scale);
150 }
151 pub(crate) fn p90(&self) -> Option<Duration> {
152 let v = if self.count < 3 {
153 self.mean.get()
154 } else {
155 self.p90.get()
156 };
157 v.map(Duration::from_secs_f64)
158 }
159}
160
161#[derive(Clone, Debug, Default)]
163pub struct Histogram {
164 counts: Vec<u64>,
165 total: u64,
166 sum_ns: u128,
167}
168
169const SUB: u32 = 8;
170const BUCKETS: usize = 64 * SUB as usize;
171
172impl Histogram {
173 #[inline]
174 fn index(ns: u64) -> usize {
175 if ns < SUB as u64 {
176 return ns as usize;
177 }
178 let msb = 63 - ns.leading_zeros(); let shift = msb - 3;
180 let sub = ((ns >> shift) & (SUB as u64 - 1)) as u32;
181 ((msb - 2) * SUB + sub) as usize
182 }
183 #[inline]
184 fn lower_bound(idx: usize) -> u64 {
185 let idx = idx as u32;
186 if idx < SUB {
187 return idx as u64;
188 }
189 let msb = idx / SUB + 2;
190 let sub = idx % SUB;
191 let v = ((SUB + sub) as u128) << (msb - 3);
193 v.min(u64::MAX as u128) as u64
194 }
195 pub fn record(&mut self, d: Duration) {
197 let ns = d.as_nanos().min(u64::MAX as u128) as u64;
198 if self.counts.is_empty() {
199 self.counts = vec![0; BUCKETS];
200 }
201 self.counts[Self::index(ns)] += 1;
202 self.total += 1;
203 self.sum_ns += ns as u128;
204 }
205 pub fn merge(&mut self, other: &Histogram) {
207 if other.total == 0 {
208 return;
209 }
210 if self.counts.is_empty() {
211 self.counts = vec![0; BUCKETS];
212 }
213 for (a, b) in self.counts.iter_mut().zip(&other.counts) {
214 *a += *b;
215 }
216 self.total += other.total;
217 self.sum_ns += other.sum_ns;
218 }
219 pub fn count(&self) -> u64 {
221 self.total
222 }
223 pub fn mean(&self) -> Duration {
225 if self.total == 0 {
226 return Duration::ZERO;
227 }
228 Duration::from_nanos((self.sum_ns / self.total as u128) as u64)
229 }
230 pub fn quantile(&self, q: f64) -> Duration {
232 if self.total == 0 {
233 return Duration::ZERO;
234 }
235 let target = ((q.clamp(0.0, 1.0) * self.total as f64).ceil() as u64).max(1);
236 let mut acc = 0;
237 for (i, c) in self.counts.iter().enumerate() {
238 acc += c;
239 if acc >= target {
240 let lo = Self::lower_bound(i);
241 let hi = Self::lower_bound(i + 1).max(lo);
242 return Duration::from_nanos(lo + (hi - lo) / 2);
243 }
244 }
245 Duration::ZERO
246 }
247 pub fn clear(&mut self) {
249 if self.total > 0 {
250 self.counts.iter_mut().for_each(|c| *c = 0);
251 }
252 self.total = 0;
253 self.sum_ns = 0;
254 }
255}
256
257#[derive(Clone, Debug, Default)]
259pub struct Bucket {
260 pub dispatched: u64,
261 pub inversions: u64,
262 pub fixed_total: u64,
263 pub fixed_hits: u64,
264 pub succeeded: u64,
265 pub failed: u64,
266 pub retried: u64,
267 pub busy_secs: f64,
268 pub capacity_secs: f64,
269 pub work_secs: f64,
270 pub wait: Histogram,
271 pub run: Histogram,
272}
273
274impl Bucket {
275 fn clear(&mut self) {
276 let wait = core::mem::take(&mut self.wait);
277 let run = core::mem::take(&mut self.run);
278 *self = Bucket {
279 wait,
280 run,
281 ..Default::default()
282 };
283 self.wait.clear();
284 self.run.clear();
285 }
286 fn merge(&mut self, o: &Bucket) {
287 self.dispatched += o.dispatched;
288 self.inversions += o.inversions;
289 self.fixed_total += o.fixed_total;
290 self.fixed_hits += o.fixed_hits;
291 self.succeeded += o.succeeded;
292 self.failed += o.failed;
293 self.retried += o.retried;
294 self.busy_secs += o.busy_secs;
295 self.capacity_secs += o.capacity_secs;
296 self.work_secs += o.work_secs;
297 self.wait.merge(&o.wait);
298 self.run.merge(&o.run);
299 }
300}
301
302#[derive(Clone, Debug)]
304pub struct Window {
305 buckets: Vec<Bucket>,
306 bucket_ns: u64,
307 current: u64,
308}
309
310const WINDOW_BUCKETS: usize = 12;
311
312impl Window {
313 pub fn new(window: Duration) -> Self {
314 let bucket_ns = (window.as_nanos() as u64 / WINDOW_BUCKETS as u64).max(1);
315 Window {
316 buckets: vec![Bucket::default(); WINDOW_BUCKETS],
317 bucket_ns,
318 current: 0,
319 }
320 }
321 pub fn at(&mut self, now: Time) -> &mut Bucket {
323 let idx = now.0 / self.bucket_ns;
324 if idx > self.current {
325 let steps = (idx - self.current).min(WINDOW_BUCKETS as u64);
326 for s in 1..=steps {
327 let b = ((self.current + s) % WINDOW_BUCKETS as u64) as usize;
328 self.buckets[b].clear();
329 }
330 self.current = idx;
331 }
332 let b = (self.current % WINDOW_BUCKETS as u64) as usize;
333 &mut self.buckets[b]
334 }
335 pub fn total(&mut self, now: Time) -> Bucket {
336 self.at(now);
337 let mut acc = Bucket::default();
338 for b in &self.buckets {
339 acc.merge(b);
340 }
341 acc
342 }
343 pub fn span(&self) -> Duration {
344 Duration::from_nanos(self.bucket_ns * WINDOW_BUCKETS as u64)
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn histogram_quantiles_are_close() {
354 let mut h = Histogram::default();
355 for i in 1..=1000u64 {
356 h.record(Duration::from_micros(i));
357 }
358 let p50 = h.quantile(0.5).as_micros() as f64;
359 let p99 = h.quantile(0.99).as_micros() as f64;
360 assert!((p50 - 500.0).abs() / 500.0 < 0.1, "p50={p50}");
361 assert!((p99 - 990.0).abs() / 990.0 < 0.1, "p99={p99}");
362 }
363
364 #[test]
365 fn histogram_index_monotonic() {
366 let mut last = 0;
367 for ns in [0u64, 1, 7, 8, 9, 15, 16, 100, 1_000, 1 << 40, u64::MAX] {
368 let i = Histogram::index(ns);
369 assert!(i >= last);
370 assert!(i < BUCKETS);
371 assert!(Histogram::lower_bound(i) <= ns);
372 last = i;
373 }
374 }
375
376 #[test]
377 fn quantile_estimate_converges() {
378 let mut d = DurStats::default();
379 for i in 0..5000 {
380 d.observe(((i * 7919) % 100) as f64 / 10.0); }
382 let p50 = d.p50.get().unwrap();
383 let p90 = d.p90.get().unwrap();
384 assert!((p50 - 5.0).abs() < 1.0, "p50={p50}");
385 assert!((p90 - 9.0).abs() < 1.0, "p90={p90}");
386 }
387
388 #[test]
389 fn window_expires_old_buckets() {
390 let mut w = Window::new(Duration::from_secs(60));
391 w.at(Time::ZERO).dispatched += 5;
392 assert_eq!(w.total(Time::from_secs_f64(30.0)).dispatched, 5);
393 assert_eq!(w.total(Time::from_secs_f64(120.0)).dispatched, 0);
394 }
395}