implement PLN-0034 internal allocation evidence

This commit is contained in:
bQUARKz 2026-04-20 08:50:01 +01:00
parent 09821cf9cc
commit 811af09ea7
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
13 changed files with 125 additions and 8 deletions

View File

@ -21,4 +21,4 @@ pub use disassembler::disassemble;
pub use layout::{compute_function_layouts, FunctionLayout};
pub use model::{BytecodeLoader, FunctionMeta, LoadError, SyscallDecl};
pub use program_image::ProgramImage;
pub use value::{HeapRef, Value};
pub use value::{string_materialization_count, HeapRef, Value};

View File

@ -75,7 +75,7 @@ impl From<BytecodeModule> for ProgramImage {
ConstantPoolEntry::Int64(v) => Value::Int64(*v),
ConstantPoolEntry::Float64(v) => Value::Float(*v),
ConstantPoolEntry::Boolean(v) => Value::Boolean(*v),
ConstantPoolEntry::String(v) => Value::String(v.clone().into()),
ConstantPoolEntry::String(v) => Value::string(v.clone()),
ConstantPoolEntry::Int32(v) => Value::Int32(*v),
})
.collect();

View File

@ -1,8 +1,11 @@
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt::Write;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::sync::Arc;
static STRING_MATERIALIZATION_COUNT: AtomicU64 = AtomicU64::new(0);
/// Opaque handle that references an object stored in the VM heap.
///
/// This is an index-based handle. It does not imply ownership and carries
@ -76,6 +79,14 @@ impl PartialOrd for Value {
}
impl Value {
pub fn string<S>(value: S) -> Self
where
S: Into<Arc<str>>,
{
STRING_MATERIALIZATION_COUNT.fetch_add(1, AtomicOrdering::Relaxed);
Value::String(value.into())
}
pub fn as_float(&self) -> Option<f64> {
match self {
Value::Int32(i) => Some(*i as f64),
@ -137,6 +148,10 @@ impl Value {
}
}
pub fn string_materialization_count() -> u64 {
STRING_MATERIALIZATION_COUNT.load(AtomicOrdering::Relaxed)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -22,6 +22,6 @@ impl<'a> HostReturn<'a> {
self.stack.push(Value::HeapRef(HeapRef(g as u32)));
}
pub fn push_string(&mut self, s: String) {
self.stack.push(Value::String(s.into()));
self.stack.push(Value::string(s));
}
}

View File

@ -24,6 +24,8 @@ pub struct TelemetryFrame {
// RAM (Heap)
pub heap_used_bytes: usize,
pub heap_max_bytes: usize,
pub vm_heap_allocations: u64,
pub vm_string_materializations: u64,
// Log Pressure from the last completed logical frame
pub logs_count: u32,
@ -53,6 +55,8 @@ pub struct AtomicTelemetry {
// RAM (Heap)
pub heap_used_bytes: AtomicUsize,
pub heap_max_bytes: AtomicUsize,
pub vm_heap_allocations: AtomicU64,
pub vm_string_materializations: AtomicU64,
// Transient in-flight log counter for the current logical frame
pub current_logs_count: Arc<AtomicU32>,
@ -83,6 +87,8 @@ impl AtomicTelemetry {
scene_slots_total: self.scene_slots_total.load(Ordering::Relaxed),
heap_used_bytes: self.heap_used_bytes.load(Ordering::Relaxed),
heap_max_bytes: self.heap_max_bytes.load(Ordering::Relaxed),
vm_heap_allocations: self.vm_heap_allocations.load(Ordering::Relaxed),
vm_string_materializations: self.vm_string_materializations.load(Ordering::Relaxed),
logs_count: self.logs_count.load(Ordering::Relaxed),
vm_steps: self.vm_steps.load(Ordering::Relaxed),
}
@ -102,6 +108,8 @@ impl AtomicTelemetry {
self.scene_slots_used.store(0, Ordering::Relaxed);
self.scene_slots_total.store(0, Ordering::Relaxed);
self.heap_used_bytes.store(0, Ordering::Relaxed);
self.vm_heap_allocations.store(0, Ordering::Relaxed);
self.vm_string_materializations.store(0, Ordering::Relaxed);
self.vm_steps.store(0, Ordering::Relaxed);
self.logs_count.store(0, Ordering::Relaxed);
self.current_logs_count.store(0, Ordering::Relaxed);
@ -313,4 +321,17 @@ mod tests {
assert_eq!(snapshot.logs_count, 3);
assert_eq!(current.load(Ordering::Relaxed), 7);
}
#[test]
fn snapshot_includes_internal_allocation_evidence() {
let current = Arc::new(AtomicU32::new(0));
let tel = AtomicTelemetry::new(current);
tel.vm_heap_allocations.store(2, Ordering::Relaxed);
tel.vm_string_materializations.store(5, Ordering::Relaxed);
let snapshot = tel.snapshot();
assert_eq!(snapshot.vm_heap_allocations, 2);
assert_eq!(snapshot.vm_string_materializations, 5);
}
}

View File

@ -7,6 +7,7 @@ mod tick;
use crate::CrashReport;
use crate::fs::{FsState, VirtualFS};
use crate::services::memcard::MemcardService;
use prometeu_bytecode::string_materialization_count;
use prometeu_hal::cartridge::AppMode;
use prometeu_hal::log::LogService;
use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
@ -38,6 +39,8 @@ pub struct VirtualMachineRuntime {
pub paused: bool,
pub debug_step_request: bool,
pub inspection_active: bool,
pub(crate) frame_start_heap_allocations: u64,
pub(crate) frame_start_string_materializations: u64,
pub(crate) needs_prepare_entry_call: bool,
pub(crate) boot_time: Instant,
}

View File

@ -32,6 +32,8 @@ impl VirtualMachineRuntime {
paused: false,
debug_step_request: false,
inspection_active: false,
frame_start_heap_allocations: 0,
frame_start_string_materializations: 0,
needs_prepare_entry_call: false,
boot_time,
};
@ -106,6 +108,8 @@ impl VirtualMachineRuntime {
self.paused = false;
self.debug_step_request = false;
self.inspection_active = false;
self.frame_start_heap_allocations = 0;
self.frame_start_string_materializations = 0;
self.needs_prepare_entry_call = false;
}

View File

@ -493,6 +493,8 @@ fn reset_clears_cartridge_scoped_runtime_state() {
Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(55) });
runtime.paused = true;
runtime.debug_step_request = true;
runtime.frame_start_heap_allocations = 11;
runtime.frame_start_string_materializations = 22;
runtime.needs_prepare_entry_call = true;
runtime.reset(&mut vm);
@ -518,10 +520,56 @@ fn reset_clears_cartridge_scoped_runtime_state() {
assert!(runtime.last_crash_report.is_none());
assert!(!runtime.paused);
assert!(!runtime.debug_step_request);
assert_eq!(runtime.frame_start_heap_allocations, 0);
assert_eq!(runtime.frame_start_string_materializations, 0);
assert!(!runtime.needs_prepare_entry_call);
assert_eq!(vm.pc(), 0);
}
#[test]
fn tick_numeric_happy_path_records_zero_internal_allocations() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code =
assemble("PUSH_I32 1\nPUSH_I32 2\nADD\nPOP_N 1\nFRAME_SYNC\nHALT").expect("assemble");
let program = serialized_single_function_module(code, vec![]);
let cartridge = cartridge_with_program(program, caps::NONE);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none());
let snapshot = runtime.atomic_telemetry.snapshot();
assert_eq!(snapshot.vm_heap_allocations, 0);
assert_eq!(snapshot.vm_string_materializations, 0);
}
#[test]
fn tick_already_materialized_string_path_records_zero_internal_allocations() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_CONST 0\nSET_GLOBAL 0\nGET_GLOBAL 0\nPOP_N 1\nFRAME_SYNC\nHALT")
.expect("assemble");
let program = serialized_single_function_module_with_consts(
code,
vec![ConstantPoolEntry::String("steady".into())],
vec![],
);
let cartridge = cartridge_with_program(program, caps::NONE);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none());
let snapshot = runtime.atomic_telemetry.snapshot();
assert_eq!(snapshot.vm_heap_allocations, 0);
assert_eq!(snapshot.vm_string_materializations, 0);
}
#[test]
fn initialize_vm_failure_clears_previous_identity_and_handles() {
let mut runtime = VirtualMachineRuntime::new(None);

View File

@ -7,6 +7,18 @@ use prometeu_vm::LogicalFrameEndingReason;
use std::sync::atomic::Ordering;
impl VirtualMachineRuntime {
fn refresh_internal_allocation_telemetry(&self, vm: &VirtualMachine) {
let heap_allocations =
vm.heap().allocation_count().saturating_sub(self.frame_start_heap_allocations);
let string_materializations =
string_materialization_count().saturating_sub(self.frame_start_string_materializations);
self.atomic_telemetry.vm_heap_allocations.store(heap_allocations, Ordering::Relaxed);
self.atomic_telemetry
.vm_string_materializations
.store(string_materializations, Ordering::Relaxed);
}
fn bank_telemetry_summary(
hw: &dyn HardwareBridge,
) -> (BankTelemetry, BankTelemetry, BankTelemetry) {
@ -94,6 +106,10 @@ impl VirtualMachineRuntime {
self.atomic_telemetry.cycles_used.store(0, Ordering::Relaxed);
self.atomic_telemetry.syscalls.store(0, Ordering::Relaxed);
self.atomic_telemetry.vm_steps.store(0, Ordering::Relaxed);
self.atomic_telemetry.vm_heap_allocations.store(0, Ordering::Relaxed);
self.atomic_telemetry.vm_string_materializations.store(0, Ordering::Relaxed);
self.frame_start_heap_allocations = vm.heap().allocation_count();
self.frame_start_string_materializations = string_materialization_count();
}
let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles);
@ -177,6 +193,7 @@ impl VirtualMachineRuntime {
self.atomic_telemetry
.heap_used_bytes
.store(vm.heap().used_bytes.load(Ordering::Relaxed), Ordering::Relaxed);
self.refresh_internal_allocation_telemetry(vm);
self.atomic_telemetry
.host_cpu_time_us
.store(start.elapsed().as_micros() as u64, Ordering::Relaxed);
@ -253,6 +270,7 @@ impl VirtualMachineRuntime {
self.atomic_telemetry
.heap_used_bytes
.store(vm.heap().used_bytes.load(Ordering::Relaxed), Ordering::Relaxed);
self.refresh_internal_allocation_telemetry(vm);
self.atomic_telemetry.frame_index.store(self.logical_frame_index, Ordering::Relaxed);
self.atomic_telemetry

View File

@ -3,7 +3,7 @@ use crate::object::{ObjectHeader, ObjectKind};
use prometeu_bytecode::{HeapRef, Value};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
/// Internal stored object: header plus opaque payload bytes.
#[derive(Debug, Clone)]
@ -79,6 +79,8 @@ pub struct Heap {
/// Total bytes currently used by all objects in the heap.
pub used_bytes: Arc<AtomicUsize>,
/// Monotonic count of heap allocation events for internal engineering telemetry.
pub allocation_count: Arc<AtomicU64>,
}
impl Heap {
@ -87,11 +89,13 @@ impl Heap {
objects: Vec::new(),
free_list: Vec::new(),
used_bytes: Arc::new(AtomicUsize::new(0)),
allocation_count: Arc::new(AtomicU64::new(0)),
}
}
fn insert_object(&mut self, obj: StoredObject) -> HeapRef {
self.used_bytes.fetch_add(obj.bytes(), Ordering::Relaxed);
self.allocation_count.fetch_add(1, Ordering::Relaxed);
if let Some(idx) = self.free_list.pop() {
debug_assert!(self.objects.get(idx).is_some_and(|slot| slot.is_none()));
self.objects[idx] = Some(obj);
@ -411,6 +415,10 @@ impl Heap {
self.objects.iter().filter(|s| s.is_some()).count()
}
pub fn allocation_count(&self) -> u64 {
self.allocation_count.load(Ordering::Relaxed)
}
/// Enumerate handles of coroutines that are currently suspended (i.e., not running):
/// Ready or Sleeping. These must be treated as GC roots by the runtime so their
/// stacks/frames are scanned during mark.

View File

@ -642,7 +642,7 @@ impl VirtualMachine {
let mut out = String::with_capacity(a.string_len_hint() + b.string_len_hint());
a.append_to_string(&mut out);
b.append_to_string(&mut out);
Ok(Value::String(out.into()))
Ok(Value::string(out))
}
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_add(*b))),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_add(*b))),

View File

@ -21,7 +21,7 @@
{"type":"discussion","id":"DSC-0026","status":"done","ticket":"render-all-scene-cache-and-camera-integration","title":"Integrate render_all with Scene Cache and Camera","created_at":"2026-04-14","updated_at":"2026-04-18","tags":["gfx","runtime","render","camera","scene"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
{"type":"discussion","id":"DSC-0027","status":"done","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-18","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"lessons/DSC-0027-frame-composer-public-syscall-surface/LSN-0032-public-abi-must-follow-the-canonical-service-boundary.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
{"type":"discussion","id":"DSC-0028","status":"done","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"lessons/DSC-0028-deferred-overlay-and-primitive-composition/LSN-0033-debug-primitives-should-be-a-final-overlay-not-part-of-game-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
{"type":"discussion","id":"DSC-0014","status":"in_progress","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-04-20","_override_reason":"User explicitly requested emitting a decision from the resolved agenda in this turn."}],"decisions":[{"id":"DEC-0018","file":"workflow/decisions/DEC-0018-vm-allocation-and-copy-pressure-baseline.md","status":"in_progress","created_at":"2026-04-20","updated_at":"2026-04-20","ref_agenda":"AGD-0013","_override_reason":"User explicitly requested emitting and then accepting the decision, followed by plan generation."}],"plans":[{"id":"PLN-0033","file":"PLN-0033-vm-hot-path-ownership-and-string-copy-pressure.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0018"]},{"id":"PLN-0034","file":"PLN-0034-internal-allocation-evidence-and-hot-path-measurement.md","status":"review","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0018"]},{"id":"PLN-0035","file":"PLN-0035-runtime-spec-wording-for-materialization-vs-copy-pressure.md","status":"review","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0018"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0014","status":"in_progress","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-04-20","_override_reason":"User explicitly requested emitting a decision from the resolved agenda in this turn."}],"decisions":[{"id":"DEC-0018","file":"workflow/decisions/DEC-0018-vm-allocation-and-copy-pressure-baseline.md","status":"in_progress","created_at":"2026-04-20","updated_at":"2026-04-20","ref_agenda":"AGD-0013","_override_reason":"User explicitly requested emitting and then accepting the decision, followed by plan generation."}],"plans":[{"id":"PLN-0033","file":"PLN-0033-vm-hot-path-ownership-and-string-copy-pressure.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0018"]},{"id":"PLN-0034","file":"PLN-0034-internal-allocation-evidence-and-hot-path-measurement.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0018"]},{"id":"PLN-0035","file":"PLN-0035-runtime-spec-wording-for-materialization-vs-copy-pressure.md","status":"review","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0018"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}

View File

@ -2,9 +2,9 @@
id: PLN-0034
ticket: perf-vm-allocation-and-copy-pressure
title: Plan - Internal Allocation Evidence and Hot Path Measurement
status: review
status: done
created: 2026-04-20
completed:
completed: 2026-04-20
tags: [perf, runtime, telemetry, allocation, engineering]
---