289 lines
8.0 KiB
Rust
289 lines
8.0 KiB
Rust
// benches/common/metrics.rs
|
|
use serde::{Deserialize, Serialize};
|
|
use std::time::Duration;
|
|
|
|
/// Custom metrics for benchmark results
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BenchmarkMetrics {
|
|
pub operation: String,
|
|
pub backend: String,
|
|
pub dataset_size: usize,
|
|
pub mean_ns: u64,
|
|
pub median_ns: u64,
|
|
pub p95_ns: u64,
|
|
pub p99_ns: u64,
|
|
pub std_dev_ns: u64,
|
|
pub throughput_ops_sec: f64,
|
|
}
|
|
|
|
impl BenchmarkMetrics {
|
|
pub fn new(
|
|
operation: String,
|
|
backend: String,
|
|
dataset_size: usize,
|
|
) -> Self {
|
|
Self {
|
|
operation,
|
|
backend,
|
|
dataset_size,
|
|
mean_ns: 0,
|
|
median_ns: 0,
|
|
p95_ns: 0,
|
|
p99_ns: 0,
|
|
std_dev_ns: 0,
|
|
throughput_ops_sec: 0.0,
|
|
}
|
|
}
|
|
|
|
/// Convert to CSV row format
|
|
pub fn to_csv_row(&self) -> String {
|
|
format!(
|
|
"{},{},{},{},{},{},{},{},{:.2}",
|
|
self.backend,
|
|
self.operation,
|
|
self.dataset_size,
|
|
self.mean_ns,
|
|
self.median_ns,
|
|
self.p95_ns,
|
|
self.p99_ns,
|
|
self.std_dev_ns,
|
|
self.throughput_ops_sec
|
|
)
|
|
}
|
|
|
|
/// Get CSV header
|
|
pub fn csv_header() -> String {
|
|
"backend,operation,dataset_size,mean_ns,median_ns,p95_ns,p99_ns,std_dev_ns,throughput_ops_sec".to_string()
|
|
}
|
|
|
|
/// Convert to JSON
|
|
pub fn to_json(&self) -> serde_json::Value {
|
|
serde_json::json!({
|
|
"backend": self.backend,
|
|
"operation": self.operation,
|
|
"dataset_size": self.dataset_size,
|
|
"metrics": {
|
|
"mean_ns": self.mean_ns,
|
|
"median_ns": self.median_ns,
|
|
"p95_ns": self.p95_ns,
|
|
"p99_ns": self.p99_ns,
|
|
"std_dev_ns": self.std_dev_ns,
|
|
"throughput_ops_sec": self.throughput_ops_sec
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Calculate throughput from mean latency
|
|
pub fn calculate_throughput(&mut self) {
|
|
if self.mean_ns > 0 {
|
|
self.throughput_ops_sec = 1_000_000_000.0 / self.mean_ns as f64;
|
|
}
|
|
}
|
|
|
|
/// Format duration for display
|
|
pub fn format_duration(nanos: u64) -> String {
|
|
if nanos < 1_000 {
|
|
format!("{} ns", nanos)
|
|
} else if nanos < 1_000_000 {
|
|
format!("{:.2} µs", nanos as f64 / 1_000.0)
|
|
} else if nanos < 1_000_000_000 {
|
|
format!("{:.2} ms", nanos as f64 / 1_000_000.0)
|
|
} else {
|
|
format!("{:.2} s", nanos as f64 / 1_000_000_000.0)
|
|
}
|
|
}
|
|
|
|
/// Pretty print the metrics
|
|
pub fn display(&self) -> String {
|
|
format!(
|
|
"{}/{} (n={}): mean={}, median={}, p95={}, p99={}, throughput={:.0} ops/sec",
|
|
self.backend,
|
|
self.operation,
|
|
self.dataset_size,
|
|
Self::format_duration(self.mean_ns),
|
|
Self::format_duration(self.median_ns),
|
|
Self::format_duration(self.p95_ns),
|
|
Self::format_duration(self.p99_ns),
|
|
self.throughput_ops_sec
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Memory metrics for profiling
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MemoryMetrics {
|
|
pub operation: String,
|
|
pub backend: String,
|
|
pub allocations: usize,
|
|
pub peak_bytes: usize,
|
|
pub avg_bytes_per_op: f64,
|
|
}
|
|
|
|
impl MemoryMetrics {
|
|
pub fn new(operation: String, backend: String) -> Self {
|
|
Self {
|
|
operation,
|
|
backend,
|
|
allocations: 0,
|
|
peak_bytes: 0,
|
|
avg_bytes_per_op: 0.0,
|
|
}
|
|
}
|
|
|
|
/// Convert to CSV row format
|
|
pub fn to_csv_row(&self) -> String {
|
|
format!(
|
|
"{},{},{},{},{:.2}",
|
|
self.backend,
|
|
self.operation,
|
|
self.allocations,
|
|
self.peak_bytes,
|
|
self.avg_bytes_per_op
|
|
)
|
|
}
|
|
|
|
/// Get CSV header
|
|
pub fn csv_header() -> String {
|
|
"backend,operation,allocations,peak_bytes,avg_bytes_per_op".to_string()
|
|
}
|
|
|
|
/// Format bytes for display
|
|
pub fn format_bytes(bytes: usize) -> String {
|
|
if bytes < 1024 {
|
|
format!("{} B", bytes)
|
|
} else if bytes < 1024 * 1024 {
|
|
format!("{:.2} KB", bytes as f64 / 1024.0)
|
|
} else if bytes < 1024 * 1024 * 1024 {
|
|
format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
|
|
} else {
|
|
format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
|
|
}
|
|
}
|
|
|
|
/// Pretty print the metrics
|
|
pub fn display(&self) -> String {
|
|
format!(
|
|
"{}/{}: {} allocations, peak={}, avg={}",
|
|
self.backend,
|
|
self.operation,
|
|
self.allocations,
|
|
Self::format_bytes(self.peak_bytes),
|
|
Self::format_bytes(self.avg_bytes_per_op as usize)
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Collection of benchmark results for comparison
|
|
#[derive(Debug, Default)]
|
|
pub struct BenchmarkResults {
|
|
pub metrics: Vec<BenchmarkMetrics>,
|
|
pub memory_metrics: Vec<MemoryMetrics>,
|
|
}
|
|
|
|
impl BenchmarkResults {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn add_metric(&mut self, metric: BenchmarkMetrics) {
|
|
self.metrics.push(metric);
|
|
}
|
|
|
|
pub fn add_memory_metric(&mut self, metric: MemoryMetrics) {
|
|
self.memory_metrics.push(metric);
|
|
}
|
|
|
|
/// Export all metrics to CSV format
|
|
pub fn to_csv(&self) -> String {
|
|
let mut output = String::new();
|
|
|
|
if !self.metrics.is_empty() {
|
|
output.push_str(&BenchmarkMetrics::csv_header());
|
|
output.push('\n');
|
|
for metric in &self.metrics {
|
|
output.push_str(&metric.to_csv_row());
|
|
output.push('\n');
|
|
}
|
|
}
|
|
|
|
if !self.memory_metrics.is_empty() {
|
|
output.push('\n');
|
|
output.push_str(&MemoryMetrics::csv_header());
|
|
output.push('\n');
|
|
for metric in &self.memory_metrics {
|
|
output.push_str(&metric.to_csv_row());
|
|
output.push('\n');
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
/// Export all metrics to JSON format
|
|
pub fn to_json(&self) -> serde_json::Value {
|
|
serde_json::json!({
|
|
"benchmarks": self.metrics.iter().map(|m| m.to_json()).collect::<Vec<_>>(),
|
|
"memory": self.memory_metrics
|
|
})
|
|
}
|
|
|
|
/// Save results to a file
|
|
pub fn save_csv(&self, path: &str) -> std::io::Result<()> {
|
|
std::fs::write(path, self.to_csv())
|
|
}
|
|
|
|
pub fn save_json(&self, path: &str) -> std::io::Result<()> {
|
|
let json = serde_json::to_string_pretty(&self.to_json())?;
|
|
std::fs::write(path, json)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_metrics_creation() {
|
|
let mut metric = BenchmarkMetrics::new(
|
|
"set".to_string(),
|
|
"redb".to_string(),
|
|
1000,
|
|
);
|
|
metric.mean_ns = 1_245;
|
|
metric.calculate_throughput();
|
|
|
|
assert!(metric.throughput_ops_sec > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_csv_export() {
|
|
let mut results = BenchmarkResults::new();
|
|
let mut metric = BenchmarkMetrics::new(
|
|
"set".to_string(),
|
|
"redb".to_string(),
|
|
1000,
|
|
);
|
|
metric.mean_ns = 1_245;
|
|
metric.calculate_throughput();
|
|
|
|
results.add_metric(metric);
|
|
let csv = results.to_csv();
|
|
|
|
assert!(csv.contains("backend,operation"));
|
|
assert!(csv.contains("redb,set"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_duration_formatting() {
|
|
assert_eq!(BenchmarkMetrics::format_duration(500), "500 ns");
|
|
assert_eq!(BenchmarkMetrics::format_duration(1_500), "1.50 µs");
|
|
assert_eq!(BenchmarkMetrics::format_duration(1_500_000), "1.50 ms");
|
|
}
|
|
|
|
#[test]
|
|
fn test_bytes_formatting() {
|
|
assert_eq!(MemoryMetrics::format_bytes(512), "512 B");
|
|
assert_eq!(MemoryMetrics::format_bytes(2048), "2.00 KB");
|
|
assert_eq!(MemoryMetrics::format_bytes(2_097_152), "2.00 MB");
|
|
}
|
|
} |