• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Xevion / Pac-Man / 17012447928

16 Aug 2025 08:12PM UTC coverage: 38.85% (-12.3%) from 51.196%
17012447928

Pull #3

github

Xevion
chore: add cargo checks to pre-commit
Pull Request #3: ECS Refactor

161 of 1172 new or added lines in 23 files covered. (13.74%)

9 existing lines in 4 files now uncovered.

777 of 2000 relevant lines covered (38.85%)

101.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.71
/src/systems/profiling.rs
1
use bevy_ecs::prelude::Resource;
2
use bevy_ecs::system::{IntoSystem, System};
3
use circular_buffer::CircularBuffer;
4
use micromap::Map;
5
use parking_lot::{Mutex, RwLock};
6
use smallvec::SmallVec;
7
use std::fmt::Display;
8
use std::time::Duration;
9
use strum::EnumCount;
10
use strum_macros::{EnumCount, IntoStaticStr};
11
use thousands::Separable;
12

13
use crate::systems::formatting;
14

15
/// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic.
16
const MAX_SYSTEMS: usize = SystemId::COUNT;
17
/// The number of durations to keep in the circular buffer.
18
const TIMING_WINDOW_SIZE: usize = 30;
19

20
#[derive(EnumCount, IntoStaticStr, Debug, PartialEq, Eq, Hash, Copy, Clone)]
21
pub enum SystemId {
22
    Input,
23
    PlayerControls,
24
    Ghost,
25
    Movement,
26
    Audio,
27
    Blinking,
28
    DirectionalRender,
29
    DirtyRender,
30
    Render,
31
    DebugRender,
32
    Present,
33
    Collision,
34
    Item,
35
    PlayerMovement,
36
}
37

38
impl Display for SystemId {
NEW
39
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
NEW
40
        write!(f, "{}", Into::<&'static str>::into(self).to_ascii_lowercase())
×
NEW
41
    }
×
42
}
43

44
#[derive(Resource, Default, Debug)]
45
pub struct SystemTimings {
46
    /// Map of system names to a queue of durations, using a circular buffer.
47
    ///
48
    /// Uses a RwLock to allow multiple readers for the HashMap, and a Mutex on the circular buffer for exclusive access.
49
    /// This is probably overkill, but it's fun to play with.
50
    ///
51
    /// Also, we use a micromap::Map as the number of systems is generally quite small.
52
    /// Just make sure to set the capacity appropriately, or it will panic.
53
    pub timings: RwLock<Map<SystemId, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, MAX_SYSTEMS>>,
54
}
55

56
impl SystemTimings {
57
    pub fn add_timing(&self, id: SystemId, duration: Duration) {
3✔
58
        // acquire a upgradable read lock
3✔
59
        let mut timings = self.timings.upgradable_read();
3✔
60

3✔
61
        // happy path, the name is already in the map (no need to mutate the hashmap)
3✔
62
        if timings.contains_key(&id) {
3✔
63
            let queue = timings
2✔
64
                .get(&id)
2✔
65
                .expect("System name not found in map after contains_key check");
2✔
66
            let mut queue = queue.lock();
2✔
67

2✔
68
            queue.push_back(duration);
2✔
69
            return;
2✔
70
        }
1✔
71

1✔
72
        // otherwise, acquire a write lock and insert a new queue
1✔
73
        timings.with_upgraded(|timings| {
1✔
74
            let queue = timings.entry(id).or_insert_with(|| Mutex::new(CircularBuffer::new()));
1✔
75
            queue.lock().push_back(duration);
1✔
76
        });
1✔
77
    }
3✔
78

79
    pub fn get_stats(&self) -> Map<SystemId, (Duration, Duration), MAX_SYSTEMS> {
1✔
80
        let timings = self.timings.read();
1✔
81
        let mut stats = Map::new();
1✔
82

83
        for (id, queue) in timings.iter() {
1✔
84
            if queue.lock().is_empty() {
1✔
NEW
85
                continue;
×
86
            }
1✔
87

1✔
88
            let durations: Vec<f64> = queue.lock().iter().map(|d| d.as_secs_f64() * 1000.0).collect();
3✔
89
            let count = durations.len() as f64;
1✔
90

1✔
91
            let sum: f64 = durations.iter().sum();
1✔
92
            let mean = sum / count;
1✔
93

1✔
94
            let variance = durations.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count;
3✔
95
            let std_dev = variance.sqrt();
1✔
96

1✔
97
            stats.insert(
1✔
98
                *id,
1✔
99
                (
1✔
100
                    Duration::from_secs_f64(mean / 1000.0),
1✔
101
                    Duration::from_secs_f64(std_dev / 1000.0),
1✔
102
                ),
1✔
103
            );
1✔
104
        }
1✔
105

106
        stats
1✔
107
    }
1✔
108

109
    pub fn get_total_stats(&self) -> (Duration, Duration) {
1✔
110
        let timings = self.timings.read();
1✔
111
        let mut all_durations = Vec::new();
1✔
112

113
        for queue in timings.values() {
1✔
114
            all_durations.extend(queue.lock().iter().map(|d| d.as_secs_f64() * 1000.0));
3✔
115
        }
1✔
116

117
        if all_durations.is_empty() {
1✔
NEW
118
            return (Duration::ZERO, Duration::ZERO);
×
119
        }
1✔
120

1✔
121
        let count = all_durations.len() as f64;
1✔
122
        let sum: f64 = all_durations.iter().sum();
1✔
123
        let mean = sum / count;
1✔
124

1✔
125
        let variance = all_durations.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count;
3✔
126
        let std_dev = variance.sqrt();
1✔
127

1✔
128
        (
1✔
129
            Duration::from_secs_f64(mean / 1000.0),
1✔
130
            Duration::from_secs_f64(std_dev / 1000.0),
1✔
131
        )
1✔
132
    }
1✔
133

NEW
134
    pub fn format_timing_display(&self) -> SmallVec<[String; SystemId::COUNT]> {
×
NEW
135
        let stats = self.get_stats();
×
NEW
136
        let (total_avg, total_std) = self.get_total_stats();
×
137

NEW
138
        let effective_fps = match 1.0 / total_avg.as_secs_f64() {
×
NEW
139
            f if f > 100.0 => (f as u32).separate_with_commas(),
×
NEW
140
            f if f < 10.0 => format!("{:.1} FPS", f),
×
NEW
141
            f => format!("{:.0} FPS", f),
×
142
        };
143

144
        // Collect timing data for formatting
NEW
145
        let mut timing_data = Vec::new();
×
NEW
146

×
NEW
147
        // Add total stats
×
NEW
148
        timing_data.push((effective_fps, total_avg, total_std));
×
NEW
149

×
NEW
150
        // Add top 5 most expensive systems
×
NEW
151
        let mut sorted_stats: Vec<_> = stats.iter().collect();
×
NEW
152
        sorted_stats.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
×
153

NEW
154
        for (name, (avg, std_dev)) in sorted_stats.iter().take(5) {
×
NEW
155
            timing_data.push((name.to_string(), *avg, *std_dev));
×
NEW
156
        }
×
157

158
        // Use the formatting module to format the data
NEW
159
        formatting::format_timing_display(timing_data)
×
NEW
160
    }
×
161
}
162

NEW
163
pub fn profile<S, M>(id: SystemId, system: S) -> impl FnMut(&mut bevy_ecs::world::World)
×
NEW
164
where
×
NEW
165
    S: IntoSystem<(), (), M> + 'static,
×
NEW
166
{
×
NEW
167
    let mut system: S::System = IntoSystem::into_system(system);
×
NEW
168
    let mut is_initialized = false;
×
NEW
169
    move |world: &mut bevy_ecs::world::World| {
×
NEW
170
        if !is_initialized {
×
NEW
171
            system.initialize(world);
×
NEW
172
            is_initialized = true;
×
NEW
173
        }
×
174

NEW
175
        let start = std::time::Instant::now();
×
NEW
176
        system.run((), world);
×
NEW
177
        let duration = start.elapsed();
×
178

NEW
179
        if let Some(timings) = world.get_resource::<SystemTimings>() {
×
NEW
180
            timings.add_timing(id, duration);
×
NEW
181
        }
×
NEW
182
    }
×
NEW
183
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc