diff --git a/crates/console/prometeu-bytecode/src/lib.rs b/crates/console/prometeu-bytecode/src/lib.rs index d56276df..19c898cb 100644 --- a/crates/console/prometeu-bytecode/src/lib.rs +++ b/crates/console/prometeu-bytecode/src/lib.rs @@ -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}; diff --git a/crates/console/prometeu-bytecode/src/program_image.rs b/crates/console/prometeu-bytecode/src/program_image.rs index 9f1890ad..66ed722c 100644 --- a/crates/console/prometeu-bytecode/src/program_image.rs +++ b/crates/console/prometeu-bytecode/src/program_image.rs @@ -75,7 +75,7 @@ impl From 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(); diff --git a/crates/console/prometeu-bytecode/src/value.rs b/crates/console/prometeu-bytecode/src/value.rs index 78b67bbe..c38f7cdb 100644 --- a/crates/console/prometeu-bytecode/src/value.rs +++ b/crates/console/prometeu-bytecode/src/value.rs @@ -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(value: S) -> Self + where + S: Into>, + { + STRING_MATERIALIZATION_COUNT.fetch_add(1, AtomicOrdering::Relaxed); + Value::String(value.into()) + } + pub fn as_float(&self) -> Option { 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::*; diff --git a/crates/console/prometeu-hal/src/host_return.rs b/crates/console/prometeu-hal/src/host_return.rs index 59cb8cae..b38bd7f2 100644 --- a/crates/console/prometeu-hal/src/host_return.rs +++ b/crates/console/prometeu-hal/src/host_return.rs @@ -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)); } } diff --git a/crates/console/prometeu-hal/src/telemetry.rs b/crates/console/prometeu-hal/src/telemetry.rs index 8dd9ea75..21a196ab 100644 --- a/crates/console/prometeu-hal/src/telemetry.rs +++ b/crates/console/prometeu-hal/src/telemetry.rs @@ -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, @@ -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); + } } diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime.rs b/crates/console/prometeu-system/src/virtual_machine_runtime.rs index 1e5edac4..682ce694 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime.rs @@ -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, } diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs index 0e022748..232c35d6 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs @@ -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; } diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index df491343..21378beb 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -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); diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs index 72c67b1d..33191103 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs @@ -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 diff --git a/crates/console/prometeu-vm/src/heap.rs b/crates/console/prometeu-vm/src/heap.rs index 0b3d3463..9deb2fe5 100644 --- a/crates/console/prometeu-vm/src/heap.rs +++ b/crates/console/prometeu-vm/src/heap.rs @@ -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, + /// Monotonic count of heap allocation events for internal engineering telemetry. + pub allocation_count: Arc, } 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. diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 215dc584..5fb65d93 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -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))), diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 10905bee..1b2e90cb 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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"}]} diff --git a/discussion/workflow/plans/PLN-0034-internal-allocation-evidence-and-hot-path-measurement.md b/discussion/workflow/plans/PLN-0034-internal-allocation-evidence-and-hot-path-measurement.md index 57101428..1fd350b7 100644 --- a/discussion/workflow/plans/PLN-0034-internal-allocation-evidence-and-hot-path-measurement.md +++ b/discussion/workflow/plans/PLN-0034-internal-allocation-evidence-and-hot-path-measurement.md @@ -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] ---