1use crate::math::usize_f64;
7#[cfg(not(feature = "std"))]
8use crate::math::FloatExt;
9use crate::pipeline::MetricSlot;
10
11pub struct TumblingWindow {
23 pub window_ms: u64,
25 current_start: u64,
27 current: MetricSlot,
29 alpha: f64,
31}
32
33impl TumblingWindow {
34 #[must_use]
36 pub fn new(window_ms: u64, alpha: f64) -> Self {
37 Self {
38 window_ms: window_ms.max(1),
39 current_start: 0,
40 current: MetricSlot::new(0, alpha),
41 alpha,
42 }
43 }
44
45 pub fn insert(&mut self, value: f64, timestamp_ms: u64) -> Option<WindowResult> {
52 if self.current_start == 0 {
54 self.current_start = timestamp_ms;
55 }
56
57 if timestamp_ms >= self.current_start.saturating_add(self.window_ms) {
59 let completed = core::mem::replace(&mut self.current, MetricSlot::new(0, self.alpha));
60 let result = WindowResult {
61 start_ms: self.current_start,
62 end_ms: self.current_start.saturating_add(self.window_ms),
63 event_count: completed.event_count,
64 counter: completed.counter,
65 gauge: completed.gauge,
66 mean: completed.ddsketch.mean(),
67 min: completed.ddsketch.min(),
68 max: completed.ddsketch.max(),
69 p50: completed.ddsketch.quantile(0.50),
70 p99: completed.ddsketch.quantile(0.99),
71 };
72
73 self.current_start = timestamp_ms - (timestamp_ms % self.window_ms);
75 self.current.ddsketch.insert(value);
76 self.current.event_count = 1;
77
78 Some(result)
79 } else {
80 self.current.ddsketch.insert(value);
81 self.current.event_count += 1;
82 None
83 }
84 }
85
86 #[must_use]
88 pub fn flush(&mut self) -> WindowResult {
89 let completed = core::mem::replace(&mut self.current, MetricSlot::new(0, self.alpha));
90 let result = WindowResult {
91 start_ms: self.current_start,
92 end_ms: self.current_start.saturating_add(self.window_ms),
93 event_count: completed.event_count,
94 counter: completed.counter,
95 gauge: completed.gauge,
96 mean: completed.ddsketch.mean(),
97 min: completed.ddsketch.min(),
98 max: completed.ddsketch.max(),
99 p50: completed.ddsketch.quantile(0.50),
100 p99: completed.ddsketch.quantile(0.99),
101 };
102 self.current_start = 0;
103 result
104 }
105
106 #[inline]
108 #[must_use]
109 pub const fn current_count(&self) -> u64 {
110 self.current.event_count
111 }
112}
113
114pub struct SlidingWindow<const N: usize> {
122 buffer: [f64; N],
124 write_pos: usize,
126 count: usize,
128 sum: f64,
130 min: f64,
132 max: f64,
134}
135
136impl<const N: usize> SlidingWindow<N> {
137 #[must_use]
139 pub const fn new() -> Self {
140 Self {
141 buffer: [0.0; N],
142 write_pos: 0,
143 count: 0,
144 sum: 0.0,
145 min: f64::INFINITY,
146 max: f64::NEG_INFINITY,
147 }
148 }
149
150 pub fn push(&mut self, value: f64) {
159 assert!(N > 0, "SlidingWindow needs N > 0 (got a zero-width window)");
160 if self.count >= N {
161 self.sum -= self.buffer[self.write_pos];
163 }
164 self.buffer[self.write_pos] = value;
165 self.sum += value;
166 self.write_pos = (self.write_pos + 1) % N;
167 if self.count < N {
168 self.count += 1;
169 }
170
171 if self.count < N && value <= self.min {
173 self.min = value;
174 }
175 if self.count < N && value >= self.max {
176 self.max = value;
177 }
178 }
179
180 #[inline]
182 #[must_use]
183 pub fn mean(&self) -> f64 {
184 if self.count == 0 {
185 return 0.0;
186 }
187 self.sum / usize_f64(self.count)
188 }
189
190 #[inline]
192 #[must_use]
193 pub const fn sum(&self) -> f64 {
194 self.sum
195 }
196
197 #[inline]
199 #[must_use]
200 pub const fn len(&self) -> usize {
201 self.count
202 }
203
204 #[inline]
206 #[must_use]
207 pub const fn is_empty(&self) -> bool {
208 self.count == 0
209 }
210
211 #[must_use]
213 pub fn min(&self) -> f64 {
214 if self.count == 0 {
215 return f64::INFINITY;
216 }
217 let mut m = f64::INFINITY;
218 for i in 0..self.count {
219 if self.buffer[i] < m {
220 m = self.buffer[i];
221 }
222 }
223 m
224 }
225
226 #[must_use]
228 pub fn max(&self) -> f64 {
229 if self.count == 0 {
230 return f64::NEG_INFINITY;
231 }
232 let mut m = f64::NEG_INFINITY;
233 for i in 0..self.count {
234 if self.buffer[i] > m {
235 m = self.buffer[i];
236 }
237 }
238 m
239 }
240
241 #[must_use]
243 pub fn variance(&self) -> f64 {
244 if self.count == 0 {
245 return 0.0;
246 }
247 let mean = self.mean();
248 let mut sum_sq = 0.0;
249 for i in 0..self.count {
250 let d = self.buffer[i] - mean;
251 sum_sq += d * d;
252 }
253 sum_sq / usize_f64(self.count)
254 }
255
256 #[inline]
258 #[must_use]
259 pub fn std_dev(&self) -> f64 {
260 self.variance().sqrt()
261 }
262
263 pub const fn clear(&mut self) {
265 self.write_pos = 0;
266 self.count = 0;
267 self.sum = 0.0;
268 self.min = f64::INFINITY;
269 self.max = f64::NEG_INFINITY;
270 }
271}
272
273impl<const N: usize> Default for SlidingWindow<N> {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279#[derive(Clone, Debug)]
285pub struct WindowResult {
286 pub start_ms: u64,
288 pub end_ms: u64,
290 pub event_count: u64,
292 pub counter: f64,
294 pub gauge: f64,
296 pub mean: f64,
298 pub min: f64,
300 pub max: f64,
302 pub p50: f64,
304 pub p99: f64,
306}
307
308pub struct HierarchicalRollup {
317 levels: [TumblingWindow; 3],
319 window_sizes: [u64; 3],
321 results: [Option<WindowResult>; 3],
323}
324
325impl HierarchicalRollup {
326 #[must_use]
330 pub fn new(level0_ms: u64, level1_ms: u64, level2_ms: u64, alpha: f64) -> Self {
331 Self {
332 levels: [
333 TumblingWindow::new(level0_ms, alpha),
334 TumblingWindow::new(level1_ms, alpha),
335 TumblingWindow::new(level2_ms, alpha),
336 ],
337 window_sizes: [level0_ms, level1_ms, level2_ms],
338 results: [None, None, None],
339 }
340 }
341
342 pub fn insert(&mut self, value: f64, timestamp_ms: u64) {
344 if let Some(r) = self.levels[0].insert(value, timestamp_ms) {
346 self.results[0] = Some(r);
347 if let Some(ref r0) = self.results[0] {
349 let mean = r0.mean;
350 if let Some(r1) = self.levels[1].insert(mean, timestamp_ms) {
351 self.results[1] = Some(r1);
352 if let Some(ref r1_res) = self.results[1] {
354 let mean1 = r1_res.mean;
355 if let Some(r2) = self.levels[2].insert(mean1, timestamp_ms) {
356 self.results[2] = Some(r2);
357 }
358 }
359 }
360 }
361 }
362 }
363
364 #[must_use]
366 pub const fn result(&self, level: usize) -> Option<&WindowResult> {
367 if level < 3 {
368 self.results[level].as_ref()
369 } else {
370 None
371 }
372 }
373
374 #[inline]
376 #[must_use]
377 pub const fn level_count(&self) -> usize {
378 3
379 }
380
381 #[must_use]
383 pub const fn window_size(&self, level: usize) -> u64 {
384 if level < 3 {
385 self.window_sizes[level]
386 } else {
387 0
388 }
389 }
390}
391
392#[cfg(test)]
397#[allow(clippy::float_cmp)]
398mod tests {
399 use super::*;
400 use crate::math::u64_f64;
401
402 #[test]
403 fn tumbling_window_basic() {
404 let mut w = TumblingWindow::new(100, 0.05);
405 assert!(w.insert(10.0, 10).is_none());
407 assert!(w.insert(20.0, 50).is_none());
408 assert_eq!(w.current_count(), 2);
409
410 let result = w.insert(30.0, 110).unwrap();
412 assert_eq!(result.start_ms, 10);
413 assert_eq!(result.end_ms, 110);
414 assert_eq!(result.event_count, 2);
415 }
416
417 #[test]
418 fn tumbling_window_flush() {
419 let mut w = TumblingWindow::new(1000, 0.05);
420 w.insert(5.0, 100);
421 w.insert(15.0, 200);
422 let result = w.flush();
423 assert_eq!(result.event_count, 2);
424 }
425
426 #[test]
427 fn tumbling_window_statistics() {
428 let mut w = TumblingWindow::new(100, 0.05);
429 for i in 0..10 {
430 w.insert(u64_f64(i + 1) * 10.0, i * 5);
431 }
432 let result = w.flush();
433 assert!(result.mean > 0.0);
434 assert!(result.min > 0.0);
435 assert!(result.max >= result.min);
436 }
437
438 #[test]
439 fn sliding_window_basic() {
440 let mut w = SlidingWindow::<4>::new();
441 w.push(10.0);
442 w.push(20.0);
443 w.push(30.0);
444 assert_eq!(w.len(), 3);
445 assert!((w.mean() - 20.0).abs() < 1e-6);
446 assert!((w.min() - 10.0).abs() < 1e-6);
447 assert!((w.max() - 30.0).abs() < 1e-6);
448 }
449
450 #[test]
451 fn sliding_window_overflow() {
452 let mut w = SlidingWindow::<3>::new();
453 w.push(10.0);
454 w.push(20.0);
455 w.push(30.0);
456 w.push(40.0); assert_eq!(w.len(), 3);
458 assert!((w.mean() - 30.0).abs() < 1e-6);
459 assert!((w.min() - 20.0).abs() < 1e-6);
460 }
461
462 #[test]
463 fn sliding_window_variance() {
464 let mut w = SlidingWindow::<4>::new();
465 w.push(2.0);
466 w.push(4.0);
467 w.push(4.0);
468 w.push(4.0);
469 assert!((w.variance() - 0.75).abs() < 1e-6);
471 }
472
473 #[test]
474 fn sliding_window_empty() {
475 let w = SlidingWindow::<8>::new();
476 assert!(w.is_empty());
477 assert!((w.mean() - 0.0).abs() < 1e-6);
478 assert!((w.variance() - 0.0).abs() < 1e-6);
479 }
480
481 #[test]
482 fn sliding_window_clear() {
483 let mut w = SlidingWindow::<8>::new();
484 w.push(1.0);
485 w.push(2.0);
486 w.clear();
487 assert!(w.is_empty());
488 assert_eq!(w.len(), 0);
489 }
490
491 #[test]
492 fn hierarchical_basic() {
493 let mut rollup = HierarchicalRollup::new(10, 50, 200, 0.05);
495 assert_eq!(rollup.level_count(), 3);
496 assert_eq!(rollup.window_size(0), 10);
497 assert_eq!(rollup.window_size(1), 50);
498 assert_eq!(rollup.window_size(2), 200);
499
500 for i in 0..20 {
502 rollup.insert(100.0, i);
503 }
504 assert!(rollup.result(0).is_some());
506 }
507
508 #[test]
509 fn hierarchical_cascading() {
510 let mut rollup = HierarchicalRollup::new(10, 30, 90, 0.05);
511 for i in 0..100 {
513 rollup.insert(50.0, i);
514 }
515 assert!(rollup.result(0).is_some());
517 }
518
519 #[test]
520 fn hierarchical_invalid_level() {
521 let rollup = HierarchicalRollup::new(10, 50, 200, 0.05);
522 assert!(rollup.result(5).is_none());
523 assert_eq!(rollup.window_size(99), 0);
524 }
525
526 #[test]
527 fn sliding_window_std_dev() {
528 let mut w = SlidingWindow::<3>::new();
529 w.push(10.0);
530 w.push(10.0);
531 w.push(10.0);
532 assert!((w.std_dev() - 0.0).abs() < 1e-6);
534 }
535
536 #[test]
537 fn sliding_window_default() {
538 let w = SlidingWindow::<16>::default();
539 assert!(w.is_empty());
540 }
541}