use std::path::Path;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::{Result, SiliceraError};
use crate::fingerprint::Fingerprint;
use crate::hardware::EnvironmentSnapshot;
use crate::specialize::DecisionTree;
use crate::tournament::TournamentResult;
pub const HNEP_VERSION: u32 = 2;
pub const HNEP_VERSION_MIN_SUPPORTED: u32 = 1;
pub const HNEP_VERSION_MAX_SUPPORTED: u32 = 2;
pub const HNEP_FORMAT: &str = "silicera-hnep";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Confidence {
High,
Medium,
Low,
Inconclusive,
}
impl Confidence {
pub fn label(self) -> &'static str {
match self {
Confidence::High => "HIGH",
Confidence::Medium => "MEDIUM",
Confidence::Low => "LOW",
Confidence::Inconclusive => "INCONCLUSIVE",
}
}
pub fn rank(self) -> u8 {
match self {
Confidence::High => 3,
Confidence::Medium => 2,
Confidence::Low => 1,
Confidence::Inconclusive => 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IntegrityDigest {
pub alg: String,
pub hex: String,
}
impl IntegrityDigest {
pub fn sha256(bytes: &[u8]) -> Self {
let mut hasher = Sha256::new();
hasher.update(bytes);
let full = hasher.finalize();
Self {
alg: "sha256".into(),
hex: hex::encode(full),
}
}
pub fn verify(&self, bytes: &[u8]) -> Result<()> {
let expected = Self::sha256(bytes);
if expected.hex != self.hex || expected.alg != self.alg {
return Err(SiliceraError::IntegrityFailed(format!(
"expected {}, got {}",
self.hex, expected.hex
)));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HnepHeader {
pub format: String,
pub version: u32,
pub silicera_version: String,
pub created_at: String,
pub fingerprint: String,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkloadEntry {
pub name: String,
pub winner: String,
pub confidence: Confidence,
pub rationale: String,
pub winner_median_ns: Option<f64>,
pub baseline_median_ns: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SizeClassEntry {
pub class: String,
pub threshold_bytes: u64,
pub working_set_bytes: u64,
pub winner: String,
pub confidence: Confidence,
pub rationale: String,
pub winner_median_ns: Option<f64>,
pub baseline_median_ns: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HnepProfile {
pub header: HnepHeader,
pub environment: EnvironmentSnapshot,
pub workloads: Vec<WorkloadEntry>,
#[serde(default)]
pub size_classes: Vec<SizeClassEntry>,
pub decision_tree: Option<DecisionTree>,
pub digest: IntegrityDigest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct HnepPayload {
header: HnepHeader,
environment: EnvironmentSnapshot,
workloads: Vec<WorkloadEntry>,
#[serde(default)]
size_classes: Vec<SizeClassEntry>,
decision_tree: Option<DecisionTree>,
}
impl HnepProfile {
pub fn from_tournaments(
fingerprint: &Fingerprint,
environment: EnvironmentSnapshot,
results: &[TournamentResult],
size_classes: Vec<SizeClassEntry>,
decision_tree: Option<DecisionTree>,
label: impl Into<String>,
) -> Result<Self> {
let workloads: Vec<WorkloadEntry> = results
.iter()
.map(|r| {
let winner_rec = r.records.iter().find(|x| x.id == r.winner);
let baseline_rec = r.records.iter().find(|x| x.id.0 == "baseline");
WorkloadEntry {
name: r.name.clone(),
winner: r.winner.0.clone(),
confidence: r.confidence,
rationale: r.rationale.clone(),
winner_median_ns: winner_rec.map(|w| w.summary.median_ns),
baseline_median_ns: baseline_rec.map(|b| b.summary.median_ns),
}
})
.collect();
let header = HnepHeader {
format: HNEP_FORMAT.into(),
version: HNEP_VERSION,
silicera_version: crate::VERSION.into(),
created_at: chrono::Utc::now().to_rfc3339(),
fingerprint: fingerprint.value.clone(),
label: label.into(),
};
let payload = HnepPayload {
header: header.clone(),
environment: environment.clone(),
workloads: workloads.clone(),
size_classes: size_classes.clone(),
decision_tree: decision_tree.clone(),
};
let canonical = serde_json::to_vec(&payload)?;
let digest = IntegrityDigest::sha256(&canonical);
Ok(Self {
header,
environment,
workloads,
size_classes,
decision_tree,
digest,
})
}
pub fn recompute_digest(&mut self) -> Result<()> {
let payload = HnepPayload {
header: self.header.clone(),
environment: self.environment.clone(),
workloads: self.workloads.clone(),
size_classes: self.size_classes.clone(),
decision_tree: self.decision_tree.clone(),
};
let canonical = serde_json::to_vec(&payload)?;
self.digest = IntegrityDigest::sha256(&canonical);
Ok(())
}
pub fn verify_integrity(&self) -> Result<()> {
let payload = HnepPayload {
header: self.header.clone(),
environment: self.environment.clone(),
workloads: self.workloads.clone(),
size_classes: self.size_classes.clone(),
decision_tree: self.decision_tree.clone(),
};
let canonical = serde_json::to_vec(&payload)?;
self.digest.verify(&canonical)
}
pub fn write_to(&self, path: &Path) -> Result<()> {
self.verify_integrity()?;
let text = serde_json::to_string_pretty(self)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, text)?;
Ok(())
}
pub fn read_from(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)?;
Self::parse_str(&text)
}
pub fn parse_str(text: &str) -> Result<Self> {
let mut profile: HnepProfile = serde_json::from_str(text)?;
if profile.header.format != HNEP_FORMAT {
return Err(SiliceraError::Parse(format!(
"unexpected format {}",
profile.header.format
)));
}
if profile.header.version < HNEP_VERSION_MIN_SUPPORTED
|| profile.header.version > HNEP_VERSION_MAX_SUPPORTED
{
return Err(SiliceraError::Parse(format!(
"unsupported HNEP version {} (supported {}..={})",
profile.header.version, HNEP_VERSION_MIN_SUPPORTED, HNEP_VERSION_MAX_SUPPORTED
)));
}
if let Err(e) = profile.verify_integrity() {
if profile.header.version == 1 && profile.size_classes.is_empty() {
#[derive(Serialize)]
struct LegacyPayload {
header: HnepHeader,
environment: EnvironmentSnapshot,
workloads: Vec<WorkloadEntry>,
decision_tree: Option<DecisionTree>,
}
let legacy = LegacyPayload {
header: profile.header.clone(),
environment: profile.environment.clone(),
workloads: profile.workloads.clone(),
decision_tree: profile.decision_tree.clone(),
};
let canonical = serde_json::to_vec(&legacy)?;
profile.digest.verify(&canonical).map_err(|_| e)?;
profile.header.version = HNEP_VERSION;
profile.recompute_digest()?;
} else {
return Err(e);
}
}
Ok(profile)
}
pub fn upsert_workload(&mut self, entry: WorkloadEntry) -> Result<()> {
if let Some(slot) = self.workloads.iter_mut().find(|w| w.name == entry.name) {
*slot = entry;
} else {
self.workloads.push(entry);
}
self.recompute_digest()
}
pub fn overall_confidence(&self) -> Confidence {
let w = self.workloads.iter().map(|w| w.confidence);
let s = self.size_classes.iter().map(|c| c.confidence);
w.chain(s)
.min_by_key(|c| c.rank())
.unwrap_or(Confidence::Inconclusive)
}
pub fn check_staleness(&self, now: &EnvironmentSnapshot, max_age_days: i64) -> Result<()> {
let policy = crate::staleness::StalenessPolicy {
soft_age_days: max_age_days,
hard_age_days: max_age_days,
watch_logical_cpus: true,
};
let report = crate::staleness::assess_staleness(self, now, None, &policy);
if report.severity == crate::staleness::DriftSeverity::Hard {
let detail = report
.signals
.iter()
.map(|s| format!("{}: {}", s.field, s.note))
.collect::<Vec<_>>()
.join("; ");
return Err(SiliceraError::StaleProfile(detail));
}
Ok(())
}
pub fn staleness_report(
&self,
now: &EnvironmentSnapshot,
live_fingerprint: Option<&str>,
policy: &crate::staleness::StalenessPolicy,
) -> crate::staleness::StalenessReport {
crate::staleness::assess_staleness(self, now, live_fingerprint, policy)
}
pub fn size_class_winner(&self, class: &str) -> Option<&SizeClassEntry> {
self.size_classes.iter().find(|c| c.class == class)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fingerprint::Fingerprint;
use crate::knowledge::Microarch;
use crate::topology::TopologyGraph;
use crate::variant::VariantId;
use crate::tournament::TournamentResult;
use crate::measure::{MeasurementSummary, Stability};
use crate::variant::VariantRecord;
fn dummy_summary(median: f64) -> MeasurementSummary {
MeasurementSummary {
n: 10,
min_ns: median,
max_ns: median,
mean_ns: median,
median_ns: median,
variance_ns2: 0.0,
stddev_ns: 0.0,
p25_ns: median,
p75_ns: median,
p95_ns: median,
p99_ns: median,
outlier_count: 0,
stability: Stability::Stable,
flags: vec![],
}
}
#[test]
fn hnep_roundtrip() {
let fp = Fingerprint::from_topology(Microarch::Zen5, 0x1A, 0x44, 0, &TopologyGraph::new());
let results = vec![TournamentResult {
name: "demo".into(),
records: vec![VariantRecord {
id: VariantId::new("baseline"),
summary: dummy_summary(100.0),
correct: true,
regression: false,
notes: vec![],
}],
winner: VariantId::new("baseline"),
confidence: Confidence::Inconclusive,
rationale: "test".into(),
}];
let profile = HnepProfile::from_tournaments(
&fp,
EnvironmentSnapshot::capture(),
&results,
Vec::new(),
None,
"test",
)
.unwrap();
assert_eq!(profile.header.version, HNEP_VERSION);
let json = serde_json::to_string(&profile).unwrap();
let parsed = HnepProfile::parse_str(&json).unwrap();
assert_eq!(parsed.header.fingerprint, fp.value);
assert!(parsed.size_classes.is_empty());
}
#[test]
fn size_class_roundtrip() {
let fp = Fingerprint::from_topology(Microarch::Zen5, 0x1A, 0x44, 0, &TopologyGraph::new());
let sc = vec![SizeClassEntry {
class: "L1".into(),
threshold_bytes: 32 * 1024,
working_set_bytes: 16 * 1024,
winner: "scan".into(),
confidence: Confidence::Medium,
rationale: "measured".into(),
winner_median_ns: Some(40.0),
baseline_median_ns: Some(50.0),
}];
let profile = HnepProfile::from_tournaments(
&fp,
EnvironmentSnapshot::capture(),
&[],
sc,
None,
"sc",
)
.unwrap();
let parsed = HnepProfile::parse_str(&serde_json::to_string(&profile).unwrap()).unwrap();
assert_eq!(parsed.size_classes.len(), 1);
assert_eq!(parsed.size_classes[0].winner, "scan");
}
}