[PERF] Runtime Telemetry Hot Path

This commit is contained in:
bQUARKz 2026-04-10 08:59:02 +01:00
parent 5e80ea46b8
commit 63892dcfb9
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
16 changed files with 537 additions and 141 deletions

View File

@ -12,6 +12,7 @@ use prometeu_hal::sample::Sample;
use prometeu_hal::sound_bank::SoundBank; use prometeu_hal::sound_bank::SoundBank;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Read; use std::io::Read;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use std::thread; use std::thread;
use std::time::Instant; use std::time::Instant;
@ -53,10 +54,26 @@ impl<T> ResidentEntry<T> {
/// This is internal to the AssetManager and not visible to peripherals. /// This is internal to the AssetManager and not visible to peripherals.
pub struct BankPolicy<T> { pub struct BankPolicy<T> {
/// Dedup table: asset_id -> resident entry (value + telemetry). /// Dedup table: asset_id -> resident entry (value + telemetry).
resident: Arc<RwLock<HashMap<AssetId, ResidentEntry<T>>>>, pub resident: Arc<RwLock<HashMap<AssetId, ResidentEntry<T>>>>,
/// Staging area: handle -> value ready to commit. /// Staging area: handle -> value ready to commit.
staging: Arc<RwLock<HashMap<HandleId, Arc<T>>>>, pub staging: Arc<RwLock<HashMap<HandleId, (Arc<T>, usize)>>>,
/// Total bytes currently in resident storage.
pub used_bytes: Arc<AtomicUsize>,
/// Bytes in staging awaiting commit.
pub inflight_bytes: Arc<AtomicUsize>,
}
impl<T> Clone for BankPolicy<T> {
fn clone(&self) -> Self {
Self {
resident: Arc::clone(&self.resident),
staging: Arc::clone(&self.staging),
used_bytes: Arc::clone(&self.used_bytes),
inflight_bytes: Arc::clone(&self.inflight_bytes),
}
}
} }
impl<T> BankPolicy<T> { impl<T> BankPolicy<T> {
@ -64,6 +81,8 @@ impl<T> BankPolicy<T> {
Self { Self {
resident: Arc::new(RwLock::new(HashMap::new())), resident: Arc::new(RwLock::new(HashMap::new())),
staging: Arc::new(RwLock::new(HashMap::new())), staging: Arc::new(RwLock::new(HashMap::new())),
used_bytes: Arc::new(AtomicUsize::new(0)),
inflight_bytes: Arc::new(AtomicUsize::new(0)),
} }
} }
@ -87,24 +106,32 @@ impl<T> BankPolicy<T> {
None => { None => {
let entry = ResidentEntry::new(Arc::clone(&value), bytes); let entry = ResidentEntry::new(Arc::clone(&value), bytes);
map.insert(asset_id, entry); map.insert(asset_id, entry);
self.used_bytes.fetch_add(bytes, Ordering::Relaxed);
value value
} }
} }
} }
/// Place a value into staging for a given handle. /// Place a value into staging for a given handle.
pub fn stage(&self, handle: HandleId, value: Arc<T>) { pub fn stage(&self, handle: HandleId, value: Arc<T>, bytes: usize) {
self.staging.write().unwrap().insert(handle, value); self.staging.write().unwrap().insert(handle, (value, bytes));
self.inflight_bytes.fetch_add(bytes, Ordering::Relaxed);
} }
/// Take staged value (used by commit path). /// Take staged value (used by commit path).
pub fn take_staging(&self, handle: HandleId) -> Option<Arc<T>> { pub fn take_staging(&self, handle: HandleId) -> Option<(Arc<T>, usize)> {
self.staging.write().unwrap().remove(&handle) let entry = self.staging.write().unwrap().remove(&handle);
if let Some((_, bytes)) = entry.as_ref() {
self.inflight_bytes.fetch_sub(*bytes, Ordering::Relaxed);
}
entry
} }
pub fn clear(&self) { pub fn clear(&self) {
self.resident.write().unwrap().clear(); self.resident.write().unwrap().clear();
self.staging.write().unwrap().clear(); self.staging.write().unwrap().clear();
self.used_bytes.store(0, Ordering::Relaxed);
self.inflight_bytes.store(0, Ordering::Relaxed);
} }
} }
@ -127,6 +154,11 @@ pub struct AssetManager {
/// Residency policy for sound banks. /// Residency policy for sound banks.
sound_policy: BankPolicy<SoundBank>, sound_policy: BankPolicy<SoundBank>,
/// Count of occupied slots for GFX.
gfx_slots_occupied: AtomicUsize,
/// Count of occupied slots for sounds.
sound_slots_occupied: AtomicUsize,
// Commits that are ready to be applied at the next frame boundary. // Commits that are ready to be applied at the next frame boundary.
pending_commits: Mutex<Vec<HandleId>>, pending_commits: Mutex<Vec<HandleId>>,
} }
@ -263,6 +295,8 @@ impl AssetManager {
sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))), sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
gfx_policy: BankPolicy::new(), gfx_policy: BankPolicy::new(),
sound_policy: BankPolicy::new(), sound_policy: BankPolicy::new(),
gfx_slots_occupied: AtomicUsize::new(0),
sound_slots_occupied: AtomicUsize::new(0),
handles: Arc::new(RwLock::new(HashMap::new())), handles: Arc::new(RwLock::new(HashMap::new())),
next_handle_id: Mutex::new(1), next_handle_id: Mutex::new(1),
assets_data: Arc::new(RwLock::new(assets_data)), assets_data: Arc::new(RwLock::new(assets_data)),
@ -379,7 +413,7 @@ impl AssetManager {
let already_resident = match entry.bank_type { let already_resident = match entry.bank_type {
BankType::GLYPH => { BankType::GLYPH => {
if let Some(bank) = self.gfx_policy.get_resident(asset_id) { if let Some(bank) = self.gfx_policy.get_resident(asset_id) {
self.gfx_policy.stage(handle_id, bank); self.gfx_policy.stage(handle_id, bank, entry.decoded_size as usize);
true true
} else { } else {
false false
@ -387,7 +421,7 @@ impl AssetManager {
} }
BankType::SOUNDS => { BankType::SOUNDS => {
if let Some(bank) = self.sound_policy.get_resident(asset_id) { if let Some(bank) = self.sound_policy.get_resident(asset_id) {
self.sound_policy.stage(handle_id, bank); self.sound_policy.stage(handle_id, bank, entry.decoded_size as usize);
true true
} else { } else {
false false
@ -414,10 +448,8 @@ impl AssetManager {
let entry_clone = entry.clone(); let entry_clone = entry.clone();
// Capture policies for the worker thread // Capture policies for the worker thread
let gfx_policy_resident = Arc::clone(&self.gfx_policy.resident); let gfx_policy = self.gfx_policy.clone();
let gfx_policy_staging = Arc::clone(&self.gfx_policy.staging); let sound_policy = self.sound_policy.clone();
let sound_policy_resident = Arc::clone(&self.sound_policy.resident);
let sound_policy_staging = Arc::clone(&self.sound_policy.staging);
thread::spawn(move || { thread::spawn(move || {
// Update status to LOADING // Update status to LOADING
@ -439,22 +471,13 @@ impl AssetManager {
let result = Self::perform_load_glyph_bank(&entry_clone, assets_data); let result = Self::perform_load_glyph_bank(&entry_clone, assets_data);
if let Ok(tilebank) = result { if let Ok(tilebank) = result {
let bank_arc = Arc::new(tilebank); let bank_arc = Arc::new(tilebank);
let resident_arc = { let resident_arc = gfx_policy.put_resident(
let mut map = gfx_policy_resident.write().unwrap(); asset_id,
if let Some(existing) = map.get_mut(&asset_id) { bank_arc,
existing.last_used = Instant::now();
existing.loads += 1;
Arc::clone(&existing.value)
} else {
let entry = ResidentEntry::new(
Arc::clone(&bank_arc),
entry_clone.decoded_size as usize, entry_clone.decoded_size as usize,
); );
map.insert(asset_id, entry); gfx_policy.stage(handle_id, resident_arc, entry_clone.decoded_size as usize);
bank_arc
}
};
gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc);
let mut handles_map = handles.write().unwrap(); let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING { if h.status == LoadStatus::LOADING {
@ -472,22 +495,14 @@ impl AssetManager {
let result = Self::perform_load_sound_bank(&entry_clone, assets_data); let result = Self::perform_load_sound_bank(&entry_clone, assets_data);
if let Ok(soundbank) = result { if let Ok(soundbank) = result {
let bank_arc = Arc::new(soundbank); let bank_arc = Arc::new(soundbank);
let resident_arc = { let resident_arc = sound_policy.put_resident(
let mut map = sound_policy_resident.write().unwrap(); asset_id,
if let Some(existing) = map.get_mut(&asset_id) { bank_arc,
existing.last_used = Instant::now();
existing.loads += 1;
Arc::clone(&existing.value)
} else {
let entry = ResidentEntry::new(
Arc::clone(&bank_arc),
entry_clone.decoded_size as usize, entry_clone.decoded_size as usize,
); );
map.insert(asset_id, entry); sound_policy
bank_arc .stage(handle_id, resident_arc, entry_clone.decoded_size as usize);
}
};
sound_policy_staging.write().unwrap().insert(handle_id, resident_arc);
let mut handles_map = handles.write().unwrap(); let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING { if h.status == LoadStatus::LOADING {
@ -699,20 +714,26 @@ impl AssetManager {
if h.status == LoadStatus::READY { if h.status == LoadStatus::READY {
match h.slot.asset_type { match h.slot.asset_type {
BankType::GLYPH => { BankType::GLYPH => {
if let Some(bank) = self.gfx_policy.take_staging(handle_id) { if let Some((bank, _)) = self.gfx_policy.take_staging(handle_id) {
self.gfx_installer.install_glyph_bank(h.slot.index, bank); self.gfx_installer.install_glyph_bank(h.slot.index, bank);
let mut slots = self.gfx_slots.write().unwrap(); let mut slots = self.gfx_slots.write().unwrap();
if h.slot.index < slots.len() { if h.slot.index < slots.len() {
if slots[h.slot.index].is_none() {
self.gfx_slots_occupied.fetch_add(1, Ordering::Relaxed);
}
slots[h.slot.index] = Some(h._asset_id); slots[h.slot.index] = Some(h._asset_id);
} }
h.status = LoadStatus::COMMITTED; h.status = LoadStatus::COMMITTED;
} }
} }
BankType::SOUNDS => { BankType::SOUNDS => {
if let Some(bank) = self.sound_policy.take_staging(handle_id) { if let Some((bank, _)) = self.sound_policy.take_staging(handle_id) {
self.sound_installer.install_sound_bank(h.slot.index, bank); self.sound_installer.install_sound_bank(h.slot.index, bank);
let mut slots = self.sound_slots.write().unwrap(); let mut slots = self.sound_slots.write().unwrap();
if h.slot.index < slots.len() { if h.slot.index < slots.len() {
if slots[h.slot.index].is_none() {
self.sound_slots_occupied.fetch_add(1, Ordering::Relaxed);
}
slots[h.slot.index] = Some(h._asset_id); slots[h.slot.index] = Some(h._asset_id);
} }
h.status = LoadStatus::COMMITTED; h.status = LoadStatus::COMMITTED;
@ -727,38 +748,9 @@ impl AssetManager {
pub fn bank_info(&self, kind: BankType) -> BankStats { pub fn bank_info(&self, kind: BankType) -> BankStats {
match kind { match kind {
BankType::GLYPH => { BankType::GLYPH => {
let mut used_bytes = 0; let used_bytes = self.gfx_policy.used_bytes.load(Ordering::Relaxed);
{ let inflight_bytes = self.gfx_policy.inflight_bytes.load(Ordering::Relaxed);
let resident = self.gfx_policy.resident.read().unwrap(); let slots_occupied = self.gfx_slots_occupied.load(Ordering::Relaxed);
for entry in resident.values() {
used_bytes += entry.bytes;
}
}
let mut inflight_bytes = 0;
{
let staging = self.gfx_policy.staging.read().unwrap();
let assets = self.assets.read().unwrap();
let handles = self.handles.read().unwrap();
for (handle_id, _) in staging.iter() {
if let Some(h) = handles.get(handle_id) {
if let Some(entry) = assets.get(&h._asset_id) {
inflight_bytes += entry.decoded_size as usize;
}
}
}
}
let mut slots_occupied = 0;
{
let slots = self.gfx_slots.read().unwrap();
for s in slots.iter() {
if s.is_some() {
slots_occupied += 1;
}
}
}
BankStats { BankStats {
total_bytes: 16 * 1024 * 1024, total_bytes: 16 * 1024 * 1024,
@ -770,38 +762,9 @@ impl AssetManager {
} }
} }
BankType::SOUNDS => { BankType::SOUNDS => {
let mut used_bytes = 0; let used_bytes = self.sound_policy.used_bytes.load(Ordering::Relaxed);
{ let inflight_bytes = self.sound_policy.inflight_bytes.load(Ordering::Relaxed);
let resident = self.sound_policy.resident.read().unwrap(); let slots_occupied = self.sound_slots_occupied.load(Ordering::Relaxed);
for entry in resident.values() {
used_bytes += entry.bytes;
}
}
let mut inflight_bytes = 0;
{
let staging = self.sound_policy.staging.read().unwrap();
let assets = self.assets.read().unwrap();
let handles = self.handles.read().unwrap();
for (handle_id, _) in staging.iter() {
if let Some(h) = handles.get(handle_id) {
if let Some(entry) = assets.get(&h._asset_id) {
inflight_bytes += entry.decoded_size as usize;
}
}
}
}
let mut slots_occupied = 0;
{
let slots = self.sound_slots.read().unwrap();
for s in slots.iter() {
if s.is_some() {
slots_occupied += 1;
}
}
}
BankStats { BankStats {
total_bytes: 32 * 1024 * 1024, total_bytes: 32 * 1024 * 1024,
@ -865,6 +828,8 @@ impl AssetManager {
pub fn shutdown(&self) { pub fn shutdown(&self) {
self.gfx_policy.clear(); self.gfx_policy.clear();
self.sound_policy.clear(); self.sound_policy.clear();
self.gfx_slots_occupied.store(0, Ordering::Relaxed);
self.sound_slots_occupied.store(0, Ordering::Relaxed);
self.handles.write().unwrap().clear(); self.handles.write().unwrap().clear();
self.pending_commits.lock().unwrap().clear(); self.pending_commits.lock().unwrap().clear();
self.gfx_slots.write().unwrap().fill(None); self.gfx_slots.write().unwrap().fill(None);
@ -1049,8 +1014,8 @@ mod tests {
assert_eq!(am.status(handle2), LoadStatus::READY); assert_eq!(am.status(handle2), LoadStatus::READY);
let staging = am.gfx_policy.staging.read().unwrap(); let staging = am.gfx_policy.staging.read().unwrap();
let bank1 = staging.get(&handle1).unwrap(); let bank1 = &staging.get(&handle1).unwrap().0;
let bank2 = staging.get(&handle2).unwrap(); let bank2 = &staging.get(&handle2).unwrap().0;
assert!(Arc::ptr_eq(bank1, bank2)); assert!(Arc::ptr_eq(bank1, bank2));
} }
@ -1208,4 +1173,60 @@ mod tests {
assert_eq!(am.status(handle), LoadStatus::CANCELED); assert_eq!(am.status(handle), LoadStatus::CANCELED);
assert_eq!(am.commit(handle), AssetOpStatus::InvalidState); assert_eq!(am.commit(handle), AssetOpStatus::InvalidState);
} }
#[test]
fn test_asset_telemetry_incremental() {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let width = 16;
let height = 16;
let decoded_bytes = expected_glyph_decoded_size(width, height);
let data = test_glyph_asset_data();
let am = AssetManager::new(
vec![test_glyph_asset_entry("test_glyphs", width, height)],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
);
// Initially zero
let info = am.bank_info(BankType::GLYPH);
assert_eq!(info.used_bytes, 0);
assert_eq!(info.inflight_bytes, 0);
assert_eq!(info.slots_occupied, 0);
// Loading
let handle = am.load(0, 0).expect("load must allocate handle");
// While LOADING or READY, it should be in inflight_bytes
let start = Instant::now();
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
thread::sleep(std::time::Duration::from_millis(10));
}
let info = am.bank_info(BankType::GLYPH);
// Note: put_resident happens in worker thread, then stage happens.
assert_eq!(info.used_bytes, decoded_bytes);
assert_eq!(info.inflight_bytes, decoded_bytes);
assert_eq!(info.slots_occupied, 0);
// Commit
am.commit(handle);
am.apply_commits();
let info = am.bank_info(BankType::GLYPH);
assert_eq!(info.used_bytes, decoded_bytes);
assert_eq!(info.inflight_bytes, 0);
assert_eq!(info.slots_occupied, 1);
// Shutdown resets
am.shutdown();
let info = am.bank_info(BankType::GLYPH);
assert_eq!(info.used_bytes, 0);
assert_eq!(info.inflight_bytes, 0);
assert_eq!(info.slots_occupied, 0);
}
} }

View File

@ -5,11 +5,17 @@ pub struct LogService {
events: VecDeque<LogEvent>, events: VecDeque<LogEvent>,
capacity: usize, capacity: usize,
next_seq: u64, next_seq: u64,
pub logs_count: u32,
} }
impl LogService { impl LogService {
pub fn new(capacity: usize) -> Self { pub fn new(capacity: usize) -> Self {
Self { events: VecDeque::with_capacity(capacity), capacity, next_seq: 0 } Self {
events: VecDeque::with_capacity(capacity),
capacity,
next_seq: 0,
logs_count: 0,
}
} }
pub fn log( pub fn log(
@ -34,6 +40,11 @@ impl LogService {
msg, msg,
}); });
self.next_seq += 1; self.next_seq += 1;
self.logs_count += 1;
}
pub fn reset_count(&mut self) {
self.logs_count = 0;
} }
pub fn get_recent(&self, n: usize) -> Vec<LogEvent> { pub fn get_recent(&self, n: usize) -> Vec<LogEvent> {

View File

@ -20,6 +20,13 @@ pub struct TelemetryFrame {
pub audio_used_bytes: usize, pub audio_used_bytes: usize,
pub audio_inflight_bytes: usize, pub audio_inflight_bytes: usize,
pub audio_slots_occupied: u32, pub audio_slots_occupied: u32,
// RAM (Heap)
pub heap_used_bytes: usize,
pub heap_max_bytes: usize,
// Log Pressure
pub logs_count: u32,
} }
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
@ -28,6 +35,10 @@ pub struct CertificationConfig {
pub cycles_budget_per_frame: Option<u64>, pub cycles_budget_per_frame: Option<u64>,
pub max_syscalls_per_frame: Option<u32>, pub max_syscalls_per_frame: Option<u32>,
pub max_host_cpu_us_per_frame: Option<u64>, pub max_host_cpu_us_per_frame: Option<u64>,
pub max_gfx_bytes: Option<usize>,
pub max_audio_bytes: Option<usize>,
pub max_heap_bytes: Option<usize>,
pub max_logs_per_frame: Option<u32>,
} }
pub struct Certifier { pub struct Certifier {
@ -51,6 +62,7 @@ impl Certifier {
let mut violations = 0; let mut violations = 0;
// 1. Cycles
if let Some(budget) = self.config.cycles_budget_per_frame if let Some(budget) = self.config.cycles_budget_per_frame
&& telemetry.cycles_used > budget && telemetry.cycles_used > budget
{ {
@ -68,6 +80,7 @@ impl Certifier {
violations += 1; violations += 1;
} }
// 2. Syscalls
if let Some(limit) = self.config.max_syscalls_per_frame if let Some(limit) = self.config.max_syscalls_per_frame
&& telemetry.syscalls > limit && telemetry.syscalls > limit
{ {
@ -85,6 +98,7 @@ impl Certifier {
violations += 1; violations += 1;
} }
// 3. CPU Time
if let Some(limit) = self.config.max_host_cpu_us_per_frame if let Some(limit) = self.config.max_host_cpu_us_per_frame
&& telemetry.host_cpu_time_us > limit && telemetry.host_cpu_time_us > limit
{ {
@ -102,6 +116,78 @@ impl Certifier {
violations += 1; violations += 1;
} }
// 4. GFX Memory
if let Some(limit) = self.config.max_gfx_bytes
&& telemetry.gfx_used_bytes > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA04,
format!(
"Cert: GFX bank exceeded memory limit ({} > {})",
telemetry.gfx_used_bytes, limit
),
);
violations += 1;
}
// 5. Audio Memory
if let Some(limit) = self.config.max_audio_bytes
&& telemetry.audio_used_bytes > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA05,
format!(
"Cert: Audio bank exceeded memory limit ({} > {})",
telemetry.audio_used_bytes, 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 violations
} }
} }
@ -118,6 +204,8 @@ mod tests {
cycles_budget_per_frame: Some(100), cycles_budget_per_frame: Some(100),
max_syscalls_per_frame: Some(5), max_syscalls_per_frame: Some(5),
max_host_cpu_us_per_frame: Some(1000), max_host_cpu_us_per_frame: Some(1000),
max_gfx_bytes: Some(1024),
..Default::default()
}; };
let cert = Certifier::new(config); let cert = Certifier::new(config);
let mut ls = LogService::new(10); let mut ls = LogService::new(10);
@ -126,13 +214,15 @@ mod tests {
tel.cycles_used = 150; tel.cycles_used = 150;
tel.syscalls = 10; tel.syscalls = 10;
tel.host_cpu_time_us = 500; tel.host_cpu_time_us = 500;
tel.gfx_used_bytes = 2048;
let violations = cert.evaluate(&tel, &mut ls, 1000); let violations = cert.evaluate(&tel, &mut ls, 1000);
assert_eq!(violations, 2); assert_eq!(violations, 3);
let logs = ls.get_recent(10); let logs = ls.get_recent(10);
assert_eq!(logs.len(), 2); assert_eq!(logs.len(), 3);
assert!(logs[0].msg.contains("cycles_used")); assert!(logs[0].msg.contains("cycles_used"));
assert!(logs[1].msg.contains("syscalls")); assert!(logs[1].msg.contains("syscalls"));
assert!(logs[2].msg.contains("GFX bank"));
} }
} }

View File

@ -37,13 +37,14 @@ pub struct VirtualMachineRuntime {
pub certifier: Certifier, pub certifier: Certifier,
pub paused: bool, pub paused: bool,
pub debug_step_request: bool, pub debug_step_request: bool,
pub inspection_active: bool,
pub(crate) needs_prepare_entry_call: bool, pub(crate) needs_prepare_entry_call: bool,
pub(crate) boot_time: Instant, pub(crate) boot_time: Instant,
} }
impl VirtualMachineRuntime { impl VirtualMachineRuntime {
pub const CYCLES_PER_LOGICAL_FRAME: u64 = 5_000_000; pub const CYCLES_PER_LOGICAL_FRAME: u64 = 1_500_000;
pub const SLICE_PER_TICK: u64 = 5_000_000; pub const SLICE_PER_TICK: u64 = 1_500_000;
pub const MAX_LOG_LEN: usize = 256; pub const MAX_LOG_LEN: usize = 256;
pub const MAX_LOGS_PER_FRAME: u32 = 10; pub const MAX_LOGS_PER_FRAME: u32 = 10;
} }

View File

@ -30,6 +30,7 @@ impl VirtualMachineRuntime {
certifier: Certifier::new(cap_config.unwrap_or_default()), certifier: Certifier::new(cap_config.unwrap_or_default()),
paused: false, paused: false,
debug_step_request: false, debug_step_request: false,
inspection_active: false,
needs_prepare_entry_call: false, needs_prepare_entry_call: false,
boot_time, boot_time,
}; };
@ -104,6 +105,7 @@ impl VirtualMachineRuntime {
self.paused = false; self.paused = false;
self.debug_step_request = false; self.debug_step_request = false;
self.inspection_active = false;
self.needs_prepare_entry_call = false; self.needs_prepare_entry_call = false;
} }

View File

@ -4,6 +4,7 @@ use prometeu_hal::asset::BankType;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::{HardwareBridge, HostContext, InputSignals}; use prometeu_hal::{HardwareBridge, HostContext, InputSignals};
use prometeu_vm::LogicalFrameEndingReason; use prometeu_vm::LogicalFrameEndingReason;
use std::sync::atomic::Ordering;
impl VirtualMachineRuntime { impl VirtualMachineRuntime {
pub fn debug_step_instruction( pub fn debug_step_instruction(
@ -128,8 +129,27 @@ impl VirtualMachineRuntime {
|| run.reason == LogicalFrameEndingReason::EndOfRom || run.reason == LogicalFrameEndingReason::EndOfRom
{ {
hw.gfx_mut().render_all(); hw.gfx_mut().render_all();
self.telemetry_current.host_cpu_time_us =
start.elapsed().as_micros() as u64; // 1. Snapshot full telemetry at logical frame end (O(1) with atomic counters)
let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
self.telemetry_current.gfx_slots_occupied = gfx_stats.slots_occupied as u32;
let audio_stats = hw.assets().bank_info(BankType::SOUNDS);
self.telemetry_current.audio_used_bytes = audio_stats.used_bytes;
self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes;
self.telemetry_current.audio_slots_occupied =
audio_stats.slots_occupied as u32;
self.telemetry_current.heap_used_bytes =
vm.heap().used_bytes.load(Ordering::Relaxed);
self.telemetry_current.heap_max_bytes = 0; // Not yet capped
self.telemetry_current.logs_count = self.log_service.logs_count;
self.log_service.reset_count();
self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64;
let ts_ms = self.boot_time.elapsed().as_millis() as u64; let ts_ms = self.boot_time.elapsed().as_millis() as u64;
self.telemetry_current.violations = self.certifier.evaluate( self.telemetry_current.violations = self.certifier.evaluate(
@ -166,6 +186,8 @@ impl VirtualMachineRuntime {
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64; self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
// 2. High-frequency telemetry update (only if inspection is active)
if self.inspection_active {
let gfx_stats = hw.assets().bank_info(BankType::GLYPH); let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes; self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes; self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
@ -176,6 +198,10 @@ impl VirtualMachineRuntime {
self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes; self.telemetry_current.audio_inflight_bytes = audio_stats.inflight_bytes;
self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32; self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32;
self.telemetry_current.heap_used_bytes = vm.heap().used_bytes.load(Ordering::Relaxed);
self.telemetry_current.logs_count = self.log_service.logs_count;
}
if !self.logical_frame_active if !self.logical_frame_active
&& self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1)
{ {
@ -187,6 +213,9 @@ impl VirtualMachineRuntime {
self.telemetry_last.audio_used_bytes = self.telemetry_current.audio_used_bytes; self.telemetry_last.audio_used_bytes = self.telemetry_current.audio_used_bytes;
self.telemetry_last.audio_inflight_bytes = self.telemetry_current.audio_inflight_bytes; self.telemetry_last.audio_inflight_bytes = self.telemetry_current.audio_inflight_bytes;
self.telemetry_last.audio_slots_occupied = self.telemetry_current.audio_slots_occupied; self.telemetry_last.audio_slots_occupied = self.telemetry_current.audio_slots_occupied;
self.telemetry_last.heap_used_bytes = self.telemetry_current.heap_used_bytes;
self.telemetry_last.heap_max_bytes = self.telemetry_current.heap_max_bytes;
self.telemetry_last.logs_count = self.telemetry_current.logs_count;
} }
None None

View File

@ -2,6 +2,9 @@ use crate::call_frame::CallFrame;
use crate::object::{ObjectHeader, ObjectKind}; use crate::object::{ObjectHeader, ObjectKind};
use prometeu_bytecode::{HeapRef, Value}; use prometeu_bytecode::{HeapRef, Value};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
/// Internal stored object: header plus opaque payload bytes. /// Internal stored object: header plus opaque payload bytes.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StoredObject { pub struct StoredObject {
@ -22,6 +25,30 @@ pub struct StoredObject {
pub coroutine: Option<CoroutineData>, pub coroutine: Option<CoroutineData>,
} }
impl StoredObject {
/// Returns the approximate memory footprint of this object in bytes.
pub fn bytes(&self) -> usize {
let mut total = std::mem::size_of::<ObjectHeader>();
total += self.payload.capacity();
if let Some(elems) = &self.array_elems {
total += std::mem::size_of::<Vec<Value>>();
total += elems.capacity() * std::mem::size_of::<Value>();
}
if let Some(env) = &self.closure_env {
total += std::mem::size_of::<Vec<Value>>();
total += env.capacity() * std::mem::size_of::<Value>();
}
if let Some(coro) = &self.coroutine {
total += std::mem::size_of::<CoroutineData>();
total += coro.stack.capacity() * std::mem::size_of::<Value>();
total += coro.frames.capacity() * std::mem::size_of::<CallFrame>();
}
total
}
}
/// Execution state of a coroutine. /// Execution state of a coroutine.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CoroutineState { pub enum CoroutineState {
@ -49,14 +76,22 @@ pub struct Heap {
objects: Vec<Option<StoredObject>>, objects: Vec<Option<StoredObject>>,
// Reclaimed slots available for deterministic reuse (LIFO). // Reclaimed slots available for deterministic reuse (LIFO).
free_list: Vec<usize>, free_list: Vec<usize>,
/// Total bytes currently used by all objects in the heap.
pub used_bytes: Arc<AtomicUsize>,
} }
impl Heap { impl Heap {
pub fn new() -> Self { pub fn new() -> Self {
Self { objects: Vec::new(), free_list: Vec::new() } Self {
objects: Vec::new(),
free_list: Vec::new(),
used_bytes: Arc::new(AtomicUsize::new(0)),
}
} }
fn insert_object(&mut self, obj: StoredObject) -> HeapRef { fn insert_object(&mut self, obj: StoredObject) -> HeapRef {
self.used_bytes.fetch_add(obj.bytes(), Ordering::Relaxed);
if let Some(idx) = self.free_list.pop() { if let Some(idx) = self.free_list.pop() {
debug_assert!(self.objects.get(idx).is_some_and(|slot| slot.is_none())); debug_assert!(self.objects.get(idx).is_some_and(|slot| slot.is_none()));
self.objects[idx] = Some(obj); self.objects[idx] = Some(obj);
@ -363,6 +398,7 @@ impl Heap {
obj.header.set_marked(false); obj.header.set_marked(false);
} else { } else {
// Unreachable: reclaim by dropping and turning into tombstone. // Unreachable: reclaim by dropping and turning into tombstone.
self.used_bytes.fetch_sub(obj.bytes(), Ordering::Relaxed);
*slot = None; *slot = None;
self.free_list.push(idx); self.free_list.push(idx);
} }

View File

@ -140,6 +140,11 @@ impl VirtualMachine {
self.operand_stack[start..].iter().rev().cloned().collect() self.operand_stack[start..].iter().rev().cloned().collect()
} }
/// Returns a reference to the VM's heap.
pub fn heap(&self) -> &Heap {
&self.heap
}
/// Returns true if the VM has executed a HALT and is not currently running. /// Returns true if the VM has executed a HALT and is not currently running.
pub fn is_halted(&self) -> bool { pub fn is_halted(&self) -> bool {
self.halted self.halted

View File

@ -129,7 +129,7 @@ impl HostRunner {
let color_bg = Color::INDIGO; // Dark blue to stand out let color_bg = Color::INDIGO; // Dark blue to stand out
let color_warn = Color::RED; let color_warn = Color::RED;
self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg); self.hardware.gfx.fill_rect(5, 5, 175, 130, color_bg);
self.hardware.gfx.draw_text( self.hardware.gfx.draw_text(
10, 10,
10, 10,
@ -187,8 +187,16 @@ impl HostRunner {
); );
} }
self.hardware.gfx.draw_text(
10,
82,
&format!("RAM: {}KB", tel.heap_used_bytes / 1024),
color_text,
);
self.hardware.gfx.draw_text(10, 90, &format!("LOGS: {}", tel.logs_count), color_text);
let cert_color = if tel.violations > 0 { color_warn } else { color_text }; let cert_color = if tel.violations > 0 { color_warn } else { color_text };
self.hardware.gfx.draw_text(10, 82, &format!("CERT LAST: {}", tel.violations), cert_color); self.hardware.gfx.draw_text(10, 98, &format!("CERT LAST: {}", tel.violations), cert_color);
if tel.violations > 0 if tel.violations > 0
&& let Some(event) = self && let Some(event) = self
@ -204,7 +212,7 @@ impl HostRunner {
if msg.len() > 30 { if msg.len() > 30 {
msg.truncate(30); msg.truncate(30);
} }
self.hardware.gfx.draw_text(10, 90, &msg, color_warn); self.hardware.gfx.draw_text(10, 106, &msg, color_warn);
} }
if let Some(report) = self.firmware.os.last_crash_report.as_ref() { if let Some(report) = self.firmware.os.last_crash_report.as_ref() {
@ -212,7 +220,7 @@ impl HostRunner {
if msg.len() > 30 { if msg.len() > 30 {
msg.truncate(30); msg.truncate(30);
} }
self.hardware.gfx.draw_text(10, 98, &msg, color_warn); self.hardware.gfx.draw_text(10, 114, &msg, color_warn);
} }
} }
} }
@ -311,6 +319,9 @@ impl ApplicationHandler for HostRunner {
// 1. Process pending debug commands from the network. // 1. Process pending debug commands from the network.
self.debugger.check_commands(&mut self.firmware, &mut self.hardware); self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
// Sync inspection mode state.
self.firmware.os.inspection_active = self.overlay_enabled || self.debugger.stream.is_some();
// 2. Maintain filesystem connection if it was lost (e.g., directory removed). // 2. Maintain filesystem connection if it was lost (e.g., directory removed).
if let Some(root) = &self.fs_root { if let Some(root) = &self.fs_root {
use prometeu_system::fs::FsState; use prometeu_system::fs::FsState;

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":23,"AGD":21,"DEC":7,"PLN":6,"LSN":26,"CLSN":1}} {"type":"meta","next_id":{"DSC":23,"AGD":21,"DEC":7,"PLN":6,"LSN":27,"CLSN":1}}
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} {"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
@ -9,7 +9,7 @@
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0008","status":"open","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0005","file":"workflow/decisions/DEC-0005-perf-push-based-telemetry-model.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0005","file":"workflow/plans/PLN-0005-perf-push-based-telemetry-implementation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0026","file":"lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}

View File

@ -0,0 +1,27 @@
---
id: LSN-0026
ticket: perf-runtime-telemetry-hot-path
title: Modelo de Telemetria Push-based
created: 2026-04-10
tags: [performance, telemetry, atomics]
---
# Modelo de Telemetria Push-based
O sistema de telemetria do PROMETEU evoluiu de um modelo de varredura sob demanda (pull) para um modelo de contadores incrementais (push), visando minimizar o impacto no *hot path* do runtime.
## O Problema Original
Anteriormente, a cada *host tick*, o runtime solicitava informações de uso de memória dos bancos de assets. Isso resultava em:
- Varreduras $O(n)$ sobre mapas de recursos.
- Múltiplas aquisições de *locks* de leitura em cada tick.
- Overhead desnecessário em hardwares handheld, onde cada microssegundo conta.
## A Solução: Modelo Push com Atômicos
A solução implementada utiliza `AtomicUsize` nos drivers e na VM para manter o estado do sistema em tempo real com custo $O(1)$ de leitura e escrita:
1. **Drivers (Assets):** Contadores atômicos em cada `BankPolicy` são atualizados durante `load`, `commit` e `cancel`.
2. **VM (Heap):** Um contador `used_bytes` na struct `Heap` rastreia alocações e liberações (sweep).
3. **System (Logs):** O `LogService` rastreia a pressão de logs emitida em cada frame.
## Dois Níveis de Observabilidade
Para equilibrar performance e depuração, a coleta foi dividida:
- **Snapshot de Frame (Sempre):** Captura automática no fim de cada frame lógico. Custo irrelevante ($O(1)$). Serve ao `Certifier` e ao log histórico.
- **Tick de Host (Sob Demanda):** A coleta detalhada em cada tick só ocorre se `inspection_active` estiver habilitado (ex: Overlay F1 ligado).
## Lições Aprendidas
- **Desacoplamento de Gatilhos:** Não devemos usar o estado do `Certifier` para habilitar funcionalidades de depuração visual (como o overlay), pois eles têm propósitos e custos diferentes.
- **Consistência Eventual é Suficiente:** Para métricas de telemetria, não é necessário travar o sistema para obter um valor exato a cada nanossegundo. A leitura relaxada de atômicos é suficiente e muito mais performática.
- **Isolamento de Custo:** Mover a lógica de agregação para o driver simplifica o runtime e garante que o custo de telemetria seja pago apenas durante mutações de estado, e não repetidamente durante a execução estável.

View File

@ -59,8 +59,30 @@ Remover varredura e agregacao lock-heavy do hot path do tick sem perder observab
## Open Questions de Arquitetura ## Open Questions de Arquitetura
1. O certifier realmente precisa de snapshot de bank a cada tick? 1. O certifier realmente precisa de snapshot de bank a cada tick?
Não. O certifier aceita dados do fim do frame anterior, pois violações de limites de bank costumam ser persistentes entre frames.
2. O overlay pode ler uma versao resumida da telemetria em vez de recalcular tudo? 2. O overlay pode ler uma versao resumida da telemetria em vez de recalcular tudo?
Sim. O `AssetManager` passará a prover uma struct `BankStats` pré-calculada via contadores atômicos.
3. Vale manter caminho "preciso" so para testes/debug e caminho "barato" para runtime normal? 3. Vale manter caminho "preciso" so para testes/debug e caminho "barato" para runtime normal?
Sim, mas a "precisão" será definida como "atualizado no último evento de mutação", o que já é suficiente para ambos os casos.
4. Como detectar o modo de depuração/inspeção de forma correta e desacoplada?
Através de um novo campo `inspection_active: bool` no `VirtualMachineRuntime`, controlado explicitamente pelo Host (ex: quando o Overlay F1 ou o Debugger remoto estão ativos). O `certifier` não deve ser usado para este propósito.
## Sugestao / Recomendacao
1. **Modelo de Métrica (Push-based):**
- Migrar de snapshot total $O(n)$ para contadores incrementais $O(1)$ no `AssetManager`.
- Utilizar `AtomicUsize` ou campos protegidos por Mutex simples para `used_bytes`, `inflight_bytes` e `slots_occupied`.
- Atualizar esses contadores apenas em eventos de mutação (`load`, `commit`, `cancel`).
2. **Frequência de Coleta (Dois Níveis):**
- **Básica (Sempre):** O Runtime deve atualizar `telemetry_current` no fechamento de cada logical frame (`FrameSync` ou `EndOfRom`). Isso garante dados para o `certifier` com custo $O(1)$.
- **Alta Frequência (Sob Demanda):** Manter atualização em todo host tick apenas se `inspection_active` for `true` (Overlay F1 visível ou Debugger conectado).
3. **Responsabilidade da Agregação (Centralizada):**
- O `AssetManager` é o dono da "verdade incremental". O Runtime consome um snapshot barato (struct POD) sem varredura de locks.
4. **Garantia de Consistência (Eventual):**
- Aceitar defasagem de até 1 frame lógico para métricas de asset bank.
## Dependencias ## Dependencias

View File

@ -52,19 +52,22 @@ Isolar o overlay de debug do custo medido do console sem perder utilidade para d
## Open Questions de Arquitetura ## Open Questions de Arquitetura
1. O overlay precisa ser representativo do hardware final ou apenas ferramenta de desktop? 1. O overlay precisa ser representativo do hardware final ou apenas ferramenta de desktop?
Não, como é HUD técnico, pode e deve ser renderizado pelo Host nativo para melhor legibilidade.
2. Vale um modo "perf puro" onde overlay nunca toca no framebuffer do console? 2. Vale um modo "perf puro" onde overlay nunca toca no framebuffer do console?
Sim. O isolamento garante que o `gfx` emulado esteja 100% livre para o jogo durante a medição.
3. O host deve oferecer toggles separados para stats, logs e overlay visual? 3. O host deve oferecer toggles separados para stats, logs e overlay visual?
Sim. O `HostRunner` deve expor controles granulares via `inspection_active`.
4. Como melhorar a legibilidade e estética (Glyphs/Transparência)?
Migrar a renderização do HUD para o Host Nativo (Winit/Pixels), permitindo o uso de fontes TrueType (monospaced) nítidas e Alpha Blending real para transparência no fundo do painel.
## Dependencias ## Dependencias
- `../specs/10-debug-inspection-and-profiling.md` - `../specs/10-debug-inspection-and-profiling.md`
- `../specs/11-portability-and-cross-platform-execution.md` - `../specs/11-portability-and-cross-platform-execution.md`
## Criterio de Saida Desta Agenda ## Sugestao / Recomendacao
Pode virar PR quando houver decisao escrita sobre: 1. **Migração para Camada Host Nativa:** Renderizar o HUD de debug em uma surface separada ou via pipeline nativo do Host (depois do upscaling do framebuffer do console).
2. **Fontes TrueType (Mono):** Substituir os glyphs bitmapped rudimentares por uma fonte nativa de alta qualidade e nítida.
- onde o overlay e composto; 3. **Composição Alpha:** Permitir fundo semi-transparente para o overlay para não bloquear a visão do jogo.
- politica de cache de texto/glyphs; 4. **Acionamento Explícito:** Host deve gerenciar `inspection_active: true` no runtime apenas quando o HUD ou Debugger estiverem ativos.
- como o custo do overlay aparece na telemetria;
- overhead maximo aceitavel em modo debug.

View File

@ -0,0 +1,57 @@
---
id: DEC-0005
title: "Decisão - [PERF] Modelo de Telemetria Push-based"
status: closed
created: 2026-03-27
resolved: 2026-03-27
agenda: AGD-0007
tags: []
---
# Decisão - [PERF] Modelo de Telemetria Push-based
## Status
**Fechada (Closed)** - Consensus reached. Implementation approved.
## Contexto
O runtime atual (`VirtualMachineRuntime::tick()`) realiza a coleta de telemetria de asset banks (`gfx` e `audio`) em todos os *host ticks*. Essa coleta envolve a chamada de `bank_info()` no `AssetManager`, que executa uma varredura $O(n)$ sobre mapas de recursos e adquire múltiplos locks de leitura. Em hardwares limitados (handhelds), esse custo repetitivo no caminho quente degrada a performance desnecessariamente, mesmo quando o sistema está estável ou em pausa.
## Decisão
1. **Modelo de Contadores no Driver e Runtime:** O `AssetManager`, a `Heap` da VM e o `LogService` devem substituir a varredura/contagem total por **contadores atômicos** (`used_bytes`, `inflight_bytes`, `slots_occupied`, `logs_count`) para cada subsistema. Esses contadores serão atualizados incrementalmente ($O(1)$) em cada mutação (load, commit, alloc, free, log).
2. **Snapshot Obrigatório de Fim de Frame:** O runtime capturará o estado desses contadores (Banks, Heap e Logs) **apenas uma vez** por fechamento de frame lógico (`FrameSync` ou `EndOfRom`). Este snapshot será usado para alimentar a `telemetry_last` e o `certifier`.
3. **Coleta sob Demanda (Inspection Mode):** A coleta em cada *host tick* será reativada **somente** se o novo sinalizador `inspection_active: bool` do runtime for verdadeiro.
4. **Desacoplamento do Certifier:** A ativação do `certifier` não habilitará mais a telemetria detalhada em cada tick. O certifier será servido exclusivamente pelos snapshots de fim de frame lógico.
## Rationale
* **Performance:** Reduz o custo do tick do runtime de $O(n)$ com locks para $O(1)$ sem locks no modo normal.
* **Observabilidade:** Mantém dados precisos para o overlay (via modo inspeção) e dados válidos para o certifier (via snapshot de frame).
* **Modularidade:** Desacopla as necessidades de depuração (Overlay F1) das necessidades de validação normativa (Certifier).
## Invariantes / Contrato
* O `AssetManager` é a única fonte da verdade para o uso de memória de assets; o runtime não deve tentar calcular esses valores manualmente.
* Contadores atômicos garantem que o runtime possa ler estatísticas de bancos sem travar mutações em andamento (consistência eventual).
* A defasagem de até 1 frame lógico é aceitável para métricas de assets bank no modo de operação normal.
## Impactos
* **Drivers:** Necessidade de adicionar e gerenciar contadores no `AssetManager`.
* **Virtual Machine:** Adicionar contador atômico de `used_bytes` na `Heap`.
* **Log Service:** Adicionar contador incremental de logs emitidos no frame no `LogService`.
* **Runtime:** Modificação no `VirtualMachineRuntime` para incluir o campo `inspection_active` e lógica condicional no `tick()`.
* **Host:** O host (ex: desktop-winit) deve agora sinalizar quando o overlay de depuração está ativo via `inspection_active`.
## Referências
* Agenda [AGD-0007-perf-runtime-telemetry-hot-path.md](../agendas/AGD-0007-perf-runtime-telemetry-hot-path.md)
* Spec `10-debug-inspection-and-profiling.md`
## Propagação Necessária
* Atualizar o `VirtualMachineRuntime` para expor o campo `inspection_active`.
* Atualizar o `HostRunner` para sinalizar `inspection_active` quando o overlay F1 for alternado.
* Atualizar a struct `TelemetryFrame` para incluir campos de Heap Memory e Log count.

View File

@ -0,0 +1,81 @@
---
id: PLN-0005
title: "Plano - [PERF] Implementação de Telemetria Push-based"
status: open
created: 2026-03-27
origin_decisions:
- DEC-0005
tags: []
---
# Plano - [PERF] Implementação de Telemetria Push-based
## Briefing
Este plano detalha as alterações técnicas para migrar o sistema de telemetria de um modelo de varredura $O(n)$ com locks para um modelo push-based com contadores atômicos $O(1)$. O objetivo é reduzir o overhead do hot path do runtime e adicionar visibilidade sobre a memória RAM (Heap) e volume de logs.
## Decisions de Origem
* [DEC-0005 - [PERF] Modelo de Telemetria Push-based](../decisions/DEC-0005-perf-push-based-telemetry-model.md)
## Alvo
* `prometeu-drivers` (AssetManager)
* `prometeu-vm` (Heap)
* `prometeu-hal` (TelemetryFrame)
* `prometeu-system` (LogService, VirtualMachineRuntime)
* `prometeu-host-desktop-winit` (HostRunner)
## Escopo
1. **Contadores Atômicos:** Implementação de `AtomicUsize` nos subsistemas de assets, heap e logs.
2. **Telemetry Frame:** Expansão da struct para incluir `heap_used`, `heap_max` e `logs_count`.
3. **Lógica de Tick:** Refatoração do `tick.rs` para usar `inspection_active` e snapshots de fim de frame.
4. **Sinalização do Host:** Integração do campo `inspection_active` com o acionamento do overlay F1.
## Fora de Escopo
* Migração da renderização do overlay para o host nativo (será tratado em plano derivado da AGD-0012).
* Telemetria de FileSystem IO ou Corrotinas.
## Plano de Execucao
### Fase 1: Drivers e VM (Modelo Push)
1. **`prometeu-drivers/src/asset.rs`:**
- Adicionar contadores em `BankPolicy`.
- Atualizar contadores em `load_internal`, `commit` e `cancel`.
- Refatorar `bank_info()` para retornar os valores atômicos sem varredura.
2. **`prometeu-vm/src/heap.rs`:**
- Adicionar contador `used_bytes` na struct `Heap`.
- Atualizar contador em `alloc()` e `free()`.
3. **`prometeu-system/src/log.rs`:**
- Adicionar contador `logs_count` (resetável por frame) no `LogService`.
### Fase 2: HAL e Runtime (Contrato e Lógica)
1. **`prometeu-hal/src/telemetry.rs`:**
- Adicionar `heap_used_bytes`, `heap_max_bytes` e `logs_count` na `TelemetryFrame`.
2. **`prometeu-system/src/virtual_machine_runtime.rs`:**
- Adicionar campo `public inspection_active: bool`.
3. **`prometeu-system/src/virtual_machine_runtime/tick.rs`:**
- Modificar `tick()` para coletar `bank_info` detalhado apenas se `inspection_active == true`.
- Implementar a captura do snapshot consolidado no fechamento do logical frame.
### Fase 3: Host e Integração
1. **`prometeu-host-desktop-winit/src/runner.rs`:**
- Sincronizar o estado do campo `overlay_enabled` com `firmware.os.inspection_active`.
## Criterios de Aceite
* O sistema compila sem avisos de tipos inexistentes.
* A telemetria de assets (`gfx`/`audio`) continua funcional no overlay F1.
* Novos campos de Heap e Logs aparecem no log de performance do console.
* O custo de telemetria no `tick` deve cair drasticamente quando o overlay estiver desligado (verificável via profiling).
## Tests / Validacao
* **Teste Unitário:** Criar teste em `asset.rs` para garantir que contadores batem com a realidade após sequências de carga e cancelamento.
* **Teste de Regressão:** Garantir que o `certifier` continua detectando violações de bank no fim do frame.
## Riscos
* **Consistência Eventual:** Como os contadores são atômicos e não travam o sistema, pode haver uma defasagem momentânea durante um `commit` pesado; isto é aceitável conforme DEC-0005.