Skip to main content

warden_vision/utils/
fps.rs

1//! Rolling frames-per-second estimate, for reporting how fast a
2//! detect/track/annotate loop is actually running.
3
4use std::collections::VecDeque;
5use std::time::Instant;
6
7/// Tracks the timestamps of the last `window_size` calls to
8/// [`FPSMonitor::tick`] and estimates current throughput from them.
9#[derive(Debug, Clone)]
10pub struct FPSMonitor {
11    timestamps: VecDeque<Instant>,
12    window_size: usize,
13}
14
15impl FPSMonitor {
16    /// Creates a monitor with the default 30-frame window.
17    pub fn new() -> Self {
18        Self::with_window_size(30)
19    }
20
21    /// Creates a monitor averaging over the last `window_size` ticks.
22    ///
23    /// # Panics
24    /// Panics if `window_size` is `0`.
25    pub fn with_window_size(window_size: usize) -> Self {
26        assert!(window_size > 0, "window_size must be at least 1");
27        Self {
28            timestamps: VecDeque::with_capacity(window_size),
29            window_size,
30        }
31    }
32
33    /// Records that a frame was just processed.
34    pub fn tick(&mut self) {
35        self.timestamps.push_back(Instant::now());
36        while self.timestamps.len() > self.window_size {
37            self.timestamps.pop_front();
38        }
39    }
40
41    /// Estimated frames per second, averaged over the current window.
42    ///
43    /// Returns `0.0` until at least two ticks have been recorded (a single
44    /// timestamp has no elapsed interval to divide by).
45    pub fn fps(&self) -> f32 {
46        if self.timestamps.len() < 2 {
47            return 0.0;
48        }
49        let first = *self.timestamps.front().expect("checked len >= 2 above");
50        let last = *self.timestamps.back().expect("checked len >= 2 above");
51        let elapsed = last.duration_since(first).as_secs_f32();
52        if elapsed <= 0.0 {
53            return 0.0;
54        }
55        (self.timestamps.len() - 1) as f32 / elapsed
56    }
57
58    /// Discards all recorded timestamps, restarting the average.
59    pub fn reset(&mut self) {
60        self.timestamps.clear();
61    }
62}
63
64impl Default for FPSMonitor {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use std::thread::sleep;
74    use std::time::Duration;
75
76    #[test]
77    fn fps_is_zero_before_two_ticks() {
78        let mut monitor = FPSMonitor::new();
79        assert_eq!(monitor.fps(), 0.0);
80        monitor.tick();
81        assert_eq!(monitor.fps(), 0.0);
82    }
83
84    #[test]
85    fn fps_is_positive_and_finite_after_ticks_with_elapsed_time() {
86        let mut monitor = FPSMonitor::new();
87        for _ in 0..5 {
88            monitor.tick();
89            sleep(Duration::from_millis(1));
90        }
91        let fps = monitor.fps();
92        assert!(fps > 0.0);
93        assert!(fps.is_finite());
94    }
95
96    #[test]
97    fn reset_clears_the_window() {
98        let mut monitor = FPSMonitor::new();
99        monitor.tick();
100        monitor.tick();
101        monitor.reset();
102        assert_eq!(monitor.fps(), 0.0);
103    }
104
105    #[test]
106    #[should_panic]
107    fn zero_window_size_panics() {
108        FPSMonitor::with_window_size(0);
109    }
110
111    #[test]
112    fn window_caps_at_configured_size() {
113        let mut monitor = FPSMonitor::with_window_size(3);
114        for _ in 0..10 {
115            monitor.tick();
116            sleep(Duration::from_millis(1));
117        }
118        // Only the last 3 ticks (2 intervals) should count; fps should
119        // still be a small positive finite number, not based on all 10.
120        let fps = monitor.fps();
121        assert!(fps > 0.0 && fps.is_finite());
122    }
123}