Skip to main content

nodedb_query/ts_functions/
rate.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Counter-aware per-second rate of increase.
4//!
5//! Mirrors Prometheus `rate()` semantics: detects monotonic counter resets
6//! and extrapolates the increase across the reset boundary.
7
8/// Compute the per-second rate of increase between consecutive samples.
9///
10/// Counter-reset aware: if `values[i] < values[i-1]`, assumes the counter
11/// wrapped and treats `values[i]` as the total increase since the reset.
12///
13/// Returns `None` for the first sample (no predecessor) and for
14/// zero-duration intervals (duplicate timestamps).
15///
16/// # Arguments
17/// * `values` — monotonic counter values (pre-sorted by time)
18/// * `timestamps_ns` — epoch nanoseconds, same length as `values`
19pub fn ts_rate(values: &[f64], timestamps_ns: &[i64]) -> Vec<Option<f64>> {
20    debug_assert_eq!(values.len(), timestamps_ns.len());
21    let n = values.len();
22    if n == 0 {
23        return vec![];
24    }
25
26    let mut result = Vec::with_capacity(n);
27    result.push(None);
28
29    for i in 1..n {
30        let dt_ns = timestamps_ns[i] - timestamps_ns[i - 1];
31        if dt_ns <= 0 {
32            result.push(None);
33            continue;
34        }
35        let dt_secs = dt_ns as f64 / 1_000_000_000.0;
36
37        // Counter reset detection: value decreased → counter wrapped.
38        let dv = if values[i] >= values[i - 1] {
39            values[i] - values[i - 1]
40        } else {
41            // After reset the counter starts from zero, so the current
42            // value *is* the total increase since reset.
43            values[i]
44        };
45
46        result.push(Some(dv / dt_secs));
47    }
48    result
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn monotonic_increase() {
57        let vals = [0.0, 10.0, 30.0, 60.0];
58        let ts = [0, 1_000_000_000, 2_000_000_000, 3_000_000_000]; // 1s intervals
59        let r = ts_rate(&vals, &ts);
60        assert_eq!(r.len(), 4);
61        assert!(r[0].is_none());
62        assert!((r[1].unwrap() - 10.0).abs() < 1e-9);
63        assert!((r[2].unwrap() - 20.0).abs() < 1e-9);
64        assert!((r[3].unwrap() - 30.0).abs() < 1e-9);
65    }
66
67    #[test]
68    fn counter_reset() {
69        // Counter goes 100 → 5 (reset) → 15
70        let vals = [100.0, 5.0, 15.0];
71        let ts = [0, 1_000_000_000, 2_000_000_000];
72        let r = ts_rate(&vals, &ts);
73        assert!((r[1].unwrap() - 5.0).abs() < 1e-9); // 5 increase since reset
74        assert!((r[2].unwrap() - 10.0).abs() < 1e-9); // normal diff
75    }
76
77    #[test]
78    fn duplicate_timestamp() {
79        let vals = [0.0, 10.0];
80        let ts = [1_000_000_000, 1_000_000_000];
81        let r = ts_rate(&vals, &ts);
82        assert!(r[1].is_none());
83    }
84
85    #[test]
86    fn empty() {
87        assert!(ts_rate(&[], &[]).is_empty());
88    }
89
90    #[test]
91    fn sub_second_interval() {
92        let vals = [0.0, 5.0];
93        let ts = [0, 500_000_000]; // 0.5s
94        let r = ts_rate(&vals, &ts);
95        assert!((r[1].unwrap() - 10.0).abs() < 1e-9); // 5 / 0.5 = 10/s
96    }
97}