1use std::path::{Path, PathBuf};
10use std::process::Command;
11use std::time::Instant;
12
13use serde::{Deserialize, Serialize};
14use silicera::hardware::HardwareInfo;
15use silicera::Result;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ArtifactArm {
20 pub arm: String,
22 pub rustflags: String,
24 pub binary: String,
26 pub kernel: String,
28 pub iterations: usize,
30 pub median_ns: f64,
32 pub stdout_excerpt: String,
34 pub notes: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct NativeArtifactReport {
41 pub host_brand: String,
43 pub fingerprint: Option<String>,
45 pub kernel: String,
47 pub portable: ArtifactArm,
49 pub native: ArtifactArm,
51 pub silicera_choice: String,
53 pub native_wins: bool,
55 pub portable_wins: bool,
57 pub conclusion: String,
59}
60
61pub fn find_workspace_root(start: &Path) -> Option<PathBuf> {
63 let mut cur = start.to_path_buf();
64 for _ in 0..8 {
65 if cur.join("benchmarks").join("arm_kernels").join("Cargo.toml").is_file() {
66 return Some(cur);
67 }
68 if !cur.pop() {
69 break;
70 }
71 }
72 None
73}
74
75pub fn run_native_artifact_compare(
77 info: &HardwareInfo,
78 kernel: &str,
79 iterations: usize,
80 min_improvement: f64,
81 workspace: &Path,
82) -> Result<NativeArtifactReport> {
83 let kernels_dir = workspace.join("benchmarks").join("arm_kernels");
84 if !kernels_dir.join("Cargo.toml").is_file() {
85 return Err(silicera::SiliceraError::Parse(format!(
86 "missing benchmarks/arm_kernels at {}",
87 kernels_dir.display()
88 )));
89 }
90
91 let target_root = workspace.join("target").join("silicera-arms");
92 std::fs::create_dir_all(&target_root)?;
93
94 let portable = build_and_run(
95 &kernels_dir,
96 &target_root.join("portable"),
97 "portable",
98 "-C target-cpu=x86-64-v2",
99 kernel,
100 iterations,
101 )?;
102 let native = build_and_run(
103 &kernels_dir,
104 &target_root.join("native"),
105 "native",
106 "-C target-cpu=native",
107 kernel,
108 iterations,
109 )?;
110
111 let native_wins =
112 native.median_ns < portable.median_ns * (1.0 - min_improvement);
113 let portable_wins = portable.median_ns <= native.median_ns;
114 let silicera_choice = if native_wins {
115 "silicera→native_artifact".into()
116 } else {
117 "silicera→portable_artifact".into()
118 };
119 let conclusion = if native_wins {
120 format!(
121 "Native artifact faster ({:.0} vs {:.0} ns, min_improvement={}). \
122 Silicera would prefer the native-compiled kernel for this host/kernel.",
123 native.median_ns, portable.median_ns, min_improvement
124 )
125 } else if portable_wins {
126 format!(
127 "Portable artifact ≤ native ({:.0} vs {:.0} ns). \
128 No specialization win from -C target-cpu=native under these knobs — report honestly.",
129 portable.median_ns, native.median_ns
130 )
131 } else {
132 format!(
133 "Difference below threshold. portable={:.0} native={:.0} ns.",
134 portable.median_ns, native.median_ns
135 )
136 };
137
138 Ok(NativeArtifactReport {
139 host_brand: info.brand.clone(),
140 fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
141 kernel: kernel.into(),
142 portable,
143 native,
144 silicera_choice,
145 native_wins,
146 portable_wins,
147 conclusion,
148 })
149}
150
151pub fn default_artifact_kernels() -> &'static [&'static str] {
153 &["dot_f32", "saxpy_f32", "checksum_u8", "reduce_i32"]
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct NativeArtifactSuite {
159 pub host_brand: String,
161 pub fingerprint: Option<String>,
163 pub reports: Vec<NativeArtifactReport>,
165 pub native_win_count: usize,
167 pub portable_win_count: usize,
169 pub summary: String,
171}
172
173pub fn run_native_artifact_suite(
175 info: &HardwareInfo,
176 kernels: &[&str],
177 iterations: usize,
178 min_improvement: f64,
179 workspace: &Path,
180) -> Result<NativeArtifactSuite> {
181 let mut reports = Vec::new();
182 for k in kernels {
183 reports.push(run_native_artifact_compare(
184 info,
185 k,
186 iterations,
187 min_improvement,
188 workspace,
189 )?);
190 }
191 let native_win_count = reports.iter().filter(|r| r.native_wins).count();
192 let portable_win_count = reports.iter().filter(|r| r.portable_wins).count();
193 let summary = format!(
194 "artifact suite: native_wins={native_win_count} portable_wins={portable_win_count} kernels={}",
195 reports.len()
196 );
197 Ok(NativeArtifactSuite {
198 host_brand: info.brand.clone(),
199 fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
200 reports,
201 native_win_count,
202 portable_win_count,
203 summary,
204 })
205}
206
207pub fn suite_to_workload_entries(suite: &NativeArtifactSuite) -> Vec<silicera::WorkloadEntry> {
209 use silicera::{Confidence, WorkloadEntry};
210 suite
211 .reports
212 .iter()
213 .map(|r| {
214 let (winner, wmed, bmed, conf) = if r.native_wins {
215 (
216 "native_artifact",
217 Some(r.native.median_ns),
218 Some(r.portable.median_ns),
219 Confidence::High,
220 )
221 } else if r.portable_wins {
222 (
223 "portable_artifact",
224 Some(r.portable.median_ns),
225 Some(r.native.median_ns),
226 Confidence::Medium,
227 )
228 } else {
229 (
230 "portable_artifact",
231 Some(r.portable.median_ns),
232 Some(r.native.median_ns),
233 Confidence::Low,
234 )
235 };
236 WorkloadEntry {
237 name: format!("artifact-{}", r.kernel),
238 winner: winner.into(),
239 confidence: conf,
240 rationale: r.conclusion.clone(),
241 winner_median_ns: wmed,
242 baseline_median_ns: bmed,
243 }
244 })
245 .collect()
246}
247
248fn build_and_run(
249 kernels_dir: &Path,
250 target_dir: &Path,
251 arm: &str,
252 rustflags: &str,
253 kernel: &str,
254 iterations: usize,
255) -> Result<ArtifactArm> {
256 std::fs::create_dir_all(target_dir)?;
257 let status = Command::new("cargo")
258 .arg("build")
259 .arg("--release")
260 .arg("--manifest-path")
261 .arg(kernels_dir.join("Cargo.toml"))
262 .env("CARGO_TARGET_DIR", target_dir)
263 .env("RUSTFLAGS", rustflags)
264 .status()
265 .map_err(|e| silicera::SiliceraError::Parse(format!("cargo build failed to start: {e}")))?;
266 if !status.success() {
267 return Err(silicera::SiliceraError::Parse(format!(
268 "cargo build ({arm}) failed with {status}"
269 )));
270 }
271
272 let bin = target_dir
273 .join("release")
274 .join(if cfg!(windows) {
275 "silicera-arm-kernels.exe"
276 } else {
277 "silicera-arm-kernels"
278 });
279 if !bin.is_file() {
280 return Err(silicera::SiliceraError::Parse(format!(
281 "expected binary at {}",
282 bin.display()
283 )));
284 }
285
286 let t0 = Instant::now();
287 let output = Command::new(&bin)
288 .arg("--kernel")
289 .arg(kernel)
290 .arg("--iterations")
291 .arg(iterations.to_string())
292 .output()
293 .map_err(|e| silicera::SiliceraError::Parse(format!("run {arm}: {e}")))?;
294 let wall_ns = t0.elapsed().as_nanos() as f64;
295 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
296 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
297 if !output.status.success() {
298 return Err(silicera::SiliceraError::Parse(format!(
299 "{arm} binary failed: {stderr}"
300 )));
301 }
302 let median_ns = parse_median_ns(&stdout).unwrap_or(wall_ns);
303 let excerpt: String = stdout.chars().take(400).collect();
304 Ok(ArtifactArm {
305 arm: arm.into(),
306 rustflags: rustflags.into(),
307 binary: bin.display().to_string(),
308 kernel: kernel.into(),
309 iterations,
310 median_ns,
311 stdout_excerpt: excerpt,
312 notes: format!("built with RUSTFLAGS='{rustflags}'"),
313 })
314}
315
316fn parse_median_ns(stdout: &str) -> Option<f64> {
317 for line in stdout.lines() {
318 let line = line.trim();
319 if let Some(rest) = line.strip_prefix("median_ns=") {
320 return rest.trim().parse().ok();
321 }
322 }
323 None
324}