dev/perf-runtime-telemetry-hot-path #13
@ -12,14 +12,22 @@ 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;
|
||||||
|
|
||||||
|
type ResidentMap<T> = HashMap<AssetId, ResidentEntry<T>>;
|
||||||
|
type StagedValue<T> = (Arc<T>, usize);
|
||||||
|
type StagingMap<T> = HashMap<HandleId, StagedValue<T>>;
|
||||||
|
|
||||||
|
type AssetTable = HashMap<AssetId, AssetEntry>;
|
||||||
|
type HandleTable = HashMap<HandleId, LoadHandleInfo>;
|
||||||
|
|
||||||
const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
|
const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
|
||||||
const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
|
const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
|
||||||
const GLYPH_BANK_PALETTE_BYTES_V1: usize =
|
const GLYPH_BANK_PALETTE_BYTES_V1: usize =
|
||||||
GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * std::mem::size_of::<u16>();
|
GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * size_of::<u16>();
|
||||||
|
|
||||||
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -53,10 +61,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<ResidentMap<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<StagingMap<T>>>,
|
||||||
|
|
||||||
|
/// 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 +88,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,30 +113,38 @@ 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<StagedValue<T>> {
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AssetManager {
|
pub struct AssetManager {
|
||||||
assets: Arc<RwLock<HashMap<AssetId, AssetEntry>>>,
|
assets: Arc<RwLock<AssetTable>>,
|
||||||
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>,
|
handles: Arc<RwLock<HandleTable>>,
|
||||||
next_handle_id: Mutex<HandleId>,
|
next_handle_id: Mutex<HandleId>,
|
||||||
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
||||||
|
|
||||||
@ -127,6 +161,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 +302,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 +420,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 +428,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 +455,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 +478,17 @@ 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();
|
entry_clone.decoded_size as usize,
|
||||||
existing.loads += 1;
|
);
|
||||||
Arc::clone(&existing.value)
|
gfx_policy.stage(
|
||||||
} else {
|
handle_id,
|
||||||
let entry = ResidentEntry::new(
|
resident_arc,
|
||||||
Arc::clone(&bank_arc),
|
entry_clone.decoded_size as usize,
|
||||||
entry_clone.decoded_size as usize,
|
);
|
||||||
);
|
|
||||||
map.insert(asset_id, entry);
|
|
||||||
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 +506,17 @@ 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();
|
entry_clone.decoded_size as usize,
|
||||||
existing.loads += 1;
|
);
|
||||||
Arc::clone(&existing.value)
|
sound_policy.stage(
|
||||||
} else {
|
handle_id,
|
||||||
let entry = ResidentEntry::new(
|
resident_arc,
|
||||||
Arc::clone(&bank_arc),
|
entry_clone.decoded_size as usize,
|
||||||
entry_clone.decoded_size as usize,
|
);
|
||||||
);
|
|
||||||
map.insert(asset_id, entry);
|
|
||||||
bank_arc
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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 +728,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 +762,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 +776,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 +842,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 +1028,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 +1187,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,12 @@ 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 +35,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> {
|
||||||
|
|||||||
@ -96,6 +96,62 @@ pub struct SyscallRegistryEntry {
|
|||||||
pub meta: SyscallMeta,
|
pub meta: SyscallMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SyscallRegistryEntry {
|
||||||
|
/// Starts the builder with mandatory fields and sensible default values.
|
||||||
|
pub const fn builder(syscall: Syscall, module: &'static str, name: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
syscall,
|
||||||
|
meta: SyscallMeta {
|
||||||
|
id: syscall as u32,
|
||||||
|
module,
|
||||||
|
name,
|
||||||
|
version: 1, // Default for new syscalls
|
||||||
|
arg_slots: 0,
|
||||||
|
ret_slots: 0,
|
||||||
|
caps: 0,
|
||||||
|
determinism: Determinism::Deterministic,
|
||||||
|
may_allocate: false,
|
||||||
|
cost_hint: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn version(mut self, n: u16) -> Self {
|
||||||
|
self.meta.version = n;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn args(mut self, n: u8) -> Self {
|
||||||
|
self.meta.arg_slots = n;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn rets(mut self, n: u16) -> Self {
|
||||||
|
self.meta.ret_slots = n;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn caps(mut self, caps: CapFlags) -> Self {
|
||||||
|
self.meta.caps = caps;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn non_deterministic(mut self) -> Self {
|
||||||
|
self.meta.determinism = Determinism::NonDeterministic;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn may_allocate(mut self) -> Self {
|
||||||
|
self.meta.may_allocate = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn cost(mut self, cost: u32) -> Self {
|
||||||
|
self.meta.cost_hint = cost;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn meta_for(syscall: Syscall) -> &'static SyscallMeta {
|
pub fn meta_for(syscall: Syscall) -> &'static SyscallMeta {
|
||||||
registry::meta_for(syscall)
|
registry::meta_for(syscall)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +1,27 @@
|
|||||||
use super::entry;
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::AssetLoad, "asset", "load")
|
||||||
Syscall::AssetLoad,
|
.args(2)
|
||||||
"asset",
|
.rets(2)
|
||||||
"load",
|
.caps(caps::ASSET)
|
||||||
1,
|
.non_deterministic()
|
||||||
2,
|
.cost(20),
|
||||||
2,
|
SyscallRegistryEntry::builder(Syscall::AssetStatus, "asset", "status")
|
||||||
caps::ASSET,
|
.args(1)
|
||||||
Determinism::NonDeterministic,
|
.rets(1)
|
||||||
false,
|
.caps(caps::ASSET)
|
||||||
20,
|
.non_deterministic(),
|
||||||
),
|
SyscallRegistryEntry::builder(Syscall::AssetCommit, "asset", "commit")
|
||||||
entry(
|
.args(1)
|
||||||
Syscall::AssetStatus,
|
.rets(1)
|
||||||
"asset",
|
.caps(caps::ASSET)
|
||||||
"status",
|
.non_deterministic()
|
||||||
1,
|
.cost(20),
|
||||||
1,
|
SyscallRegistryEntry::builder(Syscall::AssetCancel, "asset", "cancel")
|
||||||
1,
|
.args(1)
|
||||||
caps::ASSET,
|
.rets(1)
|
||||||
Determinism::NonDeterministic,
|
.caps(caps::ASSET)
|
||||||
false,
|
.non_deterministic()
|
||||||
1,
|
.cost(20),
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::AssetCommit,
|
|
||||||
"asset",
|
|
||||||
"commit",
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
caps::ASSET,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::AssetCancel,
|
|
||||||
"asset",
|
|
||||||
"cancel",
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
caps::ASSET,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,29 +1,14 @@
|
|||||||
use super::entry;
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::AudioPlaySample, "audio", "play_sample")
|
||||||
Syscall::AudioPlaySample,
|
.args(5)
|
||||||
"audio",
|
.rets(1)
|
||||||
"play_sample",
|
.caps(caps::AUDIO)
|
||||||
1,
|
.cost(5),
|
||||||
5,
|
SyscallRegistryEntry::builder(Syscall::AudioPlay, "audio", "play")
|
||||||
1,
|
.args(7)
|
||||||
caps::AUDIO,
|
.rets(1)
|
||||||
Determinism::Deterministic,
|
.caps(caps::AUDIO)
|
||||||
false,
|
.cost(5),
|
||||||
5,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::AudioPlay,
|
|
||||||
"audio",
|
|
||||||
"play",
|
|
||||||
1,
|
|
||||||
7,
|
|
||||||
1,
|
|
||||||
caps::AUDIO,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,29 +1,12 @@
|
|||||||
use super::entry;
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::BankInfo, "bank", "info")
|
||||||
Syscall::BankInfo,
|
.args(1)
|
||||||
"bank",
|
.rets(1)
|
||||||
"info",
|
.caps(caps::BANK),
|
||||||
1,
|
SyscallRegistryEntry::builder(Syscall::BankSlotInfo, "bank", "slot_info")
|
||||||
1,
|
.args(2)
|
||||||
1,
|
.rets(1)
|
||||||
caps::BANK,
|
.caps(caps::BANK),
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::BankSlotInfo,
|
|
||||||
"bank",
|
|
||||||
"slot_info",
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
1,
|
|
||||||
caps::BANK,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,150 +1,68 @@
|
|||||||
use super::entry;
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::FsOpen, "fs", "open")
|
||||||
Syscall::FsOpen,
|
.args(1)
|
||||||
"fs",
|
.rets(1)
|
||||||
"open",
|
.caps(caps::FS)
|
||||||
1,
|
.non_deterministic()
|
||||||
1,
|
.cost(20),
|
||||||
1,
|
SyscallRegistryEntry::builder(Syscall::FsRead, "fs", "read")
|
||||||
caps::FS,
|
.args(1)
|
||||||
Determinism::NonDeterministic,
|
.rets(1)
|
||||||
false,
|
.caps(caps::FS)
|
||||||
20,
|
.non_deterministic()
|
||||||
),
|
.cost(20),
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::FsWrite, "fs", "write")
|
||||||
Syscall::FsRead,
|
.args(2)
|
||||||
"fs",
|
.rets(1)
|
||||||
"read",
|
.caps(caps::FS)
|
||||||
1,
|
.non_deterministic()
|
||||||
1,
|
.cost(20),
|
||||||
1,
|
SyscallRegistryEntry::builder(Syscall::FsClose, "fs", "close").args(1).caps(caps::FS).cost(5),
|
||||||
caps::FS,
|
SyscallRegistryEntry::builder(Syscall::FsListDir, "fs", "list_dir")
|
||||||
Determinism::NonDeterministic,
|
.args(1)
|
||||||
false,
|
.rets(1)
|
||||||
20,
|
.caps(caps::FS)
|
||||||
),
|
.non_deterministic()
|
||||||
entry(
|
.cost(20),
|
||||||
Syscall::FsWrite,
|
SyscallRegistryEntry::builder(Syscall::FsExists, "fs", "exists").args(1).rets(1).caps(caps::FS),
|
||||||
"fs",
|
SyscallRegistryEntry::builder(Syscall::FsDelete, "fs", "delete")
|
||||||
"write",
|
.args(1)
|
||||||
1,
|
.caps(caps::FS)
|
||||||
2,
|
.non_deterministic()
|
||||||
1,
|
.cost(20),
|
||||||
caps::FS,
|
SyscallRegistryEntry::builder(Syscall::MemSlotCount, "mem", "slot_count")
|
||||||
Determinism::NonDeterministic,
|
.rets(2)
|
||||||
false,
|
.caps(caps::FS),
|
||||||
20,
|
SyscallRegistryEntry::builder(Syscall::MemSlotStat, "mem", "slot_stat")
|
||||||
),
|
.args(1)
|
||||||
entry(Syscall::FsClose, "fs", "close", 1, 1, 0, caps::FS, Determinism::Deterministic, false, 5),
|
.rets(5)
|
||||||
entry(
|
.caps(caps::FS)
|
||||||
Syscall::FsListDir,
|
.non_deterministic()
|
||||||
"fs",
|
.cost(5),
|
||||||
"list_dir",
|
SyscallRegistryEntry::builder(Syscall::MemSlotRead, "mem", "slot_read")
|
||||||
1,
|
.args(3)
|
||||||
1,
|
.rets(3)
|
||||||
1,
|
.caps(caps::FS)
|
||||||
caps::FS,
|
.non_deterministic()
|
||||||
Determinism::NonDeterministic,
|
.cost(20),
|
||||||
false,
|
SyscallRegistryEntry::builder(Syscall::MemSlotWrite, "mem", "slot_write")
|
||||||
20,
|
.args(3)
|
||||||
),
|
.rets(2)
|
||||||
entry(
|
.caps(caps::FS)
|
||||||
Syscall::FsExists,
|
.non_deterministic()
|
||||||
"fs",
|
.cost(20),
|
||||||
"exists",
|
SyscallRegistryEntry::builder(Syscall::MemSlotCommit, "mem", "slot_commit")
|
||||||
1,
|
.args(1)
|
||||||
1,
|
.rets(1)
|
||||||
1,
|
.caps(caps::FS)
|
||||||
caps::FS,
|
.non_deterministic()
|
||||||
Determinism::Deterministic,
|
.cost(20),
|
||||||
false,
|
SyscallRegistryEntry::builder(Syscall::MemSlotClear, "mem", "slot_clear")
|
||||||
1,
|
.args(1)
|
||||||
),
|
.rets(1)
|
||||||
entry(
|
.caps(caps::FS)
|
||||||
Syscall::FsDelete,
|
.non_deterministic()
|
||||||
"fs",
|
.cost(20),
|
||||||
"delete",
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
caps::FS,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::MemSlotCount,
|
|
||||||
"mem",
|
|
||||||
"slot_count",
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
caps::FS,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::MemSlotStat,
|
|
||||||
"mem",
|
|
||||||
"slot_stat",
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
5,
|
|
||||||
caps::FS,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::MemSlotRead,
|
|
||||||
"mem",
|
|
||||||
"slot_read",
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
caps::FS,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::MemSlotWrite,
|
|
||||||
"mem",
|
|
||||||
"slot_write",
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
2,
|
|
||||||
caps::FS,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::MemSlotCommit,
|
|
||||||
"mem",
|
|
||||||
"slot_commit",
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
caps::FS,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::MemSlotClear,
|
|
||||||
"mem",
|
|
||||||
"slot_clear",
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
caps::FS,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,113 +1,41 @@
|
|||||||
use super::entry;
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx", "clear")
|
||||||
Syscall::GfxClear,
|
.args(1)
|
||||||
"gfx",
|
.caps(caps::GFX)
|
||||||
"clear",
|
.cost(20),
|
||||||
1,
|
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx", "fill_rect")
|
||||||
1,
|
.args(5)
|
||||||
0,
|
.caps(caps::GFX)
|
||||||
caps::GFX,
|
.cost(20),
|
||||||
Determinism::Deterministic,
|
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx", "draw_line")
|
||||||
false,
|
.args(5)
|
||||||
20,
|
.caps(caps::GFX)
|
||||||
),
|
.cost(5),
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx", "draw_circle")
|
||||||
Syscall::GfxFillRect,
|
.args(4)
|
||||||
"gfx",
|
.caps(caps::GFX)
|
||||||
"fill_rect",
|
.cost(5),
|
||||||
1,
|
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx", "draw_disc")
|
||||||
5,
|
.args(5)
|
||||||
0,
|
.caps(caps::GFX)
|
||||||
caps::GFX,
|
.cost(5),
|
||||||
Determinism::Deterministic,
|
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx", "draw_square")
|
||||||
false,
|
.args(6)
|
||||||
20,
|
.caps(caps::GFX)
|
||||||
),
|
.cost(5),
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::GfxSetSprite, "gfx", "set_sprite")
|
||||||
Syscall::GfxDrawLine,
|
.args(10)
|
||||||
"gfx",
|
.rets(1)
|
||||||
"draw_line",
|
.caps(caps::GFX)
|
||||||
1,
|
.cost(5),
|
||||||
5,
|
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
|
||||||
0,
|
.args(4)
|
||||||
caps::GFX,
|
.caps(caps::GFX)
|
||||||
Determinism::Deterministic,
|
.cost(20),
|
||||||
false,
|
SyscallRegistryEntry::builder(Syscall::GfxClear565, "gfx", "clear_565")
|
||||||
5,
|
.args(1)
|
||||||
),
|
.caps(caps::GFX)
|
||||||
entry(
|
.cost(20),
|
||||||
Syscall::GfxDrawCircle,
|
|
||||||
"gfx",
|
|
||||||
"draw_circle",
|
|
||||||
1,
|
|
||||||
4,
|
|
||||||
0,
|
|
||||||
caps::GFX,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::GfxDrawDisc,
|
|
||||||
"gfx",
|
|
||||||
"draw_disc",
|
|
||||||
1,
|
|
||||||
5,
|
|
||||||
0,
|
|
||||||
caps::GFX,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::GfxDrawSquare,
|
|
||||||
"gfx",
|
|
||||||
"draw_square",
|
|
||||||
1,
|
|
||||||
6,
|
|
||||||
0,
|
|
||||||
caps::GFX,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::GfxSetSprite,
|
|
||||||
"gfx",
|
|
||||||
"set_sprite",
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
caps::GFX,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::GfxDrawText,
|
|
||||||
"gfx",
|
|
||||||
"draw_text",
|
|
||||||
1,
|
|
||||||
4,
|
|
||||||
0,
|
|
||||||
caps::GFX,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::GfxClear565,
|
|
||||||
"gfx",
|
|
||||||
"clear_565",
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
caps::GFX,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,29 +1,14 @@
|
|||||||
use super::entry;
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::LogWrite, "log", "write")
|
||||||
Syscall::LogWrite,
|
.args(2)
|
||||||
"log",
|
.caps(caps::LOG)
|
||||||
"write",
|
.non_deterministic()
|
||||||
1,
|
.cost(5),
|
||||||
2,
|
SyscallRegistryEntry::builder(Syscall::LogWriteTag, "log", "write_tag")
|
||||||
0,
|
.args(3)
|
||||||
caps::LOG,
|
.caps(caps::LOG)
|
||||||
Determinism::NonDeterministic,
|
.non_deterministic()
|
||||||
false,
|
.cost(5),
|
||||||
5,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::LogWriteTag,
|
|
||||||
"log",
|
|
||||||
"write_tag",
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
0,
|
|
||||||
caps::LOG,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
5,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -6,36 +6,7 @@ mod gfx;
|
|||||||
mod log;
|
mod log;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
use super::{CapFlags, Determinism, Syscall, SyscallMeta, SyscallRegistryEntry};
|
use super::SyscallRegistryEntry;
|
||||||
|
|
||||||
pub(crate) const fn entry(
|
|
||||||
syscall: Syscall,
|
|
||||||
module: &'static str,
|
|
||||||
name: &'static str,
|
|
||||||
version: u16,
|
|
||||||
arg_slots: u8,
|
|
||||||
ret_slots: u16,
|
|
||||||
caps: CapFlags,
|
|
||||||
determinism: Determinism,
|
|
||||||
may_allocate: bool,
|
|
||||||
cost_hint: u32,
|
|
||||||
) -> SyscallRegistryEntry {
|
|
||||||
SyscallRegistryEntry {
|
|
||||||
syscall,
|
|
||||||
meta: SyscallMeta {
|
|
||||||
id: syscall as u32,
|
|
||||||
module,
|
|
||||||
name,
|
|
||||||
version,
|
|
||||||
arg_slots,
|
|
||||||
ret_slots,
|
|
||||||
caps,
|
|
||||||
determinism,
|
|
||||||
may_allocate,
|
|
||||||
cost_hint,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntry> {
|
pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntry> {
|
||||||
system::ENTRIES
|
system::ENTRIES
|
||||||
|
|||||||
@ -1,29 +1,11 @@
|
|||||||
use super::entry;
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
use crate::syscalls::{Determinism, Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
entry(
|
SyscallRegistryEntry::builder(Syscall::SystemHasCart, "system", "has_cart")
|
||||||
Syscall::SystemHasCart,
|
.rets(1)
|
||||||
"system",
|
.caps(caps::SYSTEM),
|
||||||
"has_cart",
|
SyscallRegistryEntry::builder(Syscall::SystemRunCart, "system", "run_cart")
|
||||||
1,
|
.caps(caps::SYSTEM)
|
||||||
0,
|
.non_deterministic()
|
||||||
1,
|
.cost(50),
|
||||||
caps::SYSTEM,
|
|
||||||
Determinism::Deterministic,
|
|
||||||
false,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
Syscall::SystemRunCart,
|
|
||||||
"system",
|
|
||||||
"run_cart",
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
caps::SYSTEM,
|
|
||||||
Determinism::NonDeterministic,
|
|
||||||
false,
|
|
||||||
50,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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,75 @@ 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 +201,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 +211,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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,6 +129,26 @@ impl VirtualMachineRuntime {
|
|||||||
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
||||||
{
|
{
|
||||||
hw.gfx_mut().render_all();
|
hw.gfx_mut().render_all();
|
||||||
|
|
||||||
|
// 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 =
|
self.telemetry_current.host_cpu_time_us =
|
||||||
start.elapsed().as_micros() as u64;
|
start.elapsed().as_micros() as u64;
|
||||||
|
|
||||||
@ -166,15 +187,21 @@ 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;
|
||||||
|
|
||||||
let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
|
// 2. High-frequency telemetry update (only if inspection is active)
|
||||||
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
|
if self.inspection_active {
|
||||||
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
|
let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
|
||||||
self.telemetry_current.gfx_slots_occupied = gfx_stats.slots_occupied as u32;
|
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);
|
let audio_stats = hw.assets().bank_info(BankType::SOUNDS);
|
||||||
self.telemetry_current.audio_used_bytes = audio_stats.used_bytes;
|
self.telemetry_current.audio_used_bytes = audio_stats.used_bytes;
|
||||||
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 +214,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
|
||||||
|
|||||||
@ -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::Arc;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
/// 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,10 +146,10 @@ impl Verifier {
|
|||||||
|
|
||||||
let func_code = &code[func_start..func_end];
|
let func_code = &code[func_start..func_end];
|
||||||
|
|
||||||
// Funções vazias (sem qualquer byte de código) são consideradas válidas no verificador.
|
// Empty functions (no code bytes) are considered valid in the verifier.
|
||||||
// Elas não consomem nem produzem valores na pilha e não possuem fluxo interno.
|
// They do not consume or produce values on the stack and have no internal flow.
|
||||||
// Observação: se uma função vazia for chamada em tempo de execução e retorno/efeitos
|
// Note: if an empty function is called at runtime and return/effects
|
||||||
// forem esperados, caberá ao gerador de código/linker impedir tal situação.
|
// are expected, it is the responsibility of the code generator/linker to prevent this situation.
|
||||||
if func_code.is_empty() {
|
if func_code.is_empty() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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":[]}
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
id: LSN-0026
|
||||||
|
ticket: perf-runtime-telemetry-hot-path
|
||||||
|
title: Push-based Telemetry Model
|
||||||
|
created: 2026-04-10
|
||||||
|
tags: [performance, telemetry, atomics]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Push-based Telemetry Model
|
||||||
|
|
||||||
|
The PROMETEU telemetry system evolved from an on-demand scan model (pull) to an incremental counter model (push), aiming to minimize the impact on the runtime's hot path.
|
||||||
|
|
||||||
|
## The Original Problem
|
||||||
|
|
||||||
|
Previously, at every host tick, the runtime requested memory usage information from the asset banks. This resulted in:
|
||||||
|
- $O(n)$ scans over resource maps.
|
||||||
|
- Multiple read lock acquisitions in every tick.
|
||||||
|
- Unnecessary overhead on handheld hardware, where every microsecond counts.
|
||||||
|
|
||||||
|
## The Solution: Push Model with Atomics
|
||||||
|
|
||||||
|
The implemented solution uses `AtomicUsize` in drivers and the VM to maintain the system state in real-time with $O(1)$ read and write cost:
|
||||||
|
1. **Drivers (Assets):** Atomic counters in each `BankPolicy` are updated during `load`, `commit`, and `cancel`.
|
||||||
|
2. **VM (Heap):** A `used_bytes` counter in the `Heap` struct tracks allocations and deallocations (sweep).
|
||||||
|
3. **System (Logs):** The `LogService` tracks log pressure emitted in each frame.
|
||||||
|
|
||||||
|
## Two Levels of Observability
|
||||||
|
|
||||||
|
To balance performance and debugging, the collection was divided:
|
||||||
|
- **Frame Snapshot (Always):** Automatic capture at the end of each logical frame. Irrelevant cost ($O(1)$). Serves the `Certifier` and historical logs.
|
||||||
|
- **Host Tick (On-Demand):** Detailed collection in every tick only occurs if `inspection_active` is enabled (e.g., F1 Overlay on).
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
- **Trigger Decoupling:** We should not use the `Certifier` state to enable visual debugging features (like the overlay), as they have different purposes and costs.
|
||||||
|
- **Eventual Consistency is Sufficient:** For telemetry metrics, it is not necessary to lock the system to obtain an exact value every nanosecond. Relaxed atomic reading is sufficient and much more performant.
|
||||||
|
- **Cost Isolation:** Moving the aggregation logic to the driver simplifies the runtime and ensures that the telemetry cost is paid only during state mutations, rather than repeatedly during stable execution.
|
||||||
@ -1,77 +0,0 @@
|
|||||||
---
|
|
||||||
id: AGD-0007
|
|
||||||
ticket: perf-runtime-telemetry-hot-path
|
|
||||||
title: Agenda - [PERF] Runtime Telemetry Hot Path
|
|
||||||
status: open
|
|
||||||
created: 2026-03-27
|
|
||||||
resolved:
|
|
||||||
decision:
|
|
||||||
tags: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Agenda - [PERF] Runtime Telemetry Hot Path
|
|
||||||
|
|
||||||
## Problema
|
|
||||||
|
|
||||||
O runtime cobra telemetria de asset bank no caminho quente de todo host tick.
|
|
||||||
|
|
||||||
Hoje, `tick()` consulta `bank_info()` para `gfx` e `audio` mesmo quando nenhum logical frame foi fechado. O custo de observabilidade acaba sendo pago continuamente pela execucao normal.
|
|
||||||
|
|
||||||
## Dor
|
|
||||||
|
|
||||||
- CPU e locks sao gastos em todos os ticks, nao apenas quando a metrica muda.
|
|
||||||
- hardware barato sofre mais com trabalho pequeno e repetitivo do que com picos raros.
|
|
||||||
- overlay, stats e certifier acabam puxando custo estrutural para o core do runtime.
|
|
||||||
|
|
||||||
## Hotspots Atuais
|
|
||||||
|
|
||||||
- [tick.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs#L167)
|
|
||||||
- [asset.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/asset.rs#L618)
|
|
||||||
|
|
||||||
## Alvo da Discussao
|
|
||||||
|
|
||||||
Remover varredura e agregacao lock-heavy do hot path do tick sem perder observabilidade util.
|
|
||||||
|
|
||||||
## O Que Precisa Ser Definido
|
|
||||||
|
|
||||||
1. Modelo de metrica.
|
|
||||||
Decidir o que passa a ser contador incremental e o que continua sendo snapshot sob demanda.
|
|
||||||
|
|
||||||
2. Frequencia de coleta.
|
|
||||||
Decidir se atualizacao acontece:
|
|
||||||
- no fechamento do logical frame;
|
|
||||||
- apenas com overlay/debug ativo;
|
|
||||||
- por amostragem periodica;
|
|
||||||
- por evento de mutacao (`load`, `commit`, `cancel`).
|
|
||||||
|
|
||||||
3. Responsabilidade da agregacao.
|
|
||||||
Delimitar se a verdade dos bytes/slots fica:
|
|
||||||
- no `AssetManager`;
|
|
||||||
- no runtime;
|
|
||||||
- em uma camada propria de telemetry cache.
|
|
||||||
|
|
||||||
4. Garantia de consistencia.
|
|
||||||
Decidir qual grau de defasagem e aceitavel para handheld barato:
|
|
||||||
- exato em tempo real;
|
|
||||||
- eventual por frame;
|
|
||||||
- eventual por tick de debug.
|
|
||||||
|
|
||||||
## Open Questions de Arquitetura
|
|
||||||
|
|
||||||
1. O certifier realmente precisa de snapshot de bank a cada tick?
|
|
||||||
2. O overlay pode ler uma versao resumida da telemetria em vez de recalcular tudo?
|
|
||||||
3. Vale manter caminho "preciso" so para testes/debug e caminho "barato" para runtime normal?
|
|
||||||
|
|
||||||
## Dependencias
|
|
||||||
|
|
||||||
- `../specs/10-debug-inspection-and-profiling.md`
|
|
||||||
- `../specs/16a-syscall-policies.md`
|
|
||||||
|
|
||||||
## Criterio de Saida Desta Agenda
|
|
||||||
|
|
||||||
Pode virar PR quando houver decisao escrita sobre:
|
|
||||||
|
|
||||||
- metrica incremental vs snapshot;
|
|
||||||
- ponto canonico de atualizacao da telemetria;
|
|
||||||
- custo maximo aceitavel no hot path do tick;
|
|
||||||
- comportamento de overlay/certifier sobre dados defasados.
|
|
||||||
@ -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.
|
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user