Skip to main content

cranpose_app_shell/
fps_monitor.rs

1//! FPS monitoring for performance tracking.
2//!
3//! Designed for reactive systems (non-busy-loop):
4//! - Tracks actual rendered frames, not idle time
5//! - Separately tracks recompositions
6//! - Provides stats meaningful for optimization
7
8use std::collections::VecDeque;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::RwLock;
11use web_time::Instant;
12
13/// Global FPS tracker singleton
14static FPS_TRACKER: RwLock<Option<FpsTracker>> = RwLock::new(None);
15
16/// Global recomposition counter (can be incremented from anywhere)
17static RECOMPOSITION_COUNT: AtomicU64 = AtomicU64::new(0);
18
19/// Number of frames to average for FPS calculation
20const FRAME_HISTORY_SIZE: usize = 60;
21
22/// Tracks frame times to calculate FPS.
23pub struct FpsTracker {
24    /// Timestamps of recent frames
25    frame_times: VecDeque<Instant>,
26    /// Cached FPS value
27    last_fps: f32,
28    /// Total frames rendered
29    frame_count: u64,
30    /// Rolling average of frame duration in ms
31    avg_frame_ms: f32,
32    /// Last recomposition count seen (for delta)
33    last_recomp_count: u64,
34    /// Recompositions in last second
35    recomps_per_second: u64,
36    /// Time of last recomp/sec calculation
37    last_recomp_calc: Instant,
38}
39
40impl FpsTracker {
41    fn new() -> Self {
42        Self {
43            frame_times: VecDeque::with_capacity(FRAME_HISTORY_SIZE + 1),
44            last_fps: 0.0,
45            frame_count: 0,
46            avg_frame_ms: 0.0,
47            last_recomp_count: 0,
48            recomps_per_second: 0,
49            last_recomp_calc: Instant::now(),
50        }
51    }
52
53    fn record_frame(&mut self) {
54        let now = Instant::now();
55
56        self.frame_times.push_back(now);
57        self.frame_count += 1;
58
59        // Keep only recent frames
60        while self.frame_times.len() > FRAME_HISTORY_SIZE {
61            self.frame_times.pop_front();
62        }
63
64        // Calculate FPS from frame history
65        if self.frame_times.len() >= 2 {
66            let first = self.frame_times.front().unwrap();
67            let last = self.frame_times.back().unwrap();
68            let duration = last.duration_since(*first).as_secs_f32();
69            if duration > 0.0 {
70                self.last_fps = (self.frame_times.len() - 1) as f32 / duration;
71                self.avg_frame_ms = duration * 1000.0 / (self.frame_times.len() - 1) as f32;
72            }
73        }
74
75        // Calculate recompositions per second (update every second)
76        let elapsed = now.duration_since(self.last_recomp_calc).as_secs_f32();
77        if elapsed >= 1.0 {
78            let current_recomp = RECOMPOSITION_COUNT.load(Ordering::Relaxed);
79            self.recomps_per_second = current_recomp - self.last_recomp_count;
80            self.last_recomp_count = current_recomp;
81            self.last_recomp_calc = now;
82        }
83    }
84
85    fn stats(&self) -> FpsStats {
86        FpsStats {
87            fps: self.last_fps,
88            avg_ms: self.avg_frame_ms,
89            frame_count: self.frame_count,
90            recompositions: RECOMPOSITION_COUNT.load(Ordering::Relaxed),
91            recomps_per_second: self.recomps_per_second,
92        }
93    }
94}
95
96/// Frame statistics snapshot.
97#[derive(Clone, Copy, Debug, Default)]
98pub struct FpsStats {
99    /// Current FPS (frames per second)
100    pub fps: f32,
101    /// Average frame time in milliseconds
102    pub avg_ms: f32,
103    /// Total frame count since start
104    pub frame_count: u64,
105    /// Total recomposition count since start
106    pub recompositions: u64,
107    /// Recompositions in the last second
108    pub recomps_per_second: u64,
109}
110
111/// Initialize the FPS tracker. Call once at app startup.
112pub fn init_fps_tracker() {
113    let mut tracker = FPS_TRACKER.write().unwrap();
114    *tracker = Some(FpsTracker::new());
115}
116
117/// Record a frame. Call once per frame in the render loop.
118pub fn record_frame() {
119    if let Ok(mut tracker) = FPS_TRACKER.write() {
120        if let Some(ref mut t) = *tracker {
121            t.record_frame();
122        }
123    }
124}
125
126/// Increment the recomposition counter. Call when a scope is recomposed.
127pub fn record_recomposition() {
128    RECOMPOSITION_COUNT.fetch_add(1, Ordering::Relaxed);
129}
130
131/// Get current FPS.
132pub fn current_fps() -> f32 {
133    if let Ok(tracker) = FPS_TRACKER.read() {
134        if let Some(ref t) = *tracker {
135            return t.last_fps;
136        }
137    }
138    0.0
139}
140
141/// Get detailed frame statistics.
142pub fn fps_stats() -> FpsStats {
143    if let Ok(tracker) = FPS_TRACKER.read() {
144        if let Some(ref t) = *tracker {
145            return t.stats();
146        }
147    }
148    FpsStats::default()
149}
150
151/// Format FPS as a display string.
152pub fn fps_display() -> String {
153    let stats = fps_stats();
154    format!("{:.0} FPS ({:.1}ms)", stats.fps, stats.avg_ms)
155}
156
157/// Format detailed stats as a display string.
158pub fn fps_display_detailed() -> String {
159    let stats = fps_stats();
160    format!(
161        "{:.0} FPS | {:.1}ms | recomp: {}/s",
162        stats.fps, stats.avg_ms, stats.recomps_per_second
163    )
164}