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 profile = HnepProfile::parse_str(&serde_json::to_string(&profile)?)?;
129 let host_fp = match &host.fingerprint {
130 Some(fp) => fp.clone(),
131 None => {
132 if policy == MismatchPolicy::StrictMachine {
133 return Err(SiliceraError::ProfileMismatch(format!(
134 "strict-machine ({NAME}): host unsupported ({})",
135 host.support.message()
136 )));
137 }
138 return Ok(Self {
139 status: format!(
140 "PROFILE MISMATCH ({NAME}): host unsupported ({})",
141 host.support.message()
142 ),
143 profile,
144 host,
145 exact_match: false,
146 class_match: false,
147 policy,
148 force_baseline: true,
149 });
150 }
151 };
152
153 let profile_fp = Fingerprint::parse(&profile.header.fingerprint)?;
154 let exact_match = host_fp.exact_match(&profile_fp);
155 let class_match = host_fp.same_silicon_class(&profile_fp);
156
157 let (force_baseline, status) = if exact_match {
158 (false, format!("profile match: exact fingerprint ({NAME})"))
159 } else if class_match {
160 match policy {
161 MismatchPolicy::FallbackBaseline => (
162 true,
163 format!(
164 "PROFILE MISMATCH ({NAME}): topology/cache hash differs (class OK); falling back to baseline\n profile: {}\n host: {}",
165 profile_fp, host_fp
166 ),
167 ),
168 MismatchPolicy::StrictMachine => {
169 return Err(SiliceraError::ProfileMismatch(format!(
170 "strict-machine ({NAME}): fingerprint mismatch\n profile: {profile_fp}\n host: {host_fp}"
171 )));
172 }
173 }
174 } else {
175 match policy {
176 MismatchPolicy::FallbackBaseline => (
177 true,
178 format!(
179 "PROFILE MISMATCH ({NAME}): silicon class differs; falling back to baseline\n profile: {}\n host: {}",
180 profile_fp, host_fp
181 ),
182 ),
183 MismatchPolicy::StrictMachine => {
184 return Err(SiliceraError::ProfileMismatch(format!(
185 "strict-machine ({NAME}): silicon class mismatch\n profile: {profile_fp}\n host: {host_fp}"
186 )));
187 }
188 }
189 };
190
191 Ok(Self {
192 profile,
193 host,
194 exact_match,
195 class_match,
196 policy,
197 force_baseline,
198 status,
199 })
200 }
201
202 pub fn select_workload(&self, workload: &str) -> &str {
204 if self.force_baseline {
205 return "baseline";
206 }
207 self.profile
208 .workloads
209 .iter()
210 .find(|w| w.name == workload)
211 .map(|w| w.winner.as_str())
212 .unwrap_or("baseline")
213 }
214
215 pub fn select_size(&self, size_bytes: u64) -> &str {
217 if self.force_baseline {
218 return "baseline";
219 }
220 match &self.profile.decision_tree {
221 Some(tree) => tree.evaluate(size_bytes),
222 None => "baseline",
223 }
224 }
225
226 pub fn decision_tree(&self) -> Option<&DecisionTree> {
228 self.profile.decision_tree.as_ref()
229 }
230
231 pub fn guarded_workload<'a>(
235 &'a self,
236 workload: &str,
237 available: &[&str],
238 minimum: silicera::hnep::Confidence,
239 ) -> DispatchDecision<'a> {
240 if self.force_baseline {
241 return DispatchDecision::baseline(DispatchReason::MachineMismatch);
242 }
243 let Some(entry) = self.profile.workloads.iter().find(|w| w.name == workload) else {
244 return DispatchDecision::baseline(DispatchReason::MissingWorkload);
245 };
246 if entry.winner == "baseline" {
247 return DispatchDecision::baseline(DispatchReason::ProfileBaseline);
248 }
249 if entry.confidence.rank() < minimum.rank() {
250 return DispatchDecision::baseline(DispatchReason::InsufficientConfidence);
251 }
252 if !available.contains(&entry.winner.as_str()) {
253 return DispatchDecision::baseline(DispatchReason::UnavailableVariant);
254 }
255 DispatchDecision {
256 variant: &entry.winner,
257 reason: DispatchReason::Selected,
258 }
259 }
260
261 pub fn guarded_size<'a>(&'a self, bytes: u64, available: &[&str]) -> DispatchDecision<'a> {
264 if self.force_baseline {
265 return DispatchDecision::baseline(DispatchReason::MachineMismatch);
266 }
267 let Some(tree) = &self.profile.decision_tree else {
268 return DispatchDecision::baseline(DispatchReason::MissingTree);
269 };
270 let variant = tree.evaluate(bytes);
271 if variant == "baseline" {
272 return DispatchDecision::baseline(DispatchReason::ProfileBaseline);
273 }
274 if !available.contains(&variant) {
275 return DispatchDecision::baseline(DispatchReason::UnavailableVariant);
276 }
277 DispatchDecision {
278 variant,
279 reason: DispatchReason::Selected,
280 }
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
286#[serde(rename_all = "snake_case")]
287pub enum DispatchReason {
288 Selected,
290 ProfileBaseline,
292 MachineMismatch,
294 MissingWorkload,
296 MissingTree,
298 InsufficientConfidence,
300 UnavailableVariant,
302}
303
304#[derive(Debug, Clone, Copy, Serialize)]
306pub struct DispatchDecision<'a> {
307 pub variant: &'a str,
309 pub reason: DispatchReason,
311}
312
313impl DispatchDecision<'_> {
314 fn baseline(reason: DispatchReason) -> Self {
315 Self {
316 variant: "baseline",
317 reason,
318 }
319 }
320}
321
322pub struct Dispatcher {
324 loaded: LoadedProfile,
325}
326
327impl Dispatcher {
328 pub fn open(path: &Path, policy: MismatchPolicy) -> Result<Self> {
330 Ok(Self {
331 loaded: LoadedProfile::load(path, policy)?,
332 })
333 }
334
335 pub fn status(&self) -> &str {
337 &self.loaded.status
338 }
339
340 pub fn using_baseline(&self) -> bool {
342 self.loaded.force_baseline
343 }
344
345 pub fn workload(&self, name: &str) -> &str {
347 self.loaded.select_workload(name)
348 }
349
350 pub fn size(&self, bytes: u64) -> &str {
352 self.loaded.select_size(bytes)
353 }
354
355 pub fn loaded(&self) -> &LoadedProfile {
357 &self.loaded
358 }
359
360 pub fn about(&self) -> RuntimeInfo {
362 runtime_about()
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use silicera::brand::{
370 AFFILIATION_DISCLAIMER, BRAND_LINE, FUNDING_URL, HOMEPAGE, LICENSE, PHASE, REPOSITORY,
371 VERSION,
372 };
373 use silicera::hardware::{EnvironmentSnapshot, MockHardware, SupportStatus};
374 use silicera::hnep::{Confidence, HnepHeader, HnepProfile, IntegrityDigest, WorkloadEntry};
375 use silicera::knowledge::{KnowledgePack, Microarch};
376 use silicera::HardwareBackend;
377
378 fn make_profile(fp: &str) -> HnepProfile {
379 let header = HnepHeader {
380 format: silicera::hnep::HNEP_FORMAT.into(),
381 version: silicera::hnep::HNEP_VERSION,
382 silicera_version: silicera::VERSION.into(),
383 created_at: chrono::Utc::now().to_rfc3339(),
384 fingerprint: fp.into(),
385 label: "test".into(),
386 };
387 let environment = EnvironmentSnapshot::capture();
388 let workloads = vec![WorkloadEntry {
389 name: "demo".into(),
390 winner: "fast".into(),
391 confidence: Confidence::Medium,
392 rationale: "test".into(),
393 winner_median_ns: Some(90.0),
394 baseline_median_ns: Some(100.0),
395 }];
396 let size_classes: Vec<silicera::SizeClassEntry> = Vec::new();
397 let profile = HnepProfile {
398 header: header.clone(),
399 environment: environment.clone(),
400 workloads: workloads.clone(),
401 size_classes: size_classes.clone(),
402 decision_tree: None,
403 digest: IntegrityDigest {
404 alg: "sha256".into(),
405 hex: String::new(),
406 },
407 };
408 #[derive(serde::Serialize)]
409 struct Payload {
410 header: HnepHeader,
411 environment: EnvironmentSnapshot,
412 workloads: Vec<WorkloadEntry>,
413 size_classes: Vec<silicera::SizeClassEntry>,
414 decision_tree: Option<silicera::specialize::DecisionTree>,
415 }
416 let p = Payload {
417 header: profile.header.clone(),
418 environment: profile.environment.clone(),
419 workloads: profile.workloads.clone(),
420 size_classes: profile.size_classes.clone(),
421 decision_tree: None,
422 };
423 let bytes = serde_json::to_vec(&p).unwrap();
424 let digest = IntegrityDigest::sha256(&bytes);
425 HnepProfile { digest, ..profile }
426 }
427
428 #[test]
429 fn mismatch_falls_back() {
430 let kp = KnowledgePack::builtin();
431 let host = MockHardware::zen5_dual_ccd().discover(&kp).unwrap();
432 let fp = host.fingerprint.as_ref().unwrap().value.clone();
433 let profile = make_profile(&fp);
434 let loaded =
435 LoadedProfile::from_parts(profile, host, MismatchPolicy::FallbackBaseline).unwrap();
436 assert!(!loaded.force_baseline);
437 assert_eq!(loaded.select_workload("demo"), "fast");
438 }
439
440 #[test]
441 fn wrong_machine_baseline() {
442 let kp = KnowledgePack::builtin();
443 let host = MockHardware::zen5_dual_ccd().discover(&kp).unwrap();
444 let mut profile = make_profile("SLC:AMD:ZEN4:19:61:00:deadbeefdeadbeef:cafebabecafebabe");
445 #[derive(serde::Serialize)]
447 struct Payload {
448 header: HnepHeader,
449 environment: EnvironmentSnapshot,
450 workloads: Vec<WorkloadEntry>,
451 size_classes: Vec<silicera::SizeClassEntry>,
452 decision_tree: Option<silicera::specialize::DecisionTree>,
453 }
454 let p = Payload {
455 header: profile.header.clone(),
456 environment: profile.environment.clone(),
457 workloads: profile.workloads.clone(),
458 size_classes: profile.size_classes.clone(),
459 decision_tree: None,
460 };
461 profile.digest = IntegrityDigest::sha256(&serde_json::to_vec(&p).unwrap());
462 let loaded =
463 LoadedProfile::from_parts(profile, host, MismatchPolicy::FallbackBaseline).unwrap();
464 assert!(loaded.force_baseline);
465 assert_eq!(loaded.select_workload("demo"), "baseline");
466 assert!(loaded.status.contains("PROFILE MISMATCH"));
467 assert!(loaded.status.contains(NAME));
468 let _ = SupportStatus::Unsupported { reason: "x".into() };
469 let _ = Microarch::Zen5;
470 }
471
472 #[test]
473 fn runtime_about_has_funding() {
474 let about = runtime_about();
475 assert_eq!(about.name, NAME);
476 assert_eq!(about.phase, PHASE);
477 assert_eq!(about.version, VERSION);
478 assert!(about.funding_url.contains("thanks.dev/u/gh/theworker02"));
479 assert_eq!(about.brand_line, BRAND_LINE);
480 assert_eq!(about.license, LICENSE);
481 assert_eq!(about.homepage, HOMEPAGE);
482 assert_eq!(about.repository, REPOSITORY);
483 assert_eq!(about.funding_url, FUNDING_URL);
484 assert_eq!(about.affiliation, AFFILIATION_DISCLAIMER);
485 }
486
487 #[test]
488 fn unsupported_host_obeys_strict_policy() {
489 for policy in [
490 MismatchPolicy::FallbackBaseline,
491 MismatchPolicy::StrictMachine,
492 ] {
493 let host = MockHardware::unsupported_intel()
494 .discover(&KnowledgePack::builtin())
495 .unwrap();
496 let profile = make_profile("SLC:AMD:ZEN4:19:61:00:deadbeefdeadbeef:cafebabecafebabe");
497 let result = LoadedProfile::from_parts(profile, host, policy);
498 if policy == MismatchPolicy::StrictMachine {
499 assert!(matches!(result, Err(SiliceraError::ProfileMismatch(_))));
500 } else {
501 let loaded = result.unwrap();
502 assert_eq!(
503 loaded
504 .guarded_workload("demo", &["fast"], Confidence::Medium)
505 .reason,
506 DispatchReason::MachineMismatch
507 );
508 assert_eq!(loaded.select_size(100), "baseline");
509 }
510 }
511 }
512
513 #[test]
514 fn direct_load_rejects_tampering_and_unknown_schema() {
515 let host = MockHardware::zen5_dual_ccd()
516 .discover(&KnowledgePack::builtin())
517 .unwrap();
518 let mut profile = make_profile(&host.fingerprint.as_ref().unwrap().value);
519 profile.workloads[0].winner = "tampered".into();
520 assert!(LoadedProfile::from_parts(
521 profile.clone(),
522 host.clone(),
523 MismatchPolicy::FallbackBaseline
524 )
525 .is_err());
526 profile.header.version = 999;
527 profile.recompute_digest().unwrap();
528 assert!(
529 LoadedProfile::from_parts(profile, host, MismatchPolicy::FallbackBaseline).is_err()
530 );
531 }
532
533 #[test]
534 fn guarded_dispatch_checks_registry_confidence_and_boundaries() {
535 let host = MockHardware::zen5_dual_ccd()
536 .discover(&KnowledgePack::builtin())
537 .unwrap();
538 let mut profile = make_profile(&host.fingerprint.as_ref().unwrap().value);
539 profile.decision_tree = Some(DecisionTree::from_thresholds(
540 10, 20, 30, "fast", "missing", "baseline", "fast", "baseline",
541 ));
542 profile.recompute_digest().unwrap();
543 let loaded =
544 LoadedProfile::from_parts(profile, host, MismatchPolicy::StrictMachine).unwrap();
545 assert_eq!(
546 loaded
547 .guarded_workload("demo", &["fast"], Confidence::Medium)
548 .variant,
549 "fast"
550 );
551 assert_eq!(
552 loaded
553 .guarded_workload("demo", &["fast"], Confidence::High)
554 .reason,
555 DispatchReason::InsufficientConfidence
556 );
557 assert_eq!(
558 loaded.guarded_workload("demo", &[], Confidence::Low).reason,
559 DispatchReason::UnavailableVariant
560 );
561 assert_eq!(
562 loaded
563 .guarded_workload("unknown", &["fast"], Confidence::Low)
564 .reason,
565 DispatchReason::MissingWorkload
566 );
567 assert_eq!(loaded.guarded_size(9, &["fast"]).variant, "fast");
568 assert_eq!(
569 loaded.guarded_size(10, &["fast"]).reason,
570 DispatchReason::UnavailableVariant
571 );
572 assert_eq!(
573 loaded.guarded_size(20, &["fast"]).reason,
574 DispatchReason::ProfileBaseline
575 );
576 assert_eq!(loaded.guarded_size(u64::MAX, &["fast"]).variant, "fast");
577 }
578}