rumdl_lib/
profiling.rs

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