1use serde::{Deserialize, Serialize};
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum BenchProfile {
14 AppleSiliconMetal,
16 LinuxX86Cpu,
18 Constrained,
20}
21
22impl BenchProfile {
23 pub fn as_str(self) -> &'static str {
24 match self {
25 Self::AppleSiliconMetal => "apple_silicon_metal",
26 Self::LinuxX86Cpu => "linux_x86_cpu",
27 Self::Constrained => "constrained",
28 }
29 }
30
31 pub fn detect() -> Self {
32 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
33 {
34 return Self::AppleSiliconMetal;
35 }
36 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
37 {
38 return Self::LinuxX86Cpu;
39 }
40 #[allow(unreachable_code)]
41 Self::Constrained
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct BenchSample {
48 pub name: String,
49 pub iterations: u32,
50 pub wall_ms_p50: f64,
51 pub wall_ms_p95: f64,
52 pub wall_ms_mean: f64,
53 pub peak_rss_bytes: u64,
55 pub notes: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct BenchReport {
61 pub schema_version: u32,
62 pub profile: BenchProfile,
63 pub commit: String,
64 pub target: String,
65 pub features: String,
66 pub warm: bool,
67 pub samples: Vec<BenchSample>,
68}
69
70impl BenchReport {
71 pub fn new(profile: BenchProfile, warm: bool) -> Self {
72 Self {
73 schema_version: 1,
74 profile,
75 commit: std::env::var("GITHUB_SHA")
76 .or_else(|_| std::env::var("AURUM_BENCH_COMMIT"))
77 .unwrap_or_else(|_| "unknown".into()),
78 target: format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS),
79 features: std::env::var("AURUM_BENCH_FEATURES").unwrap_or_else(|_| "default".into()),
80 warm,
81 samples: Vec::new(),
82 }
83 }
84}
85
86pub fn time_iters<F: FnMut()>(mut f: F, iters: u32) -> Vec<Duration> {
88 let mut times = Vec::with_capacity(iters as usize);
89 for _ in 0..iters {
90 let t0 = Instant::now();
91 f();
92 times.push(t0.elapsed());
93 }
94 times.sort();
95 times
96}
97
98pub fn percentile_ms(sorted: &[Duration], p: f64) -> f64 {
99 if sorted.is_empty() {
100 return 0.0;
101 }
102 let idx =
103 ((p.clamp(0.0, 1.0) * (sorted.len() as f64 - 1.0)).round() as usize).min(sorted.len() - 1);
104 sorted[idx].as_secs_f64() * 1000.0
105}
106
107pub fn mean_ms(sorted: &[Duration]) -> f64 {
108 if sorted.is_empty() {
109 return 0.0;
110 }
111 let sum: f64 = sorted.iter().map(|d| d.as_secs_f64() * 1000.0).sum();
112 sum / sorted.len() as f64
113}
114
115pub fn sample_pcm_push(iters: u32) -> BenchSample {
117 use crate::pcm::PcmBuffer;
118 let chunk = vec![0.01f32; 1600]; let times = time_iters(
120 || {
121 let mut buf = PcmBuffer::dictation();
122 for _ in 0..20 {
123 let _ = buf.push(&chunk);
124 }
125 let _ = buf.rms();
126 },
127 iters,
128 );
129 BenchSample {
130 name: "pcm_ring_push_20x100ms".into(),
131 iterations: iters,
132 wall_ms_p50: percentile_ms(×, 0.50),
133 wall_ms_p95: percentile_ms(×, 0.95),
134 wall_ms_mean: mean_ms(×),
135 peak_rss_bytes: 0,
136 notes: "no model download; CPU only".into(),
137 }
138}
139
140pub fn sample_srt_stream(segments: usize, iters: u32) -> BenchSample {
142 use crate::output::{write_result_with_limit, OutputFormat};
143 use crate::providers::{Segment, TranscriptionResult};
144 let segs: Vec<Segment> = (0..segments)
145 .map(|i| {
146 Segment::from_parts_unchecked(
147 i as f64 * 0.5,
148 i as f64 * 0.5 + 0.4,
149 format!("segment number {i}"),
150 )
151 })
152 .collect();
153 let result = TranscriptionResult::local(
154 "bench".into(),
155 segs,
156 Some("en".into()),
157 "bench".into(),
158 segments as f64,
159 );
160 let times = time_iters(
161 || {
162 let mut out = Vec::with_capacity(segments * 64);
163 let _ = write_result_with_limit(&result, OutputFormat::Srt, &mut out, 16 * 1024 * 1024);
164 },
165 iters,
166 );
167 BenchSample {
168 name: format!("srt_stream_{segments}_segments"),
169 iterations: iters,
170 wall_ms_p50: percentile_ms(×, 0.50),
171 wall_ms_p95: percentile_ms(×, 0.95),
172 wall_ms_mean: mean_ms(×),
173 peak_rss_bytes: 0,
174 notes: "streaming SRT writer".into(),
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct SmokeBudgets {
181 pub pcm_push_p95_ms: f64,
182 pub srt_1k_p95_ms: f64,
183}
184
185impl SmokeBudgets {
186 pub fn for_profile(p: BenchProfile) -> Self {
187 match p {
188 BenchProfile::Constrained => Self {
189 pcm_push_p95_ms: 50.0,
190 srt_1k_p95_ms: 200.0,
191 },
192 _ => Self {
193 pcm_push_p95_ms: 25.0,
194 srt_1k_p95_ms: 100.0,
195 },
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn smoke_micro_benches_run() {
206 let profile = BenchProfile::detect();
207 let mut report = BenchReport::new(profile, true);
208 report.samples.push(sample_pcm_push(5));
209 report.samples.push(sample_srt_stream(200, 3));
210 let budgets = SmokeBudgets::for_profile(profile);
211 let pcm = report
212 .samples
213 .iter()
214 .find(|s| s.name.contains("pcm"))
215 .unwrap();
216 assert!(
217 pcm.wall_ms_p95 < budgets.pcm_push_p95_ms * 20.0,
218 "pcm p95 too high: {}",
219 pcm.wall_ms_p95
220 );
221 let json = serde_json::to_string_pretty(&report).unwrap();
222 assert!(json.contains("schema_version"));
223 }
224}