Skip to main content

aria2_core/util/
perf_monitor.rs

1//! Performance monitoring module for aria2-rust
2//!
3//! This module provides lightweight performance metrics collection with minimal overhead (< 1%).
4//! It tracks throughput, latency, memory usage, and lock wait times.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{Duration, Instant};
11
12/// Performance metrics snapshot
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Metrics {
15    /// Throughput in bytes per second
16    pub throughput: u64,
17    /// Latency in milliseconds
18    pub latency: u64,
19    /// Memory usage in bytes
20    pub memory_usage: u64,
21    /// Lock wait time in milliseconds
22    pub lock_wait_time: u64,
23    /// Timestamp when metrics were recorded (epoch millis)
24    pub timestamp: u64,
25    /// Optional label for the metric
26    pub label: Option<String>,
27}
28
29impl Metrics {
30    /// Create a new metrics snapshot
31    pub fn new(throughput: u64, latency: u64, memory_usage: u64, lock_wait_time: u64) -> Self {
32        Self {
33            throughput,
34            latency,
35            memory_usage,
36            lock_wait_time,
37            timestamp: std::time::SystemTime::now()
38                .duration_since(std::time::UNIX_EPOCH)
39                .unwrap_or_default()
40                .as_millis() as u64,
41            label: None,
42        }
43    }
44
45    /// Create metrics with a label
46    pub fn with_label(mut self, label: impl Into<String>) -> Self {
47        self.label = Some(label.into());
48        self
49    }
50
51    /// Calculate the overall performance score (0-100)
52    pub fn performance_score(&self) -> f64 {
53        // Simple scoring: higher throughput and lower latency/lock_wait is better
54        let throughput_score = (self.throughput as f64 / 1_000_000.0).min(50.0); // Max 50 points for throughput
55        let latency_penalty = (self.latency as f64 / 100.0).min(25.0); // Max 25 points penalty
56        let lock_penalty = (self.lock_wait_time as f64 / 100.0).min(25.0); // Max 25 points penalty
57
58        (throughput_score + 50.0 - latency_penalty - lock_penalty).clamp(0.0, 100.0)
59    }
60}
61
62impl Default for Metrics {
63    fn default() -> Self {
64        Self::new(0, 0, 0, 0)
65    }
66}
67
68/// Atomic counters for low-overhead metric collection
69#[derive(Debug)]
70pub struct AtomicMetrics {
71    throughput: AtomicU64,
72    latency: AtomicU64,
73    memory_usage: AtomicU64,
74    lock_wait_time: AtomicU64,
75    start_time: Instant,
76}
77
78impl AtomicMetrics {
79    /// Create new atomic metrics
80    pub fn new() -> Self {
81        Self {
82            throughput: AtomicU64::new(0),
83            latency: AtomicU64::new(0),
84            memory_usage: AtomicU64::new(0),
85            lock_wait_time: AtomicU64::new(0),
86            start_time: Instant::now(),
87        }
88    }
89
90    /// Record throughput (bytes/sec)
91    #[inline]
92    pub fn record_throughput(&self, bytes_per_sec: u64) {
93        self.throughput.store(bytes_per_sec, Ordering::Relaxed);
94    }
95
96    /// Record latency (ms)
97    #[inline]
98    pub fn record_latency(&self, ms: u64) {
99        self.latency.store(ms, Ordering::Relaxed);
100    }
101
102    /// Record memory usage (bytes)
103    #[inline]
104    pub fn record_memory(&self, bytes: u64) {
105        self.memory_usage.store(bytes, Ordering::Relaxed);
106    }
107
108    /// Record lock wait time (ms)
109    #[inline]
110    pub fn record_lock_wait(&self, ms: u64) {
111        self.lock_wait_time.fetch_add(ms, Ordering::Relaxed);
112    }
113
114    /// Snapshot current metrics
115    pub fn snapshot(&self) -> Metrics {
116        Metrics::new(
117            self.throughput.load(Ordering::Relaxed),
118            self.latency.load(Ordering::Relaxed),
119            self.memory_usage.load(Ordering::Relaxed),
120            self.lock_wait_time.load(Ordering::Relaxed),
121        )
122    }
123
124    /// Get elapsed time since monitoring started
125    pub fn elapsed(&self) -> Duration {
126        self.start_time.elapsed()
127    }
128
129    /// Reset all counters
130    pub fn reset(&self) {
131        self.throughput.store(0, Ordering::Relaxed);
132        self.latency.store(0, Ordering::Relaxed);
133        self.memory_usage.store(0, Ordering::Relaxed);
134        self.lock_wait_time.store(0, Ordering::Relaxed);
135    }
136}
137
138impl Default for AtomicMetrics {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144/// Performance report containing aggregated metrics
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct PerformanceReport {
147    /// Collection of metrics by label
148    pub metrics: HashMap<String, Vec<Metrics>>,
149    /// Report generation timestamp
150    pub generated_at: u64,
151    /// Total duration of monitoring (ms)
152    pub duration_ms: u64,
153    /// Summary statistics
154    pub summary: ReportSummary,
155}
156
157impl PerformanceReport {
158    /// Create a new performance report
159    pub fn new(metrics: HashMap<String, Vec<Metrics>>, duration_ms: u64) -> Self {
160        let summary = Self::calculate_summary(&metrics);
161        Self {
162            metrics,
163            generated_at: std::time::SystemTime::now()
164                .duration_since(std::time::UNIX_EPOCH)
165                .unwrap_or_default()
166                .as_millis() as u64,
167            duration_ms,
168            summary,
169        }
170    }
171
172    /// Calculate summary statistics from metrics
173    fn calculate_summary(metrics: &HashMap<String, Vec<Metrics>>) -> ReportSummary {
174        let mut total_throughput = 0u64;
175        let mut total_latency = 0u64;
176        let mut total_memory = 0u64;
177        let mut total_lock_wait = 0u64;
178        let mut count = 0usize;
179
180        for metric_list in metrics.values() {
181            for m in metric_list.iter() {
182                total_throughput += m.throughput;
183                total_latency += m.latency;
184                total_memory += m.memory_usage;
185                total_lock_wait += m.lock_wait_time;
186                count += 1;
187            }
188        }
189
190        ReportSummary {
191            avg_throughput: if count > 0 {
192                total_throughput / count as u64
193            } else {
194                0
195            },
196            avg_latency: if count > 0 {
197                total_latency / count as u64
198            } else {
199                0
200            },
201            avg_memory_usage: if count > 0 {
202                total_memory / count as u64
203            } else {
204                0
205            },
206            avg_lock_wait_time: if count > 0 {
207                total_lock_wait / count as u64
208            } else {
209                0
210            },
211            total_samples: count,
212        }
213    }
214}
215
216/// Summary statistics for the performance report
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ReportSummary {
219    /// Average throughput (bytes/sec)
220    pub avg_throughput: u64,
221    /// Average latency (ms)
222    pub avg_latency: u64,
223    /// Average memory usage (bytes)
224    pub avg_memory_usage: u64,
225    /// Average lock wait time (ms)
226    pub avg_lock_wait_time: u64,
227    /// Total number of samples
228    pub total_samples: usize,
229}
230
231/// Performance monitor for collecting and reporting metrics
232pub struct PerformanceMonitor {
233    metrics: Arc<tokio::sync::RwLock<HashMap<String, Vec<Metrics>>>>,
234    start_time: Instant,
235}
236
237impl PerformanceMonitor {
238    /// Create a new performance monitor
239    pub fn new() -> Self {
240        Self {
241            metrics: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
242            start_time: Instant::now(),
243        }
244    }
245
246    /// Get the elapsed time since monitoring started
247    pub fn elapsed(&self) -> Duration {
248        self.start_time.elapsed()
249    }
250}
251
252impl Default for PerformanceMonitor {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258impl PerformanceMonitor {
259    /// Record a metric with the given label
260    pub fn record_metric(&self, label: &str, metrics: Metrics) {
261        // Use try_write to avoid blocking in hot paths
262        // This is a trade-off: we might miss some metrics under high contention
263        // but it ensures minimal overhead
264        if let Ok(mut guard) = self.metrics.try_write() {
265            guard
266                .entry(label.to_string())
267                .or_insert_with(Vec::new)
268                .push(metrics);
269        }
270    }
271
272    /// Generate a performance report
273    pub fn generate_report(&self) -> PerformanceReport {
274        // Use try_read to avoid blocking in async contexts
275        // If we can't get the lock, return an empty report
276        let metrics = self
277            .metrics
278            .try_read()
279            .map(|g| g.clone())
280            .unwrap_or_else(|_| HashMap::new());
281        PerformanceReport::new(metrics, self.elapsed().as_millis() as u64)
282    }
283
284    /// Export metrics as JSON string
285    pub fn export_json(&self) -> String {
286        let report = self.generate_report();
287        serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
288    }
289
290    /// Export metrics as human-readable text
291    pub fn export_text(&self) -> String {
292        let report = self.generate_report();
293        let mut output = String::new();
294
295        output.push_str("Performance Report\n");
296        output.push_str("==================\n");
297        output.push_str(&format!("Generated at: {} ms\n", report.generated_at));
298        output.push_str(&format!("Duration: {} ms\n\n", report.duration_ms));
299
300        output.push_str("Summary:\n");
301        output.push_str("--------\n");
302        output.push_str(&format!(
303            "  Total samples: {}\n",
304            report.summary.total_samples
305        ));
306        output.push_str(&format!(
307            "  Avg throughput: {} bytes/sec\n",
308            report.summary.avg_throughput
309        ));
310        output.push_str(&format!(
311            "  Avg latency: {} ms\n",
312            report.summary.avg_latency
313        ));
314        output.push_str(&format!(
315            "  Avg memory usage: {} bytes\n",
316            report.summary.avg_memory_usage
317        ));
318        output.push_str(&format!(
319            "  Avg lock wait time: {} ms\n\n",
320            report.summary.avg_lock_wait_time
321        ));
322
323        output.push_str("Detailed Metrics:\n");
324        output.push_str("-----------------\n");
325        for (label, metrics_list) in &report.metrics {
326            output.push_str(&format!("\n[{}]\n", label));
327            for (i, m) in metrics_list.iter().enumerate() {
328                output.push_str(&format!(
329                    "  Sample {}: throughput={} B/s, latency={} ms, memory={} B, lock_wait={} ms\n",
330                    i + 1,
331                    m.throughput,
332                    m.latency,
333                    m.memory_usage,
334                    m.lock_wait_time
335                ));
336            }
337        }
338
339        output
340    }
341}
342
343/// RAII guard for measuring operation duration
344pub struct ScopedTimer {
345    label: String,
346    start: Instant,
347    monitor: Arc<PerformanceMonitor>,
348}
349
350impl ScopedTimer {
351    /// Create a new scoped timer
352    pub fn new(label: impl Into<String>, monitor: Arc<PerformanceMonitor>) -> Self {
353        Self {
354            label: label.into(),
355            start: Instant::now(),
356            monitor,
357        }
358    }
359}
360
361impl Drop for ScopedTimer {
362    fn drop(&mut self) {
363        let elapsed = self.start.elapsed().as_millis() as u64;
364        let metrics = Metrics::new(0, elapsed, 0, 0).with_label(&self.label);
365        self.monitor.record_metric(&self.label, metrics);
366    }
367}
368
369/// Helper macro for creating a scoped timer
370#[macro_export]
371macro_rules! scoped_perf_timer {
372    ($label:expr, $monitor:expr) => {
373        $crate::util::perf_monitor::ScopedTimer::new($label, $monitor)
374    };
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_metrics_creation() {
383        let m = Metrics::new(1000, 50, 1024 * 1024, 10);
384        assert_eq!(m.throughput, 1000);
385        assert_eq!(m.latency, 50);
386        assert_eq!(m.memory_usage, 1024 * 1024);
387        assert_eq!(m.lock_wait_time, 10);
388        assert!(m.timestamp > 0);
389    }
390
391    #[test]
392    fn test_metrics_with_label() {
393        let m = Metrics::new(1000, 50, 1024, 10).with_label("download");
394        assert_eq!(m.label, Some("download".to_string()));
395    }
396
397    #[test]
398    fn test_performance_score() {
399        // High throughput, low latency should have high score
400        let m1 = Metrics::new(10_000_000, 10, 1024, 5);
401        let score1 = m1.performance_score();
402        assert!(score1 > 50.0, "Score should be > 50, got {}", score1);
403
404        // Low throughput, high latency should have lower score
405        let m2 = Metrics::new(1000, 1000, 1024, 100);
406        let score2 = m2.performance_score();
407        assert!(score2 < score1, "Score should be lower, got {}", score2);
408    }
409
410    #[test]
411    fn test_atomic_metrics() {
412        let am = AtomicMetrics::new();
413        am.record_throughput(1000);
414        am.record_latency(50);
415        am.record_memory(1024);
416        am.record_lock_wait(10);
417
418        let snapshot = am.snapshot();
419        assert_eq!(snapshot.throughput, 1000);
420        assert_eq!(snapshot.latency, 50);
421        assert_eq!(snapshot.memory_usage, 1024);
422        assert_eq!(snapshot.lock_wait_time, 10);
423    }
424
425    #[test]
426    fn test_atomic_metrics_reset() {
427        let am = AtomicMetrics::new();
428        am.record_throughput(1000);
429        am.record_latency(50);
430        am.reset();
431
432        let snapshot = am.snapshot();
433        assert_eq!(snapshot.throughput, 0);
434        assert_eq!(snapshot.latency, 0);
435    }
436
437    #[test]
438    fn test_performance_monitor() {
439        let monitor = PerformanceMonitor::new();
440        let m1 = Metrics::new(1000, 50, 1024, 10);
441        let m2 = Metrics::new(2000, 30, 2048, 5);
442
443        monitor.record_metric("download", m1);
444        monitor.record_metric("download", m2);
445
446        let report = monitor.generate_report();
447        assert!(report.metrics.contains_key("download"));
448        assert_eq!(report.metrics.get("download").unwrap().len(), 2);
449        assert_eq!(report.summary.total_samples, 2);
450        assert_eq!(report.summary.avg_throughput, 1500); // (1000 + 2000) / 2
451    }
452
453    #[test]
454    fn test_export_json() {
455        let monitor = PerformanceMonitor::new();
456        let m = Metrics::new(1000, 50, 1024, 10);
457        monitor.record_metric("test", m);
458
459        let json = monitor.export_json();
460        assert!(json.contains("test"));
461        assert!(json.contains("throughput"));
462    }
463
464    #[test]
465    fn test_export_text() {
466        let monitor = PerformanceMonitor::new();
467        let m = Metrics::new(1000, 50, 1024, 10);
468        monitor.record_metric("test", m);
469
470        let text = monitor.export_text();
471        assert!(text.contains("Performance Report"));
472        assert!(text.contains("test"));
473        assert!(text.contains("1000 B/s"));
474    }
475
476    #[test]
477    fn test_scoped_timer() {
478        let monitor = Arc::new(PerformanceMonitor::new());
479        {
480            let _timer = ScopedTimer::new("operation", monitor.clone());
481            std::thread::sleep(std::time::Duration::from_millis(10));
482        }
483
484        let report = monitor.generate_report();
485        assert!(report.metrics.contains_key("operation"));
486        let metrics = report.metrics.get("operation").unwrap();
487        assert!(!metrics.is_empty());
488        assert!(metrics[0].latency >= 10);
489    }
490
491    #[tokio::test]
492    async fn test_concurrent_recording() {
493        let monitor = Arc::new(PerformanceMonitor::new());
494        let mut handles = vec![];
495
496        for i in 0..10 {
497            let m = monitor.clone();
498            handles.push(tokio::spawn(async move {
499                let metric = Metrics::new(i * 100, i * 10, i * 1024, i);
500                m.record_metric("concurrent", metric);
501            }));
502        }
503
504        for handle in handles {
505            handle.await.unwrap();
506        }
507
508        let report = monitor.generate_report();
509        assert!(report.metrics.contains_key("concurrent"));
510        // Note: Due to try_write, some metrics might be missed under high contention
511        // This is acceptable for minimal overhead
512    }
513}