1use std::collections::HashMap;
5use std::sync::LazyLock;
6use std::sync::Mutex;
7use std::time::{Duration, Instant};
8
9static PROFILER: LazyLock<Mutex<Profiler>> = LazyLock::new(|| Mutex::new(Profiler::new()));
11
12#[cfg(feature = "profiling")]
14pub(crate) const PROFILING_ENABLED: bool = true;
15
16#[cfg(not(feature = "profiling"))]
17pub(crate) const PROFILING_ENABLED: bool = false;
18
19pub 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 pub fn new() -> Self {
34 Profiler {
35 measurements: HashMap::new(),
36 active_timers: HashMap::new(),
37 }
38 }
39
40 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 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(§ion_name) {
53 let elapsed = start_time.elapsed();
54 self.record_duration(§ion_name, elapsed);
55 }
56 }
57 }
58
59 pub fn record_duration(&mut self, section: &str, duration: Duration) {
61 if PROFILING_ENABLED {
62 let entry = self
63 .measurements
64 .entry(section.to_string())
65 .or_insert((Duration::new(0, 0), 0));
66 entry.0 += duration;
67 entry.1 += 1;
68 }
69 }
70
71 pub fn get_report(&self) -> String {
73 if !PROFILING_ENABLED || self.measurements.is_empty() {
74 return "Profiling disabled or no measurements recorded.".to_string();
75 }
76
77 let mut sorted_measurements: Vec<_> = self.measurements.iter().collect();
79 sorted_measurements.sort_by(|a, b| b.1.0.cmp(&a.1.0));
80
81 let total_time: Duration = sorted_measurements.iter().map(|(_, (duration, _))| duration).sum();
83
84 let mut report = String::new();
86 report.push_str("=== Profiling Report ===\n");
87 report.push_str(&format!(
88 "Total measured time: {:.6} seconds (parallel sections may overlap)\n\n",
89 total_time.as_secs_f64()
90 ));
91 report.push_str(
92 "Section | Total Time (s) | Calls | Avg Time (ms) | % of Total\n",
93 );
94 report.push_str(
95 "------------------------------------------|----------------|-------|---------------|----------\n",
96 );
97
98 for (section, (duration, calls)) in sorted_measurements {
99 let total_seconds = duration.as_secs_f64();
100 let avg_ms = (duration.as_nanos() as f64) / (calls * 1_000_000) as f64;
101 let percentage = (total_seconds / total_time.as_secs_f64()) * 100.0;
102
103 report.push_str(&format!(
104 "{section:<42} | {total_seconds:<14.6} | {calls:<5} | {avg_ms:<13.3} | {percentage:<8.2}%\n"
105 ));
106 }
107
108 report
109 }
110
111 pub fn reset(&mut self) {
113 self.measurements.clear();
114 self.active_timers.clear();
115 }
116}
117
118pub fn start_timer(section: &str) {
122 if PROFILING_ENABLED && let Ok(mut profiler) = PROFILER.lock() {
123 profiler.start_timer(section);
124 }
125}
126
127pub fn stop_timer(section: &str) {
131 if PROFILING_ENABLED && let Ok(mut profiler) = PROFILER.lock() {
132 profiler.stop_timer(section);
133 }
134}
135
136pub fn record_duration(section: &str, duration: Duration) {
140 if PROFILING_ENABLED && let Ok(mut profiler) = PROFILER.lock() {
141 profiler.record_duration(section, duration);
142 }
143}
144
145pub fn get_report() -> String {
149 if PROFILING_ENABLED {
150 match PROFILER.lock() {
151 Ok(profiler) => profiler.get_report(),
152 Err(_) => "Profiling report unavailable (mutex poisoned).".to_string(),
153 }
154 } else {
155 "Profiling is disabled.".to_string()
156 }
157}
158
159pub fn reset() {
163 if PROFILING_ENABLED && let Ok(mut profiler) = PROFILER.lock() {
164 profiler.reset();
165 }
166}
167
168pub struct ScopedTimer {
170 section: String,
171 enabled: bool,
172 start: Option<Instant>,
173}
174
175impl ScopedTimer {
176 pub fn new(section: &str) -> Self {
178 let enabled = PROFILING_ENABLED;
179 ScopedTimer {
180 section: section.to_string(),
181 enabled,
182 start: enabled.then(Instant::now),
183 }
184 }
185}
186
187impl Drop for ScopedTimer {
188 fn drop(&mut self) {
189 if self.enabled
190 && let Some(start) = self.start.take()
191 {
192 record_duration(&self.section, start.elapsed());
193 }
194 }
195}
196
197#[cfg(feature = "profiling")]
203#[macro_export]
204macro_rules! time_section {
205 ($section:expr, $block:block) => {{
206 let _timer = $crate::profiling::ScopedTimer::new($section);
207 $block
208 }};
209}
210
211#[cfg(not(feature = "profiling"))]
212#[macro_export]
213macro_rules! time_section {
214 ($section:expr, $block:block) => {{ $block }};
215}
216
217#[cfg(feature = "profiling")]
221#[macro_export]
222macro_rules! time_function {
223 ($section:expr, $func:expr) => {{
224 let _timer = $crate::profiling::ScopedTimer::new($section);
225 $func
226 }};
227}
228
229#[cfg(not(feature = "profiling"))]
230#[macro_export]
231macro_rules! time_function {
232 ($section:expr, $func:expr) => {{ $func }};
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use std::thread;
239 use std::time::Duration;
240
241 #[test]
242 fn test_profiler_new() {
243 let profiler = Profiler::new();
244 assert!(profiler.measurements.is_empty());
245 assert!(profiler.active_timers.is_empty());
246 }
247
248 #[test]
249 fn test_profiler_default() {
250 let profiler = Profiler::default();
251 assert!(profiler.measurements.is_empty());
252 assert!(profiler.active_timers.is_empty());
253 }
254
255 #[test]
256 fn test_profiler_start_stop_timer() {
257 let mut profiler = Profiler::new();
258
259 if PROFILING_ENABLED {
261 profiler.start_timer("test_section");
262 thread::sleep(Duration::from_millis(10));
263 profiler.stop_timer("test_section");
264
265 assert!(profiler.measurements.contains_key("test_section"));
266 let (duration, count) = profiler.measurements.get("test_section").unwrap();
267 assert!(*count == 1);
268 assert!(duration.as_millis() >= 10);
269 }
270 }
271
272 #[test]
273 fn test_profiler_multiple_measurements() {
274 let mut profiler = Profiler::new();
275
276 if PROFILING_ENABLED {
277 for _ in 0..3 {
279 profiler.start_timer("test_section");
280 thread::sleep(Duration::from_millis(5));
281 profiler.stop_timer("test_section");
282 }
283
284 assert!(profiler.measurements.contains_key("test_section"));
285 let (duration, count) = profiler.measurements.get("test_section").unwrap();
286 assert_eq!(*count, 3);
287 assert!(duration.as_millis() >= 15);
288 }
289 }
290
291 #[test]
292 fn test_profiler_get_report() {
293 let mut profiler = Profiler::new();
294
295 if PROFILING_ENABLED {
296 profiler.start_timer("section1");
297 thread::sleep(Duration::from_millis(20));
298 profiler.stop_timer("section1");
299
300 profiler.start_timer("section2");
301 thread::sleep(Duration::from_millis(10));
302 profiler.stop_timer("section2");
303
304 let report = profiler.get_report();
305 assert!(report.contains("Profiling Report"));
306 assert!(report.contains("section1"));
307 assert!(report.contains("section2"));
308 assert!(report.contains("Total measured time"));
309 } else {
310 let report = profiler.get_report();
311 assert_eq!(report, "Profiling disabled or no measurements recorded.");
312 }
313 }
314
315 #[test]
316 fn test_profiler_reset() {
317 let mut profiler = Profiler::new();
318
319 if PROFILING_ENABLED {
320 profiler.start_timer("test_section");
321 profiler.stop_timer("test_section");
322
323 assert!(!profiler.measurements.is_empty());
324
325 profiler.reset();
326 assert!(profiler.measurements.is_empty());
327 assert!(profiler.active_timers.is_empty());
328 }
329 }
330
331 #[test]
332 fn test_profiler_stop_without_start() {
333 let mut profiler = Profiler::new();
334
335 profiler.stop_timer("nonexistent_section");
337 assert!(profiler.measurements.is_empty());
338 }
339
340 #[test]
341 #[serial_test::serial]
342 fn test_global_start_stop_timer() {
343 if PROFILING_ENABLED {
344 reset(); start_timer("global_test");
347 thread::sleep(Duration::from_millis(10));
348 stop_timer("global_test");
349
350 let report = get_report();
351 assert!(report.contains("global_test"));
352 }
353 }
354
355 #[test]
356 fn test_global_get_report() {
357 let report = get_report();
358 if PROFILING_ENABLED {
359 assert!(report.contains("Profiling Report") || report.contains("no measurements"));
360 } else {
361 assert_eq!(report, "Profiling is disabled.");
362 }
363 }
364
365 #[test]
366 #[serial_test::serial]
367 fn test_global_reset() {
368 if PROFILING_ENABLED {
369 start_timer("test_reset");
370 stop_timer("test_reset");
371
372 reset();
373 let report = get_report();
374 assert!(!report.contains("test_reset"));
375 }
376 }
377
378 #[test]
379 #[serial_test::serial]
380 fn test_scoped_timer() {
381 if PROFILING_ENABLED {
382 reset();
383
384 {
385 let _timer = ScopedTimer::new("scoped_test");
386 thread::sleep(Duration::from_millis(10));
387 } let report = get_report();
390 assert!(report.contains("scoped_test"));
391 }
392 }
393
394 #[test]
395 fn test_scoped_timer_drop() {
396 let timer = ScopedTimer::new("drop_test");
397 assert_eq!(timer.section, "drop_test");
398 assert_eq!(timer.enabled, PROFILING_ENABLED);
399 }
401
402 #[test]
403 fn test_empty_report() {
404 let profiler = Profiler::new();
405 let report = profiler.get_report();
406
407 if PROFILING_ENABLED {
408 assert_eq!(report, "Profiling disabled or no measurements recorded.");
409 }
410 }
411
412 #[test]
413 fn test_report_formatting() {
414 let mut profiler = Profiler::new();
415
416 if PROFILING_ENABLED {
417 profiler
419 .measurements
420 .insert("test1".to_string(), (Duration::from_secs(1), 10));
421 profiler
422 .measurements
423 .insert("test2".to_string(), (Duration::from_millis(500), 5));
424
425 let report = profiler.get_report();
426
427 assert!(report.contains("Section"));
429 assert!(report.contains("Total Time (s)"));
430 assert!(report.contains("Calls"));
431 assert!(report.contains("Avg Time (ms)"));
432 assert!(report.contains("% of Total"));
433
434 let test1_pos = report.find("test1").unwrap();
436 let test2_pos = report.find("test2").unwrap();
437 assert!(test1_pos < test2_pos);
438 }
439 }
440
441 #[test]
442 #[serial_test::serial]
443 fn test_concurrent_access() {
444 use std::sync::Arc;
445 use std::sync::Barrier;
446
447 if PROFILING_ENABLED {
448 reset();
449
450 let barrier = Arc::new(Barrier::new(3));
451 let mut handles = vec![];
452
453 for i in 0..3 {
454 let b = barrier.clone();
455 let handle = thread::spawn(move || {
456 b.wait();
457 start_timer(&format!("thread_{i}"));
458 thread::sleep(Duration::from_millis(10));
459 stop_timer(&format!("thread_{i}"));
460 });
461 handles.push(handle);
462 }
463
464 for handle in handles {
465 handle.join().unwrap();
466 }
467
468 let report = get_report();
469 assert!(report.contains("thread_0"));
470 assert!(report.contains("thread_1"));
471 assert!(report.contains("thread_2"));
472 }
473 }
474}