309 lines
9.7 KiB
Rust
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(¤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);
|
|
}
|
|
}
|