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| Segment {
146 start: i as f64 * 0.5,
147 end: i as f64 * 0.5 + 0.4,
148 text: format!("segment number {i}"),
149 })
150 .collect();
151 let result = TranscriptionResult::local(
152 "bench".into(),
153 segs,
154 Some("en".into()),
155 "bench".into(),
156 segments as f64,
157 );
158 let times = time_iters(
159 || {
160 let mut out = Vec::with_capacity(segments * 64);
161 let _ = write_result_with_limit(&result, OutputFormat::Srt, &mut out, 16 * 1024 * 1024);
162 },
163 iters,
164 );
165 BenchSample {
166 name: format!("srt_stream_{segments}_segments"),
167 iterations: iters,
168 wall_ms_p50: percentile_ms(×, 0.50),
169 wall_ms_p95: percentile_ms(×, 0.95),
170 wall_ms_mean: mean_ms(×),
171 peak_rss_bytes: 0,
172 notes: "streaming SRT writer".into(),
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct SmokeBudgets {
179 pub pcm_push_p95_ms: f64,
180 pub srt_1k_p95_ms: f64,
181}
182
183impl SmokeBudgets {
184 pub fn for_profile(p: BenchProfile) -> Self {
185 match p {
186 BenchProfile::Constrained => Self {
187 pcm_push_p95_ms: 50.0,
188 srt_1k_p95_ms: 200.0,
189 },
190 _ => Self {
191 pcm_push_p95_ms: 25.0,
192 srt_1k_p95_ms: 100.0,
193 },
194 }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn smoke_micro_benches_run() {
204 let profile = BenchProfile::detect();
205 let mut report = BenchReport::new(profile, true);
206 report.samples.push(sample_pcm_push(5));
207 report.samples.push(sample_srt_stream(200, 3));
208 let budgets = SmokeBudgets::for_profile(profile);
209 let pcm = report
210 .samples
211 .iter()
212 .find(|s| s.name.contains("pcm"))
213 .unwrap();
214 assert!(
215 pcm.wall_ms_p95 < budgets.pcm_push_p95_ms * 20.0,
216 "pcm p95 too high: {}",
217 pcm.wall_ms_p95
218 );
219 let json = serde_json::to_string_pretty(&report).unwrap();
220 assert!(json.contains("schema_version"));
221 }
222}