windowed_stats/
lib.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5pub mod aggregations;
6pub mod experimental;
7
8use crate::aggregations::SumAndCount;
9use fuchsia_async as fasync;
10use fuchsia_inspect::{ArrayProperty, Node as InspectNode};
11
12use std::collections::VecDeque;
13use std::fmt::{self, Debug};
14
15pub struct WindowedStats<T> {
16    stats: VecDeque<T>,
17    capacity: usize,
18    aggregation_fn: Box<dyn Fn(&T, &T) -> T + Send>,
19}
20
21impl<T: Debug> Debug for WindowedStats<T> {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        f.debug_struct("WindowedStats")
24            .field("stats", &self.stats)
25            .field("capacity", &self.capacity)
26            .finish_non_exhaustive()
27    }
28}
29
30impl<T: Default> WindowedStats<T> {
31    pub fn new(capacity: usize, aggregation_fn: Box<dyn Fn(&T, &T) -> T + Send>) -> Self {
32        let mut stats = VecDeque::with_capacity(capacity);
33        stats.push_back(T::default());
34        Self { stats, capacity, aggregation_fn }
35    }
36
37    /// Get stat of all the windows that are still kept if `n` is None.
38    /// Otherwise, get stat for up to `n` windows.
39    pub fn windowed_stat(&self, n: Option<usize>) -> T {
40        let mut aggregated_value = T::default();
41        let n = n.unwrap_or(self.stats.len());
42        for value in self.stats.iter().rev().take(n) {
43            aggregated_value = (self.aggregation_fn)(&aggregated_value, &value);
44        }
45        aggregated_value
46    }
47
48    pub fn slide_window(&mut self) {
49        if !self.stats.is_empty() && self.stats.len() >= self.capacity {
50            let _ = self.stats.pop_front();
51        }
52        self.stats.push_back(T::default());
53    }
54}
55
56impl<T> WindowedStats<T> {
57    pub fn log_value(&mut self, value: &T) {
58        if let Some(latest) = self.stats.back_mut() {
59            *latest = (self.aggregation_fn)(latest, value);
60        }
61    }
62}
63
64impl<T: Into<u64> + Clone> WindowedStats<T> {
65    pub fn log_inspect_uint_array(&self, node: &InspectNode, property_name: &'static str) {
66        let iter = self.stats.iter();
67        let inspect_array = node.create_uint_array(property_name, iter.len());
68        for (i, c) in iter.enumerate() {
69            inspect_array.set(i, (*c).clone());
70        }
71        node.record(inspect_array);
72    }
73}
74
75impl<T: Into<i64> + Clone> WindowedStats<T> {
76    pub fn log_inspect_int_array(&self, node: &InspectNode, property_name: &'static str) {
77        let iter = self.stats.iter();
78        let inspect_array = node.create_int_array(property_name, iter.len());
79        for (i, c) in iter.enumerate() {
80            inspect_array.set(i, (*c).clone());
81        }
82        node.record(inspect_array);
83    }
84}
85
86impl WindowedStats<SumAndCount> {
87    pub fn log_avg_inspect_double_array(&self, node: &InspectNode, property_name: &'static str) {
88        let iter = self.stats.iter();
89        let inspect_array = node.create_double_array(property_name, iter.len());
90        for (i, c) in iter.enumerate() {
91            let value = if c.avg().is_finite() { c.avg() } else { 0f64 };
92            inspect_array.set(i, value);
93        }
94        node.record(inspect_array);
95    }
96}
97
98/// Given `timestamp`, return the start bound of its enclosing window with the specified
99/// `granularity`.
100fn get_start_bound(
101    timestamp: fasync::BootInstant,
102    granularity: zx::BootDuration,
103) -> fasync::BootInstant {
104    timestamp - zx::BootDuration::from_nanos(timestamp.into_nanos() % granularity.into_nanos())
105}
106
107/// Given the `prev` and `current` timestamps, return how many windows need to be slided
108/// for if each window has the specified `granularity`.
109fn get_num_slides_needed(
110    prev: fasync::BootInstant,
111    current: fasync::BootInstant,
112    granularity: zx::BootDuration,
113) -> usize {
114    let prev_start_bound = get_start_bound(prev, granularity);
115    let current_start_bound = get_start_bound(current, granularity);
116    ((current_start_bound - prev_start_bound).into_nanos() / granularity.into_nanos())
117        .try_into()
118        .unwrap_or(0)
119}
120
121pub struct MinutelyWindows(pub usize);
122pub struct FifteenMinutelyWindows(pub usize);
123pub struct HourlyWindows(pub usize);
124
125pub struct TimeSeries<T> {
126    minutely: WindowedStats<T>,
127    fifteen_minutely: WindowedStats<T>,
128    hourly: WindowedStats<T>,
129    /// Time time when the `TimeSeries` was first created
130    created_timestamp: fasync::BootInstant,
131    /// The time when data in the current `TimeSeries` was last updated
132    last_timestamp: fasync::BootInstant,
133}
134
135impl<T: Debug> Debug for TimeSeries<T> {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("TimeSeries")
138            .field("minutely", &self.minutely)
139            .field("fifteen_minutely", &self.fifteen_minutely)
140            .field("hourly", &self.hourly)
141            .field("created_timestamp", &self.created_timestamp)
142            .field("last_timestamp", &self.last_timestamp)
143            .finish()
144    }
145}
146
147impl<T: Default> TimeSeries<T> {
148    pub fn new(create_aggregation_fn: impl Fn() -> Box<dyn Fn(&T, &T) -> T + Send>) -> Self {
149        Self::with_n_windows(
150            MinutelyWindows(60),
151            FifteenMinutelyWindows(24),
152            HourlyWindows(24),
153            create_aggregation_fn,
154        )
155    }
156
157    pub fn with_n_windows(
158        minutely_windows: MinutelyWindows,
159        fifteen_minutely_windows: FifteenMinutelyWindows,
160        hourly_windows: HourlyWindows,
161        create_aggregation_fn: impl Fn() -> Box<dyn Fn(&T, &T) -> T + Send>,
162    ) -> Self {
163        let now = fasync::BootInstant::now();
164        Self {
165            minutely: WindowedStats::new(minutely_windows.0, create_aggregation_fn()),
166            fifteen_minutely: WindowedStats::new(
167                fifteen_minutely_windows.0,
168                create_aggregation_fn(),
169            ),
170            hourly: WindowedStats::new(hourly_windows.0, create_aggregation_fn()),
171            created_timestamp: now,
172            last_timestamp: now,
173        }
174    }
175
176    /// Check whether the current time has exceeded the bound of the existing windows. If yes
177    /// then slide windows as many times as required until the window encompasses the current time.
178    pub fn update_windows(&mut self) {
179        let now = fasync::BootInstant::now();
180        for _i in
181            0..get_num_slides_needed(self.last_timestamp, now, zx::BootDuration::from_minutes(1))
182        {
183            self.minutely.slide_window();
184        }
185        for _i in
186            0..get_num_slides_needed(self.last_timestamp, now, zx::BootDuration::from_minutes(15))
187        {
188            self.fifteen_minutely.slide_window();
189        }
190        for _i in
191            0..get_num_slides_needed(self.last_timestamp, now, zx::BootDuration::from_hours(1))
192        {
193            self.hourly.slide_window();
194        }
195        self.last_timestamp = now;
196    }
197
198    /// Log the value into `TimeSeries`. This operation automatically updates the windows.
199    pub fn log_value(&mut self, item: &T) {
200        self.update_windows();
201        self.minutely.log_value(item);
202        self.fifteen_minutely.log_value(item);
203        self.hourly.log_value(item);
204    }
205
206    /// Get Iterator to traverse the minutely windows
207    pub fn minutely_iter<'a>(&'a self) -> impl ExactSizeIterator<Item = &'a T> {
208        self.minutely.stats.iter()
209    }
210
211    /// Get the aggregated value of the data that are still kept
212    pub fn get_aggregated_value(&mut self) -> T {
213        self.hourly.windowed_stat(None)
214    }
215
216    pub fn record_schema(&mut self, node: &InspectNode) {
217        let schema = node.create_child("schema");
218        schema.record_int("created_timestamp", self.created_timestamp.into_nanos());
219        schema.record_int("last_timestamp", self.last_timestamp.into_nanos());
220        node.record(schema);
221    }
222}
223
224impl<T: Into<u64> + Clone + Default> TimeSeries<T> {
225    pub fn log_inspect_uint_array(&mut self, node: &InspectNode, child_name: &'static str) {
226        self.update_windows();
227
228        let child = node.create_child(child_name);
229        self.record_schema(&child);
230        self.minutely.log_inspect_uint_array(&child, "1m");
231        self.fifteen_minutely.log_inspect_uint_array(&child, "15m");
232        self.hourly.log_inspect_uint_array(&child, "1h");
233        node.record(child);
234    }
235}
236
237impl<T: Into<i64> + Clone + Default> TimeSeries<T> {
238    pub fn log_inspect_int_array(&mut self, node: &InspectNode, child_name: &'static str) {
239        self.update_windows();
240
241        let child = node.create_child(child_name);
242        self.record_schema(&child);
243        self.minutely.log_inspect_int_array(&child, "1m");
244        self.fifteen_minutely.log_inspect_int_array(&child, "15m");
245        self.hourly.log_inspect_int_array(&child, "1h");
246        node.record(child);
247    }
248}
249
250impl TimeSeries<SumAndCount> {
251    pub fn log_avg_inspect_double_array(&mut self, node: &InspectNode, child_name: &'static str) {
252        self.update_windows();
253
254        let child = node.create_child(child_name);
255        self.record_schema(&child);
256        self.minutely.log_avg_inspect_double_array(&child, "1m");
257        self.fifteen_minutely.log_avg_inspect_double_array(&child, "15m");
258        self.hourly.log_avg_inspect_double_array(&child, "1h");
259        node.record(child);
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::aggregations::create_saturating_add_fn;
267    use diagnostics_assertions::assert_data_tree;
268    use fuchsia_inspect::Inspector;
269
270    #[test]
271    fn windowed_stats_some_windows_populated() {
272        let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
273        windowed_stats.log_value(&1u32);
274        windowed_stats.log_value(&2u32);
275        assert_eq!(windowed_stats.windowed_stat(None), 3u32);
276
277        windowed_stats.slide_window();
278        windowed_stats.log_value(&3u32);
279        assert_eq!(windowed_stats.windowed_stat(None), 6u32);
280    }
281
282    #[test]
283    fn windowed_stats_all_windows_populated() {
284        let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
285        windowed_stats.log_value(&1u32);
286        assert_eq!(windowed_stats.windowed_stat(None), 1u32);
287
288        windowed_stats.slide_window();
289        windowed_stats.log_value(&2u32);
290        assert_eq!(windowed_stats.windowed_stat(None), 3u32);
291
292        windowed_stats.slide_window();
293        windowed_stats.log_value(&3u32);
294        assert_eq!(windowed_stats.windowed_stat(None), 6u32);
295
296        windowed_stats.slide_window();
297        windowed_stats.log_value(&10u32);
298        // Value 1 from the first window is excluded
299        assert_eq!(windowed_stats.windowed_stat(None), 15u32);
300    }
301
302    #[test]
303    fn windowed_stats_large_number() {
304        let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
305        windowed_stats.log_value(&10u32);
306
307        windowed_stats.slide_window();
308        windowed_stats.log_value(&10u32);
309
310        windowed_stats.slide_window();
311        windowed_stats.log_value(&(u32::MAX - 20u32));
312        assert_eq!(windowed_stats.windowed_stat(None), u32::MAX);
313
314        windowed_stats.slide_window();
315        windowed_stats.log_value(&9u32);
316        assert_eq!(windowed_stats.windowed_stat(None), u32::MAX - 1);
317    }
318
319    #[test]
320    fn windowed_stats_test_overflow() {
321        let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
322        // Overflow in a single window
323        windowed_stats.log_value(&u32::MAX);
324        windowed_stats.log_value(&1u32);
325        assert_eq!(windowed_stats.windowed_stat(None), u32::MAX);
326
327        windowed_stats.slide_window();
328        windowed_stats.log_value(&10u32);
329        assert_eq!(windowed_stats.windowed_stat(None), u32::MAX);
330        windowed_stats.slide_window();
331        windowed_stats.log_value(&5u32);
332        assert_eq!(windowed_stats.windowed_stat(None), u32::MAX);
333        windowed_stats.slide_window();
334        windowed_stats.log_value(&3u32);
335        assert_eq!(windowed_stats.windowed_stat(None), 18u32);
336    }
337
338    #[test]
339    fn windowed_stats_n_arg() {
340        let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
341        windowed_stats.log_value(&1u32);
342        assert_eq!(windowed_stats.windowed_stat(Some(0)), 0u32);
343        assert_eq!(windowed_stats.windowed_stat(Some(1)), 1u32);
344        assert_eq!(windowed_stats.windowed_stat(Some(2)), 1u32);
345
346        windowed_stats.slide_window();
347        windowed_stats.log_value(&2u32);
348        assert_eq!(windowed_stats.windowed_stat(Some(1)), 2u32);
349        assert_eq!(windowed_stats.windowed_stat(Some(2)), 3u32);
350    }
351
352    #[fuchsia::test]
353    async fn windowed_stats_log_inspect_uint_array() {
354        let inspector = Inspector::default();
355        let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
356        windowed_stats.log_value(&1u32);
357        windowed_stats.slide_window();
358        windowed_stats.log_value(&2u32);
359
360        windowed_stats.log_inspect_uint_array(inspector.root(), "stats");
361
362        assert_data_tree!(inspector, root: {
363            stats: vec![1u64, 2],
364        });
365    }
366
367    #[fuchsia::test]
368    async fn windowed_stats_log_inspect_int_array() {
369        let inspector = Inspector::default();
370        let mut windowed_stats = WindowedStats::<i32>::new(3, create_saturating_add_fn());
371        windowed_stats.log_value(&1i32);
372        windowed_stats.slide_window();
373        windowed_stats.log_value(&2i32);
374
375        windowed_stats.log_inspect_int_array(inspector.root(), "stats");
376
377        assert_data_tree!(inspector, root: {
378            stats: vec![1i64, 2],
379        });
380    }
381
382    #[fuchsia::test]
383    async fn windowed_stats_sum_and_count_log_avg_inspect_double_array() {
384        let inspector = Inspector::default();
385        let mut windowed_stats = WindowedStats::<SumAndCount>::new(3, create_saturating_add_fn());
386        windowed_stats.log_value(&SumAndCount { sum: 1u32, count: 1 });
387        windowed_stats.log_value(&SumAndCount { sum: 2u32, count: 1 });
388
389        windowed_stats.log_avg_inspect_double_array(inspector.root(), "stats");
390
391        assert_data_tree!(inspector, root: {
392            stats: vec![3f64 / 2f64],
393        });
394    }
395
396    #[fuchsia::test]
397    async fn windowed_stats_sum_and_count_log_avg_inspect_double_array_with_nan() {
398        let inspector = Inspector::default();
399        let windowed_stats = WindowedStats::<SumAndCount>::new(3, create_saturating_add_fn());
400
401        windowed_stats.log_avg_inspect_double_array(inspector.root(), "stats");
402
403        assert_data_tree!(inspector, root: {
404            stats: vec![0f64],
405        });
406    }
407
408    #[test]
409    fn time_series_window_transition() {
410        let mut exec = fasync::TestExecutor::new_with_fake_time();
411        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3599_000_000_000));
412        let inspector = Inspector::default();
413
414        let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
415        time_series.log_value(&1u32);
416
417        // This should create a new windows for all of `1m`, `15m`, `1h` because we
418        // just crossed the 3600th second mark.
419        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3600_000_000_001));
420        time_series.log_value(&2u32);
421
422        time_series.log_inspect_uint_array(inspector.root(), "stats");
423        assert_data_tree!(@executor exec, inspector, root: {
424            stats: {
425                "1m": vec![1u64, 2],
426                "15m": vec![1u64, 2],
427                "1h": vec![1u64, 2],
428                schema: {
429                    "created_timestamp": 3599_000_000_000i64,
430                    "last_timestamp": 3600_000_000_001i64,
431                }
432            }
433        });
434
435        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3659_000_000_000));
436        time_series.log_value(&3u32);
437        time_series.log_inspect_uint_array(inspector.root(), "stats2");
438
439        // No new window transition because we have not crossed the 3660th second mark
440        assert_data_tree!(@executor exec, inspector, root: contains {
441            stats2: {
442                "1m": vec![1u64, 5],
443                "15m": vec![1u64, 5],
444                "1h": vec![1u64, 5],
445                schema: {
446                    "created_timestamp": 3599_000_000_000i64,
447                    "last_timestamp": 3659_000_000_000i64,
448                }
449            }
450        });
451
452        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3660_000_000_000));
453        time_series.log_value(&4u32);
454        time_series.log_inspect_uint_array(inspector.root(), "stats3");
455
456        // Only the `1m` window has a new transition because 3660th second marks a new
457        // minutely threshold, but not hourly or fifteen-minutely threshold.
458        assert_data_tree!(@executor exec, inspector, root: contains {
459            stats3: {
460                "1m": vec![1u64, 5, 4],
461                "15m": vec![1u64, 9],
462                "1h": vec![1u64, 9],
463                schema: {
464                    "created_timestamp": 3599_000_000_000i64,
465                    "last_timestamp": 3660_000_000_000i64,
466                }
467            }
468        });
469
470        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(4500_000_000_001));
471        time_series.log_value(&5u32);
472        time_series.log_inspect_uint_array(inspector.root(), "stats4");
473
474        // Now the `15m` window has a new transition, too, because 3900th second marks a new
475        // fifteen-minutely threshold (but not hourly threshold).
476        // Also, the `1m` window has multiple transitions.
477        assert_data_tree!(@executor exec, inspector, root: contains {
478            stats4: {
479                "1m": vec![1u64, 5, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5],
480                "15m": vec![1u64, 9, 5],
481                "1h": vec![1u64, 14],
482                schema: {
483                    "created_timestamp": 3599_000_000_000i64,
484                    "last_timestamp": 4500_000_000_001i64,
485                }
486            }
487        });
488    }
489
490    #[test]
491    fn time_series_minutely_iter() {
492        let exec = fasync::TestExecutor::new_with_fake_time();
493        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(59_000_000_000));
494        let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
495        time_series.log_value(&1u32);
496        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
497        time_series.log_value(&2u32);
498        let minutely_data: Vec<_> = time_series.minutely_iter().map(|v| *v).collect();
499        assert_eq!(minutely_data, [1, 2]);
500    }
501
502    #[test]
503    fn time_series_get_aggregated_value() {
504        let exec = fasync::TestExecutor::new_with_fake_time();
505        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3599_000_000_000));
506        let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
507        time_series.log_value(&1u32);
508        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3600_000_000_001));
509        time_series.log_value(&2u32);
510        assert_eq!(time_series.get_aggregated_value(), 3);
511    }
512
513    #[fuchsia::test]
514    fn time_series_log_inspect_uint_array() {
515        let mut exec = fasync::TestExecutor::new_with_fake_time();
516        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
517        let inspector = Inspector::default();
518
519        let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
520        time_series.log_value(&1u32);
521
522        time_series.log_inspect_uint_array(inspector.root(), "stats");
523
524        assert_data_tree!(@executor exec, inspector, root: {
525            stats: {
526                "1m": vec![1u64],
527                "15m": vec![1u64],
528                "1h": vec![1u64],
529                schema: {
530                    "created_timestamp": 961_000_000_000i64,
531                    "last_timestamp": 961_000_000_000i64,
532                }
533            }
534        });
535    }
536
537    #[test]
538    fn time_series_log_inspect_uint_array_automatically_update_windows() {
539        let mut exec = fasync::TestExecutor::new_with_fake_time();
540        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
541        let inspector = Inspector::default();
542
543        let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
544        time_series.log_value(&1u32);
545
546        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1021_000_000_000));
547        time_series.log_inspect_uint_array(inspector.root(), "stats");
548
549        assert_data_tree!(@executor exec, inspector, root: {
550            stats: {
551                "1m": vec![1u64, 0],
552                "15m": vec![1u64],
553                "1h": vec![1u64],
554                schema: {
555                    "created_timestamp": 961_000_000_000i64,
556                    "last_timestamp": 1021_000_000_000i64,
557                }
558            }
559        });
560    }
561
562    #[test]
563    fn time_series_stats_log_inspect_int_array() {
564        let mut exec = fasync::TestExecutor::new_with_fake_time();
565        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
566        let inspector = Inspector::default();
567
568        let mut time_series = TimeSeries::<i32>::new(create_saturating_add_fn);
569        time_series.log_value(&1i32);
570
571        time_series.log_inspect_int_array(inspector.root(), "stats");
572
573        assert_data_tree!(@executor exec, inspector, root: {
574            stats: {
575                "1m": vec![1i64],
576                "15m": vec![1i64],
577                "1h": vec![1i64],
578                schema: {
579                    "created_timestamp": 961_000_000_000i64,
580                    "last_timestamp": 961_000_000_000i64,
581                }
582            }
583        });
584    }
585
586    #[test]
587    fn time_series_log_inspect_int_array_automatically_update_windows() {
588        let mut exec = fasync::TestExecutor::new_with_fake_time();
589        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
590        let inspector = Inspector::default();
591
592        let mut time_series = TimeSeries::<i32>::new(create_saturating_add_fn);
593        time_series.log_value(&1i32);
594
595        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1021_000_000_000));
596        time_series.log_inspect_int_array(inspector.root(), "stats");
597
598        assert_data_tree!(@executor exec, inspector, root: {
599            stats: {
600                "1m": vec![1i64, 0],
601                "15m": vec![1i64],
602                "1h": vec![1i64],
603                schema: {
604                    "created_timestamp": 961_000_000_000i64,
605                    "last_timestamp": 1021_000_000_000i64,
606                }
607            }
608        });
609    }
610
611    #[test]
612    fn time_series_sum_and_count_log_avg_inspect_double_array() {
613        let mut exec = fasync::TestExecutor::new_with_fake_time();
614        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
615        let inspector = Inspector::default();
616        let mut time_series = TimeSeries::<SumAndCount>::new(create_saturating_add_fn);
617        time_series.log_value(&SumAndCount { sum: 1u32, count: 1 });
618        time_series.log_value(&SumAndCount { sum: 2u32, count: 1 });
619
620        time_series.log_avg_inspect_double_array(inspector.root(), "stats");
621
622        assert_data_tree!(@executor exec, inspector, root: {
623            stats: {
624                "1m": vec![3f64 / 2f64],
625                "15m": vec![3f64 / 2f64],
626                "1h": vec![3f64 / 2f64],
627                schema: {
628                    "created_timestamp": 961_000_000_000i64,
629                    "last_timestamp": 961_000_000_000i64,
630                }
631            }
632        });
633    }
634
635    #[test]
636    fn time_series_sum_and_count_log_avg_inspect_double_array_automatically_update_windows() {
637        let mut exec = fasync::TestExecutor::new_with_fake_time();
638        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
639        let inspector = Inspector::default();
640        let mut time_series = TimeSeries::<SumAndCount>::new(create_saturating_add_fn);
641        time_series.log_value(&SumAndCount { sum: 1u32, count: 1 });
642        time_series.log_value(&SumAndCount { sum: 2u32, count: 1 });
643
644        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1021_000_000_000));
645        time_series.log_avg_inspect_double_array(inspector.root(), "stats");
646
647        assert_data_tree!(@executor exec, inspector, root: {
648            stats: {
649                "1m": vec![3f64 / 2f64, 0f64],
650                "15m": vec![3f64 / 2f64],
651                "1h": vec![3f64 / 2f64],
652                schema: {
653                    "created_timestamp": 961_000_000_000i64,
654                    "last_timestamp": 1021_000_000_000i64,
655                }
656            }
657        });
658    }
659}