use std::time::Duration;
#[derive(Debug, Clone, Copy)]
pub struct PlatformSpecs {
pub min_read_speed_mbps: f64,
pub min_decomp_speed_mbps: f64,
pub crc32_throughput_mbps: f64,
pub ed25519_verify_us: f64,
pub deserialize_throughput_bps: f64,
}
impl PlatformSpecs {
#[must_use]
pub const fn new(
min_read_speed_mbps: f64,
min_decomp_speed_mbps: f64,
crc32_throughput_mbps: f64,
ed25519_verify_us: f64,
deserialize_throughput_bps: f64,
) -> Self {
Self {
min_read_speed_mbps,
min_decomp_speed_mbps,
crc32_throughput_mbps,
ed25519_verify_us,
deserialize_throughput_bps,
}
}
#[must_use]
pub fn effective_throughput_mbps(&self) -> f64 {
self.min_read_speed_mbps.min(self.min_decomp_speed_mbps)
}
}
pub mod platforms {
use super::PlatformSpecs;
pub const AUTOMOTIVE_S32G: PlatformSpecs = PlatformSpecs {
min_read_speed_mbps: 50.0, min_decomp_speed_mbps: 200.0, crc32_throughput_mbps: 2000.0, ed25519_verify_us: 800.0, deserialize_throughput_bps: 500_000_000.0,
};
pub const AEROSPACE_RAD750: PlatformSpecs = PlatformSpecs {
min_read_speed_mbps: 10.0, min_decomp_speed_mbps: 50.0, crc32_throughput_mbps: 100.0, ed25519_verify_us: 5000.0, deserialize_throughput_bps: 50_000_000.0,
};
pub const EDGE_RPI4: PlatformSpecs = PlatformSpecs {
min_read_speed_mbps: 100.0,
min_decomp_speed_mbps: 400.0,
crc32_throughput_mbps: 3000.0,
ed25519_verify_us: 200.0,
deserialize_throughput_bps: 1_000_000_000.0,
};
pub const DESKTOP_X86: PlatformSpecs = PlatformSpecs {
min_read_speed_mbps: 500.0, min_decomp_speed_mbps: 800.0, crc32_throughput_mbps: 5000.0,
ed25519_verify_us: 50.0,
deserialize_throughput_bps: 2_000_000_000.0,
};
pub const WASM_BROWSER: PlatformSpecs = PlatformSpecs {
min_read_speed_mbps: 20.0, min_decomp_speed_mbps: 100.0, crc32_throughput_mbps: 500.0,
ed25519_verify_us: 1000.0,
deserialize_throughput_bps: 200_000_000.0,
};
pub const INDUSTRIAL_PLC: PlatformSpecs = PlatformSpecs {
min_read_speed_mbps: 25.0,
min_decomp_speed_mbps: 80.0,
crc32_throughput_mbps: 800.0,
ed25519_verify_us: 2000.0,
deserialize_throughput_bps: 100_000_000.0,
};
}
#[derive(Debug, Clone, Copy)]
pub struct HeaderInfo {
pub compressed_size_bytes: u64,
pub uncompressed_size_bytes: u64,
pub payload_size_bytes: u64,
pub is_signed: bool,
pub model_type: u16,
}
impl HeaderInfo {
#[must_use]
pub const fn new(
compressed_size_bytes: u64,
uncompressed_size_bytes: u64,
is_signed: bool,
) -> Self {
Self {
compressed_size_bytes,
uncompressed_size_bytes,
payload_size_bytes: compressed_size_bytes,
is_signed,
model_type: 0,
}
}
#[must_use]
pub fn compression_ratio(&self) -> f64 {
if self.compressed_size_bytes == 0 {
1.0
} else {
self.uncompressed_size_bytes as f64 / self.compressed_size_bytes as f64
}
}
}
#[must_use]
pub fn calculate_wcet(header: &HeaderInfo, platform: &PlatformSpecs) -> Duration {
let header_time_us = 1.0;
let compressed_mb = header.compressed_size_bytes as f64 / (1024.0 * 1024.0);
let read_time_us = (compressed_mb / platform.min_read_speed_mbps) * 1_000_000.0;
let uncompressed_mb = header.uncompressed_size_bytes as f64 / (1024.0 * 1024.0);
let decomp_time_us = (uncompressed_mb / platform.min_decomp_speed_mbps) * 1_000_000.0;
let payload_mb = header.payload_size_bytes as f64 / (1024.0 * 1024.0);
let crc32_time_us = (payload_mb / platform.crc32_throughput_mbps) * 1_000_000.0;
let verify_time_us = if header.is_signed {
crc32_time_us + platform.ed25519_verify_us
} else {
crc32_time_us
};
let deserialize_time_us =
header.uncompressed_size_bytes as f64 / platform.deserialize_throughput_bps * 1_000_000.0;
let total_us =
(header_time_us + read_time_us + decomp_time_us + verify_time_us + deserialize_time_us)
* 1.1;
Duration::from_micros(total_us.ceil() as u64)
}
#[must_use]
pub fn calculate_wcet_breakdown(header: &HeaderInfo, platform: &PlatformSpecs) -> WcetBreakdown {
let compressed_mb = header.compressed_size_bytes as f64 / (1024.0 * 1024.0);
let uncompressed_mb = header.uncompressed_size_bytes as f64 / (1024.0 * 1024.0);
let payload_mb = header.payload_size_bytes as f64 / (1024.0 * 1024.0);
let header_us = 1.0;
let read_us = (compressed_mb / platform.min_read_speed_mbps) * 1_000_000.0;
let decomp_us = (uncompressed_mb / platform.min_decomp_speed_mbps) * 1_000_000.0;
let crc32_us = (payload_mb / platform.crc32_throughput_mbps) * 1_000_000.0;
let verify_us = if header.is_signed {
crc32_us + platform.ed25519_verify_us
} else {
crc32_us
};
let deserialize_us =
header.uncompressed_size_bytes as f64 / platform.deserialize_throughput_bps * 1_000_000.0;
let total_us = (header_us + read_us + decomp_us + verify_us + deserialize_us) * 1.1;
WcetBreakdown {
header: Duration::from_micros(header_us.ceil() as u64),
read: Duration::from_micros(read_us.ceil() as u64),
decompress: Duration::from_micros(decomp_us.ceil() as u64),
verify: Duration::from_micros(verify_us.ceil() as u64),
deserialize: Duration::from_micros(deserialize_us.ceil() as u64),
total: Duration::from_micros(total_us.ceil() as u64),
safety_margin: 0.1,
}
}
#[derive(Debug, Clone)]
pub struct WcetBreakdown {
pub header: Duration,
pub read: Duration,
pub decompress: Duration,
pub verify: Duration,
pub deserialize: Duration,
pub total: Duration,
pub safety_margin: f64,
}
impl WcetBreakdown {
#[must_use]
pub fn dominant_component(&self) -> &'static str {
let max = self
.header
.max(self.read)
.max(self.decompress)
.max(self.verify)
.max(self.deserialize);
if max == self.read {
"Storage I/O"
} else if max == self.decompress {
"Decompression"
} else if max == self.verify {
"Verification"
} else if max == self.deserialize {
"Deserialization"
} else {
"Header validation"
}
}
#[must_use]
pub fn percentages(&self) -> WcetPercentages {
let base_total = self.total.as_micros() as f64 / 1.1; WcetPercentages {
header: self.header.as_micros() as f64 / base_total * 100.0,
read: self.read.as_micros() as f64 / base_total * 100.0,
decompress: self.decompress.as_micros() as f64 / base_total * 100.0,
verify: self.verify.as_micros() as f64 / base_total * 100.0,
deserialize: self.deserialize.as_micros() as f64 / base_total * 100.0,
}
}
}
#[derive(Debug, Clone)]
pub struct WcetPercentages {
pub header: f64,
pub read: f64,
pub decompress: f64,
pub verify: f64,
pub deserialize: f64,
}
#[must_use]
pub fn estimate_max_size_for_budget(platform: &PlatformSpecs, budget: Duration) -> usize {
let budget_us = budget.as_micros() as f64;
let effective_throughput = platform.effective_throughput_mbps();
let max_mb = (budget_us / 1_000_000.0) * effective_throughput * 0.8; (max_mb * 1024.0 * 1024.0) as usize
}
#[must_use]
pub fn min_ring_buffer_size(decomp_max_mbps: f64, consume_min_mbps: f64, window_ms: f64) -> usize {
let rate_diff_mbps = decomp_max_mbps - consume_min_mbps;
if rate_diff_mbps <= 0.0 {
return 64 * 1024; }
let buffer_mb = rate_diff_mbps * (window_ms / 1000.0);
let buffer_bytes = (buffer_mb * 1024.0 * 1024.0).ceil() as usize;
buffer_bytes.div_ceil(4096) * 4096
}
#[derive(Debug, Clone)]
pub enum SafetyError {
TimeBudgetExceeded {
budget: Duration,
worst_case: Duration,
model_type: u16,
compressed_size: u64,
recommendation: String,
},
MemoryBudgetExceeded {
budget: usize,
required: usize,
},
IntegrityCheckFailed {
expected: u32,
computed: u32,
},
}
impl std::fmt::Display for SafetyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TimeBudgetExceeded {
budget,
worst_case,
recommendation,
..
} => {
write!(
f,
"Time budget exceeded: budget={budget:?}, worst_case={worst_case:?}. {recommendation}"
)
}
Self::MemoryBudgetExceeded { budget, required } => {
write!(
f,
"Memory budget exceeded: budget={budget} bytes, required={required} bytes"
)
}
Self::IntegrityCheckFailed { expected, computed } => {
write!(
f,
"Integrity check failed: expected=0x{expected:08X}, computed=0x{computed:08X}"
)
}
}
}
}
impl std::error::Error for SafetyError {}
pub fn assert_time_budget(
header: &HeaderInfo,
platform: &PlatformSpecs,
budget: Duration,
) -> Result<(), SafetyError> {
let worst_case = calculate_wcet(header, platform);
if worst_case > budget {
return Err(SafetyError::TimeBudgetExceeded {
budget,
worst_case,
model_type: header.model_type,
compressed_size: header.compressed_size_bytes,
recommendation: format!(
"Reduce model size below {} bytes or use faster storage",
estimate_max_size_for_budget(platform, budget)
),
});
}
Ok(())
}
pub fn assert_memory_budget(required: usize, budget: usize) -> Result<(), SafetyError> {
if required > budget {
return Err(SafetyError::MemoryBudgetExceeded { budget, required });
}
Ok(())
}
#[cfg(test)]
#[path = "wcet_tests.rs"]
mod tests;