From 514a4471628bcc05f49e58aa92c99cba1471089f Mon Sep 17 00:00:00 2001 From: Xevion Date: Fri, 15 Aug 2025 20:52:48 -0500 Subject: [PATCH] refactor: use strum::EnumCount for const compile time system mapping --- src/game/mod.rs | 29 +++++++++++---------- src/systems/debug.rs | 10 ++----- src/systems/formatting.rs | 9 +++++-- src/systems/ghost.rs | 2 +- src/systems/profiling.rs | 55 ++++++++++++++++++++++++++++++--------- tests/profiling.rs | 10 +++---- 6 files changed, 72 insertions(+), 43 deletions(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index 1fafca6..895d951 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -9,6 +9,7 @@ use crate::events::GameEvent; use crate::map::builder::Map; use crate::systems::blinking::Blinking; use crate::systems::movement::{Movable, MovementState, Position}; +use crate::systems::profiling::SystemId; use crate::systems::{ audio::{audio_system, AudioEvent, AudioResource}, blinking::blinking_system, @@ -20,7 +21,7 @@ use crate::systems::{ }, control::player_system, debug::{debug_render_system, DebugState, DebugTextureResource}, - ghost::ghost_ai_system, + ghost::ghost_system, input::input_system, item::item_system, movement::movement_system, @@ -212,20 +213,20 @@ impl Game { ); schedule.add_systems( ( - profile("input", input_system), - profile("player", player_system), - profile("ghost_ai", ghost_ai_system), - profile("movement", movement_system), - profile("collision", collision_system), - profile("item", item_system), - profile("audio", audio_system), - profile("blinking", blinking_system), - profile("directional_render", directional_render_system), - profile("dirty_render", dirty_render_system), - profile("render", render_system), - profile("debug_render", debug_render_system), + profile(SystemId::Input, input_system), + profile(SystemId::Player, player_system), + profile(SystemId::Ghost, ghost_system), + profile(SystemId::Movement, movement_system), + profile(SystemId::Collision, collision_system), + profile(SystemId::Item, item_system), + profile(SystemId::Audio, audio_system), + profile(SystemId::Blinking, blinking_system), + profile(SystemId::DirectionalRender, directional_render_system), + profile(SystemId::DirtyRender, dirty_render_system), + profile(SystemId::Render, render_system), + profile(SystemId::DebugRender, debug_render_system), profile( - "present", + SystemId::Present, |mut canvas: NonSendMut<&mut Canvas>, backbuffer: NonSendMut, debug_state: Res, diff --git a/src/systems/debug.rs b/src/systems/debug.rs index e4b5337..428add0 100644 --- a/src/systems/debug.rs +++ b/src/systems/debug.rs @@ -76,20 +76,14 @@ fn render_timing_display( let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap(); // Format timing information using the formatting module - let timing_text = timings.format_timing_display(); - - // Split text by newlines and render each line separately - let lines: Vec<&str> = timing_text.lines().collect(); - if lines.is_empty() { - return; - } + let lines = timings.format_timing_display(); let line_height = 14; // Approximate line height for 12pt font let padding = 10; // Calculate background dimensions let max_width = lines .iter() - .filter(|&&l| !l.is_empty()) // Don't consider empty lines for width + .filter(|l| !l.is_empty()) // Don't consider empty lines for width .map(|line| font.size_of(line).unwrap().0) .max() .unwrap_or(0); diff --git a/src/systems/formatting.rs b/src/systems/formatting.rs index bc88528..1b4e93e 100644 --- a/src/systems/formatting.rs +++ b/src/systems/formatting.rs @@ -1,6 +1,9 @@ use num_width::NumberWidth; use smallvec::SmallVec; use std::time::Duration; +use strum::EnumCount; + +use crate::systems::profiling::SystemId; // Helper to split a duration into a integer, decimal, and unit fn get_value(duration: &Duration) -> (u64, u32, &'static str) { @@ -34,7 +37,9 @@ fn get_value(duration: &Duration) -> (u64, u32, &'static str) { } /// Formats timing data into a vector of strings with proper alignment -pub fn format_timing_display(timing_data: impl IntoIterator) -> SmallVec<[String; 12]> { +pub fn format_timing_display( + timing_data: impl IntoIterator, +) -> SmallVec<[String; SystemId::COUNT]> { let mut iter = timing_data.into_iter().peekable(); if iter.peek().is_none() { return SmallVec::new(); @@ -98,5 +103,5 @@ pub fn format_timing_display(timing_data: impl IntoIterator>() + }).collect::>() } diff --git a/src/systems/ghost.rs b/src/systems/ghost.rs index 74139fc..65bd29f 100644 --- a/src/systems/ghost.rs +++ b/src/systems/ghost.rs @@ -15,7 +15,7 @@ use crate::{ /// /// This system runs on all ghosts and makes periodic decisions about /// which direction to move in when they reach intersections. -pub fn ghost_ai_system( +pub fn ghost_system( map: Res, delta_time: Res, mut ghosts: Query<(&mut GhostBehavior, &mut Movable, &Position, &EntityType, &GhostType)>, diff --git a/src/systems/profiling.rs b/src/systems/profiling.rs index 955fbc4..94227a4 100644 --- a/src/systems/profiling.rs +++ b/src/systems/profiling.rs @@ -3,14 +3,43 @@ use bevy_ecs::system::{IntoSystem, System}; use circular_buffer::CircularBuffer; use micromap::Map; use parking_lot::{Mutex, RwLock}; +use smallvec::SmallVec; +use std::fmt::Display; use std::time::Duration; +use strum::EnumCount; +use strum_macros::{EnumCount, IntoStaticStr}; use thousands::Separable; +use crate::systems::formatting; + /// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic. -const MAX_SYSTEMS: usize = 13; +const MAX_SYSTEMS: usize = SystemId::COUNT; /// The number of durations to keep in the circular buffer. const TIMING_WINDOW_SIZE: usize = 30; +#[derive(EnumCount, IntoStaticStr, Debug, PartialEq, Eq, Hash, Copy, Clone)] +pub enum SystemId { + Input, + Player, + Ghost, + Movement, + Audio, + Blinking, + DirectionalRender, + DirtyRender, + Render, + DebugRender, + Present, + Collision, + Item, +} + +impl Display for SystemId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Into::<&'static str>::into(self).to_ascii_lowercase()) + } +} + #[derive(Resource, Default, Debug)] pub struct SystemTimings { /// Map of system names to a queue of durations, using a circular buffer. @@ -20,18 +49,18 @@ pub struct SystemTimings { /// /// Also, we use a micromap::Map as the number of systems is generally quite small. /// Just make sure to set the capacity appropriately, or it will panic. - pub timings: RwLock>, MAX_SYSTEMS>>, + pub timings: RwLock>, MAX_SYSTEMS>>, } impl SystemTimings { - pub fn add_timing(&self, name: &'static str, duration: Duration) { + pub fn add_timing(&self, id: SystemId, duration: Duration) { // acquire a upgradable read lock let mut timings = self.timings.upgradable_read(); // happy path, the name is already in the map (no need to mutate the hashmap) - if timings.contains_key(name) { + if timings.contains_key(&id) { let queue = timings - .get(name) + .get(&id) .expect("System name not found in map after contains_key check"); let mut queue = queue.lock(); @@ -41,16 +70,16 @@ impl SystemTimings { // otherwise, acquire a write lock and insert a new queue timings.with_upgraded(|timings| { - let queue = timings.entry(name).or_insert_with(|| Mutex::new(CircularBuffer::new())); + let queue = timings.entry(id).or_insert_with(|| Mutex::new(CircularBuffer::new())); queue.lock().push_back(duration); }); } - pub fn get_stats(&self) -> Map<&'static str, (Duration, Duration), MAX_SYSTEMS> { + pub fn get_stats(&self) -> Map { let timings = self.timings.read(); let mut stats = Map::new(); - for (name, queue) in timings.iter() { + for (id, queue) in timings.iter() { if queue.lock().is_empty() { continue; } @@ -65,7 +94,7 @@ impl SystemTimings { let std_dev = variance.sqrt(); stats.insert( - *name, + *id, ( Duration::from_secs_f64(mean / 1000.0), Duration::from_secs_f64(std_dev / 1000.0), @@ -101,7 +130,7 @@ impl SystemTimings { ) } - pub fn format_timing_display(&self) -> String { + pub fn format_timing_display(&self) -> SmallVec<[String; SystemId::COUNT]> { let stats = self.get_stats(); let (total_avg, total_std) = self.get_total_stats(); @@ -126,11 +155,11 @@ impl SystemTimings { } // Use the formatting module to format the data - crate::systems::formatting::format_timing_display(timing_data).join("\n") + formatting::format_timing_display(timing_data) } } -pub fn profile(name: &'static str, system: S) -> impl FnMut(&mut bevy_ecs::world::World) +pub fn profile(id: SystemId, system: S) -> impl FnMut(&mut bevy_ecs::world::World) where S: IntoSystem<(), (), M> + 'static, { @@ -147,7 +176,7 @@ where let duration = start.elapsed(); if let Some(timings) = world.get_resource::() { - timings.add_timing(name, duration); + timings.add_timing(id, duration); } } } diff --git a/tests/profiling.rs b/tests/profiling.rs index 9b8d727..d453cc5 100644 --- a/tests/profiling.rs +++ b/tests/profiling.rs @@ -1,4 +1,4 @@ -use pacman::systems::profiling::SystemTimings; +use pacman::systems::profiling::{SystemId, SystemTimings}; use std::time::Duration; #[test] @@ -6,12 +6,12 @@ fn test_timing_statistics() { let timings = SystemTimings::default(); // Add some test data - timings.add_timing("test_system", Duration::from_millis(10)); - timings.add_timing("test_system", Duration::from_millis(12)); - timings.add_timing("test_system", Duration::from_millis(8)); + timings.add_timing(SystemId::PlayerControls, Duration::from_millis(10)); + timings.add_timing(SystemId::PlayerControls, Duration::from_millis(12)); + timings.add_timing(SystemId::PlayerControls, Duration::from_millis(8)); let stats = timings.get_stats(); - let (avg, std_dev) = stats.get("test_system").unwrap(); + let (avg, std_dev) = stats.get(&SystemId::PlayerControls).unwrap(); // Average should be 10ms, standard deviation should be small assert!((avg.as_millis() as f64 - 10.0).abs() < 1.0);