vflight 0.9.2

Share files over the Veilid distributed network with content-addressable storage
Documentation
//! Performance metrics collection for debugging and tuning.
//!
//! This module provides timing and byte tracking for key operations.
//! Enable metrics output with the `--metrics` CLI flag.
//!
//! Metrics are also logged via tracing at trace level for integration
//! with the existing logging infrastructure.

use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tracing::trace;

/// Categories of operations to measure.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MetricCategory {
    /// File read/write operations
    FileIO,
    /// BLAKE3 hash computation
    HashCompute,
    /// DHT operations (open, read, write, close)
    DhtOperation,
    /// Network chunk transfer latency
    ChunkTransfer,
}

impl MetricCategory {
    fn name(&self) -> &'static str {
        match self {
            MetricCategory::FileIO => "FileIO",
            MetricCategory::HashCompute => "HashCompute",
            MetricCategory::DhtOperation => "DhtOperation",
            MetricCategory::ChunkTransfer => "ChunkTransfer",
        }
    }
}

/// Accumulated metrics for a category.
#[derive(Debug, Default)]
struct CategoryMetrics {
    count: u64,
    total_duration: Duration,
    min_duration: Option<Duration>,
    max_duration: Option<Duration>,
    total_bytes: u64,
}

impl CategoryMetrics {
    fn record(&mut self, duration: Duration, bytes: u64) {
        self.count += 1;
        self.total_duration += duration;
        self.total_bytes += bytes;

        self.min_duration = Some(match self.min_duration {
            Some(min) => min.min(duration),
            None => duration,
        });

        self.max_duration = Some(match self.max_duration {
            Some(max) => max.max(duration),
            None => duration,
        });
    }
}

/// Global metrics collector.
pub struct Metrics {
    categories: Mutex<HashMap<MetricCategory, CategoryMetrics>>,
    start_time: Instant,
}

impl Metrics {
    /// Create a new metrics collector.
    pub fn new() -> Self {
        Self {
            categories: Mutex::new(HashMap::new()),
            start_time: Instant::now(),
        }
    }

    /// Record a timing measurement with optional byte count.
    ///
    /// Also emits a trace event for integration with tracing output.
    pub fn record(&self, category: MetricCategory, duration: Duration, bytes: u64) {
        // Emit trace event for tracing integration
        trace!(
            category = category.name(),
            duration_ms = duration.as_secs_f64() * 1000.0,
            bytes = bytes,
            "metric recorded"
        );

        let mut categories = self.categories.lock().unwrap();
        categories
            .entry(category)
            .or_default()
            .record(duration, bytes);
    }

    /// Print a formatted summary of all metrics.
    pub fn print_summary(&self) {
        let categories = self.categories.lock().unwrap();
        let elapsed = self.start_time.elapsed();

        if categories.is_empty() {
            return;
        }

        println!();
        println!("Performance Summary (elapsed: {:.2?})", elapsed);
        println!("────────────────────────────────────────────────────────────────────");
        println!(
            "{:<14} {:>6} {:>10} {:>10} {:>10} {:>10} {:>10}",
            "Category", "Count", "Total", "Avg", "Min", "Max", "Bytes"
        );

        // Print in consistent order
        let order = [
            MetricCategory::FileIO,
            MetricCategory::HashCompute,
            MetricCategory::DhtOperation,
            MetricCategory::ChunkTransfer,
        ];

        for category in order {
            if let Some(metrics) = categories.get(&category) {
                let avg = if metrics.count > 0 {
                    metrics.total_duration / metrics.count as u32
                } else {
                    Duration::ZERO
                };

                let bytes_str = if metrics.total_bytes > 0 {
                    format_bytes(metrics.total_bytes)
                } else {
                    "-".to_string()
                };

                println!(
                    "{:<14} {:>6} {:>10} {:>10} {:>10} {:>10} {:>10}",
                    category.name(),
                    metrics.count,
                    format_duration(metrics.total_duration),
                    format_duration(avg),
                    format_duration(metrics.min_duration.unwrap_or(Duration::ZERO)),
                    format_duration(metrics.max_duration.unwrap_or(Duration::ZERO)),
                    bytes_str,
                );
            }
        }
        println!("────────────────────────────────────────────────────────────────────");
    }
}

impl Default for Metrics {
    fn default() -> Self {
        Self::new()
    }
}

/// Format a duration for display.
fn format_duration(d: Duration) -> String {
    let millis = d.as_secs_f64() * 1000.0;
    if millis < 1.0 {
        format!("{:.2}ms", millis)
    } else if millis < 1000.0 {
        format!("{:.1}ms", millis)
    } else {
        format!("{:.2}s", d.as_secs_f64())
    }
}

/// Format bytes for display.
fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = 1024 * KB;
    const GB: u64 = 1024 * MB;

    if bytes >= GB {
        format!("{:.1} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.1} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

/// Global metrics instance.
static GLOBAL_METRICS: std::sync::LazyLock<Metrics> = std::sync::LazyLock::new(Metrics::new);

/// Get the global metrics collector.
pub fn global_metrics() -> &'static Metrics {
    &GLOBAL_METRICS
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_record_timing() {
        let metrics = Metrics::new();
        metrics.record(MetricCategory::FileIO, Duration::from_millis(100), 1024);
        metrics.record(MetricCategory::FileIO, Duration::from_millis(200), 2048);

        let categories = metrics.categories.lock().unwrap();
        let file_io = categories.get(&MetricCategory::FileIO).unwrap();

        assert_eq!(file_io.count, 2);
        assert_eq!(file_io.total_bytes, 3072);
        assert_eq!(file_io.min_duration, Some(Duration::from_millis(100)));
        assert_eq!(file_io.max_duration, Some(Duration::from_millis(200)));
    }

    #[test]
    fn test_record_with_duration() {
        let metrics = Metrics::new();
        metrics.record(MetricCategory::HashCompute, Duration::from_millis(10), 1000);

        let categories = metrics.categories.lock().unwrap();
        let hash = categories.get(&MetricCategory::HashCompute).unwrap();
        assert_eq!(hash.count, 1);
        assert_eq!(hash.total_bytes, 1000);
        assert_eq!(hash.total_duration, Duration::from_millis(10));
    }

    #[test]
    fn test_format_bytes() {
        assert_eq!(format_bytes(500), "500 B");
        assert_eq!(format_bytes(1024), "1.0 KB");
        assert_eq!(format_bytes(1536), "1.5 KB");
        assert_eq!(format_bytes(1048576), "1.0 MB");
        assert_eq!(format_bytes(1073741824), "1.0 GB");
    }

    #[test]
    fn test_format_duration() {
        assert_eq!(format_duration(Duration::from_micros(500)), "0.50ms");
        assert_eq!(format_duration(Duration::from_millis(50)), "50.0ms");
        assert_eq!(format_duration(Duration::from_secs(2)), "2.00s");
    }
}