1#![allow(clippy::cast_precision_loss)] #![allow(clippy::cast_possible_wrap)] use std::collections::HashMap;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8use sysinfo::System;
9use tracing::{Level, debug, info, span};
10
11#[derive(Debug, Clone)]
13pub struct PerformanceMetrics {
14 pub search_count: Arc<AtomicU64>,
16 pub total_search_time: Arc<AtomicU64>,
18 pub index_build_count: Arc<AtomicU64>,
20 pub total_index_time: Arc<AtomicU64>,
22 pub bytes_processed: Arc<AtomicU64>,
24 pub lines_searched: Arc<AtomicU64>,
26}
27
28impl Default for PerformanceMetrics {
29 fn default() -> Self {
30 Self {
31 search_count: Arc::new(AtomicU64::new(0)),
32 total_search_time: Arc::new(AtomicU64::new(0)),
33 index_build_count: Arc::new(AtomicU64::new(0)),
34 total_index_time: Arc::new(AtomicU64::new(0)),
35 bytes_processed: Arc::new(AtomicU64::new(0)),
36 lines_searched: Arc::new(AtomicU64::new(0)),
37 }
38 }
39}
40
41impl PerformanceMetrics {
42 #[allow(clippy::cast_possible_truncation)] pub fn record_search(&self, duration: Duration, lines_count: usize) {
45 self.search_count.fetch_add(1, Ordering::Relaxed);
46 let inc = duration.as_micros().min(u128::from(u64::MAX)) as u64;
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(lines_count as u64, Ordering::Relaxed);
54 }
55
56 #[allow(clippy::cast_possible_truncation)] pub fn record_index_build(&self, duration: Duration, bytes_count: usize) {
59 self.index_build_count.fetch_add(1, Ordering::Relaxed);
60 let inc = duration.as_micros().min(u128::from(u64::MAX)) as u64;
61 let _ = self
62 .total_index_time
63 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |cur| {
64 Some(cur.saturating_add(inc))
65 });
66 self.bytes_processed
67 .fetch_add(bytes_count as u64, Ordering::Relaxed);
68 }
69
70 #[allow(clippy::cast_precision_loss)] #[must_use]
73 pub fn avg_search_time_micros(&self) -> f64 {
74 let count = self.search_count.load(Ordering::Relaxed);
75 let total = self.total_search_time.load(Ordering::Relaxed);
76 if count == 0 {
77 0.0
78 } else {
79 total as f64 / count as f64
80 }
81 }
82
83 #[allow(clippy::cast_precision_loss)] #[must_use]
86 pub fn avg_index_time_millis(&self) -> f64 {
87 let count = self.index_build_count.load(Ordering::Relaxed);
88 let total = self.total_index_time.load(Ordering::Relaxed);
89 if count == 0 {
90 0.0
91 } else {
92 (total as f64 / count as f64) / 1000.0
93 }
94 }
95
96 #[must_use]
98 pub fn search_throughput_lines_per_sec(&self) -> f64 {
99 let lines = self.lines_searched.load(Ordering::Relaxed);
100 let time_seconds = (self.total_search_time.load(Ordering::Relaxed) as f64) / 1_000_000.0;
101 if time_seconds == 0.0 {
102 0.0
103 } else {
104 lines as f64 / time_seconds
105 }
106 }
107
108 #[must_use]
110 pub fn index_throughput_mbps(&self) -> f64 {
111 let bytes = self.bytes_processed.load(Ordering::Relaxed);
112 let time_seconds = (self.total_index_time.load(Ordering::Relaxed) as f64) / 1_000_000.0;
113 if time_seconds == 0.0 {
114 0.0
115 } else {
116 (bytes as f64 / (1024.0 * 1024.0)) / time_seconds
117 }
118 }
119
120 pub fn print_summary(&self) {
122 let searches = self.search_count.load(Ordering::Relaxed);
123 let indexes = self.index_build_count.load(Ordering::Relaxed);
124
125 println!("\n{}", "Performance Summary".bold());
126 println!("{}", "===================".bold());
127
128 if searches > 0 {
129 println!("Search Operations:");
130 println!(" Total searches: {searches}");
131 println!(
132 " Average time: {:.2}ms",
133 self.avg_search_time_micros() / 1000.0
134 );
135 println!(
136 " Total lines searched: {}",
137 self.lines_searched.load(Ordering::Relaxed)
138 );
139 println!(
140 " Throughput: {:.0} lines/sec",
141 self.search_throughput_lines_per_sec()
142 );
143 }
144
145 if indexes > 0 {
146 println!("Index Operations:");
147 println!(" Total builds: {indexes}");
148 println!(" Average time: {:.2}ms", self.avg_index_time_millis());
149 println!(
150 " Total bytes processed: {}",
151 format_bytes(self.bytes_processed.load(Ordering::Relaxed))
152 );
153 println!(" Throughput: {:.2} MB/s", self.index_throughput_mbps());
154 }
155 }
156}
157
158pub struct OperationTimer {
160 start: Instant,
162 operation: String,
164 metrics: Option<PerformanceMetrics>,
166}
167
168impl OperationTimer {
169 pub fn new(operation: &str) -> Self {
171 info!("Starting operation: {}", operation);
172 Self {
173 start: Instant::now(),
174 operation: operation.to_string(),
175 metrics: None,
176 }
177 }
178
179 pub fn with_metrics(operation: &str, metrics: PerformanceMetrics) -> Self {
181 info!("Starting operation with metrics: {}", operation);
182 Self {
183 start: Instant::now(),
184 operation: operation.to_string(),
185 metrics: Some(metrics),
186 }
187 }
188
189 pub fn finish(self) -> Duration {
191 let duration = self.start.elapsed();
192 info!(
193 "Completed {}: {:.2}ms",
194 self.operation,
195 duration.as_millis()
196 );
197 duration
198 }
199
200 pub fn finish_search(self, lines_count: usize) -> Duration {
202 let duration = self.start.elapsed();
203 info!(
204 "Completed {} search: {:.2}ms ({} lines)",
205 self.operation,
206 duration.as_millis(),
207 lines_count
208 );
209
210 if let Some(metrics) = &self.metrics {
211 metrics.record_search(duration, lines_count);
212 }
213 duration
214 }
215
216 pub fn finish_index(self, bytes_count: usize) -> Duration {
218 let duration = self.start.elapsed();
219 info!(
220 "Completed {} indexing: {:.2}ms ({} bytes)",
221 self.operation,
222 duration.as_millis(),
223 bytes_count
224 );
225
226 if let Some(metrics) = &self.metrics {
227 metrics.record_index_build(duration, bytes_count);
228 }
229 duration
230 }
231}
232
233#[derive(Debug, Default)]
235pub struct ComponentTimings {
236 timings: HashMap<String, Duration>,
238}
239
240impl ComponentTimings {
241 #[must_use]
243 pub fn new() -> Self {
244 Self::default()
245 }
246
247 pub fn time<T, F>(&mut self, component: &str, operation: F) -> T
249 where
250 F: FnOnce() -> T,
251 {
252 let _span = span!(Level::DEBUG, "component_timing", component = component);
253 let start = Instant::now();
254 let result = operation();
255 let duration = start.elapsed();
256
257 self.timings.insert(
258 component.to_string(),
259 self.timings.get(component).copied().unwrap_or_default() + duration,
260 );
261
262 debug!("Component {}: {:.2}ms", component, duration.as_millis());
263 result
264 }
265
266 #[must_use]
268 pub fn get_timing(&self, component: &str) -> Option<Duration> {
269 self.timings.get(component).copied()
270 }
271
272 #[must_use]
274 pub fn total_time(&self) -> Duration {
275 self.timings.values().sum()
276 }
277
278 pub fn print_breakdown(&self) {
280 if self.timings.is_empty() {
281 return;
282 }
283
284 let total = self.total_time();
285 println!("\n{}", "Component Breakdown".bold());
286 println!("{}", "==================".bold());
287
288 let mut sorted_timings: Vec<_> = self.timings.iter().collect();
289 sorted_timings.sort_by(|a, b| b.1.cmp(a.1));
290
291 for (component, duration) in sorted_timings {
292 let percentage = if total.as_micros() > 0 {
293 (duration.as_micros() as f64 / total.as_micros() as f64) * 100.0
294 } else {
295 0.0
296 };
297
298 println!(
299 " {:<20}: {:>8.2}ms ({:>5.1}%)",
300 component,
301 duration.as_millis(),
302 percentage
303 );
304 }
305
306 println!(" {:<20}: {:>8.2}ms", "TOTAL", total.as_millis());
307 }
308}
309
310pub struct ResourceMonitor {
312 system: System,
314 pid: u32,
316 initial_memory: u64,
318}
319
320impl Default for ResourceMonitor {
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326impl ResourceMonitor {
327 #[must_use]
329 pub fn new() -> Self {
330 let mut system = System::new_all();
331 system.refresh_all();
332 let pid = std::process::id();
333
334 let initial_memory = system
335 .process(sysinfo::Pid::from(pid as usize))
336 .map_or(0, sysinfo::Process::memory);
337
338 Self {
339 system,
340 pid,
341 initial_memory,
342 }
343 }
344
345 pub fn refresh(&mut self) {
347 self.system.refresh_all();
348 }
349
350 pub fn current_memory_mb(&mut self) -> f64 {
352 self.refresh();
353 self.system
354 .process(sysinfo::Pid::from(self.pid as usize))
355 .map_or(0.0, |process| process.memory() as f64 / (1024.0 * 1024.0))
356 }
357
358 pub fn memory_delta_mb(&mut self) -> f64 {
360 self.refresh();
361 if let Some(process) = self.system.process(sysinfo::Pid::from(self.pid as usize)) {
362 let current = process.memory();
363 (current as i64 - self.initial_memory as i64) as f64 / (1024.0 * 1024.0)
364 } else {
365 0.0
366 }
367 }
368
369 pub fn cpu_usage(&mut self) -> f32 {
371 self.refresh();
372 self.system
373 .process(sysinfo::Pid::from(self.pid as usize))
374 .map_or(0.0, sysinfo::Process::cpu_usage)
375 }
376
377 pub fn print_resource_usage(&mut self) {
379 println!("\n{}", "Resource Usage".bold());
380 println!("{}", "==============".bold());
381 println!(
382 "Memory: {:.1} MB (Δ{:+.1} MB)",
383 self.current_memory_mb(),
384 self.memory_delta_mb()
385 );
386 println!("CPU: {:.1}%", self.cpu_usage());
387 }
388}
389
390#[cfg(feature = "flamegraph")]
392pub fn start_profiling() -> Result<pprof::ProfilerGuard<'static>, Box<dyn std::error::Error>> {
393 let guard = pprof::ProfilerGuardBuilder::default()
394 .frequency(1000) .blocklist(&["libc", "libgcc", "pthread", "vdso"])
396 .build()?;
397 Ok(guard)
398}
399
400#[cfg(feature = "flamegraph")]
402pub fn stop_profiling_and_report(
403 guard: &pprof::ProfilerGuard,
404) -> Result<(), Box<dyn std::error::Error>> {
405 match guard.report().build() {
406 Ok(report) => {
407 let file = std::fs::File::create("flamegraph.svg")?;
412 report.flamegraph(file)?;
413 println!("Flamegraph saved to flamegraph.svg");
414 },
415 Err(e) => {
416 eprintln!("Failed to generate profile report: {e}");
417 },
418 }
419 Ok(())
420}
421
422#[cfg(not(feature = "flamegraph"))]
424#[allow(clippy::unnecessary_wraps)] pub fn start_profiling() -> Result<(), Box<dyn std::error::Error>> {
426 debug!("CPU profiling not available (flamegraph feature not enabled)");
427 Ok(())
428}
429
430#[cfg(not(feature = "flamegraph"))]
432#[allow(clippy::unnecessary_wraps)] pub fn stop_profiling_and_report(_guard: ()) -> Result<(), Box<dyn std::error::Error>> {
434 debug!("CPU profiling not available (flamegraph feature not enabled)");
435 Ok(())
436}
437
438fn format_bytes(bytes: u64) -> String {
440 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
441 let mut size = bytes as f64;
442 let mut unit_index = 0;
443
444 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
445 size /= 1024.0;
446 unit_index += 1;
447 }
448
449 if unit_index == 0 {
450 format!("{} {}", bytes, UNITS[unit_index])
451 } else {
452 format!("{:.1} {}", size, UNITS[unit_index])
453 }
454}
455
456trait BoldFormat {
458 fn bold(&self) -> &Self;
459}
460
461impl BoldFormat for str {
462 fn bold(&self) -> &Self {
463 self
466 }
467}
468
469#[cfg(test)]
470#[allow(clippy::unwrap_used)]
471mod tests {
472 use super::*;
473 use std::thread;
474
475 #[test]
476 fn test_performance_metrics() {
477 let metrics = PerformanceMetrics::default();
478
479 metrics.record_search(Duration::from_millis(5), 1000);
481 metrics.record_search(Duration::from_millis(7), 1500);
482
483 assert_eq!(metrics.search_count.load(Ordering::Relaxed), 2);
484 assert_eq!(metrics.lines_searched.load(Ordering::Relaxed), 2500);
485
486 assert!((metrics.avg_search_time_micros() - 6000.0).abs() < 1.0);
488 }
489
490 #[test]
491 fn test_operation_timer() {
492 let timer = OperationTimer::new("test_operation");
493 thread::sleep(Duration::from_millis(1));
494 let duration = timer.finish();
495
496 assert!(duration >= Duration::from_millis(1));
497 }
498
499 #[test]
500 fn test_component_timings() {
501 let mut timings = ComponentTimings::new();
502
503 timings.time("parsing", || {
504 thread::sleep(Duration::from_millis(2));
505 "parsed"
506 });
507
508 timings.time("indexing", || {
509 thread::sleep(Duration::from_millis(3));
510 "indexed"
511 });
512
513 let parsing_time = timings.get_timing("parsing").unwrap();
514 let indexing_time = timings.get_timing("indexing").unwrap();
515
516 assert!(parsing_time >= Duration::from_millis(2));
517 assert!(indexing_time >= Duration::from_millis(3));
518 assert!(timings.total_time() >= Duration::from_millis(5));
519 }
520
521 #[test]
522 fn test_resource_monitor() {
523 let mut monitor = ResourceMonitor::new();
524 let memory = monitor.current_memory_mb();
525 let _cpu = monitor.cpu_usage();
526
527 assert!(memory > 0.0, "Should report some memory usage");
528 }
529
530 #[test]
531 fn test_format_bytes() {
532 assert_eq!(format_bytes(500), "500 B");
533 assert_eq!(format_bytes(1536), "1.5 KB");
534 assert_eq!(format_bytes(1_048_576), "1.0 MB");
535 assert_eq!(format_bytes(2_097_152), "2.0 MB");
536 }
537}