rumdl_lib/
profiling.rs

1//!
2//! This module provides profiling utilities for measuring and reporting execution times in rumdl.
3
4use std::collections::HashMap;
5use std::sync::LazyLock;
6use std::sync::Mutex;
7use std::time::{Duration, Instant};
8
9// Global profiler state
10static PROFILER: LazyLock<Mutex<Profiler>> = LazyLock::new(|| Mutex::new(Profiler::new()));
11
12// Enable/disable profiling with a feature flag
13#[cfg(feature = "profiling")]
14pub(crate) const PROFILING_ENABLED: bool = true;
15
16#[cfg(not(feature = "profiling"))]
17pub(crate) const PROFILING_ENABLED: bool = false;
18
19/// A simple profiling utility to measure and report execution times
20pub struct Profiler {
21    measurements: HashMap<String, (Duration, usize)>,
22    active_timers: HashMap<String, Instant>,
23}
24
25impl Default for Profiler {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl Profiler {
32    /// Create a new profiler instance
33    pub fn new() -> Self {
34        Profiler {
35            measurements: HashMap::new(),
36            active_timers: HashMap::new(),
37        }
38    }
39
40    /// Start a timer for a specific section of code
41    pub fn start_timer(&mut self, section: &str) {
42        if PROFILING_ENABLED {
43            let section_name = section.to_string();
44            self.active_timers.insert(section_name, Instant::now());
45        }
46    }
47
48    /// Stop the timer for a section and record the elapsed time
49    pub fn stop_timer(&mut self, section: &str) {
50        if PROFILING_ENABLED {
51            let section_name = section.to_string();
52            if let Some(start_time) = self.active_timers.remove(&section_name) {
53                let elapsed = start_time.elapsed();
54
55                // Update or insert the measurement
56                let entry = self
57                    .measurements
58                    .entry(section_name)
59                    .or_insert((Duration::new(0, 0), 0));
60                entry.0 += elapsed;
61                entry.1 += 1;
62            }
63        }
64    }
65
66    /// Get a report of all measurements
67    pub fn get_report(&self) -> String {
68        if !PROFILING_ENABLED || self.measurements.is_empty() {
69            return "Profiling disabled or no measurements recorded.".to_string();
70        }
71
72        // Sort measurements by total time (descending)
73        let mut sorted_measurements: Vec<_> = self.measurements.iter().collect();
74        sorted_measurements.sort_by(|a, b| b.1.0.cmp(&a.1.0));
75
76        // Calculate total time across all sections
77        let total_time: Duration = sorted_measurements.iter().map(|(_, (duration, _))| duration).sum();
78
79        // Generate the report
80        let mut report = String::new();
81        report.push_str("=== Profiling Report ===\n");
82        report.push_str(&format!(
83            "Total execution time: {:.6} seconds\n\n",
84            total_time.as_secs_f64()
85        ));
86        report.push_str(
87            "Section                                  | Total Time (s) | Calls | Avg Time (ms) | % of Total\n",
88        );
89        report.push_str(
90            "------------------------------------------|----------------|-------|---------------|----------\n",
91        );
92
93        for (section, (duration, calls)) in sorted_measurements {
94            let total_seconds = duration.as_secs_f64();
95            let avg_ms = (duration.as_nanos() as f64) / (calls * 1_000_000) as f64;
96            let percentage = (total_seconds / total_time.as_secs_f64()) * 100.0;
97
98            report.push_str(&format!(
99                "{section:<42} | {total_seconds:<14.6} | {calls:<5} | {avg_ms:<13.3} | {percentage:<8.2}%\n"
100            ));
101        }
102
103        report
104    }
105
106    /// Reset all measurements
107    pub fn reset(&mut self) {
108        self.measurements.clear();
109        self.active_timers.clear();
110    }
111}
112
113/// Start a timer for a section
114///
115/// If the mutex is poisoned, this is a no-op. Profiling failures should not crash the application.
116pub fn start_timer(section: &str) {
117    if PROFILING_ENABLED && let Ok(mut profiler) = PROFILER.lock() {
118        profiler.start_timer(section);
119    }
120}
121
122/// Stop a timer for a section
123///
124/// If the mutex is poisoned, this is a no-op. Profiling failures should not crash the application.
125pub fn stop_timer(section: &str) {
126    if PROFILING_ENABLED && let Ok(mut profiler) = PROFILER.lock() {
127        profiler.stop_timer(section);
128    }
129}
130
131/// Get a report of all measurements
132///
133/// If the mutex is poisoned, returns a message indicating the error rather than panicking.
134pub fn get_report() -> String {
135    if PROFILING_ENABLED {
136        match PROFILER.lock() {
137            Ok(profiler) => profiler.get_report(),
138            Err(_) => "Profiling report unavailable (mutex poisoned).".to_string(),
139        }
140    } else {
141        "Profiling is disabled.".to_string()
142    }
143}
144
145/// Reset all measurements
146///
147/// If the mutex is poisoned, this is a no-op. Profiling failures should not crash the application.
148pub fn reset() {
149    if PROFILING_ENABLED && let Ok(mut profiler) = PROFILER.lock() {
150        profiler.reset();
151    }
152}
153
154/// A utility struct to time a section of code using RAII
155pub struct ScopedTimer {
156    section: String,
157    enabled: bool,
158}
159
160impl ScopedTimer {
161    /// Create a new scoped timer
162    pub fn new(section: &str) -> Self {
163        let enabled = PROFILING_ENABLED;
164        if enabled {
165            start_timer(section);
166        }
167        ScopedTimer {
168            section: section.to_string(),
169            enabled,
170        }
171    }
172}
173
174impl Drop for ScopedTimer {
175    fn drop(&mut self) {
176        if self.enabled {
177            stop_timer(&self.section);
178        }
179    }
180}
181
182/// Convenience macro to time a block of code
183#[macro_export]
184macro_rules! time_section {
185    ($section:expr, $block:block) => {{
186        let _timer = $crate::profiling::ScopedTimer::new($section);
187        $block
188    }};
189}
190
191/// Convenience macro to time a function call
192#[macro_export]
193macro_rules! time_function {
194    ($section:expr, $func:expr) => {{
195        let _timer = $crate::profiling::ScopedTimer::new($section);
196        $func
197    }};
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use std::thread;
204    use std::time::Duration;
205
206    #[test]
207    fn test_profiler_new() {
208        let profiler = Profiler::new();
209        assert!(profiler.measurements.is_empty());
210        assert!(profiler.active_timers.is_empty());
211    }
212
213    #[test]
214    fn test_profiler_default() {
215        let profiler = Profiler::default();
216        assert!(profiler.measurements.is_empty());
217        assert!(profiler.active_timers.is_empty());
218    }
219
220    #[test]
221    fn test_profiler_start_stop_timer() {
222        let mut profiler = Profiler::new();
223
224        // Force profiling to be enabled for this test
225        if PROFILING_ENABLED {
226            profiler.start_timer("test_section");
227            thread::sleep(Duration::from_millis(10));
228            profiler.stop_timer("test_section");
229
230            assert!(profiler.measurements.contains_key("test_section"));
231            let (duration, count) = profiler.measurements.get("test_section").unwrap();
232            assert!(*count == 1);
233            assert!(duration.as_millis() >= 10);
234        }
235    }
236
237    #[test]
238    fn test_profiler_multiple_measurements() {
239        let mut profiler = Profiler::new();
240
241        if PROFILING_ENABLED {
242            // Multiple measurements of same section
243            for _ in 0..3 {
244                profiler.start_timer("test_section");
245                thread::sleep(Duration::from_millis(5));
246                profiler.stop_timer("test_section");
247            }
248
249            assert!(profiler.measurements.contains_key("test_section"));
250            let (duration, count) = profiler.measurements.get("test_section").unwrap();
251            assert_eq!(*count, 3);
252            assert!(duration.as_millis() >= 15);
253        }
254    }
255
256    #[test]
257    fn test_profiler_get_report() {
258        let mut profiler = Profiler::new();
259
260        if PROFILING_ENABLED {
261            profiler.start_timer("section1");
262            thread::sleep(Duration::from_millis(20));
263            profiler.stop_timer("section1");
264
265            profiler.start_timer("section2");
266            thread::sleep(Duration::from_millis(10));
267            profiler.stop_timer("section2");
268
269            let report = profiler.get_report();
270            assert!(report.contains("Profiling Report"));
271            assert!(report.contains("section1"));
272            assert!(report.contains("section2"));
273            assert!(report.contains("Total execution time"));
274        } else {
275            let report = profiler.get_report();
276            assert_eq!(report, "Profiling disabled or no measurements recorded.");
277        }
278    }
279
280    #[test]
281    fn test_profiler_reset() {
282        let mut profiler = Profiler::new();
283
284        if PROFILING_ENABLED {
285            profiler.start_timer("test_section");
286            profiler.stop_timer("test_section");
287
288            assert!(!profiler.measurements.is_empty());
289
290            profiler.reset();
291            assert!(profiler.measurements.is_empty());
292            assert!(profiler.active_timers.is_empty());
293        }
294    }
295
296    #[test]
297    fn test_profiler_stop_without_start() {
298        let mut profiler = Profiler::new();
299
300        // Should not panic
301        profiler.stop_timer("nonexistent_section");
302        assert!(profiler.measurements.is_empty());
303    }
304
305    #[test]
306    #[serial_test::serial]
307    fn test_global_start_stop_timer() {
308        if PROFILING_ENABLED {
309            reset(); // Clear any previous measurements
310
311            start_timer("global_test");
312            thread::sleep(Duration::from_millis(10));
313            stop_timer("global_test");
314
315            let report = get_report();
316            assert!(report.contains("global_test"));
317        }
318    }
319
320    #[test]
321    fn test_global_get_report() {
322        let report = get_report();
323        if PROFILING_ENABLED {
324            assert!(report.contains("Profiling Report") || report.contains("no measurements"));
325        } else {
326            assert_eq!(report, "Profiling is disabled.");
327        }
328    }
329
330    #[test]
331    #[serial_test::serial]
332    fn test_global_reset() {
333        if PROFILING_ENABLED {
334            start_timer("test_reset");
335            stop_timer("test_reset");
336
337            reset();
338            let report = get_report();
339            assert!(!report.contains("test_reset"));
340        }
341    }
342
343    #[test]
344    #[serial_test::serial]
345    fn test_scoped_timer() {
346        if PROFILING_ENABLED {
347            reset();
348
349            {
350                let _timer = ScopedTimer::new("scoped_test");
351                thread::sleep(Duration::from_millis(10));
352            } // Timer should stop here
353
354            let report = get_report();
355            assert!(report.contains("scoped_test"));
356        }
357    }
358
359    #[test]
360    fn test_scoped_timer_drop() {
361        let timer = ScopedTimer::new("drop_test");
362        assert_eq!(timer.section, "drop_test");
363        assert_eq!(timer.enabled, PROFILING_ENABLED);
364        // Timer will be dropped and stop_timer called
365    }
366
367    #[test]
368    fn test_empty_report() {
369        let profiler = Profiler::new();
370        let report = profiler.get_report();
371
372        if PROFILING_ENABLED {
373            assert_eq!(report, "Profiling disabled or no measurements recorded.");
374        }
375    }
376
377    #[test]
378    fn test_report_formatting() {
379        let mut profiler = Profiler::new();
380
381        if PROFILING_ENABLED {
382            // Create predictable measurements
383            profiler
384                .measurements
385                .insert("test1".to_string(), (Duration::from_secs(1), 10));
386            profiler
387                .measurements
388                .insert("test2".to_string(), (Duration::from_millis(500), 5));
389
390            let report = profiler.get_report();
391
392            // Check report structure
393            assert!(report.contains("Section"));
394            assert!(report.contains("Total Time (s)"));
395            assert!(report.contains("Calls"));
396            assert!(report.contains("Avg Time (ms)"));
397            assert!(report.contains("% of Total"));
398
399            // Check that test1 appears before test2 (sorted by duration)
400            let test1_pos = report.find("test1").unwrap();
401            let test2_pos = report.find("test2").unwrap();
402            assert!(test1_pos < test2_pos);
403        }
404    }
405
406    #[test]
407    #[serial_test::serial]
408    fn test_concurrent_access() {
409        use std::sync::Arc;
410        use std::sync::Barrier;
411
412        if PROFILING_ENABLED {
413            reset();
414
415            let barrier = Arc::new(Barrier::new(3));
416            let mut handles = vec![];
417
418            for i in 0..3 {
419                let b = barrier.clone();
420                let handle = thread::spawn(move || {
421                    b.wait();
422                    start_timer(&format!("thread_{i}"));
423                    thread::sleep(Duration::from_millis(10));
424                    stop_timer(&format!("thread_{i}"));
425                });
426                handles.push(handle);
427            }
428
429            for handle in handles {
430                handle.join().unwrap();
431            }
432
433            let report = get_report();
434            assert!(report.contains("thread_0"));
435            assert!(report.contains("thread_1"));
436            assert!(report.contains("thread_2"));
437        }
438    }
439}