use std::{fs::File, path::PathBuf, time::Duration}; use bevy::prelude::*; use serde::Serialize; use super::{artefact_paths::ArtefactPaths, StartTime, TestQuitEvent}; #[derive(Debug, Default, Resource)] struct FrameCollector(Vec); const OUTLIERS: usize = 10; impl FrameCollector { fn write_to(&self, path: PathBuf) { let mut sum = Duration::default(); let mut best = vec![]; let mut worst = vec![]; // For some reason, first one has a delta time of 0.0 for frame in self.0.iter().skip(1) { sum += *frame; if best.len() < OUTLIERS { best.push(*frame); worst.push(*frame); continue; } let best_of_worst = worst.iter().min().unwrap().to_owned(); let worst_of_best = best.iter().max().unwrap().to_owned(); if *frame > best_of_worst { worst.retain(|f| f != &best_of_worst); worst.push(*frame); } else if *frame < worst_of_best { best.retain(|f| f != &worst_of_best); best.push(*frame); } } best.sort(); worst.sort(); let metrics = FrameMetrics { frames: to_millivec(self.0.clone()), average: 1000.0 * sum.as_secs_f32() / self.0.len() as f32, best: to_millivec(best), worst: to_millivec(worst), }; let mut file = File::create(path).unwrap(); serde_json::to_writer_pretty(&mut file, &metrics).unwrap(); } } fn to_millivec(durations: Vec) -> Vec { durations .into_iter() .map(|f| 1000.0 * f.as_secs_f32()) .collect() } #[derive(Debug, Default, Serialize)] struct FrameMetrics { average: f32, best: Vec, worst: Vec, frames: Vec, } #[derive(Debug)] pub struct FrameMetricPlugin; impl Plugin for FrameMetricPlugin { fn build(&self, app: &mut App) { app.add_systems(Update, record_frame_metrics) .add_systems( Update, write_frame_metrics.run_if(on_event::()), ) .init_resource::(); } } fn record_frame_metrics( mut frame_metrics: ResMut, time: Res>, first_update: Option>, ) { if first_update.is_some() { frame_metrics.0.push(time.delta()); } } fn write_frame_metrics( frame_metrics: Res, artefact_paths: Res, mut test_quit_events: EventReader, ) { if test_quit_events.read().next().is_some() { frame_metrics.write_to(artefact_paths.frame_metrics()); } }