1#![warn(missing_docs)]
13#![deny(unsafe_op_in_unsafe_fn)]
14
15use std::path::Path;
16
17use serde::{Deserialize, Serialize};
18use silicera::brand::{BrandInfo, NAME};
19use silicera::fingerprint::Fingerprint;
20use silicera::hardware::{detect_hardware, HardwareInfo};
21use silicera::hnep::HnepProfile;
22use silicera::specialize::DecisionTree;
23use silicera::{Result, SiliceraError};
24
25#[cfg(feature = "c-abi")]
26pub mod ffi;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct RuntimeInfo {
31 pub name: String,
33 pub version: String,
35 pub phase: String,
37 pub brand_line: String,
39 pub license: String,
41 pub homepage: String,
43 pub repository: String,
45 pub funding_url: String,
47 pub affiliation: String,
49}
50
51impl RuntimeInfo {
52 pub fn current() -> Self {
54 let b = BrandInfo::current();
55 Self {
56 name: b.name.into(),
57 version: b.version.into(),
58 phase: b.phase.into(),
59 brand_line: b.brand_line.into(),
60 license: b.license.into(),
61 homepage: b.homepage.into(),
62 repository: b.repository.into(),
63 funding_url: b.funding_url.into(),
64 affiliation: b.affiliation.into(),
65 }
66 }
67
68 pub fn to_json_pretty(&self) -> Result<String> {
70 Ok(serde_json::to_string_pretty(self)?)
71 }
72}
73
74pub fn runtime_about() -> RuntimeInfo {
76 RuntimeInfo::current()
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81pub enum MismatchPolicy {
82 FallbackBaseline,
84 StrictMachine,
86}
87
88impl Default for MismatchPolicy {
89 fn default() -> Self {
90 Self::FallbackBaseline
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct LoadedProfile {
97 pub profile: HnepProfile,
99 pub host: HardwareInfo,
101 pub exact_match: bool,
103 pub class_match: bool,
105 pub policy: MismatchPolicy,
107 pub force_baseline: bool,
109 pub status: String,
111}
112
113impl LoadedProfile {
114 pub fn load(path: &Path, policy: MismatchPolicy) -> Result<Self> {
116 let profile = HnepProfile::read_from(path)?;
117 let host = detect_hardware()?;
118 Self::from_parts(profile, host, policy)
119 }
120
121 pub fn from_parts(
123 profile: HnepProfile,
124 host: HardwareInfo,
125 policy: MismatchPolicy,
126 ) -> Result<Self> {
127 let host_fp = match &host.fingerprint {
128 Some(fp) => fp.clone(),
129 None => {
130 return Ok(Self {
131 status: format!(
132 "PROFILE MISMATCH ({NAME}): host unsupported ({})",
133 host.support.message()
134 ),
135 profile,
136 host,
137 exact_match: false,
138 class_match: false,
139 policy,
140 force_baseline: true,
141 });
142 }
143 };
144
145 let profile_fp = Fingerprint::parse(&profile.header.fingerprint)?;
146 let exact_match = host_fp.exact_match(&profile_fp);
147 let class_match = host_fp.same_silicon_class(&profile_fp);
148
149 let (force_baseline, status) = if exact_match {
150 (
151 false,
152 format!("profile match: exact fingerprint ({NAME})"),
153 )
154 } else if class_match {
155 match policy {
156 MismatchPolicy::FallbackBaseline => (
157 true,
158 format!(
159 "PROFILE MISMATCH ({NAME}): topology/cache hash differs (class OK); falling back to baseline\n profile: {}\n host: {}",
160 profile_fp, host_fp
161 ),
162 ),
163 MismatchPolicy::StrictMachine => {
164 return Err(SiliceraError::ProfileMismatch(format!(
165 "strict-machine ({NAME}): fingerprint mismatch\n profile: {profile_fp}\n host: {host_fp}"
166 )));
167 }
168 }
169 } else {
170 match policy {
171 MismatchPolicy::FallbackBaseline => (
172 true,
173 format!(
174 "PROFILE MISMATCH ({NAME}): silicon class differs; falling back to baseline\n profile: {}\n host: {}",
175 profile_fp, host_fp
176 ),
177 ),
178 MismatchPolicy::StrictMachine => {
179 return Err(SiliceraError::ProfileMismatch(format!(
180 "strict-machine ({NAME}): silicon class mismatch\n profile: {profile_fp}\n host: {host_fp}"
181 )));
182 }
183 }
184 };
185
186 Ok(Self {
187 profile,
188 host,
189 exact_match,
190 class_match,
191 policy,
192 force_baseline,
193 status,
194 })
195 }
196
197 pub fn select_workload(&self, workload: &str) -> &str {
199 if self.force_baseline {
200 return "baseline";
201 }
202 self.profile
203 .workloads
204 .iter()
205 .find(|w| w.name == workload)
206 .map(|w| w.winner.as_str())
207 .unwrap_or("baseline")
208 }
209
210 pub fn select_size(&self, size_bytes: u64) -> &str {
212 if self.force_baseline {
213 return "baseline";
214 }
215 match &self.profile.decision_tree {
216 Some(tree) => tree.evaluate(size_bytes),
217 None => "baseline",
218 }
219 }
220
221 pub fn decision_tree(&self) -> Option<&DecisionTree> {
223 self.profile.decision_tree.as_ref()
224 }
225}
226
227pub struct Dispatcher {
229 loaded: LoadedProfile,
230}
231
232impl Dispatcher {
233 pub fn open(path: &Path, policy: MismatchPolicy) -> Result<Self> {
235 Ok(Self {
236 loaded: LoadedProfile::load(path, policy)?,
237 })
238 }
239
240 pub fn status(&self) -> &str {
242 &self.loaded.status
243 }
244
245 pub fn using_baseline(&self) -> bool {
247 self.loaded.force_baseline
248 }
249
250 pub fn workload(&self, name: &str) -> &str {
252 self.loaded.select_workload(name)
253 }
254
255 pub fn size(&self, bytes: u64) -> &str {
257 self.loaded.select_size(bytes)
258 }
259
260 pub fn loaded(&self) -> &LoadedProfile {
262 &self.loaded
263 }
264
265 pub fn about(&self) -> RuntimeInfo {
267 runtime_about()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use silicera::brand::{
275 AFFILIATION_DISCLAIMER, BRAND_LINE, FUNDING_URL, HOMEPAGE, LICENSE, PHASE, REPOSITORY,
276 VERSION,
277 };
278 use silicera::hardware::{EnvironmentSnapshot, MockHardware, SupportStatus};
279 use silicera::hnep::{Confidence, HnepHeader, HnepProfile, IntegrityDigest, WorkloadEntry};
280 use silicera::knowledge::{KnowledgePack, Microarch};
281 use silicera::HardwareBackend;
282
283 fn make_profile(fp: &str) -> HnepProfile {
284 let header = HnepHeader {
285 format: silicera::hnep::HNEP_FORMAT.into(),
286 version: silicera::hnep::HNEP_VERSION,
287 silicera_version: silicera::VERSION.into(),
288 created_at: chrono::Utc::now().to_rfc3339(),
289 fingerprint: fp.into(),
290 label: "test".into(),
291 };
292 let environment = EnvironmentSnapshot::capture();
293 let workloads = vec![WorkloadEntry {
294 name: "demo".into(),
295 winner: "fast".into(),
296 confidence: Confidence::Medium,
297 rationale: "test".into(),
298 winner_median_ns: Some(90.0),
299 baseline_median_ns: Some(100.0),
300 }];
301 let size_classes: Vec<silicera::SizeClassEntry> = Vec::new();
302 let profile = HnepProfile {
303 header: header.clone(),
304 environment: environment.clone(),
305 workloads: workloads.clone(),
306 size_classes: size_classes.clone(),
307 decision_tree: None,
308 digest: IntegrityDigest {
309 alg: "sha256".into(),
310 hex: String::new(),
311 },
312 };
313 #[derive(serde::Serialize)]
314 struct Payload {
315 header: HnepHeader,
316 environment: EnvironmentSnapshot,
317 workloads: Vec<WorkloadEntry>,
318 size_classes: Vec<silicera::SizeClassEntry>,
319 decision_tree: Option<silicera::specialize::DecisionTree>,
320 }
321 let p = Payload {
322 header: profile.header.clone(),
323 environment: profile.environment.clone(),
324 workloads: profile.workloads.clone(),
325 size_classes: profile.size_classes.clone(),
326 decision_tree: None,
327 };
328 let bytes = serde_json::to_vec(&p).unwrap();
329 let digest = IntegrityDigest::sha256(&bytes);
330 HnepProfile {
331 digest,
332 ..profile
333 }
334 }
335
336 #[test]
337 fn mismatch_falls_back() {
338 let kp = KnowledgePack::builtin();
339 let host = MockHardware::zen5_dual_ccd().discover(&kp).unwrap();
340 let fp = host.fingerprint.as_ref().unwrap().value.clone();
341 let profile = make_profile(&fp);
342 let loaded =
343 LoadedProfile::from_parts(profile, host, MismatchPolicy::FallbackBaseline).unwrap();
344 assert!(!loaded.force_baseline);
345 assert_eq!(loaded.select_workload("demo"), "fast");
346 }
347
348 #[test]
349 fn wrong_machine_baseline() {
350 let kp = KnowledgePack::builtin();
351 let host = MockHardware::zen5_dual_ccd().discover(&kp).unwrap();
352 let mut profile = make_profile("SLC:AMD:ZEN4:19:61:00:deadbeefdeadbeef:cafebabecafebabe");
353 #[derive(serde::Serialize)]
355 struct Payload {
356 header: HnepHeader,
357 environment: EnvironmentSnapshot,
358 workloads: Vec<WorkloadEntry>,
359 size_classes: Vec<silicera::SizeClassEntry>,
360 decision_tree: Option<silicera::specialize::DecisionTree>,
361 }
362 let p = Payload {
363 header: profile.header.clone(),
364 environment: profile.environment.clone(),
365 workloads: profile.workloads.clone(),
366 size_classes: profile.size_classes.clone(),
367 decision_tree: None,
368 };
369 profile.digest = IntegrityDigest::sha256(&serde_json::to_vec(&p).unwrap());
370 let loaded =
371 LoadedProfile::from_parts(profile, host, MismatchPolicy::FallbackBaseline).unwrap();
372 assert!(loaded.force_baseline);
373 assert_eq!(loaded.select_workload("demo"), "baseline");
374 assert!(loaded.status.contains("PROFILE MISMATCH"));
375 assert!(loaded.status.contains(NAME));
376 let _ = SupportStatus::Unsupported {
377 reason: "x".into(),
378 };
379 let _ = Microarch::Zen5;
380 }
381
382 #[test]
383 fn runtime_about_has_funding() {
384 let about = runtime_about();
385 assert_eq!(about.name, NAME);
386 assert_eq!(about.phase, PHASE);
387 assert_eq!(about.version, VERSION);
388 assert!(about.funding_url.contains("thanks.dev/u/gh/theworker02"));
389 assert_eq!(about.brand_line, BRAND_LINE);
390 assert_eq!(about.license, LICENSE);
391 assert_eq!(about.homepage, HOMEPAGE);
392 assert_eq!(about.repository, REPOSITORY);
393 assert_eq!(about.funding_url, FUNDING_URL);
394 assert_eq!(about.affiliation, AFFILIATION_DISCLAIMER);
395 }
396}