Skip to main content

alice_analytics/
window.rs

1//! Time-Window Aggregation
2//!
3//! タンブリング/スライディングウィンドウによる時系列集約。
4//! MetricSlotをウィンドウ単位でrotateし、時間ベースの集約を提供。
5
6use crate::math::usize_f64;
7// a no_std unit-test build links std (libtest), whose inherent methods win;
8// FloatExt only covers correctly rounded operations, so the bits are the same
9#[cfg(not(any(feature = "std", test)))]
10use crate::math::FloatExt;
11use crate::pipeline::MetricSlot;
12
13// ============================================================================
14// Tumbling Window — 固定幅の重複なしウィンドウ
15// ============================================================================
16
17/// 固定幅タンブリングウィンドウ。
18///
19/// ウィンドウ境界でMetricSlotをrotateし、完了したウィンドウを返す。
20/// ```text
21/// |---window 0---|---window 1---|---window 2---|
22///    ↑ events      ↑ events       ↑ events
23/// ```
24pub struct TumblingWindow {
25    /// ウィンドウ幅(ミリ秒)。
26    pub window_ms: u64,
27    /// 現在のウィンドウ開始時刻。
28    current_start: u64,
29    /// 現在のウィンドウの `MetricSlot`。
30    current: MetricSlot,
31    /// `DDSketch` alpha。
32    alpha: f64,
33}
34
35impl TumblingWindow {
36    /// 新しいタンブリングウィンドウを作成。
37    #[must_use]
38    pub fn new(window_ms: u64, alpha: f64) -> Self {
39        Self {
40            window_ms: window_ms.max(1),
41            current_start: 0,
42            current: MetricSlot::new(0, alpha),
43            alpha,
44        }
45    }
46
47    /// イベントを投入。ウィンドウ境界を超えた場合、完了したスロットを返す。
48    ///
49    /// 時刻は `saturating_add` で足す: `timestamp_ms` が `u64::MAX` 近傍だと
50    /// `current_start + window_ms` が溢れ、2026-10-07 までは debug build で
51    /// panic し release build では wrap して境界時刻が巻き戻っていた
52    /// 飽和させると最後のウィンドウが閉じなくなるだけで、`flush()` で取り出せる
53    pub fn insert(&mut self, value: f64, timestamp_ms: u64) -> Option<WindowResult> {
54        // 初回: ウィンドウ開始時刻を設定
55        if self.current_start == 0 {
56            self.current_start = timestamp_ms;
57        }
58
59        // ウィンドウ境界を超えたか
60        if timestamp_ms >= self.current_start.saturating_add(self.window_ms) {
61            let completed = core::mem::replace(&mut self.current, MetricSlot::new(0, self.alpha));
62            let result = WindowResult {
63                start_ms: self.current_start,
64                end_ms: self.current_start.saturating_add(self.window_ms),
65                event_count: completed.event_count,
66                counter: completed.counter,
67                gauge: completed.gauge,
68                mean: completed.ddsketch.mean(),
69                min: completed.ddsketch.min(),
70                max: completed.ddsketch.max(),
71                p50: completed.ddsketch.quantile(0.50),
72                p99: completed.ddsketch.quantile(0.99),
73            };
74
75            // 新ウィンドウ開始
76            self.current_start = timestamp_ms - (timestamp_ms % self.window_ms);
77            self.current.ddsketch.insert(value);
78            self.current.event_count = 1;
79
80            Some(result)
81        } else {
82            self.current.ddsketch.insert(value);
83            self.current.event_count += 1;
84            None
85        }
86    }
87
88    /// 現在のウィンドウを強制的にフラッシュ。
89    #[must_use]
90    pub fn flush(&mut self) -> WindowResult {
91        let completed = core::mem::replace(&mut self.current, MetricSlot::new(0, self.alpha));
92        let result = WindowResult {
93            start_ms: self.current_start,
94            end_ms: self.current_start.saturating_add(self.window_ms),
95            event_count: completed.event_count,
96            counter: completed.counter,
97            gauge: completed.gauge,
98            mean: completed.ddsketch.mean(),
99            min: completed.ddsketch.min(),
100            max: completed.ddsketch.max(),
101            p50: completed.ddsketch.quantile(0.50),
102            p99: completed.ddsketch.quantile(0.99),
103        };
104        self.current_start = 0;
105        result
106    }
107
108    /// 現在のウィンドウのイベント数。
109    #[inline]
110    #[must_use]
111    pub const fn current_count(&self) -> u64 {
112        self.current.event_count
113    }
114}
115
116// ============================================================================
117// Sliding Window — 重複ありスライディングウィンドウ
118// ============================================================================
119
120/// リングバッファベースのスライディングウィンドウ。
121///
122/// 最新N個の観測値を保持し、ウィンドウ全体の統計量を提供。
123pub struct SlidingWindow<const N: usize> {
124    /// リングバッファ。
125    buffer: [f64; N],
126    /// 書き込み位置。
127    write_pos: usize,
128    /// 有効な要素数。
129    count: usize,
130    /// 合計値(Running sum)。
131    sum: f64,
132    /// 最小値。
133    min: f64,
134    /// 最大値。
135    max: f64,
136}
137
138impl<const N: usize> SlidingWindow<N> {
139    /// 新しいスライディングウィンドウを作成。
140    #[must_use]
141    pub const fn new() -> Self {
142        Self {
143            buffer: [0.0; N],
144            write_pos: 0,
145            count: 0,
146            sum: 0.0,
147            min: f64::INFINITY,
148            max: f64::NEG_INFINITY,
149        }
150    }
151
152    /// 値を追加。バッファが満杯の場合、最古の値を上書き。
153    ///
154    /// # Panics
155    ///
156    /// `N == 0` の場合 (幅 0 のウィンドウは保持できない) 回復可能な入力の誤りでは
157    /// なく型引数の前提違反なので `Result` にせず panic する
158    /// `SlidingWindow::<0>::new()` 自体は成功し、最初の `push` で panic する
159    /// 契約は `tests/panic_contract.rs` が pin する
160    pub fn push(&mut self, value: f64) {
161        assert!(N > 0, "SlidingWindow needs N > 0 (got a zero-width window)");
162        if self.count >= N {
163            // 最古の値をsumから除去
164            self.sum -= self.buffer[self.write_pos];
165        }
166        self.buffer[self.write_pos] = value;
167        self.sum += value;
168        self.write_pos = (self.write_pos + 1) % N;
169        if self.count < N {
170            self.count += 1;
171        }
172
173        // min/maxはpush毎に全走査が必要(値が消えるため)
174        if self.count < N && value <= self.min {
175            self.min = value;
176        }
177        if self.count < N && value >= self.max {
178            self.max = value;
179        }
180    }
181
182    /// 平均値。
183    #[inline]
184    #[must_use]
185    pub fn mean(&self) -> f64 {
186        if self.count == 0 {
187            return 0.0;
188        }
189        self.sum / usize_f64(self.count)
190    }
191
192    /// 合計値。
193    #[inline]
194    #[must_use]
195    pub const fn sum(&self) -> f64 {
196        self.sum
197    }
198
199    /// 有効な要素数。
200    #[inline]
201    #[must_use]
202    pub const fn len(&self) -> usize {
203        self.count
204    }
205
206    /// 空かどうか。
207    #[inline]
208    #[must_use]
209    pub const fn is_empty(&self) -> bool {
210        self.count == 0
211    }
212
213    /// 最小値(全走査)。
214    #[must_use]
215    pub fn min(&self) -> f64 {
216        if self.count == 0 {
217            return f64::INFINITY;
218        }
219        let mut m = f64::INFINITY;
220        for i in 0..self.count {
221            m = alice_det_math::min64(m, self.buffer[i]);
222        }
223        m
224    }
225
226    /// 最大値(全走査)。
227    #[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            m = alice_det_math::max64(m, self.buffer[i]);
235        }
236        m
237    }
238
239    /// 分散(母集団分散)。
240    #[must_use]
241    pub fn variance(&self) -> f64 {
242        if self.count == 0 {
243            return 0.0;
244        }
245        let mean = self.mean();
246        let mut sum_sq = 0.0;
247        for i in 0..self.count {
248            let d = self.buffer[i] - mean;
249            sum_sq += d * d;
250        }
251        sum_sq / usize_f64(self.count)
252    }
253
254    /// 標準偏差。
255    #[inline]
256    #[must_use]
257    pub fn std_dev(&self) -> f64 {
258        self.variance().sqrt()
259    }
260
261    /// ウィンドウをクリア。
262    pub const fn clear(&mut self) {
263        self.write_pos = 0;
264        self.count = 0;
265        self.sum = 0.0;
266        self.min = f64::INFINITY;
267        self.max = f64::NEG_INFINITY;
268    }
269}
270
271impl<const N: usize> Default for SlidingWindow<N> {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277// ============================================================================
278// Window Result
279// ============================================================================
280
281/// 完了したウィンドウの集約結果。
282#[derive(Clone, Debug)]
283pub struct WindowResult {
284    /// ウィンドウ開始時刻(ミリ秒)。
285    pub start_ms: u64,
286    /// ウィンドウ終了時刻(ミリ秒)。
287    pub end_ms: u64,
288    /// イベント数。
289    pub event_count: u64,
290    /// カウンター合計。
291    pub counter: f64,
292    /// 最終ゲージ値。
293    pub gauge: f64,
294    /// 平均値。
295    pub mean: f64,
296    /// 最小値。
297    pub min: f64,
298    /// 最大値。
299    pub max: f64,
300    /// P50。
301    pub p50: f64,
302    /// P99。
303    pub p99: f64,
304}
305
306// ============================================================================
307// Hierarchical Rollup — 階層的集約 (1m → 5m → 1h)
308// ============================================================================
309
310/// 階層的ロールアップ。
311///
312/// 複数レベルの時間ウィンドウで自動集約。
313/// 例: 1分 → 5分 → 1時間
314pub struct HierarchicalRollup {
315    /// 各レベルのウィンドウ。
316    levels: [TumblingWindow; 3],
317    /// 各レベルのウィンドウ幅(ミリ秒)。
318    window_sizes: [u64; 3],
319    /// 各レベルの最新結果。
320    results: [Option<WindowResult>; 3],
321}
322
323impl HierarchicalRollup {
324    /// 3レベルロールアップを作成(ミリ秒単位)。
325    ///
326    /// 典型例: `new(60_000, 300_000, 3_600_000, 0.05)` → 1分/5分/1時間
327    #[must_use]
328    pub fn new(level0_ms: u64, level1_ms: u64, level2_ms: u64, alpha: f64) -> Self {
329        Self {
330            levels: [
331                TumblingWindow::new(level0_ms, alpha),
332                TumblingWindow::new(level1_ms, alpha),
333                TumblingWindow::new(level2_ms, alpha),
334            ],
335            window_sizes: [level0_ms, level1_ms, level2_ms],
336            results: [None, None, None],
337        }
338    }
339
340    /// 値を投入。各レベルで完了したウィンドウがあれば記録。
341    pub fn insert(&mut self, value: f64, timestamp_ms: u64) {
342        // レベル0に直接投入
343        if let Some(r) = self.levels[0].insert(value, timestamp_ms) {
344            self.results[0] = Some(r);
345            // レベル0完了 → レベル1にmeanを投入
346            if let Some(ref r0) = self.results[0] {
347                let mean = r0.mean;
348                if let Some(r1) = self.levels[1].insert(mean, timestamp_ms) {
349                    self.results[1] = Some(r1);
350                    // レベル1完了 → レベル2にmeanを投入
351                    if let Some(ref r1_res) = self.results[1] {
352                        let mean1 = r1_res.mean;
353                        if let Some(r2) = self.levels[2].insert(mean1, timestamp_ms) {
354                            self.results[2] = Some(r2);
355                        }
356                    }
357                }
358            }
359        }
360    }
361
362    /// 指定レベルの最新結果を取得。
363    #[must_use]
364    pub const fn result(&self, level: usize) -> Option<&WindowResult> {
365        if level < 3 {
366            self.results[level].as_ref()
367        } else {
368            None
369        }
370    }
371
372    /// レベル数。
373    #[inline]
374    #[must_use]
375    pub const fn level_count(&self) -> usize {
376        3
377    }
378
379    /// 指定レベルのウィンドウ幅(ミリ秒)。
380    #[must_use]
381    pub const fn window_size(&self, level: usize) -> u64 {
382        if level < 3 {
383            self.window_sizes[level]
384        } else {
385            0
386        }
387    }
388}
389
390// ============================================================================
391// Tests
392// ============================================================================
393
394#[cfg(test)]
395#[allow(clippy::float_cmp)]
396mod tests {
397    use super::*;
398    use crate::math::u64_f64;
399
400    #[test]
401    fn tumbling_window_basic() {
402        let mut w = TumblingWindow::new(100, 0.05);
403        // ウィンドウ内のイベント
404        assert!(w.insert(10.0, 10).is_none());
405        assert!(w.insert(20.0, 50).is_none());
406        assert_eq!(w.current_count(), 2);
407
408        // ウィンドウ境界超え → 完了結果
409        let result = w.insert(30.0, 110).unwrap();
410        assert_eq!(result.start_ms, 10);
411        assert_eq!(result.end_ms, 110);
412        assert_eq!(result.event_count, 2);
413    }
414
415    #[test]
416    fn tumbling_window_flush() {
417        let mut w = TumblingWindow::new(1000, 0.05);
418        w.insert(5.0, 100);
419        w.insert(15.0, 200);
420        let result = w.flush();
421        assert_eq!(result.event_count, 2);
422    }
423
424    #[test]
425    fn tumbling_window_statistics() {
426        let mut w = TumblingWindow::new(100, 0.05);
427        for i in 0..10 {
428            w.insert(u64_f64(i + 1) * 10.0, i * 5);
429        }
430        let result = w.flush();
431        assert!(result.mean > 0.0);
432        assert!(result.min > 0.0);
433        assert!(result.max >= result.min);
434    }
435
436    #[test]
437    fn sliding_window_basic() {
438        let mut w = SlidingWindow::<4>::new();
439        w.push(10.0);
440        w.push(20.0);
441        w.push(30.0);
442        assert_eq!(w.len(), 3);
443        assert!((w.mean() - 20.0).abs() < 1e-6);
444        assert!((w.min() - 10.0).abs() < 1e-6);
445        assert!((w.max() - 30.0).abs() < 1e-6);
446    }
447
448    #[test]
449    fn sliding_window_overflow() {
450        let mut w = SlidingWindow::<3>::new();
451        w.push(10.0);
452        w.push(20.0);
453        w.push(30.0);
454        w.push(40.0); // 10.0が押し出される
455        assert_eq!(w.len(), 3);
456        assert!((w.mean() - 30.0).abs() < 1e-6);
457        assert!((w.min() - 20.0).abs() < 1e-6);
458    }
459
460    #[test]
461    fn sliding_window_variance() {
462        let mut w = SlidingWindow::<4>::new();
463        w.push(2.0);
464        w.push(4.0);
465        w.push(4.0);
466        w.push(4.0);
467        // mean=3.5, var = ((2-3.5)^2 + 3*(4-3.5)^2)/4 = (2.25+0.75)/4 = 0.75
468        assert!((w.variance() - 0.75).abs() < 1e-6);
469    }
470
471    #[test]
472    fn sliding_window_empty() {
473        let w = SlidingWindow::<8>::new();
474        assert!(w.is_empty());
475        assert!((w.mean() - 0.0).abs() < 1e-6);
476        assert!((w.variance() - 0.0).abs() < 1e-6);
477    }
478
479    #[test]
480    fn sliding_window_clear() {
481        let mut w = SlidingWindow::<8>::new();
482        w.push(1.0);
483        w.push(2.0);
484        w.clear();
485        assert!(w.is_empty());
486        assert_eq!(w.len(), 0);
487    }
488
489    #[test]
490    fn hierarchical_basic() {
491        // 10ms / 50ms / 200ms レベル
492        let mut rollup = HierarchicalRollup::new(10, 50, 200, 0.05);
493        assert_eq!(rollup.level_count(), 3);
494        assert_eq!(rollup.window_size(0), 10);
495        assert_eq!(rollup.window_size(1), 50);
496        assert_eq!(rollup.window_size(2), 200);
497
498        // レベル0のウィンドウ完了まで投入
499        for i in 0..20 {
500            rollup.insert(100.0, i);
501        }
502        // レベル0は完了しているはず
503        assert!(rollup.result(0).is_some());
504    }
505
506    #[test]
507    fn hierarchical_cascading() {
508        let mut rollup = HierarchicalRollup::new(10, 30, 90, 0.05);
509        // 十分なイベントを投入してレベル1まで完了させる
510        for i in 0..100 {
511            rollup.insert(50.0, i);
512        }
513        // レベル0は確実に完了
514        assert!(rollup.result(0).is_some());
515    }
516
517    #[test]
518    fn hierarchical_invalid_level() {
519        let rollup = HierarchicalRollup::new(10, 50, 200, 0.05);
520        assert!(rollup.result(5).is_none());
521        assert_eq!(rollup.window_size(99), 0);
522    }
523
524    #[test]
525    fn sliding_window_std_dev() {
526        let mut w = SlidingWindow::<3>::new();
527        w.push(10.0);
528        w.push(10.0);
529        w.push(10.0);
530        // 全て同じ値 → 標準偏差=0
531        assert!((w.std_dev() - 0.0).abs() < 1e-6);
532    }
533
534    #[test]
535    fn sliding_window_default() {
536        let w = SlidingWindow::<16>::default();
537        assert!(w.is_empty());
538    }
539}