309 lines
9.7 KiB
Rust

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<AtomicU32>,
// Persisted log count from the last completed logical frame
pub logs_count: AtomicU32,
}
impl AtomicTelemetry {
pub fn new(current_logs_count: Arc<AtomicU32>) -> 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<u64>,
pub max_syscalls_per_frame: Option<u32>,
pub max_host_cpu_us_per_frame: Option<u64>,
pub max_glyph_slots_used: Option<u32>,
pub max_sound_slots_used: Option<u32>,
pub max_heap_bytes: Option<usize>,
pub max_logs_per_frame: Option<u32>,
}
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(&current));
tel.logs_count.store(3, Ordering::Relaxed);
let snapshot = tel.snapshot();
assert_eq!(snapshot.logs_count, 3);
assert_eq!(current.load(Ordering::Relaxed), 7);
}
}