use std::collections::VecDeque;
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct FPSMonitor {
timestamps: VecDeque<Instant>,
window_size: usize,
}
impl FPSMonitor {
pub fn new() -> Self {
Self::with_window_size(30)
}
pub fn with_window_size(window_size: usize) -> Self {
assert!(window_size > 0, "window_size must be at least 1");
Self {
timestamps: VecDeque::with_capacity(window_size),
window_size,
}
}
pub fn tick(&mut self) {
self.timestamps.push_back(Instant::now());
while self.timestamps.len() > self.window_size {
self.timestamps.pop_front();
}
}
pub fn fps(&self) -> f32 {
if self.timestamps.len() < 2 {
return 0.0;
}
let first = *self.timestamps.front().expect("checked len >= 2 above");
let last = *self.timestamps.back().expect("checked len >= 2 above");
let elapsed = last.duration_since(first).as_secs_f32();
if elapsed <= 0.0 {
return 0.0;
}
(self.timestamps.len() - 1) as f32 / elapsed
}
pub fn reset(&mut self) {
self.timestamps.clear();
}
}
impl Default for FPSMonitor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
use std::time::Duration;
#[test]
fn fps_is_zero_before_two_ticks() {
let mut monitor = FPSMonitor::new();
assert_eq!(monitor.fps(), 0.0);
monitor.tick();
assert_eq!(monitor.fps(), 0.0);
}
#[test]
fn fps_is_positive_and_finite_after_ticks_with_elapsed_time() {
let mut monitor = FPSMonitor::new();
for _ in 0..5 {
monitor.tick();
sleep(Duration::from_millis(1));
}
let fps = monitor.fps();
assert!(fps > 0.0);
assert!(fps.is_finite());
}
#[test]
fn reset_clears_the_window() {
let mut monitor = FPSMonitor::new();
monitor.tick();
monitor.tick();
monitor.reset();
assert_eq!(monitor.fps(), 0.0);
}
#[test]
#[should_panic]
fn zero_window_size_panics() {
FPSMonitor::with_window_size(0);
}
#[test]
fn window_caps_at_configured_size() {
let mut monitor = FPSMonitor::with_window_size(3);
for _ in 0..10 {
monitor.tick();
sleep(Duration::from_millis(1));
}
let fps = monitor.fps();
assert!(fps > 0.0 && fps.is_finite());
}
}