    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67

    use std::time::Instant;

    /// A metrics counter storing only latest `N` records.
    pub struct MetricsCounter<const N: usize> {
        /// Slots to store metrics.
        slots: [(usize, Instant); N],
        /// The slot of the oldest record.
        /// Also the next slot to store the new record.
        index: usize,
    }

    impl<const N: usize> MetricsCounter<N> {
        /// Creates a new counter with an initial value.
        pub fn new(init: usize, init_at: Instant) -> Self {
            assert!(N > 0, "number of slots must be greater than zero");
            Self {
                slots: [(init, init_at); N],
                index: 0,
            }
        }

        /// Adds record to the counter.
        pub fn add(&mut self, data: usize, added_at: Instant) {
            self.slots[self.index] = (data, added_at);
            self.index = (self.index + 1) % N;
        }

        /// Calculates per-second average rate of all slots.
        pub fn rate(&self) -> f32 {
            let latest = self.slots[self.index.checked_sub(1).unwrap_or(N - 1)];
            let oldest = self.slots[self.index];
            let duration = (latest.1 - oldest.1).as_secs_f32();
            let avg = (latest.0 - oldest.0) as f32 / duration;
            if f32::is_nan(avg) {
                0f32
            } else {
                avg
            }
        }
    }

    #[cfg(test)]
    mod tests {
        use super::MetricsCounter;
        use std::time::{Duration, Instant};

        #[test]
        fn counter() {
            let now = Instant::now();
            let mut counter = MetricsCounter::<3>::new(0, now);
            assert_eq!(counter.rate(), 0f32);
            counter.add(1, now + Duration::from_secs(1));
            assert_eq!(counter.rate(), 1f32);
            counter.add(4, now + Duration::from_secs(2));
            assert_eq!(counter.rate(), 2f32);
            counter.add(7, now + Duration::from_secs(3));
            assert_eq!(counter.rate(), 3f32);
            counter.add(12, now + Duration::from_secs(4));
            assert_eq!(counter.rate(), 4f32);
        }

        #[test]
        #[should_panic(expected = "number of slots must be greater than zero")]
        fn counter_zero_slot() {
            let _counter = MetricsCounter::<0>::new(0, Instant::now());
        }
    }