diff options
| author | grothedev <grothedev@gmail.com> | 2025-10-02 10:25:04 -0400 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2025-10-02 10:25:04 -0400 |
| commit | 836459dce3f50767d41978be4a2f7ac788e6a9ba (patch) | |
| tree | 9fd17f4f7e0bb808f8467a14932355e3e72875ef /src/metrics.rs | |
| parent | a162c98ce54159e3e7dbe867d908ce3276b7f633 (diff) | |
added metrics for rust impl. having trouble with c++ atm
Diffstat (limited to 'src/metrics.rs')
| -rw-r--r-- | src/metrics.rs | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/src/metrics.rs b/src/metrics.rs new file mode 100644 index 0000000..369aff8 --- /dev/null +++ b/src/metrics.rs @@ -0,0 +1,290 @@ +use std::time::{Duration, Instant}; +use std::collections::VecDeque; + +/// Rolling average calculator for smooth metric display +pub struct RollingAverage { + values: VecDeque<f64>, + capacity: usize, + sum: f64, +} + +impl RollingAverage { + pub fn new(capacity: usize) -> Self { + Self { + values: VecDeque::with_capacity(capacity), + capacity, + sum: 0.0, + } + } + + pub fn push(&mut self, value: f64) { + if self.values.len() >= self.capacity { + if let Some(old) = self.values.pop_front() { + self.sum -= old; + } + } + self.values.push_back(value); + self.sum += value; + } + + pub fn average(&self) -> f64 { + if self.values.is_empty() { + 0.0 + } else { + self.sum / self.values.len() as f64 + } + } + + pub fn min(&self) -> f64 { + self.values.iter().copied().fold(f64::INFINITY, f64::min) + } + + pub fn max(&self) -> f64 { + self.values.iter().copied().fold(f64::NEG_INFINITY, f64::max) + } + + pub fn latest(&self) -> f64 { + self.values.back().copied().unwrap_or(0.0) + } +} + +/// Frame timing breakdown +#[derive(Debug, Clone)] +pub struct FrameTiming { + pub total_ms: f64, + pub update_ms: f64, + pub render_ms: f64, + pub vertex_count: usize, + pub line_count: usize, +} + +/// Performance metrics collector with rolling averages +pub struct PerformanceMetrics { + // Rolling averages (default: 60 frames) + frame_time: RollingAverage, + update_time: RollingAverage, + render_time: RollingAverage, + vertex_count: RollingAverage, + line_count: RollingAverage, + + // Session-wide statistics + pub total_frames: u64, + session_start: Instant, + + // Current frame timing + frame_start: Option<Instant>, + update_start: Option<Instant>, + render_start: Option<Instant>, + + // Historical data for export + history: VecDeque<FrameTiming>, + history_capacity: usize, +} + +impl PerformanceMetrics { + pub fn new(rolling_window: usize, history_capacity: usize) -> Self { + Self { + frame_time: RollingAverage::new(rolling_window), + update_time: RollingAverage::new(rolling_window), + render_time: RollingAverage::new(rolling_window), + vertex_count: RollingAverage::new(rolling_window), + line_count: RollingAverage::new(rolling_window), + total_frames: 0, + session_start: Instant::now(), + frame_start: None, + update_start: None, + render_start: None, + history: VecDeque::with_capacity(history_capacity), + history_capacity, + } + } + + // Frame timing markers + pub fn begin_frame(&mut self) { + self.frame_start = Some(Instant::now()); + } + + pub fn begin_update(&mut self) { + self.update_start = Some(Instant::now()); + } + + pub fn end_update(&mut self) -> f64 { + if let Some(start) = self.update_start.take() { + let duration = start.elapsed(); + duration.as_secs_f64() * 1000.0 + } else { + 0.0 + } + } + + pub fn begin_render(&mut self) { + self.render_start = Some(Instant::now()); + } + + pub fn end_render(&mut self) -> f64 { + if let Some(start) = self.render_start.take() { + let duration = start.elapsed(); + duration.as_secs_f64() * 1000.0 + } else { + 0.0 + } + } + + pub fn end_frame(&mut self, update_ms: f64, render_ms: f64, vertex_count: usize, line_count: usize) { + if let Some(start) = self.frame_start.take() { + let total_ms = start.elapsed().as_secs_f64() * 1000.0; + + // Update rolling averages + self.frame_time.push(total_ms); + self.update_time.push(update_ms); + self.render_time.push(render_ms); + self.vertex_count.push(vertex_count as f64); + self.line_count.push(line_count as f64); + + // Record to history + let timing = FrameTiming { + total_ms, + update_ms, + render_ms, + vertex_count, + line_count, + }; + + if self.history.len() >= self.history_capacity { + self.history.pop_front(); + } + self.history.push_back(timing); + + self.total_frames += 1; + } + } + + // Getters for current metrics + pub fn fps(&self) -> f64 { + let avg_frame_time = self.frame_time.average(); + if avg_frame_time > 0.0 { + 1000.0 / avg_frame_time + } else { + 0.0 + } + } + + pub fn avg_frame_time_ms(&self) -> f64 { + self.frame_time.average() + } + + pub fn avg_update_time_ms(&self) -> f64 { + self.update_time.average() + } + + pub fn avg_render_time_ms(&self) -> f64 { + self.render_time.average() + } + + pub fn avg_vertex_count(&self) -> f64 { + self.vertex_count.average() + } + + pub fn avg_line_count(&self) -> f64 { + self.line_count.average() + } + + pub fn min_fps(&self) -> f64 { + let max_frame_time = self.frame_time.max(); + if max_frame_time > 0.0 && max_frame_time.is_finite() { + 1000.0 / max_frame_time + } else { + 0.0 + } + } + + pub fn max_fps(&self) -> f64 { + let min_frame_time = self.frame_time.min(); + if min_frame_time > 0.0 && min_frame_time.is_finite() { + 1000.0 / min_frame_time + } else { + 0.0 + } + } + + pub fn session_duration(&self) -> Duration { + self.session_start.elapsed() + } + + // Export functionality + pub fn export_to_csv(&self, path: &str) -> std::io::Result<()> { + use std::io::Write; + let mut file = std::fs::File::create(path)?; + + // Write header + writeln!(file, "frame,total_ms,update_ms,render_ms,vertex_count,line_count,fps")?; + + // Write data + for (i, timing) in self.history.iter().enumerate() { + let fps = if timing.total_ms > 0.0 { + 1000.0 / timing.total_ms + } else { + 0.0 + }; + writeln!( + file, + "{},{},{},{},{},{},{}", + i, + timing.total_ms, + timing.update_ms, + timing.render_ms, + timing.vertex_count, + timing.line_count, + fps + )?; + } + + Ok(()) + } + + pub fn format_summary(&self) -> String { + format!( + "FPS: {:.1} (min: {:.1}, max: {:.1}) | Frame: {:.2}ms | Update: {:.2}ms | Render: {:.2}ms | Vertices: {:.0} | Lines: {:.0}", + self.fps(), + self.min_fps(), + self.max_fps(), + self.avg_frame_time_ms(), + self.avg_update_time_ms(), + self.avg_render_time_ms(), + self.avg_vertex_count(), + self.avg_line_count() + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rolling_average() { + let mut avg = RollingAverage::new(3); + avg.push(10.0); + avg.push(20.0); + avg.push(30.0); + assert_eq!(avg.average(), 20.0); + + avg.push(40.0); + assert_eq!(avg.average(), 30.0); // (20 + 30 + 40) / 3 + } + + #[test] + fn test_metrics_lifecycle() { + let mut metrics = PerformanceMetrics::new(60, 1000); + + metrics.begin_frame(); + metrics.begin_update(); + let update_ms = metrics.end_update(); + metrics.begin_render(); + let render_ms = metrics.end_render(); + metrics.end_frame(update_ms, render_ms, 1000, 10); + + assert_eq!(metrics.total_frames, 1); + assert!(metrics.fps() > 0.0); + } +} |
