Skip to main content

alice_physics/
profiling.rs

1//! Physics Profiling API
2//!
3//! Lightweight performance counters and timers for physics pipeline stages.
4//! Uses deterministic "tick" counting rather than wall-clock time for reproducibility.
5//!
6//! # Profiled Stages
7//!
8//! - Broadphase (AABB overlap tests)
9//! - Narrowphase (GJK/EPA collision detection)
10//! - Solver (constraint solving iterations)
11//! - CCD (continuous collision detection)
12//! - Integration (velocity/position updates)
13
14#[cfg(not(feature = "std"))]
15use alloc::vec;
16#[cfg(not(feature = "std"))]
17use alloc::vec::Vec;
18
19/// A single profiling timer entry
20#[derive(Clone, Debug)]
21pub struct ProfileEntry {
22    /// Stage name
23    pub name: &'static str,
24    /// Total accumulated ticks
25    pub total_ticks: u64,
26    /// Number of invocations
27    pub call_count: u64,
28    /// Last frame's ticks
29    pub last_ticks: u64,
30    /// Peak ticks (single frame)
31    pub peak_ticks: u64,
32}
33
34impl ProfileEntry {
35    /// Create a new profile entry
36    #[must_use]
37    pub const fn new(name: &'static str) -> Self {
38        Self {
39            name,
40            total_ticks: 0,
41            call_count: 0,
42            last_ticks: 0,
43            peak_ticks: 0,
44        }
45    }
46
47    /// Average ticks per call
48    #[inline]
49    #[must_use]
50    pub const fn average_ticks(&self) -> u64 {
51        match self.total_ticks.checked_div(self.call_count) {
52            Some(avg) => avg,
53            None => 0,
54        }
55    }
56
57    /// Record a measurement. The total and the call count saturate at
58    /// `u64::MAX` instead of wrapping (a wrapped total would go down and make
59    /// the average lie).
60    pub fn record(&mut self, ticks: u64) {
61        self.total_ticks = self.total_ticks.saturating_add(ticks);
62        self.call_count = self.call_count.saturating_add(1);
63        self.last_ticks = ticks;
64        if ticks > self.peak_ticks {
65            self.peak_ticks = ticks;
66        }
67    }
68
69    /// Reset all counters
70    pub fn reset(&mut self) {
71        self.total_ticks = 0;
72        self.call_count = 0;
73        self.last_ticks = 0;
74        self.peak_ticks = 0;
75    }
76}
77
78/// Physics step statistics (per-frame counters)
79#[derive(Clone, Copy, Debug, Default)]
80pub struct StepStats {
81    /// Number of broadphase pairs tested
82    pub broadphase_pairs: u32,
83    /// Number of narrowphase tests performed
84    pub narrowphase_tests: u32,
85    /// Number of active contacts
86    pub active_contacts: u32,
87    /// Number of contact manifolds
88    pub active_manifolds: u32,
89    /// Number of active bodies (non-sleeping)
90    pub active_bodies: u32,
91    /// Number of static bodies
92    pub static_bodies: u32,
93    /// Number of solver iterations performed
94    pub solver_iterations: u32,
95    /// Number of CCD checks
96    pub ccd_checks: u32,
97    /// Number of joint constraints
98    pub joint_count: u32,
99}
100
101/// Physics profiler: collects timing and statistics
102pub struct PhysicsProfiler {
103    /// Stage timers
104    entries: Vec<ProfileEntry>,
105    /// Current frame stats
106    pub stats: StepStats,
107    /// Frame counter
108    pub frame_count: u64,
109    /// Whether profiling is enabled
110    pub enabled: bool,
111}
112
113/// Stage indices for fast lookup
114pub const STAGE_BROADPHASE: usize = 0;
115/// Narrowphase stage index
116pub const STAGE_NARROWPHASE: usize = 1;
117/// Solver stage index
118pub const STAGE_SOLVER: usize = 2;
119/// CCD stage index
120pub const STAGE_CCD: usize = 3;
121/// Integration stage index
122pub const STAGE_INTEGRATION: usize = 4;
123/// Contact cache stage index
124pub const STAGE_CONTACT_CACHE: usize = 5;
125/// Total step stage index
126pub const STAGE_TOTAL_STEP: usize = 6;
127
128impl PhysicsProfiler {
129    /// Create a new profiler with default stages
130    #[must_use]
131    pub fn new() -> Self {
132        let entries = vec![
133            ProfileEntry::new("broadphase"),
134            ProfileEntry::new("narrowphase"),
135            ProfileEntry::new("solver"),
136            ProfileEntry::new("ccd"),
137            ProfileEntry::new("integration"),
138            ProfileEntry::new("contact_cache"),
139            ProfileEntry::new("total_step"),
140        ];
141
142        Self {
143            entries,
144            stats: StepStats::default(),
145            frame_count: 0,
146            enabled: true,
147        }
148    }
149
150    /// Record ticks for a stage
151    #[inline]
152    pub fn record(&mut self, stage: usize, ticks: u64) {
153        if self.enabled && stage < self.entries.len() {
154            self.entries[stage].record(ticks);
155        }
156    }
157
158    /// Begin a new frame (reset per-frame stats)
159    pub fn begin_frame(&mut self) {
160        self.stats = StepStats::default();
161        self.frame_count += 1;
162    }
163
164    /// Get a profile entry by stage index
165    #[must_use]
166    pub fn get(&self, stage: usize) -> Option<&ProfileEntry> {
167        self.entries.get(stage)
168    }
169
170    /// Get last frame's ticks for a stage
171    #[must_use]
172    pub fn last_ticks(&self, stage: usize) -> u64 {
173        self.entries.get(stage).map_or(0, |e| e.last_ticks)
174    }
175
176    /// Get average ticks for a stage. After the total has saturated at
177    /// `u64::MAX` this is `u64::MAX / count`, a lower bound on the true average.
178    pub fn average_ticks(&self, stage: usize) -> u64 {
179        self.entries
180            .get(stage)
181            .map_or(0, ProfileEntry::average_ticks)
182    }
183
184    /// Reset all profiling data
185    pub fn reset(&mut self) {
186        for entry in &mut self.entries {
187            entry.reset();
188        }
189        self.stats = StepStats::default();
190        self.frame_count = 0;
191    }
192
193    /// Get a summary of all stages
194    #[must_use]
195    pub fn summary(&self) -> Vec<(&'static str, u64, u64, u64)> {
196        self.entries
197            .iter()
198            .map(|e| (e.name, e.last_ticks, e.average_ticks(), e.peak_ticks))
199            .collect()
200    }
201}
202
203impl Default for PhysicsProfiler {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209/// Deterministic tick counter (monotonic, platform-independent)
210///
211/// Uses a simple incrementing counter. For real-time profiling,
212/// wrap with platform-specific high-resolution timer.
213pub struct TickCounter {
214    current: u64,
215}
216
217impl TickCounter {
218    /// Create a new tick counter
219    #[must_use]
220    pub const fn new() -> Self {
221        Self { current: 0 }
222    }
223
224    /// Start timing
225    #[inline]
226    #[must_use]
227    pub const fn start(&self) -> u64 {
228        self.current
229    }
230
231    /// Stop timing and return elapsed ticks
232    #[inline]
233    #[must_use]
234    pub const fn elapsed(&self, start: u64) -> u64 {
235        self.current.wrapping_sub(start)
236    }
237
238    /// Advance the counter by N ticks
239    #[inline]
240    pub fn advance(&mut self, ticks: u64) {
241        self.current = self.current.wrapping_add(ticks);
242    }
243
244    /// Current tick value
245    #[inline]
246    #[must_use]
247    pub const fn now(&self) -> u64 {
248        self.current
249    }
250}
251
252impl Default for TickCounter {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_profile_entry() {
264        let mut entry = ProfileEntry::new("test");
265        entry.record(100);
266        entry.record(200);
267        entry.record(150);
268
269        assert_eq!(entry.call_count, 3);
270        assert_eq!(entry.total_ticks, 450);
271        assert_eq!(entry.average_ticks(), 150);
272        assert_eq!(entry.peak_ticks, 200);
273        assert_eq!(entry.last_ticks, 150);
274    }
275
276    #[test]
277    fn test_profiler() {
278        let mut profiler = PhysicsProfiler::new();
279        profiler.begin_frame();
280
281        profiler.record(STAGE_BROADPHASE, 100);
282        profiler.record(STAGE_NARROWPHASE, 200);
283        profiler.record(STAGE_SOLVER, 500);
284
285        assert_eq!(profiler.last_ticks(STAGE_BROADPHASE), 100);
286        assert_eq!(profiler.last_ticks(STAGE_SOLVER), 500);
287    }
288
289    #[test]
290    fn test_profiler_summary() {
291        let mut profiler = PhysicsProfiler::new();
292        profiler.record(STAGE_BROADPHASE, 42);
293
294        let summary = profiler.summary();
295        assert!(summary.len() >= 7);
296        assert_eq!(summary[STAGE_BROADPHASE].1, 42); // last_ticks
297    }
298
299    #[test]
300    fn test_tick_counter() {
301        let mut counter = TickCounter::new();
302        let start = counter.start();
303        counter.advance(100);
304        let elapsed = counter.elapsed(start);
305        assert_eq!(elapsed, 100);
306    }
307
308    #[test]
309    fn test_step_stats() {
310        let stats = StepStats::default();
311        assert_eq!(stats.broadphase_pairs, 0);
312        assert_eq!(stats.active_contacts, 0);
313    }
314
315    #[test]
316    fn test_profiler_reset() {
317        let mut profiler = PhysicsProfiler::new();
318        profiler.record(STAGE_BROADPHASE, 100);
319        profiler.reset();
320        assert_eq!(profiler.last_ticks(STAGE_BROADPHASE), 0);
321        assert_eq!(profiler.frame_count, 0);
322    }
323}