use crate::math::usize_f64;
#[cfg(not(any(feature = "std", test)))]
use crate::math::FloatExt;
use crate::pipeline::MetricSlot;
pub struct TumblingWindow {
pub window_ms: u64,
current_start: u64,
current: MetricSlot,
alpha: f64,
}
impl TumblingWindow {
#[must_use]
pub fn new(window_ms: u64, alpha: f64) -> Self {
Self {
window_ms: window_ms.max(1),
current_start: 0,
current: MetricSlot::new(0, alpha),
alpha,
}
}
pub fn insert(&mut self, value: f64, timestamp_ms: u64) -> Option<WindowResult> {
if self.current_start == 0 {
self.current_start = timestamp_ms;
}
if timestamp_ms >= self.current_start.saturating_add(self.window_ms) {
let completed = core::mem::replace(&mut self.current, MetricSlot::new(0, self.alpha));
let result = WindowResult {
start_ms: self.current_start,
end_ms: self.current_start.saturating_add(self.window_ms),
event_count: completed.event_count,
counter: completed.counter,
gauge: completed.gauge,
mean: completed.ddsketch.mean(),
min: completed.ddsketch.min(),
max: completed.ddsketch.max(),
p50: completed.ddsketch.quantile(0.50),
p99: completed.ddsketch.quantile(0.99),
};
self.current_start = timestamp_ms - (timestamp_ms % self.window_ms);
self.current.ddsketch.insert(value);
self.current.event_count = 1;
Some(result)
} else {
self.current.ddsketch.insert(value);
self.current.event_count += 1;
None
}
}
#[must_use]
pub fn flush(&mut self) -> WindowResult {
let completed = core::mem::replace(&mut self.current, MetricSlot::new(0, self.alpha));
let result = WindowResult {
start_ms: self.current_start,
end_ms: self.current_start.saturating_add(self.window_ms),
event_count: completed.event_count,
counter: completed.counter,
gauge: completed.gauge,
mean: completed.ddsketch.mean(),
min: completed.ddsketch.min(),
max: completed.ddsketch.max(),
p50: completed.ddsketch.quantile(0.50),
p99: completed.ddsketch.quantile(0.99),
};
self.current_start = 0;
result
}
#[inline]
#[must_use]
pub const fn current_count(&self) -> u64 {
self.current.event_count
}
}
pub struct SlidingWindow<const N: usize> {
buffer: [f64; N],
write_pos: usize,
count: usize,
sum: f64,
min: f64,
max: f64,
}
impl<const N: usize> SlidingWindow<N> {
#[must_use]
pub const fn new() -> Self {
Self {
buffer: [0.0; N],
write_pos: 0,
count: 0,
sum: 0.0,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
}
}
pub fn push(&mut self, value: f64) {
assert!(N > 0, "SlidingWindow needs N > 0 (got a zero-width window)");
if self.count >= N {
self.sum -= self.buffer[self.write_pos];
}
self.buffer[self.write_pos] = value;
self.sum += value;
self.write_pos = (self.write_pos + 1) % N;
if self.count < N {
self.count += 1;
}
if self.count < N && value <= self.min {
self.min = value;
}
if self.count < N && value >= self.max {
self.max = value;
}
}
#[inline]
#[must_use]
pub fn mean(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
self.sum / usize_f64(self.count)
}
#[inline]
#[must_use]
pub const fn sum(&self) -> f64 {
self.sum
}
#[inline]
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
#[must_use]
pub fn min(&self) -> f64 {
if self.count == 0 {
return f64::INFINITY;
}
let mut m = f64::INFINITY;
for i in 0..self.count {
if self.buffer[i] < m {
m = self.buffer[i];
}
}
m
}
#[must_use]
pub fn max(&self) -> f64 {
if self.count == 0 {
return f64::NEG_INFINITY;
}
let mut m = f64::NEG_INFINITY;
for i in 0..self.count {
if self.buffer[i] > m {
m = self.buffer[i];
}
}
m
}
#[must_use]
pub fn variance(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
let mean = self.mean();
let mut sum_sq = 0.0;
for i in 0..self.count {
let d = self.buffer[i] - mean;
sum_sq += d * d;
}
sum_sq / usize_f64(self.count)
}
#[inline]
#[must_use]
pub fn std_dev(&self) -> f64 {
self.variance().sqrt()
}
pub const fn clear(&mut self) {
self.write_pos = 0;
self.count = 0;
self.sum = 0.0;
self.min = f64::INFINITY;
self.max = f64::NEG_INFINITY;
}
}
impl<const N: usize> Default for SlidingWindow<N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct WindowResult {
pub start_ms: u64,
pub end_ms: u64,
pub event_count: u64,
pub counter: f64,
pub gauge: f64,
pub mean: f64,
pub min: f64,
pub max: f64,
pub p50: f64,
pub p99: f64,
}
pub struct HierarchicalRollup {
levels: [TumblingWindow; 3],
window_sizes: [u64; 3],
results: [Option<WindowResult>; 3],
}
impl HierarchicalRollup {
#[must_use]
pub fn new(level0_ms: u64, level1_ms: u64, level2_ms: u64, alpha: f64) -> Self {
Self {
levels: [
TumblingWindow::new(level0_ms, alpha),
TumblingWindow::new(level1_ms, alpha),
TumblingWindow::new(level2_ms, alpha),
],
window_sizes: [level0_ms, level1_ms, level2_ms],
results: [None, None, None],
}
}
pub fn insert(&mut self, value: f64, timestamp_ms: u64) {
if let Some(r) = self.levels[0].insert(value, timestamp_ms) {
self.results[0] = Some(r);
if let Some(ref r0) = self.results[0] {
let mean = r0.mean;
if let Some(r1) = self.levels[1].insert(mean, timestamp_ms) {
self.results[1] = Some(r1);
if let Some(ref r1_res) = self.results[1] {
let mean1 = r1_res.mean;
if let Some(r2) = self.levels[2].insert(mean1, timestamp_ms) {
self.results[2] = Some(r2);
}
}
}
}
}
}
#[must_use]
pub const fn result(&self, level: usize) -> Option<&WindowResult> {
if level < 3 {
self.results[level].as_ref()
} else {
None
}
}
#[inline]
#[must_use]
pub const fn level_count(&self) -> usize {
3
}
#[must_use]
pub const fn window_size(&self, level: usize) -> u64 {
if level < 3 {
self.window_sizes[level]
} else {
0
}
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::math::u64_f64;
#[test]
fn tumbling_window_basic() {
let mut w = TumblingWindow::new(100, 0.05);
assert!(w.insert(10.0, 10).is_none());
assert!(w.insert(20.0, 50).is_none());
assert_eq!(w.current_count(), 2);
let result = w.insert(30.0, 110).unwrap();
assert_eq!(result.start_ms, 10);
assert_eq!(result.end_ms, 110);
assert_eq!(result.event_count, 2);
}
#[test]
fn tumbling_window_flush() {
let mut w = TumblingWindow::new(1000, 0.05);
w.insert(5.0, 100);
w.insert(15.0, 200);
let result = w.flush();
assert_eq!(result.event_count, 2);
}
#[test]
fn tumbling_window_statistics() {
let mut w = TumblingWindow::new(100, 0.05);
for i in 0..10 {
w.insert(u64_f64(i + 1) * 10.0, i * 5);
}
let result = w.flush();
assert!(result.mean > 0.0);
assert!(result.min > 0.0);
assert!(result.max >= result.min);
}
#[test]
fn sliding_window_basic() {
let mut w = SlidingWindow::<4>::new();
w.push(10.0);
w.push(20.0);
w.push(30.0);
assert_eq!(w.len(), 3);
assert!((w.mean() - 20.0).abs() < 1e-6);
assert!((w.min() - 10.0).abs() < 1e-6);
assert!((w.max() - 30.0).abs() < 1e-6);
}
#[test]
fn sliding_window_overflow() {
let mut w = SlidingWindow::<3>::new();
w.push(10.0);
w.push(20.0);
w.push(30.0);
w.push(40.0); assert_eq!(w.len(), 3);
assert!((w.mean() - 30.0).abs() < 1e-6);
assert!((w.min() - 20.0).abs() < 1e-6);
}
#[test]
fn sliding_window_variance() {
let mut w = SlidingWindow::<4>::new();
w.push(2.0);
w.push(4.0);
w.push(4.0);
w.push(4.0);
assert!((w.variance() - 0.75).abs() < 1e-6);
}
#[test]
fn sliding_window_empty() {
let w = SlidingWindow::<8>::new();
assert!(w.is_empty());
assert!((w.mean() - 0.0).abs() < 1e-6);
assert!((w.variance() - 0.0).abs() < 1e-6);
}
#[test]
fn sliding_window_clear() {
let mut w = SlidingWindow::<8>::new();
w.push(1.0);
w.push(2.0);
w.clear();
assert!(w.is_empty());
assert_eq!(w.len(), 0);
}
#[test]
fn hierarchical_basic() {
let mut rollup = HierarchicalRollup::new(10, 50, 200, 0.05);
assert_eq!(rollup.level_count(), 3);
assert_eq!(rollup.window_size(0), 10);
assert_eq!(rollup.window_size(1), 50);
assert_eq!(rollup.window_size(2), 200);
for i in 0..20 {
rollup.insert(100.0, i);
}
assert!(rollup.result(0).is_some());
}
#[test]
fn hierarchical_cascading() {
let mut rollup = HierarchicalRollup::new(10, 30, 90, 0.05);
for i in 0..100 {
rollup.insert(50.0, i);
}
assert!(rollup.result(0).is_some());
}
#[test]
fn hierarchical_invalid_level() {
let rollup = HierarchicalRollup::new(10, 50, 200, 0.05);
assert!(rollup.result(5).is_none());
assert_eq!(rollup.window_size(99), 0);
}
#[test]
fn sliding_window_std_dev() {
let mut w = SlidingWindow::<3>::new();
w.push(10.0);
w.push(10.0);
w.push(10.0);
assert!((w.std_dev() - 0.0).abs() < 1e-6);
}
#[test]
fn sliding_window_default() {
let w = SlidingWindow::<16>::default();
assert!(w.is_empty());
}
}