use std::collections::HashMap;
use std::time::Duration;
pub struct RequestResult {
pub latency: Duration,
pub status: Option<u16>,
pub bytes: usize,
}
#[derive(Debug, Clone)]
pub struct BenchmarkResults {
pub total_requests: usize,
pub successful_requests: usize,
pub failed_requests: usize,
pub duration: Duration,
pub throughput: f64,
pub latency_min: Duration,
pub latency_max: Duration,
pub latency_mean: Duration,
pub latency_p50: Duration,
pub latency_p90: Duration,
pub latency_p95: Duration,
pub latency_p99: Duration,
pub status_codes: HashMap<u16, usize>,
pub total_bytes: u64,
}
impl BenchmarkResults {
pub fn print(&self) {
println!("\n--- Benchmark Complete ---");
println!(
"Requests: {} total, {} success, {} errors",
self.total_requests, self.successful_requests, self.failed_requests
);
println!("Duration: {:.2}s", self.duration.as_secs_f64());
println!("Throughput: {:.2} req/s", self.throughput);
println!(
"Transferred: {:.2} MB",
self.total_bytes as f64 / 1_048_576.0
);
println!("\nLatency:");
println!(" Min: {}", format_duration(self.latency_min));
println!(" Max: {}", format_duration(self.latency_max));
println!(" Mean: {}", format_duration(self.latency_mean));
println!(" p50: {}", format_duration(self.latency_p50));
println!(" p90: {}", format_duration(self.latency_p90));
println!(" p95: {}", format_duration(self.latency_p95));
println!(" p99: {}", format_duration(self.latency_p99));
if !self.status_codes.is_empty() {
println!("\nStatus codes:");
let mut codes: Vec<_> = self.status_codes.iter().collect();
codes.sort_by_key(|(k, _)| *k);
for (code, count) in codes {
println!(" {}: {}", code, count);
}
}
}
}
pub struct Metrics {
pub total: usize,
pub success: usize,
pub latencies: Vec<Duration>,
pub status_codes: HashMap<u16, usize>,
pub total_bytes: u64,
}
impl Metrics {
pub fn new() -> Self {
Metrics {
total: 0,
success: 0,
latencies: Vec::new(),
status_codes: HashMap::new(),
total_bytes: 0,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Metrics {
total: 0,
success: 0,
latencies: Vec::with_capacity(capacity),
status_codes: HashMap::new(),
total_bytes: 0,
}
}
pub fn record(&mut self, result: RequestResult) {
self.total += 1;
self.total_bytes += result.bytes as u64;
if let Some(status) = result.status {
*self.status_codes.entry(status).or_insert(0) += 1;
if (200..300).contains(&status) {
self.success += 1;
}
}
self.latencies.push(result.latency);
}
pub fn into_results(mut self, elapsed: Duration) -> BenchmarkResults {
self.latencies.sort();
let sorted = &self.latencies;
let (
latency_min,
latency_max,
latency_mean,
latency_p50,
latency_p90,
latency_p95,
latency_p99,
) = if sorted.is_empty() {
(
Duration::ZERO,
Duration::ZERO,
Duration::ZERO,
Duration::ZERO,
Duration::ZERO,
Duration::ZERO,
Duration::ZERO,
)
} else {
let min = *sorted.first().unwrap();
let max = *sorted.last().unwrap();
let sum: Duration = sorted.iter().sum();
let mean = sum / sorted.len() as u32;
(
min,
max,
mean,
percentile(sorted, 50),
percentile(sorted, 90),
percentile(sorted, 95),
percentile(sorted, 99),
)
};
BenchmarkResults {
total_requests: self.total,
successful_requests: self.success,
failed_requests: self.total - self.success,
duration: elapsed,
throughput: self.total as f64 / elapsed.as_secs_f64(),
latency_min,
latency_max,
latency_mean,
latency_p50,
latency_p90,
latency_p95,
latency_p99,
status_codes: self.status_codes,
total_bytes: self.total_bytes,
}
}
}
impl Default for Metrics {
fn default() -> Self {
Self::new()
}
}
fn percentile(sorted: &[Duration], p: usize) -> Duration {
let idx = (sorted.len() * p / 100).saturating_sub(1).max(0);
sorted[idx]
}
fn format_duration(d: Duration) -> String {
let micros = d.as_micros();
if micros < 1000 {
format!("{}us", micros)
} else if micros < 1_000_000 {
format!("{:.2}ms", micros as f64 / 1000.0)
} else {
format!("{:.2}s", d.as_secs_f64())
}
}