summaryrefslogtreecommitdiff
path: root/src/metrics.rs
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2025-10-02 10:25:04 -0400
committergrothedev <grothedev@gmail.com>2025-10-02 10:25:04 -0400
commit836459dce3f50767d41978be4a2f7ac788e6a9ba (patch)
tree9fd17f4f7e0bb808f8467a14932355e3e72875ef /src/metrics.rs
parenta162c98ce54159e3e7dbe867d908ce3276b7f633 (diff)
added metrics for rust impl. having trouble with c++ atm
Diffstat (limited to 'src/metrics.rs')
-rw-r--r--src/metrics.rs290
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);
+ }
+}