hashiverse_lib/tools/
decaying_counter.rs1use crate::tools::time::{DurationMillis, TimeMillis};
19
20#[derive(Debug, Clone)]
23pub struct DecayingCounter {
24 value: f64,
25 last_update_millis: TimeMillis,
26 time_constant_millis: f64,
27}
28
29impl DecayingCounter {
30 pub fn new(time_constant: DurationMillis) -> Self {
32 Self {
33 value: 0.0,
34 last_update_millis: TimeMillis::zero(),
38 time_constant_millis: time_constant.0 as f64,
39 }
40 }
41
42 pub fn record(&mut self, now: TimeMillis, count: u64) {
44 self.value = self.estimate(now) + count as f64;
45 self.last_update_millis = now;
46 }
47
48 pub fn estimate(&self, now: TimeMillis) -> f64 {
52 let elapsed_millis = (now.0 - self.last_update_millis.0).max(0) as f64;
55 self.value * (-elapsed_millis / self.time_constant_millis).exp()
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::tools::time::{MILLIS_IN_HOUR, MILLIS_IN_MINUTE};
63
64 #[test]
65 fn zero_before_any_record() {
66 let counter = DecayingCounter::new(MILLIS_IN_HOUR);
67 assert_eq!(counter.estimate(TimeMillis::zero()), 0.0);
68 assert_eq!(counter.estimate(TimeMillis(MILLIS_IN_HOUR.0)), 0.0);
69 }
70
71 #[test]
72 fn single_record_reads_count_immediately_then_decays() {
73 let mut counter = DecayingCounter::new(MILLIS_IN_HOUR);
74 let t0 = TimeMillis(MILLIS_IN_HOUR.0); counter.record(t0, 100);
76
77 assert!((counter.estimate(t0) - 100.0).abs() < 1e-9, "got {}", counter.estimate(t0));
79
80 let after_one_tau = TimeMillis(t0.0 + MILLIS_IN_HOUR.0);
82 let expected = 100.0 / std::f64::consts::E;
83 assert!((counter.estimate(after_one_tau) - expected).abs() < 1e-6, "got {}", counter.estimate(after_one_tau));
84
85 let much_later = TimeMillis(t0.0 + MILLIS_IN_HOUR.0 * 100);
87 assert!(counter.estimate(much_later) < 1e-6, "got {}", counter.estimate(much_later));
88 }
89
90 #[test]
91 fn steady_cadence_converges_near_rate_times_tau() {
92 let mut counter = DecayingCounter::new(MILLIS_IN_HOUR);
94 let mut now = TimeMillis::zero();
95 for _ in 0..10_000 {
96 now = TimeMillis(now.0 + MILLIS_IN_MINUTE.0);
97 counter.record(now, 1);
98 }
99 let estimate = counter.estimate(now);
100 assert!((estimate - 60.0).abs() < 1.0, "expected ~60, got {}", estimate);
101 }
102
103 #[test]
104 fn estimate_is_non_mutating() {
105 let mut counter = DecayingCounter::new(MILLIS_IN_HOUR);
106 counter.record(TimeMillis(MILLIS_IN_HOUR.0), 50);
107 let later = TimeMillis(MILLIS_IN_HOUR.0 * 3);
108 let first = counter.estimate(later);
109 let second = counter.estimate(later);
110 assert_eq!(first, second);
111 }
112
113 #[test]
114 fn backwards_now_does_not_inflate() {
115 let mut counter = DecayingCounter::new(MILLIS_IN_HOUR);
116 let t0 = TimeMillis(MILLIS_IN_HOUR.0 * 5);
117 counter.record(t0, 100);
118 let earlier = TimeMillis(t0.0 - MILLIS_IN_HOUR.0);
120 assert!(counter.estimate(earlier) <= 100.0 + 1e-9, "got {}", counter.estimate(earlier));
121 assert!((counter.estimate(earlier) - 100.0).abs() < 1e-9, "got {}", counter.estimate(earlier));
122 }
123}