use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchReport {
pub scenario: ScenarioMetadata,
pub backend: BackendInfo,
pub host: HostInfo,
pub iterations: IterationStats,
pub response_bytes: Option<usize>,
pub expected_match: Option<bool>,
pub passes_applied: Vec<String>,
pub compiler_visible_allocs: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackendInfo {
pub name: String,
pub aver_version: String,
pub build: String,
pub wasmtime_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostInfo {
pub os: String,
pub arch: String,
pub cpus: usize,
}
impl BackendInfo {
pub fn for_target(target: crate::bench::manifest::BenchTarget) -> Self {
let build = if cfg!(debug_assertions) {
"debug"
} else {
"release"
};
let wasmtime_version = match target {
crate::bench::manifest::BenchTarget::WasmLocal => Some(WASMTIME_VERSION.to_string()),
_ => None,
};
Self {
name: target.name().to_string(),
aver_version: env!("CARGO_PKG_VERSION").to_string(),
build: build.to_string(),
wasmtime_version,
}
}
}
impl HostInfo {
pub fn capture() -> Self {
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
Self {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
cpus,
}
}
}
const WASMTIME_VERSION: &str = "29";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioMetadata {
pub name: String,
pub entry: String,
pub target: String,
pub iterations_count: usize,
pub warmup_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IterationStats {
pub min_ms: f64,
pub max_ms: f64,
pub mean_ms: f64,
pub p50_ms: f64,
pub p95_ms: f64,
pub p99_ms: f64,
}
pub fn format_human(report: &BenchReport) -> String {
use std::fmt::Write;
fn fmt_ms(ms: f64) -> String {
if ms >= 1.0 {
format!("{:.2}ms", ms)
} else {
format!("{:.0}µs", ms * 1000.0)
}
}
let mut out = String::new();
let s = &report.scenario;
let b = &report.backend;
let h = &report.host;
let it = &report.iterations;
writeln!(out, "{} [{}]", s.name, s.target).ok();
writeln!(out, " entry: {}", s.entry).ok();
let mut backend_line = format!("aver {} ({})", b.aver_version, b.build);
if let Some(wt) = &b.wasmtime_version {
backend_line.push_str(&format!(", wasmtime {}", wt));
}
writeln!(out, " backend: {}", backend_line).ok();
writeln!(out, " host: {}/{} ({} cpus)", h.os, h.arch, h.cpus).ok();
writeln!(
out,
" iterations: {} (warmup {})",
s.iterations_count, s.warmup_count
)
.ok();
writeln!(
out,
" passes: {}",
if report.passes_applied.is_empty() {
"(none)".to_string()
} else {
report.passes_applied.join(", ")
}
)
.ok();
writeln!(
out,
" wall_time: min={} p50={} p95={} max={} mean={}",
fmt_ms(it.min_ms),
fmt_ms(it.p50_ms),
fmt_ms(it.p95_ms),
fmt_ms(it.max_ms),
fmt_ms(it.mean_ms),
)
.ok();
if let Some(bytes) = report.response_bytes {
writeln!(out, " response: {} bytes", bytes).ok();
}
if let Some(matched) = report.expected_match {
writeln!(
out,
" expected: {}",
if matched { "ok" } else { "MISMATCH" }
)
.ok();
}
out
}
impl IterationStats {
pub fn from_samples(samples: &[f64]) -> Self {
assert!(!samples.is_empty(), "IterationStats requires ≥1 sample");
let mut sorted: Vec<f64> = samples.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = sorted.len();
let percentile = |p: f64| -> f64 {
let idx = ((p / 100.0) * (n as f64)).ceil() as usize;
let idx = idx.saturating_sub(1).min(n - 1);
sorted[idx]
};
IterationStats {
min_ms: *sorted.first().unwrap(),
max_ms: *sorted.last().unwrap(),
mean_ms: sorted.iter().sum::<f64>() / (n as f64),
p50_ms: percentile(50.0),
p95_ms: percentile(95.0),
p99_ms: percentile(99.0),
}
}
}