blz_core/
profiling.rs

1#![allow(clippy::cast_precision_loss)] // Performance metrics inherently lose precision when converting to f64
2#![allow(clippy::cast_possible_wrap)] // Wrapping is acceptable for memory delta calculations
3
4use 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/// Global performance metrics collector
12#[derive(Debug, Clone)]
13pub struct PerformanceMetrics {
14    /// Number of search operations performed
15    pub search_count: Arc<AtomicU64>,
16    /// Total time spent in search operations (in microseconds)
17    pub total_search_time: Arc<AtomicU64>,
18    /// Number of index build operations performed
19    pub index_build_count: Arc<AtomicU64>,
20    /// Total time spent in index build operations (in microseconds)
21    pub total_index_time: Arc<AtomicU64>,
22    /// Total bytes processed during indexing
23    pub bytes_processed: Arc<AtomicU64>,
24    /// Total lines searched across all operations
25    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    /// Record a search operation
43    #[allow(clippy::cast_possible_truncation)] // Saturating at u64::MAX is acceptable for timing metrics
44    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    /// Record an index build operation
57    #[allow(clippy::cast_possible_truncation)] // Saturating at u64::MAX is acceptable for timing metrics
58    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    /// Get average search time in microseconds
71    #[allow(clippy::cast_precision_loss)] // Precision loss is acceptable for performance metrics
72    #[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    /// Get average index build time in milliseconds
84    #[allow(clippy::cast_precision_loss)] // Precision loss is acceptable for performance metrics
85    #[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    /// Get throughput in lines per second for search operations
97    #[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    /// Get processing throughput in MB/s for indexing operations
109    #[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    /// Print performance summary
121    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
158/// Timer for measuring operation duration with automatic metrics recording
159pub struct OperationTimer {
160    /// Start time of the operation
161    start: Instant,
162    /// Name or description of the operation being timed
163    operation: String,
164    /// Optional metrics collector for recording results
165    metrics: Option<PerformanceMetrics>,
166}
167
168impl OperationTimer {
169    /// Creates a new operation timer with basic logging
170    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    /// Creates a new operation timer with metrics collection
180    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    /// Finish timing and optionally record metrics
190    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    /// Finish timing a search operation with line count
201    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    /// Finish timing an index operation with byte count
217    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/// Component-level timing breakdown for detailed analysis
234#[derive(Debug, Default)]
235pub struct ComponentTimings {
236    /// Map of component names to their cumulative durations
237    timings: HashMap<String, Duration>,
238}
239
240impl ComponentTimings {
241    /// Creates a new component timings tracker
242    #[must_use]
243    pub fn new() -> Self {
244        Self::default()
245    }
246
247    /// Times an operation and records it under the given component name
248    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    /// Gets the cumulative timing for a specific component
267    #[must_use]
268    pub fn get_timing(&self, component: &str) -> Option<Duration> {
269        self.timings.get(component).copied()
270    }
271
272    /// Calculates the total time across all components
273    #[must_use]
274    pub fn total_time(&self) -> Duration {
275        self.timings.values().sum()
276    }
277
278    /// Prints a formatted breakdown of component timings
279    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
310/// System resource monitor for memory and CPU usage
311pub struct ResourceMonitor {
312    /// System information collector from sysinfo crate
313    system: System,
314    /// Process ID of the current process
315    pid: u32,
316    /// Memory usage at monitor creation time (in bytes)
317    initial_memory: u64,
318}
319
320impl Default for ResourceMonitor {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326impl ResourceMonitor {
327    /// Creates a new resource monitor and captures initial state
328    #[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    /// Refreshes system information
346    pub fn refresh(&mut self) {
347        self.system.refresh_all();
348    }
349
350    /// Gets the current memory usage in megabytes
351    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    /// Gets the memory usage change since initialization in megabytes
359    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    /// Gets the current CPU usage percentage for this process
370    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    /// Prints formatted resource usage information
378    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/// Start CPU profiling (requires --features=flamegraph)
391#[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) // 1kHz sampling
395        .blocklist(&["libc", "libgcc", "pthread", "vdso"])
396        .build()?;
397    Ok(guard)
398}
399
400/// Stop profiling and generate flamegraph
401#[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            // Note: Protobuf output temporarily disabled due to API changes
408            // TODO: Re-enable once pprof protobuf API is clarified
409
410            // Generate flamegraph if possible
411            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/// Fallback profiling stubs when flamegraph feature is disabled
423#[cfg(not(feature = "flamegraph"))]
424#[allow(clippy::unnecessary_wraps)] // Need to match the API of the feature-enabled version
425pub fn start_profiling() -> Result<(), Box<dyn std::error::Error>> {
426    debug!("CPU profiling not available (flamegraph feature not enabled)");
427    Ok(())
428}
429
430/// Stops CPU profiling and generates a flamegraph report (no-op when flamegraph feature is disabled)
431#[cfg(not(feature = "flamegraph"))]
432#[allow(clippy::unnecessary_wraps)] // Need to match the API of the feature-enabled version
433pub 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
438/// Format bytes in human-readable format
439fn 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
456// Extension trait to add bold formatting (simple implementation for this example)
457trait BoldFormat {
458    fn bold(&self) -> &Self;
459}
460
461impl BoldFormat for str {
462    fn bold(&self) -> &Self {
463        // In a real implementation, you might use colored crate or similar
464        // For now, just return the string as-is
465        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        // Record some search operations
480        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        // Average should be 6ms = 6000 microseconds
487        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}