hyperi_rustlib/scaling/
rate_window.rs1use std::time::{Duration, Instant};
18
19use parking_lot::RwLock;
20
21pub struct RateWindow {
43 inner: RwLock<WindowInner>,
44}
45
46struct WindowInner {
47 samples: Vec<(Instant, u64)>,
48 window_size: Duration,
49}
50
51impl RateWindow {
52 #[must_use]
56 pub fn new(window_size: Duration) -> Self {
57 Self {
58 inner: RwLock::new(WindowInner {
59 samples: Vec::with_capacity(64),
60 window_size,
61 }),
62 }
63 }
64
65 #[must_use]
67 pub fn default_window() -> Self {
68 Self::new(Duration::from_mins(1))
69 }
70
71 pub fn record(&self, counter_value: u64) {
75 let now = Instant::now();
76 let mut inner = self.inner.write();
77 let cutoff = now.checked_sub(inner.window_size).unwrap_or(now);
78 inner.samples.retain(|&(t, _)| t >= cutoff);
79 inner.samples.push((now, counter_value));
80 }
81
82 #[cfg(test)]
84 fn record_at(&self, at: Instant, counter_value: u64) {
85 let mut inner = self.inner.write();
86 let cutoff = at.checked_sub(inner.window_size).unwrap_or(at);
87 inner.samples.retain(|&(t, _)| t >= cutoff);
88 inner.samples.push((at, counter_value));
89 }
90
91 #[must_use]
95 pub fn rate_per_second(&self) -> f64 {
96 let inner = self.inner.read();
97 if inner.samples.len() < 2 {
98 return 0.0;
99 }
100
101 let first = inner.samples.first().unwrap();
102 let last = inner.samples.last().unwrap();
103
104 let duration = last.0.duration_since(first.0).as_secs_f64();
105 if duration <= 0.0 {
106 return 0.0;
107 }
108
109 let delta = last.1.saturating_sub(first.1) as f64;
110 delta / duration
111 }
112
113 #[must_use]
115 pub fn sample_count(&self) -> usize {
116 self.inner.read().samples.len()
117 }
118
119 pub fn clear(&self) {
121 self.inner.write().samples.clear();
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_empty_window() {
131 let w = RateWindow::default_window();
132 assert!((w.rate_per_second()).abs() < f64::EPSILON);
133 assert_eq!(w.sample_count(), 0);
134 }
135
136 #[test]
137 fn test_single_sample() {
138 let w = RateWindow::default_window();
139 w.record(100);
140 assert!((w.rate_per_second()).abs() < f64::EPSILON);
141 assert_eq!(w.sample_count(), 1);
142 }
143
144 #[test]
145 fn test_two_samples_rate() {
146 let w = RateWindow::new(Duration::from_mins(1));
147 let now = Instant::now();
148 w.record_at(now, 0);
149 w.record_at(now + Duration::from_secs(10), 1000);
150
151 let rate = w.rate_per_second();
152 assert!((rate - 100.0).abs() < 0.01, "Expected ~100.0, got {rate}");
154 }
155
156 #[test]
157 fn test_multiple_samples() {
158 let w = RateWindow::new(Duration::from_mins(1));
159 let now = Instant::now();
160 w.record_at(now, 0);
161 w.record_at(now + Duration::from_secs(5), 500);
162 w.record_at(now + Duration::from_secs(10), 1000);
163
164 let rate = w.rate_per_second();
165 assert!((rate - 100.0).abs() < 0.01, "Expected ~100.0, got {rate}");
167 }
168
169 #[test]
170 fn test_window_pruning() {
171 let w = RateWindow::new(Duration::from_secs(5));
172 let now = Instant::now();
173
174 w.record_at(now.checked_sub(Duration::from_secs(10)).unwrap(), 0);
176 w.record_at(now.checked_sub(Duration::from_secs(2)).unwrap(), 800);
178 w.record_at(now, 1000);
179
180 assert_eq!(w.sample_count(), 2);
182
183 let rate = w.rate_per_second();
184 assert!((rate - 100.0).abs() < 0.01, "Expected ~100.0, got {rate}");
186 }
187
188 #[test]
189 fn test_clear() {
190 let w = RateWindow::default_window();
191 w.record(100);
192 w.record(200);
193 assert_eq!(w.sample_count(), 2);
194
195 w.clear();
196 assert_eq!(w.sample_count(), 0);
197 assert!((w.rate_per_second()).abs() < f64::EPSILON);
198 }
199
200 #[test]
201 fn test_zero_duration() {
202 let w = RateWindow::new(Duration::from_mins(1));
203 let now = Instant::now();
204 w.record_at(now, 0);
206 w.record_at(now, 1000);
207 assert!((w.rate_per_second()).abs() < f64::EPSILON);
209 }
210
211 #[test]
212 fn test_counter_wraparound() {
213 let w = RateWindow::new(Duration::from_mins(1));
214 let now = Instant::now();
215 w.record_at(now, 1000);
217 w.record_at(now + Duration::from_secs(10), 500);
218 assert!((w.rate_per_second()).abs() < f64::EPSILON);
220 }
221}