use crate::log::{LogLevel, LogService, LogSource}; use std::sync::Arc; use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering}; #[derive(Debug, Clone, Copy, Default)] pub struct TelemetryFrame { pub frame_index: u64, pub vm_steps: u32, pub cycles_used: u64, pub cycles_budget: u64, pub syscalls: u32, pub host_cpu_time_us: u64, pub completed_logical_frames: u32, pub violations: u32, // Bank telemetry pub glyph_slots_used: u32, pub glyph_slots_total: u32, pub sound_slots_used: u32, pub sound_slots_total: u32, // RAM (Heap) pub heap_used_bytes: usize, pub heap_max_bytes: usize, // Log Pressure from the last completed logical frame pub logs_count: u32, } /// Thread-safe, atomic telemetry storage for real-time monitoring by the host. /// This follows the push-based model from DEC-0005 to avoid expensive scans or locks. #[derive(Debug, Default)] pub struct AtomicTelemetry { pub frame_index: AtomicU64, pub cycles_used: AtomicU64, pub cycles_budget: AtomicU64, pub syscalls: AtomicU32, pub host_cpu_time_us: AtomicU64, pub vm_steps: AtomicU32, pub completed_logical_frames: AtomicU32, pub violations: AtomicU32, // Bank telemetry pub glyph_slots_used: AtomicU32, pub glyph_slots_total: AtomicU32, pub sound_slots_used: AtomicU32, pub sound_slots_total: AtomicU32, // RAM (Heap) pub heap_used_bytes: AtomicUsize, pub heap_max_bytes: AtomicUsize, // Transient in-flight log counter for the current logical frame pub current_logs_count: Arc, // Persisted log count from the last completed logical frame pub logs_count: AtomicU32, } impl AtomicTelemetry { pub fn new(current_logs_count: Arc) -> Self { Self { current_logs_count, ..Default::default() } } /// Snapshots the current atomic state into a TelemetryFrame. pub fn snapshot(&self) -> TelemetryFrame { TelemetryFrame { frame_index: self.frame_index.load(Ordering::Relaxed), cycles_used: self.cycles_used.load(Ordering::Relaxed), cycles_budget: self.cycles_budget.load(Ordering::Relaxed), syscalls: self.syscalls.load(Ordering::Relaxed), host_cpu_time_us: self.host_cpu_time_us.load(Ordering::Relaxed), completed_logical_frames: self.completed_logical_frames.load(Ordering::Relaxed), violations: self.violations.load(Ordering::Relaxed), glyph_slots_used: self.glyph_slots_used.load(Ordering::Relaxed), glyph_slots_total: self.glyph_slots_total.load(Ordering::Relaxed), sound_slots_used: self.sound_slots_used.load(Ordering::Relaxed), sound_slots_total: self.sound_slots_total.load(Ordering::Relaxed), heap_used_bytes: self.heap_used_bytes.load(Ordering::Relaxed), heap_max_bytes: self.heap_max_bytes.load(Ordering::Relaxed), logs_count: self.logs_count.load(Ordering::Relaxed), vm_steps: self.vm_steps.load(Ordering::Relaxed), } } pub fn reset(&self) { self.frame_index.store(0, Ordering::Relaxed); self.cycles_used.store(0, Ordering::Relaxed); self.syscalls.store(0, Ordering::Relaxed); self.host_cpu_time_us.store(0, Ordering::Relaxed); self.completed_logical_frames.store(0, Ordering::Relaxed); self.violations.store(0, Ordering::Relaxed); self.glyph_slots_used.store(0, Ordering::Relaxed); self.glyph_slots_total.store(0, Ordering::Relaxed); self.sound_slots_used.store(0, Ordering::Relaxed); self.sound_slots_total.store(0, Ordering::Relaxed); self.heap_used_bytes.store(0, Ordering::Relaxed); self.vm_steps.store(0, Ordering::Relaxed); self.logs_count.store(0, Ordering::Relaxed); self.current_logs_count.store(0, Ordering::Relaxed); } } #[derive(Debug, Clone, Copy, Default)] pub struct CertificationConfig { pub enabled: bool, pub cycles_budget_per_frame: Option, pub max_syscalls_per_frame: Option, pub max_host_cpu_us_per_frame: Option, pub max_glyph_slots_used: Option, pub max_sound_slots_used: Option, pub max_heap_bytes: Option, pub max_logs_per_frame: Option, } pub struct Certifier { pub config: CertificationConfig, } impl Certifier { pub fn new(config: CertificationConfig) -> Self { Self { config } } pub fn evaluate( &self, telemetry: &TelemetryFrame, log_service: &mut LogService, ts_ms: u64, ) -> usize { if !self.config.enabled { return 0; } let mut violations = 0; // 1. Cycles if let Some(budget) = self.config.cycles_budget_per_frame && telemetry.cycles_used > budget { log_service.log( ts_ms, telemetry.frame_index, LogLevel::Warn, LogSource::Pos, 0xCA01, format!( "Cert: cycles_used exceeded budget ({} > {})", telemetry.cycles_used, budget ), ); violations += 1; } // 2. Syscalls if let Some(limit) = self.config.max_syscalls_per_frame && telemetry.syscalls > limit { log_service.log( ts_ms, telemetry.frame_index, LogLevel::Warn, LogSource::Pos, 0xCA02, format!( "Cert: syscalls per frame exceeded limit ({} > {})", telemetry.syscalls, limit ), ); violations += 1; } // 3. CPU Time if let Some(limit) = self.config.max_host_cpu_us_per_frame && telemetry.host_cpu_time_us > limit { log_service.log( ts_ms, telemetry.frame_index, LogLevel::Warn, LogSource::Pos, 0xCA03, format!( "Cert: host_cpu_time_us exceeded limit ({} > {})", telemetry.host_cpu_time_us, limit ), ); violations += 1; } // 4. GLYPH bank slots if let Some(limit) = self.config.max_glyph_slots_used && telemetry.glyph_slots_used > limit { log_service.log( ts_ms, telemetry.frame_index, LogLevel::Warn, LogSource::Pos, 0xCA04, format!( "Cert: GLYPH bank exceeded slot limit ({} > {})", telemetry.glyph_slots_used, limit ), ); violations += 1; } // 5. SOUNDS bank slots if let Some(limit) = self.config.max_sound_slots_used && telemetry.sound_slots_used > limit { log_service.log( ts_ms, telemetry.frame_index, LogLevel::Warn, LogSource::Pos, 0xCA05, format!( "Cert: SOUNDS bank exceeded slot limit ({} > {})", telemetry.sound_slots_used, limit ), ); violations += 1; } // 6. Heap Memory if let Some(limit) = self.config.max_heap_bytes && telemetry.heap_used_bytes > limit { log_service.log( ts_ms, telemetry.frame_index, LogLevel::Warn, LogSource::Pos, 0xCA06, format!( "Cert: Heap memory exceeded limit ({} > {})", telemetry.heap_used_bytes, limit ), ); violations += 1; } // 7. Log Pressure if let Some(limit) = self.config.max_logs_per_frame && telemetry.logs_count > limit { log_service.log( ts_ms, telemetry.frame_index, LogLevel::Warn, LogSource::Pos, 0xCA07, format!("Cert: Log pressure exceeded limit ({} > {})", telemetry.logs_count, limit), ); violations += 1; } violations } } #[cfg(test)] mod tests { use super::*; use crate::log::LogService; #[test] fn test_certifier_violations() { let mut ls = LogService::new(10); let config = CertificationConfig { enabled: true, cycles_budget_per_frame: Some(100), max_syscalls_per_frame: Some(5), max_host_cpu_us_per_frame: Some(1000), max_glyph_slots_used: Some(1), ..Default::default() }; let cert = Certifier::new(config); let mut tel = TelemetryFrame::default(); tel.cycles_used = 150; tel.syscalls = 10; tel.host_cpu_time_us = 500; tel.glyph_slots_used = 2; let violations = cert.evaluate(&tel, &mut ls, 1000); assert_eq!(violations, 3); let logs = ls.get_recent(10); assert_eq!(logs.len(), 3); assert!(logs[0].msg.contains("cycles_used")); assert!(logs[1].msg.contains("syscalls")); assert!(logs[2].msg.contains("GLYPH bank")); } #[test] fn snapshot_uses_persisted_last_frame_logs() { let current = Arc::new(AtomicU32::new(7)); let tel = AtomicTelemetry::new(Arc::clone(¤t)); tel.logs_count.store(3, Ordering::Relaxed); let snapshot = tel.snapshot(); assert_eq!(snapshot.logs_count, 3); assert_eq!(current.load(Ordering::Relaxed), 7); } }