use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EmaF64 {
value: f64,
initialized: bool,
}
impl Default for EmaF64 {
fn default() -> Self {
Self { value: 0.0, initialized: false }
}
}
impl EmaF64 {
pub fn new() -> Self {
Self::default()
}
pub fn seeded(value: f64) -> Self {
Self { value, initialized: true }
}
pub fn update(&mut self, sample: f64, alpha: f64) -> f64 {
self.value = if self.initialized { self.value * (1.0 - alpha) + sample * alpha } else { sample };
self.initialized = true;
self.value
}
pub fn value(&self) -> f64 {
self.value
}
pub fn is_initialized(&self) -> bool {
self.initialized
}
}
#[derive(Debug, Clone)]
pub struct FrameProfiler {
alpha: f64,
stages: HashMap<&'static str, EmaF64>,
frames: u64,
}
impl FrameProfiler {
pub const DEFAULT_ALPHA: f64 = 0.08;
pub fn new(alpha: f64) -> Self {
Self { alpha, stages: HashMap::new(), frames: 0 }
}
pub fn record_ms(&mut self, stage: &'static str, sample_ms: f64) {
self.stages.entry(stage).or_default().update(sample_ms, self.alpha);
}
pub fn stage_ms(&self, stage: &str) -> f64 {
self.stages.get(stage).map_or(0.0, EmaF64::value)
}
pub fn stages(&self) -> impl Iterator<Item = (&'static str, f64)> + '_ {
self.stages.iter().map(|(&name, ema)| (name, ema.value()))
}
pub fn frame_count(&self) -> u64 {
self.frames
}
pub fn end_frame(&mut self) {
self.frames = self.frames.wrapping_add(1);
}
pub fn to_json(&self) -> serde_json::Value {
let mut stages = serde_json::Map::new();
for (name, ms) in self.stages() {
stages.insert(name.to_owned(), serde_json::json!(ms));
}
serde_json::json!({ "frames": self.frames, "stages": stages })
}
}
impl Default for FrameProfiler {
fn default() -> Self {
Self::new(Self::DEFAULT_ALPHA)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ema_first_sample_wins_outright_when_uninitialized() {
let mut ema = EmaF64::new();
assert!(!ema.is_initialized());
assert_eq!(ema.update(42.0, 0.1), 42.0);
assert!(ema.is_initialized());
}
#[test]
fn ema_blends_subsequent_samples_by_alpha() {
let mut ema = EmaF64::new();
ema.update(100.0, 0.1);
let next = ema.update(0.0, 0.1);
assert!((next - 90.0).abs() < 1e-9, "expected 100*0.9 + 0*0.1 = 90, got {next}");
}
#[test]
fn ema_seeded_blends_the_seed_on_the_very_first_real_sample() {
let mut ema = EmaF64::seeded(60.0);
assert!(ema.is_initialized());
let next = ema.update(120.0, 0.1);
assert!((next - (60.0 * 0.9 + 120.0 * 0.1)).abs() < 1e-9);
}
#[test]
fn ema_converges_toward_a_sustained_constant_sample() {
let mut ema = EmaF64::new();
for _ in 0..500 {
ema.update(16.0, 0.2);
}
assert!((ema.value() - 16.0).abs() < 1e-6, "a sustained constant sample must converge, got {}", ema.value());
}
#[test]
fn frame_profiler_default_alpha_matches_the_source_apps_own_smoothing_constant() {
assert_eq!(FrameProfiler::DEFAULT_ALPHA, 0.08);
let profiler = FrameProfiler::default();
assert_eq!(profiler.frame_count(), 0);
}
#[test]
fn frame_profiler_record_ms_creates_and_smooths_a_named_stage() {
let mut profiler = FrameProfiler::new(0.08);
profiler.record_ms("grid", 10.0);
assert_eq!(profiler.stage_ms("grid"), 10.0, "first sample must win outright");
let smoothed = {
profiler.record_ms("grid", 20.0);
profiler.stage_ms("grid")
};
assert!((smoothed - (10.0 * 0.92 + 20.0 * 0.08)).abs() < 1e-9);
}
#[test]
fn frame_profiler_stage_ms_returns_zero_for_a_never_recorded_stage() {
let profiler = FrameProfiler::new(0.08);
assert_eq!(profiler.stage_ms("never_recorded"), 0.0);
}
#[test]
fn frame_profiler_end_frame_increments_the_frame_counter_independent_of_stage_recording() {
let mut profiler = FrameProfiler::new(0.08);
profiler.end_frame();
profiler.record_ms("grid", 1.0);
profiler.end_frame();
assert_eq!(profiler.frame_count(), 2);
}
#[test]
fn frame_profiler_stages_iterates_every_recorded_stage() {
let mut profiler = FrameProfiler::new(0.08);
profiler.record_ms("grid", 1.0);
profiler.record_ms("overlay", 2.0);
let mut names: Vec<&str> = profiler.stages().map(|(name, _)| name).collect();
names.sort_unstable();
assert_eq!(names, vec!["grid", "overlay"]);
}
#[test]
fn frame_profiler_to_json_reports_every_recorded_stage_and_the_frame_count() {
let mut profiler = FrameProfiler::new(0.08);
profiler.record_ms("grid", 5.0);
profiler.end_frame();
let json = profiler.to_json();
assert_eq!(json["frames"], 1);
assert_eq!(json["stages"]["grid"], 5.0);
}
}