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    fn test_global_start_stop_timer() {
302        if PROFILING_ENABLED {
303            reset(); // Clear any previous measurements
304
305            start_timer("global_test");
306            thread::sleep(Duration::from_millis(10));
307            stop_timer("global_test");
308
309            let report = get_report();
310            assert!(report.contains("global_test"));
311        }
312    }
313
314    #[test]
315    fn test_global_get_report() {
316        let report = get_report();
317        if PROFILING_ENABLED {
318            assert!(report.contains("Profiling Report") || report.contains("no measurements"));
319        } else {
320            assert_eq!(report, "Profiling is disabled.");
321        }
322    }
323
324    #[test]
325    fn test_global_reset() {
326        if PROFILING_ENABLED {
327            start_timer("test_reset");
328            stop_timer("test_reset");
329
330            reset();
331            let report = get_report();
332            assert!(!report.contains("test_reset"));
333        }
334    }
335
336    #[test]
337    fn test_scoped_timer() {
338        if PROFILING_ENABLED {
339            reset();
340
341            {
342                let _timer = ScopedTimer::new("scoped_test");
343                thread::sleep(Duration::from_millis(10));
344            } // Timer should stop here
345
346            let report = get_report();
347            assert!(report.contains("scoped_test"));
348        }
349    }
350
351    #[test]
352    fn test_scoped_timer_drop() {
353        let timer = ScopedTimer::new("drop_test");
354        assert_eq!(timer.section, "drop_test");
355        assert_eq!(timer.enabled, PROFILING_ENABLED);
356        // Timer will be dropped and stop_timer called
357    }
358
359    #[test]
360    fn test_empty_report() {
361        let profiler = Profiler::new();
362        let report = profiler.get_report();
363
364        if PROFILING_ENABLED {
365            assert_eq!(report, "Profiling disabled or no measurements recorded.");
366        }
367    }
368
369    #[test]
370    fn test_report_formatting() {
371        let mut profiler = Profiler::new();
372
373        if PROFILING_ENABLED {
374            // Create predictable measurements
375            profiler
376                .measurements
377                .insert("test1".to_string(), (Duration::from_secs(1), 10));
378            profiler
379                .measurements
380                .insert("test2".to_string(), (Duration::from_millis(500), 5));
381
382            let report = profiler.get_report();
383
384            // Check report structure
385            assert!(report.contains("Section"));
386            assert!(report.contains("Total Time (s)"));
387            assert!(report.contains("Calls"));
388            assert!(report.contains("Avg Time (ms)"));
389            assert!(report.contains("% of Total"));
390
391            // Check that test1 appears before test2 (sorted by duration)
392            let test1_pos = report.find("test1").unwrap();
393            let test2_pos = report.find("test2").unwrap();
394            assert!(test1_pos < test2_pos);
395        }
396    }
397
398    #[test]
399    fn test_concurrent_access() {
400        use std::sync::Arc;
401        use std::sync::Barrier;
402
403        if PROFILING_ENABLED {
404            reset();
405
406            let barrier = Arc::new(Barrier::new(3));
407            let mut handles = vec![];
408
409            for i in 0..3 {
410                let b = barrier.clone();
411                let handle = thread::spawn(move || {
412                    b.wait();
413                    start_timer(&format!("thread_{i}"));
414                    thread::sleep(Duration::from_millis(10));
415                    stop_timer(&format!("thread_{i}"));
416                });
417                handles.push(handle);
418            }
419
420            for handle in handles {
421                handle.join().unwrap();
422            }
423
424            let report = get_report();
425            assert!(report.contains("thread_0"));
426            assert!(report.contains("thread_1"));
427            assert!(report.contains("thread_2"));
428        }
429    }
430}