use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tracing::trace;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MetricCategory {
FileIO,
HashCompute,
DhtOperation,
ChunkTransfer,
}
impl MetricCategory {
fn name(&self) -> &'static str {
match self {
MetricCategory::FileIO => "FileIO",
MetricCategory::HashCompute => "HashCompute",
MetricCategory::DhtOperation => "DhtOperation",
MetricCategory::ChunkTransfer => "ChunkTransfer",
}
}
}
#[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,
});
}
}
pub struct Metrics {
categories: Mutex<HashMap<MetricCategory, CategoryMetrics>>,
start_time: Instant,
}
impl Metrics {
pub fn new() -> Self {
Self {
categories: Mutex::new(HashMap::new()),
start_time: Instant::now(),
}
}
pub fn record(&self, category: MetricCategory, duration: Duration, bytes: u64) {
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);
}
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"
);
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()
}
}
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())
}
}
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)
}
}
static GLOBAL_METRICS: std::sync::LazyLock<Metrics> = std::sync::LazyLock::new(Metrics::new);
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");
}
}