1#![allow(clippy::cast_possible_wrap)] use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{Duration, Instant};
7use sysinfo::System;
8use tracing::{Level, debug, info, span};
9
10use crate::numeric::{duration_to_micros_saturating, safe_average, u64_to_f64_lossy, usize_to_u64};
11
12#[derive(Debug, Clone)]
14pub struct PerformanceMetrics {
15 pub search_count: Arc<AtomicU64>,
17 pub total_search_time: Arc<AtomicU64>,
19 pub index_build_count: Arc<AtomicU64>,
21 pub total_index_time: Arc<AtomicU64>,
23 pub bytes_processed: Arc<AtomicU64>,
25 pub lines_searched: Arc<AtomicU64>,
27}
28
29impl Default for PerformanceMetrics {
30 fn default() -> Self {
31 Self {
32 search_count: Arc::new(AtomicU64::new(0)),
33 total_search_time: Arc::new(AtomicU64::new(0)),
34 index_build_count: Arc::new(AtomicU64::new(0)),
35 total_index_time: Arc::new(AtomicU64::new(0)),
36 bytes_processed: Arc::new(AtomicU64::new(0)),
37 lines_searched: Arc::new(AtomicU64::new(0)),
38 }
39 }
40}
41
42impl PerformanceMetrics {
43 pub fn record_search(&self, duration: Duration, lines_count: usize) {
45 self.search_count.fetch_add(1, Ordering::Relaxed);
46 let inc = duration_to_micros_saturating(duration);
47 let _ = self
48 .total_search_time
49 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |cur| {
50 Some(cur.saturating_add(inc))
51 });
52 self.lines_searched
53 .fetch_add(usize_to_u64(lines_count), Ordering::Relaxed);
54 }
55
56 pub fn record_index_build(&self, duration: Duration, bytes_count: usize) {
58 self.index_build_count.fetch_add(1, Ordering::Relaxed);
59 let inc = duration_to_micros_saturating(duration);
60 let _ = self
61 .total_index_time
62 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |cur| {
63 Some(cur.saturating_add(inc))
64 });
65 self.bytes_processed
66 .fetch_add(usize_to_u64(bytes_count), Ordering::Relaxed);
67 }
68
69 #[must_use]
71 pub fn avg_search_time_micros(&self) -> f64 {
72 let count = self.search_count.load(Ordering::Relaxed);
73 let total = self.total_search_time.load(Ordering::Relaxed);
74 safe_average(total, count)
75 }
76
77 #[must_use]
79 pub fn avg_index_time_millis(&self) -> f64 {
80 let count = self.index_build_count.load(Ordering::Relaxed);
81 let total = self.total_index_time.load(Ordering::Relaxed);
82 safe_average(total, count) / 1000.0
83 }
84
85 #[must_use]
87 pub fn search_throughput_lines_per_sec(&self) -> f64 {
88 let lines = self.lines_searched.load(Ordering::Relaxed);
89 let time_seconds =
90 u64_to_f64_lossy(self.total_search_time.load(Ordering::Relaxed)) / 1_000_000.0;
91 if time_seconds == 0.0 {
92 0.0
93 } else {
94 u64_to_f64_lossy(lines) / time_seconds
95 }
96 }
97
98 #[must_use]
100 pub fn index_throughput_mbps(&self) -> f64 {
101 let bytes = self.bytes_processed.load(Ordering::Relaxed);
102 let time_seconds =
103 u64_to_f64_lossy(self.total_index_time.load(Ordering::Relaxed)) / 1_000_000.0;
104 if time_seconds == 0.0 {
105 0.0
106 } else {
107 (u64_to_f64_lossy(bytes) / (1024.0 * 1024.0)) / time_seconds
108 }
109 }
110
111 pub fn print_summary(&self) {
113 let searches = self.search_count.load(Ordering::Relaxed);
114 let indexes = self.index_build_count.load(Ordering::Relaxed);
115
116 println!("\n{}", "Performance Summary".bold());
117 println!("{}", "===================".bold());
118
119 if searches > 0 {
120 println!("Search Operations:");
121 println!(" Total searches: {searches}");
122 println!(
123 " Average time: {:.2}ms",
124 self.avg_search_time_micros() / 1000.0
125 );
126 println!(
127 " Total lines searched: {}",
128 self.lines_searched.load(Ordering::Relaxed)
129 );
130 println!(
131 " Throughput: {:.0} lines/sec",
132 self.search_throughput_lines_per_sec()
133 );
134 }
135
136 if indexes > 0 {
137 println!("Index Operations:");
138 println!(" Total builds: {indexes}");
139 println!(" Average time: {:.2}ms", self.avg_index_time_millis());
140 println!(
141 " Total bytes processed: {}",
142 format_bytes(self.bytes_processed.load(Ordering::Relaxed))
143 );
144 println!(" Throughput: {:.2} MB/s", self.index_throughput_mbps());
145 }
146 }
147}
148
149pub struct OperationTimer {
151 start: Instant,
153 operation: String,
155 metrics: Option<PerformanceMetrics>,
157}
158
159impl OperationTimer {
160 pub fn new(operation: &str) -> Self {
162 info!("Starting operation: {}", operation);
163 Self {
164 start: Instant::now(),
165 operation: operation.to_string(),
166 metrics: None,
167 }
168 }
169
170 pub fn with_metrics(operation: &str, metrics: PerformanceMetrics) -> Self {
172 info!("Starting operation with metrics: {}", operation);
173 Self {
174 start: Instant::now(),
175 operation: operation.to_string(),
176 metrics: Some(metrics),
177 }
178 }
179
180 pub fn finish(self) -> Duration {
182 let duration = self.start.elapsed();
183 info!(
184 "Completed {}: {:.2}ms",
185 self.operation,
186 duration.as_millis()
187 );
188 duration
189 }
190
191 pub fn finish_search(self, lines_count: usize) -> Duration {
193 let duration = self.start.elapsed();
194 info!(
195 "Completed {} search: {:.2}ms ({} lines)",
196 self.operation,
197 duration.as_millis(),
198 lines_count
199 );
200
201 if let Some(metrics) = &self.metrics {
202 metrics.record_search(duration, lines_count);
203 }
204 duration
205 }
206
207 pub fn finish_index(self, bytes_count: usize) -> Duration {
209 let duration = self.start.elapsed();
210 info!(
211 "Completed {} indexing: {:.2}ms ({} bytes)",
212 self.operation,
213 duration.as_millis(),
214 bytes_count
215 );
216
217 if let Some(metrics) = &self.metrics {
218 metrics.record_index_build(duration, bytes_count);
219 }
220 duration
221 }
222}
223
224#[derive(Debug, Default)]
226pub struct ComponentTimings {
227 timings: HashMap<String, Duration>,
229}
230
231impl ComponentTimings {
232 #[must_use]
234 pub fn new() -> Self {
235 Self::default()
236 }
237
238 pub fn time<T, F>(&mut self, component: &str, operation: F) -> T
240 where
241 F: FnOnce() -> T,
242 {
243 let _span = span!(Level::DEBUG, "component_timing", component = component);
244 let start = Instant::now();
245 let result = operation();
246 let duration = start.elapsed();
247
248 self.timings.insert(
249 component.to_string(),
250 self.timings.get(component).copied().unwrap_or_default() + duration,
251 );
252
253 debug!("Component {}: {:.2}ms", component, duration.as_millis());
254 result
255 }
256
257 #[must_use]
259 pub fn get_timing(&self, component: &str) -> Option<Duration> {
260 self.timings.get(component).copied()
261 }
262
263 #[must_use]
265 pub fn total_time(&self) -> Duration {
266 self.timings.values().sum()
267 }
268
269 pub fn print_breakdown(&self) {
271 if self.timings.is_empty() {
272 return;
273 }
274
275 let total = self.total_time();
276 println!("\n{}", "Component Breakdown".bold());
277 println!("{}", "==================".bold());
278
279 let mut sorted_timings: Vec<_> = self.timings.iter().collect();
280 sorted_timings.sort_by(|a, b| b.1.cmp(a.1));
281
282 for (component, duration) in sorted_timings {
283 #[allow(clippy::cast_precision_loss)] let percentage = if total.as_micros() > 0 {
285 (duration.as_micros() as f64 / total.as_micros() as f64) * 100.0
286 } else {
287 0.0
288 };
289
290 println!(
291 " {:<20}: {:>8.2}ms ({:>5.1}%)",
292 component,
293 duration.as_millis(),
294 percentage
295 );
296 }
297
298 println!(" {:<20}: {:>8.2}ms", "TOTAL", total.as_millis());
299 }
300
301 pub fn print_breakdown_stderr(&self, source: Option<&str>) {
312 use std::fmt::Write as FmtWrite;
313 use std::io::Write as IoWrite;
314
315 if self.timings.is_empty() {
316 return;
317 }
318
319 let total = self.total_time();
320
321 let mut output = String::new();
323
324 output.push('\n');
326 if let Some(alias) = source {
327 let _ = writeln!(output, "[{alias}] Component Breakdown");
328 } else {
329 output.push_str("Component Breakdown\n");
330 }
331 output.push_str("==================\n");
332
333 let mut sorted_timings: Vec<_> = self.timings.iter().collect();
334 sorted_timings.sort_by(|a, b| b.1.cmp(a.1));
335
336 for (component, duration) in sorted_timings {
337 #[allow(clippy::cast_precision_loss)] let percentage = if total.as_micros() > 0 {
339 (duration.as_micros() as f64 / total.as_micros() as f64) * 100.0
340 } else {
341 0.0
342 };
343
344 let _ = writeln!(
345 output,
346 " {:<20}: {:>8}ms ({:>5.1}%)",
347 component,
348 duration.as_millis(),
349 percentage
350 );
351 }
352
353 let _ = writeln!(output, " {:<20}: {:>8}ms", "TOTAL", total.as_millis());
354
355 let stderr = std::io::stderr();
357 let mut handle = stderr.lock();
358 let _ = handle.write_all(output.as_bytes());
360 }
361}
362
363pub struct ResourceMonitor {
365 system: System,
367 pid: u32,
369 initial_memory: u64,
371}
372
373impl Default for ResourceMonitor {
374 fn default() -> Self {
375 Self::new()
376 }
377}
378
379impl ResourceMonitor {
380 #[must_use]
382 pub fn new() -> Self {
383 let mut system = System::new_all();
384 system.refresh_all();
385 let pid = std::process::id();
386
387 let initial_memory = system
388 .process(sysinfo::Pid::from(pid as usize))
389 .map_or(0, sysinfo::Process::memory);
390
391 Self {
392 system,
393 pid,
394 initial_memory,
395 }
396 }
397
398 pub fn refresh(&mut self) {
400 self.system.refresh_all();
401 }
402
403 #[allow(clippy::cast_precision_loss)] pub fn current_memory_mb(&mut self) -> f64 {
406 self.refresh();
407 self.system
408 .process(sysinfo::Pid::from(self.pid as usize))
409 .map_or(0.0, |process| process.memory() as f64 / (1024.0 * 1024.0))
410 }
411
412 #[allow(clippy::cast_precision_loss)] pub fn memory_delta_mb(&mut self) -> f64 {
415 self.refresh();
416 if let Some(process) = self.system.process(sysinfo::Pid::from(self.pid as usize)) {
417 let current = process.memory();
418 (current as i64 - self.initial_memory as i64) as f64 / (1024.0 * 1024.0)
419 } else {
420 0.0
421 }
422 }
423
424 pub fn cpu_usage(&mut self) -> f32 {
426 self.refresh();
427 self.system
428 .process(sysinfo::Pid::from(self.pid as usize))
429 .map_or(0.0, sysinfo::Process::cpu_usage)
430 }
431
432 pub fn print_resource_usage(&mut self) {
434 println!("\n{}", "Resource Usage".bold());
435 println!("{}", "==============".bold());
436 println!(
437 "Memory: {:.1} MB (Δ{:+.1} MB)",
438 self.current_memory_mb(),
439 self.memory_delta_mb()
440 );
441 println!("CPU: {:.1}%", self.cpu_usage());
442 }
443}
444
445#[cfg(feature = "flamegraph")]
451pub fn start_profiling() -> Result<pprof::ProfilerGuard<'static>, Box<dyn std::error::Error>> {
452 let guard = pprof::ProfilerGuardBuilder::default()
453 .frequency(1000) .blocklist(&["libc", "libgcc", "pthread", "vdso"])
455 .build()?;
456 Ok(guard)
457}
458
459#[cfg(feature = "flamegraph")]
465pub fn stop_profiling_and_report(
466 guard: &pprof::ProfilerGuard,
467) -> Result<(), Box<dyn std::error::Error>> {
468 match guard.report().build() {
469 Ok(report) => {
470 let file = std::fs::File::create("flamegraph.svg")?;
475 report.flamegraph(file)?;
476 println!("Flamegraph saved to flamegraph.svg");
477 },
478 Err(e) => {
479 eprintln!("Failed to generate profile report: {e}");
480 },
481 }
482 Ok(())
483}
484
485#[cfg(not(feature = "flamegraph"))]
491#[allow(clippy::unnecessary_wraps)] pub fn start_profiling() -> Result<(), Box<dyn std::error::Error>> {
493 debug!("CPU profiling not available (flamegraph feature not enabled)");
494 Ok(())
495}
496
497#[cfg(not(feature = "flamegraph"))]
503#[allow(clippy::unnecessary_wraps)] pub fn stop_profiling_and_report(_guard: ()) -> Result<(), Box<dyn std::error::Error>> {
505 debug!("CPU profiling not available (flamegraph feature not enabled)");
506 Ok(())
507}
508
509fn format_bytes(bytes: u64) -> String {
511 use crate::numeric::u64_to_f64_lossy;
512 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
513 let mut size = u64_to_f64_lossy(bytes);
514 let mut unit_index = 0;
515
516 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
517 size /= 1024.0;
518 unit_index += 1;
519 }
520
521 if unit_index == 0 {
522 format!("{} {}", bytes, UNITS[unit_index])
523 } else {
524 format!("{:.1} {}", size, UNITS[unit_index])
525 }
526}
527
528trait BoldFormat {
530 fn bold(&self) -> &Self;
531}
532
533impl BoldFormat for str {
534 fn bold(&self) -> &Self {
535 self
538 }
539}
540
541#[cfg(test)]
542#[allow(clippy::unwrap_used)]
543mod tests {
544 use super::*;
545 use std::thread;
546
547 #[test]
548 fn test_performance_metrics() {
549 let metrics = PerformanceMetrics::default();
550
551 metrics.record_search(Duration::from_millis(5), 1000);
553 metrics.record_search(Duration::from_millis(7), 1500);
554
555 assert_eq!(metrics.search_count.load(Ordering::Relaxed), 2);
556 assert_eq!(metrics.lines_searched.load(Ordering::Relaxed), 2500);
557
558 assert!((metrics.avg_search_time_micros() - 6000.0).abs() < 1.0);
560 }
561
562 #[test]
563 fn test_operation_timer() {
564 let timer = OperationTimer::new("test_operation");
565 thread::sleep(Duration::from_millis(1));
566 let duration = timer.finish();
567
568 assert!(duration >= Duration::from_millis(1));
569 }
570
571 #[test]
572 fn test_component_timings() {
573 let mut timings = ComponentTimings::new();
574
575 timings.time("parsing", || {
576 thread::sleep(Duration::from_millis(2));
577 "parsed"
578 });
579
580 timings.time("indexing", || {
581 thread::sleep(Duration::from_millis(3));
582 "indexed"
583 });
584
585 let parsing_time = timings.get_timing("parsing").unwrap();
586 let indexing_time = timings.get_timing("indexing").unwrap();
587
588 assert!(parsing_time >= Duration::from_millis(2));
589 assert!(indexing_time >= Duration::from_millis(3));
590 assert!(timings.total_time() >= Duration::from_millis(5));
591 }
592
593 #[test]
594 fn test_resource_monitor() {
595 let mut monitor = ResourceMonitor::new();
596 let memory = monitor.current_memory_mb();
597 let _cpu = monitor.cpu_usage();
598
599 assert!(memory > 0.0, "Should report some memory usage");
600 }
601
602 #[test]
603 fn test_format_bytes() {
604 assert_eq!(format_bytes(500), "500 B");
605 assert_eq!(format_bytes(1536), "1.5 KB");
606 assert_eq!(format_bytes(1_048_576), "1.0 MB");
607 assert_eq!(format_bytes(2_097_152), "2.0 MB");
608 }
609}