Skip to main content

silicera_runtime/
lib.rs

1//! # silicera-runtime
2//!
3//! Lightweight HNEP load and cheap decision-tree dispatch.
4//!
5//! This crate intentionally does **not** depend on `silicera-lab`. Specialized
6//! binaries embed or load a profile and select variants without re-running
7//! tournaments.
8//!
9//! On fingerprint mismatch the runtime reports `PROFILE MISMATCH` and falls
10//! back to the baseline variant unless `--strict-machine` / strict API is used.
11
12#![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/// Embedder-facing about / identity block (version, phase, brand, funding).
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct RuntimeInfo {
31    /// Product name.
32    pub name: String,
33    /// Runtime crate version (same workspace semver).
34    pub version: String,
35    /// Phase marker.
36    pub phase: String,
37    /// Brand line.
38    pub brand_line: String,
39    /// SPDX license.
40    pub license: String,
41    /// Homepage.
42    pub homepage: String,
43    /// Repository URL.
44    pub repository: String,
45    /// Funding / thanks.dev URL.
46    pub funding_url: String,
47    /// Affiliation disclaimer.
48    pub affiliation: String,
49}
50
51impl RuntimeInfo {
52    /// Capture current runtime identity from the shared brand module.
53    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    /// Pretty JSON for tooling.
69    pub fn to_json_pretty(&self) -> Result<String> {
70        Ok(serde_json::to_string_pretty(self)?)
71    }
72}
73
74/// Convenience: brand/version/phase for embedders.
75pub fn runtime_about() -> RuntimeInfo {
76    RuntimeInfo::current()
77}
78
79/// Runtime policy when the loaded profile does not match the host.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81pub enum MismatchPolicy {
82    /// Print/return mismatch and use baseline fallback.
83    FallbackBaseline,
84    /// Hard error — refuse to dispatch specialized variants.
85    StrictMachine,
86}
87
88impl Default for MismatchPolicy {
89    fn default() -> Self {
90        Self::FallbackBaseline
91    }
92}
93
94/// Outcome of loading a profile against the current machine.
95#[derive(Debug, Clone)]
96pub struct LoadedProfile {
97    /// Parsed HNEP.
98    pub profile: HnepProfile,
99    /// Host hardware at load time.
100    pub host: HardwareInfo,
101    /// Whether fingerprint matched exactly.
102    pub exact_match: bool,
103    /// Whether silicon class matched (vendor/microarch/family/model).
104    pub class_match: bool,
105    /// Active mismatch policy.
106    pub policy: MismatchPolicy,
107    /// True if dispatch should use baseline only.
108    pub force_baseline: bool,
109    /// Operator-facing status line.
110    pub status: String,
111}
112
113impl LoadedProfile {
114    /// Load from path and compare to live hardware.
115    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    /// Construct from already-loaded pieces (tests / inject mock host).
122    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    /// Select variant for a workload by name (or baseline if forced).
198    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    /// Evaluate size-dependent tree, or baseline on mismatch / missing tree.
211    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    /// Access decision tree when present.
222    pub fn decision_tree(&self) -> Option<&DecisionTree> {
223        self.profile.decision_tree.as_ref()
224    }
225}
226
227/// Convenience: dispatch API used by specialized binaries.
228pub struct Dispatcher {
229    loaded: LoadedProfile,
230}
231
232impl Dispatcher {
233    /// Load profile with policy.
234    pub fn open(path: &Path, policy: MismatchPolicy) -> Result<Self> {
235        Ok(Self {
236            loaded: LoadedProfile::load(path, policy)?,
237        })
238    }
239
240    /// Status line (match / mismatch).
241    pub fn status(&self) -> &str {
242        &self.loaded.status
243    }
244
245    /// Whether baseline is forced.
246    pub fn using_baseline(&self) -> bool {
247        self.loaded.force_baseline
248    }
249
250    /// Select by workload name.
251    pub fn workload(&self, name: &str) -> &str {
252        self.loaded.select_workload(name)
253    }
254
255    /// Select by working-set size.
256    pub fn size(&self, bytes: u64) -> &str {
257        self.loaded.select_size(bytes)
258    }
259
260    /// Borrow loaded profile.
261    pub fn loaded(&self) -> &LoadedProfile {
262        &self.loaded
263    }
264
265    /// Brand / version about block for embedders.
266    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        // Fix digest for wrong fp profile
354        #[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}