Skip to main content

alice_analytics/
window.rs

1//! Time-Window Aggregation
2//!
3//! タンブリング/スライディングウィンドウによる時系列集約。
4//! MetricSlotをウィンドウ単位でrotateし、時間ベースの集約を提供。
5
6use crate::math::usize_f64;
7#[cfg(not(feature = "std"))]
8use crate::math::FloatExt;
9use crate::pipeline::MetricSlot;
10
11// ============================================================================
12// Tumbling Window — 固定幅の重複なしウィンドウ
13// ============================================================================
14
15/// 固定幅タンブリングウィンドウ。
16///
17/// ウィンドウ境界でMetricSlotをrotateし、完了したウィンドウを返す。
18/// ```text
19/// |---window 0---|---window 1---|---window 2---|
20///    ↑ events      ↑ events       ↑ events
21/// ```
22pub struct TumblingWindow {
23    /// ウィンドウ幅(ミリ秒)。
24    pub window_ms: u64,
25    /// 現在のウィンドウ開始時刻。
26    current_start: u64,
27    /// 現在のウィンドウの `MetricSlot`。
28    current: MetricSlot,
29    /// `DDSketch` alpha。
30    alpha: f64,
31}
32
33impl TumblingWindow {
34    /// 新しいタンブリングウィンドウを作成。
35    #[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    /// イベントを投入。ウィンドウ境界を超えた場合、完了したスロットを返す。
46    ///
47    /// 時刻は `saturating_add` で足す: `timestamp_ms` が `u64::MAX` 近傍だと
48    /// `current_start + window_ms` が溢れ、2026-10-07 までは debug build で
49    /// panic し release build では wrap して境界時刻が巻き戻っていた
50    /// 飽和させると最後のウィンドウが閉じなくなるだけで、`flush()` で取り出せる
51    pub fn insert(&mut self, value: f64, timestamp_ms: u64) -> Option<WindowResult> {
52        // 初回: ウィンドウ開始時刻を設定
53        if self.current_start == 0 {
54            self.current_start = timestamp_ms;
55        }
56
57        // ウィンドウ境界を超えたか
58        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            // 新ウィンドウ開始
74            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    /// 現在のウィンドウを強制的にフラッシュ。
87    #[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    /// 現在のウィンドウのイベント数。
107    #[inline]
108    #[must_use]
109    pub const fn current_count(&self) -> u64 {
110        self.current.event_count
111    }
112}
113
114// ============================================================================
115// Sliding Window — 重複ありスライディングウィンドウ
116// ============================================================================
117
118/// リングバッファベースのスライディングウィンドウ。
119///
120/// 最新N個の観測値を保持し、ウィンドウ全体の統計量を提供。
121pub struct SlidingWindow<const N: usize> {
122    /// リングバッファ。
123    buffer: [f64; N],
124    /// 書き込み位置。
125    write_pos: usize,
126    /// 有効な要素数。
127    count: usize,
128    /// 合計値(Running sum)。
129    sum: f64,
130    /// 最小値。
131    min: f64,
132    /// 最大値。
133    max: f64,
134}
135
136impl<const N: usize> SlidingWindow<N> {
137    /// 新しいスライディングウィンドウを作成。
138    #[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    /// 値を追加。バッファが満杯の場合、最古の値を上書き。
151    ///
152    /// # Panics
153    ///
154    /// `N == 0` の場合 (幅 0 のウィンドウは保持できない) 回復可能な入力の誤りでは
155    /// なく型引数の前提違反なので `Result` にせず panic する
156    /// `SlidingWindow::<0>::new()` 自体は成功し、最初の `push` で panic する
157    /// 契約は `tests/panic_contract.rs` が pin する
158    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            // 最古の値をsumから除去
162            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        // min/maxはpush毎に全走査が必要(値が消えるため)
172        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    /// 平均値。
181    #[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    /// 合計値。
191    #[inline]
192    #[must_use]
193    pub const fn sum(&self) -> f64 {
194        self.sum
195    }
196
197    /// 有効な要素数。
198    #[inline]
199    #[must_use]
200    pub const fn len(&self) -> usize {
201        self.count
202    }
203
204    /// 空かどうか。
205    #[inline]
206    #[must_use]
207    pub const fn is_empty(&self) -> bool {
208        self.count == 0
209    }
210
211    /// 最小値(全走査)。
212    #[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    /// 最大値(全走査)。
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            if self.buffer[i] > m {
235                m = self.buffer[i];
236            }
237        }
238        m
239    }
240
241    /// 分散(母集団分散)。
242    #[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    /// 標準偏差。
257    #[inline]
258    #[must_use]
259    pub fn std_dev(&self) -> f64 {
260        self.variance().sqrt()
261    }
262
263    /// ウィンドウをクリア。
264    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// ============================================================================
280// Window Result
281// ============================================================================
282
283/// 完了したウィンドウの集約結果。
284#[derive(Clone, Debug)]
285pub struct WindowResult {
286    /// ウィンドウ開始時刻(ミリ秒)。
287    pub start_ms: u64,
288    /// ウィンドウ終了時刻(ミリ秒)。
289    pub end_ms: u64,
290    /// イベント数。
291    pub event_count: u64,
292    /// カウンター合計。
293    pub counter: f64,
294    /// 最終ゲージ値。
295    pub gauge: f64,
296    /// 平均値。
297    pub mean: f64,
298    /// 最小値。
299    pub min: f64,
300    /// 最大値。
301    pub max: f64,
302    /// P50。
303    pub p50: f64,
304    /// P99。
305    pub p99: f64,
306}
307
308// ============================================================================
309// Hierarchical Rollup — 階層的集約 (1m → 5m → 1h)
310// ============================================================================
311
312/// 階層的ロールアップ。
313///
314/// 複数レベルの時間ウィンドウで自動集約。
315/// 例: 1分 → 5分 → 1時間
316pub struct HierarchicalRollup {
317    /// 各レベルのウィンドウ。
318    levels: [TumblingWindow; 3],
319    /// 各レベルのウィンドウ幅(ミリ秒)。
320    window_sizes: [u64; 3],
321    /// 各レベルの最新結果。
322    results: [Option<WindowResult>; 3],
323}
324
325impl HierarchicalRollup {
326    /// 3レベルロールアップを作成(ミリ秒単位)。
327    ///
328    /// 典型例: `new(60_000, 300_000, 3_600_000, 0.05)` → 1分/5分/1時間
329    #[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    /// 値を投入。各レベルで完了したウィンドウがあれば記録。
343    pub fn insert(&mut self, value: f64, timestamp_ms: u64) {
344        // レベル0に直接投入
345        if let Some(r) = self.levels[0].insert(value, timestamp_ms) {
346            self.results[0] = Some(r);
347            // レベル0完了 → レベル1にmeanを投入
348            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                    // レベル1完了 → レベル2にmeanを投入
353                    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    /// 指定レベルの最新結果を取得。
365    #[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    /// レベル数。
375    #[inline]
376    #[must_use]
377    pub const fn level_count(&self) -> usize {
378        3
379    }
380
381    /// 指定レベルのウィンドウ幅(ミリ秒)。
382    #[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// ============================================================================
393// Tests
394// ============================================================================
395
396#[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        // ウィンドウ内のイベント
406        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        // ウィンドウ境界超え → 完了結果
411        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); // 10.0が押し出される
457        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        // mean=3.5, var = ((2-3.5)^2 + 3*(4-3.5)^2)/4 = (2.25+0.75)/4 = 0.75
470        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        // 10ms / 50ms / 200ms レベル
494        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        // レベル0のウィンドウ完了まで投入
501        for i in 0..20 {
502            rollup.insert(100.0, i);
503        }
504        // レベル0は完了しているはず
505        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        // 十分なイベントを投入してレベル1まで完了させる
512        for i in 0..100 {
513            rollup.insert(50.0, i);
514        }
515        // レベル0は確実に完了
516        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        // 全て同じ値 → 標準偏差=0
533        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}