1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BenchReport {
11 pub scenario: ScenarioMetadata,
12 pub backend: BackendInfo,
16 pub host: HostInfo,
20 pub iterations: IterationStats,
21 pub response_bytes: Option<usize>,
25 pub expected_match: Option<bool>,
28 pub passes_applied: Vec<String>,
32 pub compiler_visible_allocs: Option<usize>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct BackendInfo {
39 pub name: String,
41 pub aver_version: String,
44 pub build: String,
47 pub wasmtime_version: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct HostInfo {
54 pub os: String,
56 pub arch: String,
58 pub cpus: usize,
60}
61
62impl BackendInfo {
63 pub fn for_target(target: crate::bench::manifest::BenchTarget) -> Self {
64 let build = if cfg!(debug_assertions) {
65 "debug"
66 } else {
67 "release"
68 };
69 let wasmtime_version = match target {
70 crate::bench::manifest::BenchTarget::WasmLocal => Some(WASMTIME_VERSION.to_string()),
71 _ => None,
72 };
73 Self {
74 name: target.name().to_string(),
75 aver_version: env!("CARGO_PKG_VERSION").to_string(),
76 build: build.to_string(),
77 wasmtime_version,
78 }
79 }
80}
81
82impl HostInfo {
83 pub fn capture() -> Self {
84 let cpus = std::thread::available_parallelism()
85 .map(|n| n.get())
86 .unwrap_or(1);
87 Self {
88 os: std::env::consts::OS.to_string(),
89 arch: std::env::consts::ARCH.to_string(),
90 cpus,
91 }
92 }
93}
94
95const WASMTIME_VERSION: &str = "29";
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ScenarioMetadata {
103 pub name: String,
104 pub entry: String,
105 pub target: String,
106 pub iterations_count: usize,
107 pub warmup_count: usize,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct IterationStats {
113 pub min_ms: f64,
114 pub max_ms: f64,
115 pub mean_ms: f64,
116 pub p50_ms: f64,
117 pub p95_ms: f64,
118 pub p99_ms: f64,
119}
120
121pub fn format_human(report: &BenchReport) -> String {
126 use std::fmt::Write;
127
128 fn fmt_ms(ms: f64) -> String {
129 if ms >= 1.0 {
130 format!("{:.2}ms", ms)
131 } else {
132 format!("{:.0}µs", ms * 1000.0)
133 }
134 }
135
136 let mut out = String::new();
137 let s = &report.scenario;
138 let b = &report.backend;
139 let h = &report.host;
140 let it = &report.iterations;
141 writeln!(out, "{} [{}]", s.name, s.target).ok();
142 writeln!(out, " entry: {}", s.entry).ok();
143 let mut backend_line = format!("aver {} ({})", b.aver_version, b.build);
144 if let Some(wt) = &b.wasmtime_version {
145 backend_line.push_str(&format!(", wasmtime {}", wt));
146 }
147 writeln!(out, " backend: {}", backend_line).ok();
148 writeln!(out, " host: {}/{} ({} cpus)", h.os, h.arch, h.cpus).ok();
149 writeln!(
150 out,
151 " iterations: {} (warmup {})",
152 s.iterations_count, s.warmup_count
153 )
154 .ok();
155 writeln!(
156 out,
157 " passes: {}",
158 if report.passes_applied.is_empty() {
159 "(none)".to_string()
160 } else {
161 report.passes_applied.join(", ")
162 }
163 )
164 .ok();
165 writeln!(
166 out,
167 " wall_time: min={} p50={} p95={} max={} mean={}",
168 fmt_ms(it.min_ms),
169 fmt_ms(it.p50_ms),
170 fmt_ms(it.p95_ms),
171 fmt_ms(it.max_ms),
172 fmt_ms(it.mean_ms),
173 )
174 .ok();
175 if let Some(bytes) = report.response_bytes {
176 writeln!(out, " response: {} bytes", bytes).ok();
177 }
178 if let Some(matched) = report.expected_match {
179 writeln!(
180 out,
181 " expected: {}",
182 if matched { "ok" } else { "MISMATCH" }
183 )
184 .ok();
185 }
186 out
187}
188
189impl IterationStats {
190 pub fn from_samples(samples: &[f64]) -> Self {
191 assert!(!samples.is_empty(), "IterationStats requires ≥1 sample");
192 let mut sorted: Vec<f64> = samples.to_vec();
193 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
194 let n = sorted.len();
195 let percentile = |p: f64| -> f64 {
196 let idx = ((p / 100.0) * (n as f64)).ceil() as usize;
200 let idx = idx.saturating_sub(1).min(n - 1);
201 sorted[idx]
202 };
203 IterationStats {
204 min_ms: *sorted.first().unwrap(),
205 max_ms: *sorted.last().unwrap(),
206 mean_ms: sorted.iter().sum::<f64>() / (n as f64),
207 p50_ms: percentile(50.0),
208 p95_ms: percentile(95.0),
209 p99_ms: percentile(99.0),
210 }
211 }
212}