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

Xevion / Pac-Man / 17506959295

05 Sep 2025 11:49PM UTC coverage: 31.007% (-0.2%) from 31.246%
17506959295

push

github

Xevion
feat: re-implement CustomFormatter to clone Full formatterr

0 of 61 new or added lines in 1 file covered. (0.0%)

161 existing lines in 4 files now uncovered.

1093 of 3525 relevant lines covered (31.01%)

772.4 hits per line

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

71.43
/src/systems/profiling.rs
1
use bevy_ecs::system::IntoSystem;
2
use bevy_ecs::{resource::Resource, system::System};
3
use circular_buffer::CircularBuffer;
4
use num_width::NumberWidth;
5
use parking_lot::Mutex;
6
use smallvec::SmallVec;
7
use std::fmt::Display;
8
use std::sync::atomic::{AtomicU64, Ordering};
9
use std::time::Duration;
10
use strum::{EnumCount, IntoEnumIterator};
11
use strum_macros::{EnumCount, EnumIter, IntoStaticStr};
12
use thousands::Separable;
13

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

19
/// A timing buffer that tracks durations and automatically inserts zero durations for skipped ticks.
20
#[derive(Debug, Default)]
21
pub struct TimingBuffer {
22
    /// Circular buffer storing timing durations
23
    buffer: CircularBuffer<TIMING_WINDOW_SIZE, Duration>,
24
    /// The last tick when this buffer was updated
25
    last_tick: u64,
26
}
27

28
impl TimingBuffer {
29
    /// Adds a timing duration for the current tick.
30
    ///
31
    /// # Panics
32
    ///
33
    /// Panics if `current_tick` is less than `last_tick`, indicating time went backwards.
34
    pub fn add_timing(&mut self, duration: Duration, current_tick: u64) {
31✔
35
        if current_tick < self.last_tick {
31✔
UNCOV
36
            panic!(
×
UNCOV
37
                "Time went backwards: current_tick ({}) < last_tick ({})",
×
UNCOV
38
                current_tick, self.last_tick
×
UNCOV
39
            );
×
40
        }
31✔
41

31✔
42
        // Insert zero durations for any skipped ticks (but not the current tick)
31✔
43
        if current_tick > self.last_tick {
31✔
44
            let skipped_ticks = current_tick - self.last_tick - 1;
31✔
45
            for _ in 0..skipped_ticks {
31✔
UNCOV
46
                self.buffer.push_back(Duration::ZERO);
×
UNCOV
47
            }
×
UNCOV
48
        }
×
49

50
        // Add the actual timing
51
        self.buffer.push_back(duration);
31✔
52
        self.last_tick = current_tick;
31✔
53
    }
31✔
54

55
    /// Gets statistics for this timing buffer.
56
    ///
57
    /// # Panics
58
    ///
59
    /// Panics if `current_tick` is less than `last_tick`, indicating time went backwards.
60
    pub fn get_stats(&mut self, current_tick: u64) -> (Duration, Duration) {
84✔
61
        // Insert zero durations for any skipped ticks since last update (but not the current tick)
84✔
62
        if current_tick > self.last_tick {
84✔
63
            let skipped_ticks = current_tick - self.last_tick - 1;
59✔
64
            for _ in 0..skipped_ticks {
78✔
65
                self.buffer.push_back(Duration::ZERO);
78✔
66
            }
78✔
67
            self.last_tick = current_tick;
59✔
68
        }
25✔
69

70
        // Calculate statistics using Welford's algorithm
71
        let mut sample_count = 0u16;
84✔
72
        let mut running_mean = 0.0;
84✔
73
        let mut sum_squared_diff = 0.0;
84✔
74

84✔
75
        let skip = self.last_tick.saturating_sub(current_tick);
84✔
76
        for duration in self.buffer.iter().skip(skip as usize) {
109✔
77
            let duration_secs = duration.as_secs_f32();
109✔
78
            sample_count += 1;
109✔
79

109✔
80
            let diff_from_mean = duration_secs - running_mean;
109✔
81
            running_mean += diff_from_mean / sample_count as f32;
109✔
82

109✔
83
            let diff_from_new_mean = duration_secs - running_mean;
109✔
84
            sum_squared_diff += diff_from_mean * diff_from_new_mean;
109✔
85
        }
109✔
86

87
        if sample_count > 0 {
84✔
88
            let variance = if sample_count > 1 {
64✔
89
                sum_squared_diff / (sample_count - 1) as f32
42✔
90
            } else {
91
                0.0
22✔
92
            };
93

94
            (
64✔
95
                Duration::from_secs_f32(running_mean),
64✔
96
                Duration::from_secs_f32(variance.sqrt()),
64✔
97
            )
64✔
98
        } else {
99
            (Duration::ZERO, Duration::ZERO)
20✔
100
        }
101
    }
84✔
102
}
103

104
/// A resource that tracks the current game tick using an atomic counter.
105
/// This ensures thread-safe access to the tick counter across systems.
106
#[derive(Resource, Debug)]
107
pub struct Timing {
108
    /// Atomic counter for the current game tick
109
    current_tick: AtomicU64,
110
}
111

112
impl Timing {
113
    /// Creates a new Timing resource starting at tick 0
UNCOV
114
    pub fn new() -> Self {
×
UNCOV
115
        Self {
×
UNCOV
116
            current_tick: AtomicU64::new(0),
×
UNCOV
117
        }
×
UNCOV
118
    }
×
119

120
    /// Gets the current tick value
UNCOV
121
    pub fn get_current_tick(&self) -> u64 {
×
UNCOV
122
        self.current_tick.load(Ordering::Relaxed)
×
UNCOV
123
    }
×
124

125
    /// Increments the tick counter and returns the new value
UNCOV
126
    pub fn increment_tick(&self) -> u64 {
×
UNCOV
127
        self.current_tick.fetch_add(1, Ordering::Relaxed) + 1
×
UNCOV
128
    }
×
129
}
130

131
impl Default for Timing {
UNCOV
132
    fn default() -> Self {
×
UNCOV
133
        Self::new()
×
UNCOV
134
    }
×
135
}
136

137
#[derive(EnumCount, EnumIter, IntoStaticStr, Debug, PartialEq, Eq, Hash, Copy, Clone)]
138
pub enum SystemId {
139
    Total,
140
    Input,
141
    PlayerControls,
142
    Ghost,
143
    Movement,
144
    Audio,
145
    Blinking,
146
    DirectionalRender,
147
    LinearRender,
148
    DirtyRender,
149
    HudRender,
150
    Render,
151
    DebugRender,
152
    Present,
153
    Collision,
154
    Item,
155
    PlayerMovement,
156
    GhostCollision,
157
    Stage,
158
    GhostStateAnimation,
159
    EatenGhost,
160
}
161

162
impl Display for SystemId {
163
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84✔
164
        // Use strum_macros::IntoStaticStr to get the static string
84✔
165
        write!(f, "{}", Into::<&'static str>::into(self).to_ascii_lowercase())
84✔
166
    }
84✔
167
}
168

169
#[derive(Resource, Debug)]
170
pub struct SystemTimings {
171
    /// Statically sized map of system names to timing buffers.
172
    pub timings: micromap::Map<SystemId, Mutex<TimingBuffer>, MAX_SYSTEMS>,
173
}
174

175
impl Default for SystemTimings {
176
    fn default() -> Self {
4✔
177
        let mut timings = micromap::Map::new();
4✔
178

179
        // Pre-populate with all SystemId variants to avoid runtime allocations
180
        for id in SystemId::iter() {
88✔
181
            timings.insert(id, Mutex::new(TimingBuffer::default()));
84✔
182
        }
84✔
183

184
        Self { timings }
4✔
185
    }
4✔
186
}
187

188
impl SystemTimings {
189
    pub fn add_timing(&self, id: SystemId, duration: Duration, current_tick: u64) {
31✔
190
        // Since all SystemId variants are pre-populated, we can use a simple read lock
31✔
191
        let buffer = self
31✔
192
            .timings
31✔
193
            .get(&id)
31✔
194
            .expect("SystemId not found in pre-populated map - this is a bug");
31✔
195
        buffer.lock().add_timing(duration, current_tick);
31✔
196
    }
31✔
197

198
    /// Add timing for the Total system (total frame time including scheduler.run)
199
    pub fn add_total_timing(&self, duration: Duration, current_tick: u64) {
3✔
200
        self.add_timing(SystemId::Total, duration, current_tick);
3✔
201
    }
3✔
202

203
    pub fn get_stats(&self, current_tick: u64) -> micromap::Map<SystemId, (Duration, Duration), MAX_SYSTEMS> {
4✔
204
        let mut stats = micromap::Map::new();
4✔
205

206
        // Iterate over all SystemId variants to ensure every system has an entry
207
        for id in SystemId::iter() {
88✔
208
            let buffer = self
84✔
209
                .timings
84✔
210
                .get(&id)
84✔
211
                .expect("SystemId not found in pre-populated map - this is a bug");
84✔
212

84✔
213
            let (average, standard_deviation) = buffer.lock().get_stats(current_tick);
84✔
214
            stats.insert(id, (average, standard_deviation));
84✔
215
        }
84✔
216

217
        stats
4✔
218
    }
4✔
219

UNCOV
220
    pub fn format_timing_display(&self, current_tick: u64) -> SmallVec<[String; SystemId::COUNT]> {
×
UNCOV
221
        let stats = self.get_stats(current_tick);
×
UNCOV
222

×
UNCOV
223
        // Get the Total system metrics instead of averaging all systems
×
UNCOV
224
        let (total_avg, total_std) = stats
×
UNCOV
225
            .get(&SystemId::Total)
×
UNCOV
226
            .copied()
×
UNCOV
227
            .unwrap_or((Duration::ZERO, Duration::ZERO));
×
228

UNCOV
229
        let effective_fps = match 1.0 / total_avg.as_secs_f64() {
×
UNCOV
230
            f if f > 100.0 => format!("{:>5} FPS", (f as u32).separate_with_commas()),
×
231
            f if f < 10.0 => format!("{:.1} FPS", f),
×
UNCOV
232
            f => format!("{:5.0} FPS", f),
×
233
        };
234

235
        // Collect timing data for formatting
UNCOV
236
        let mut timing_data = vec![(effective_fps, total_avg, total_std)];
×
UNCOV
237

×
UNCOV
238
        // Sort the stats by average duration, excluding the Total system
×
UNCOV
239
        let mut sorted_stats: Vec<_> = stats.iter().filter(|(id, _)| **id != SystemId::Total).collect();
×
UNCOV
240
        sorted_stats.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
×
241

242
        // Add the top 7 most expensive systems (excluding Total)
UNCOV
243
        for (name, (avg, std_dev)) in sorted_stats.iter().take(9) {
×
UNCOV
244
            timing_data.push((name.to_string(), *avg, *std_dev));
×
UNCOV
245
        }
×
246

247
        // Use the formatting module to format the data
UNCOV
248
        format_timing_display(timing_data)
×
UNCOV
249
    }
×
250
}
251

UNCOV
252
pub fn profile<S, M>(id: SystemId, system: S) -> impl FnMut(&mut bevy_ecs::world::World)
×
UNCOV
253
where
×
UNCOV
254
    S: IntoSystem<(), (), M> + 'static,
×
UNCOV
255
{
×
UNCOV
256
    let mut system: S::System = IntoSystem::into_system(system);
×
UNCOV
257
    let mut is_initialized = false;
×
UNCOV
258
    move |world: &mut bevy_ecs::world::World| {
×
UNCOV
259
        if !is_initialized {
×
UNCOV
260
            system.initialize(world);
×
UNCOV
261
            is_initialized = true;
×
UNCOV
262
        }
×
263

UNCOV
264
        let start = std::time::Instant::now();
×
UNCOV
265
        system.run((), world);
×
UNCOV
266
        let duration = start.elapsed();
×
267

UNCOV
268
        if let (Some(timings), Some(timing)) = (world.get_resource::<SystemTimings>(), world.get_resource::<Timing>()) {
×
UNCOV
269
            let current_tick = timing.get_current_tick();
×
UNCOV
270
            timings.add_timing(id, duration, current_tick);
×
UNCOV
271
        }
×
UNCOV
272
    }
×
UNCOV
273
}
×
274

275
// Helper to split a duration into a integer, decimal, and unit
276
fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
30✔
277
    let (int, decimal, unit) = match duration {
30✔
278
        // if greater than 1 second, return as seconds
279
        n if n >= &Duration::from_secs(1) => {
30✔
280
            let secs = n.as_secs();
1✔
281
            let decimal = n.as_millis() as u64 % 1000;
1✔
282
            (secs, decimal as u32, "s")
1✔
283
        }
284
        // if greater than 1 millisecond, return as milliseconds
285
        n if n >= &Duration::from_millis(1) => {
29✔
286
            let ms = n.as_millis() as u64;
4✔
287
            let decimal = n.as_micros() as u64 % 1000;
4✔
288
            (ms, decimal as u32, "ms")
4✔
289
        }
290
        // if greater than 1 microsecond, return as microseconds
291
        n if n >= &Duration::from_micros(1) => {
25✔
292
            let us = n.as_micros() as u64;
18✔
293
            let decimal = n.as_nanos() as u64 % 1000;
18✔
294
            (us, decimal as u32, "µs")
18✔
295
        }
296
        // otherwise, return as nanoseconds
297
        n => {
7✔
298
            let ns = n.as_nanos() as u64;
7✔
299
            (ns, 0, "ns")
7✔
300
        }
301
    };
302

303
    (int, decimal, unit)
30✔
304
}
30✔
305

306
/// Formats timing data into a vector of strings with proper alignment
307
pub fn format_timing_display(
4✔
308
    timing_data: impl IntoIterator<Item = (String, Duration, Duration)>,
4✔
309
) -> SmallVec<[String; SystemId::COUNT]> {
4✔
310
    let mut iter = timing_data.into_iter().peekable();
4✔
311
    if iter.peek().is_none() {
4✔
UNCOV
312
        return SmallVec::new();
×
313
    }
4✔
314

315
    struct Entry {
316
        name: String,
317
        avg_int: u64,
318
        avg_decimal: u32,
319
        avg_unit: &'static str,
320
        std_int: u64,
321
        std_decimal: u32,
322
        std_unit: &'static str,
323
    }
324

325
    let entries = iter
4✔
326
        .map(|(name, avg, std_dev)| {
15✔
327
            let (avg_int, avg_decimal, avg_unit) = get_value(&avg);
15✔
328
            let (std_int, std_decimal, std_unit) = get_value(&std_dev);
15✔
329

15✔
330
            Entry {
15✔
331
                name: name.clone(),
15✔
332
                avg_int,
15✔
333
                avg_decimal,
15✔
334
                avg_unit,
15✔
335
                std_int,
15✔
336
                std_decimal,
15✔
337
                std_unit,
15✔
338
            }
15✔
339
        })
15✔
340
        .collect::<SmallVec<[Entry; 12]>>();
4✔
341

4✔
342
    let (max_avg_int_width, max_avg_decimal_width, max_std_int_width, max_std_decimal_width) =
4✔
343
        entries
4✔
344
            .iter()
4✔
345
            .fold((0, 3, 0, 3), |(avg_int_w, avg_dec_w, std_int_w, std_dec_w), e| {
15✔
346
                (
15✔
347
                    avg_int_w.max(e.avg_int.width() as usize),
15✔
348
                    avg_dec_w.max(e.avg_decimal.width() as usize),
15✔
349
                    std_int_w.max(e.std_int.width() as usize),
15✔
350
                    std_dec_w.max(e.std_decimal.width() as usize),
15✔
351
                )
15✔
352
            });
15✔
353

4✔
354
    let max_name_width = SystemId::iter()
4✔
355
        .map(|id| id.to_string().len())
84✔
356
        .max()
4✔
357
        .expect("SystemId::iter() returned an empty iterator");
4✔
358

4✔
359
    entries.iter().map(|e| {
15✔
360
            format!(
15✔
361
                "{name:max_name_width$} : {avg_int:max_avg_int_width$}.{avg_decimal:<max_avg_decimal_width$}{avg_unit} ± {std_int:max_std_int_width$}.{std_decimal:<max_std_decimal_width$}{std_unit}",
15✔
362
                // Content
15✔
363
                name = e.name,
15✔
364
                avg_int = e.avg_int,
15✔
365
                avg_decimal = e.avg_decimal,
15✔
366
                std_int = e.std_int,
15✔
367
                std_decimal = e.std_decimal,
15✔
368
                // Units
15✔
369
                avg_unit = e.avg_unit,
15✔
370
                std_unit = e.std_unit,
15✔
371
                // Padding
15✔
372
                max_name_width = max_name_width,
15✔
373
                max_avg_int_width = max_avg_int_width,
15✔
374
                max_avg_decimal_width = max_avg_decimal_width,
15✔
375
                max_std_int_width = max_std_int_width,
15✔
376
                max_std_decimal_width = max_std_decimal_width
15✔
377
            )
15✔
378
        }).collect::<SmallVec<[String; SystemId::COUNT]>>()
15✔
379
}
4✔
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