diff --git a/crates/console/prometeu-hal/src/telemetry.rs b/crates/console/prometeu-hal/src/telemetry.rs index 21a196ab..17253ac7 100644 --- a/crates/console/prometeu-hal/src/telemetry.rs +++ b/crates/console/prometeu-hal/src/telemetry.rs @@ -29,6 +29,23 @@ pub struct TelemetryFrame { // Log Pressure from the last completed logical frame pub logs_count: u32, + + // Render handoff/presentation telemetry + pub produced_submissions: u64, + pub replaced_before_consume: u64, + pub consumed_submissions: u64, + pub presented_frames: u64, + pub repeated_presents: u64, + pub render_errors: u64, + pub present_errors: u64, + pub stale_epoch_discards: u64, + pub shutdown_discards: u64, + pub last_produced_frame_id: u64, + pub last_consumed_frame_id: u64, + pub last_presented_frame_id: u64, + pub last_dropped_frame_id: u64, + pub last_error_frame_id: u64, + pub active_render_epoch: u64, } /// Thread-safe, atomic telemetry storage for real-time monitoring by the host. @@ -62,6 +79,23 @@ pub struct AtomicTelemetry { pub current_logs_count: Arc, // Persisted log count from the last completed logical frame pub logs_count: AtomicU32, + + // Render handoff/presentation telemetry + pub produced_submissions: AtomicU64, + pub replaced_before_consume: AtomicU64, + pub consumed_submissions: AtomicU64, + pub presented_frames: AtomicU64, + pub repeated_presents: AtomicU64, + pub render_errors: AtomicU64, + pub present_errors: AtomicU64, + pub stale_epoch_discards: AtomicU64, + pub shutdown_discards: AtomicU64, + pub last_produced_frame_id: AtomicU64, + pub last_consumed_frame_id: AtomicU64, + pub last_presented_frame_id: AtomicU64, + pub last_dropped_frame_id: AtomicU64, + pub last_error_frame_id: AtomicU64, + pub active_render_epoch: AtomicU64, } impl AtomicTelemetry { @@ -91,6 +125,21 @@ impl AtomicTelemetry { 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), + produced_submissions: self.produced_submissions.load(Ordering::Relaxed), + replaced_before_consume: self.replaced_before_consume.load(Ordering::Relaxed), + consumed_submissions: self.consumed_submissions.load(Ordering::Relaxed), + presented_frames: self.presented_frames.load(Ordering::Relaxed), + repeated_presents: self.repeated_presents.load(Ordering::Relaxed), + render_errors: self.render_errors.load(Ordering::Relaxed), + present_errors: self.present_errors.load(Ordering::Relaxed), + stale_epoch_discards: self.stale_epoch_discards.load(Ordering::Relaxed), + shutdown_discards: self.shutdown_discards.load(Ordering::Relaxed), + last_produced_frame_id: self.last_produced_frame_id.load(Ordering::Relaxed), + last_consumed_frame_id: self.last_consumed_frame_id.load(Ordering::Relaxed), + last_presented_frame_id: self.last_presented_frame_id.load(Ordering::Relaxed), + last_dropped_frame_id: self.last_dropped_frame_id.load(Ordering::Relaxed), + last_error_frame_id: self.last_error_frame_id.load(Ordering::Relaxed), + active_render_epoch: self.active_render_epoch.load(Ordering::Relaxed), } } @@ -113,6 +162,21 @@ impl AtomicTelemetry { self.vm_steps.store(0, Ordering::Relaxed); self.logs_count.store(0, Ordering::Relaxed); self.current_logs_count.store(0, Ordering::Relaxed); + self.produced_submissions.store(0, Ordering::Relaxed); + self.replaced_before_consume.store(0, Ordering::Relaxed); + self.consumed_submissions.store(0, Ordering::Relaxed); + self.presented_frames.store(0, Ordering::Relaxed); + self.repeated_presents.store(0, Ordering::Relaxed); + self.render_errors.store(0, Ordering::Relaxed); + self.present_errors.store(0, Ordering::Relaxed); + self.stale_epoch_discards.store(0, Ordering::Relaxed); + self.shutdown_discards.store(0, Ordering::Relaxed); + self.last_produced_frame_id.store(0, Ordering::Relaxed); + self.last_consumed_frame_id.store(0, Ordering::Relaxed); + self.last_presented_frame_id.store(0, Ordering::Relaxed); + self.last_dropped_frame_id.store(0, Ordering::Relaxed); + self.last_error_frame_id.store(0, Ordering::Relaxed); + self.active_render_epoch.store(0, Ordering::Relaxed); } } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs index 6085b99a..6241e955 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/render_manager.rs @@ -1,8 +1,10 @@ use prometeu_hal::app_mode::AppMode; +use prometeu_hal::telemetry::AtomicTelemetry; use prometeu_hal::{ FrameId, Game2DFramePacket, RenderOwnership, RenderSubmission, RenderSubmissionPacket, ShellUiFramePacket, }; +use std::sync::atomic::Ordering; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenderTransitionState { @@ -59,6 +61,78 @@ pub trait RenderSurface { fn consume_submission(&mut self, submission: &RenderSubmission); } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct RenderTelemetrySnapshot { + pub produced_submissions: u64, + pub replaced_before_consume: u64, + pub consumed_submissions: u64, + pub presented_frames: u64, + pub repeated_presents: u64, + pub render_errors: u64, + pub present_errors: u64, + pub stale_epoch_discards: u64, + pub shutdown_discards: u64, + pub last_produced_frame_id: u64, + pub last_consumed_frame_id: u64, + pub last_presented_frame_id: u64, + pub last_dropped_frame_id: u64, + pub last_error_frame_id: u64, + pub active_render_epoch: u64, +} + +#[derive(Debug, Clone, Default)] +struct RenderTelemetry { + snapshot: RenderTelemetrySnapshot, +} + +impl RenderTelemetry { + fn record_produced(&mut self, frame_id: FrameId) { + self.snapshot.produced_submissions = self.snapshot.produced_submissions.wrapping_add(1); + self.snapshot.last_produced_frame_id = frame_id.get(); + } + + fn record_replaced_before_consume(&mut self, frame_id: FrameId) { + self.snapshot.replaced_before_consume = + self.snapshot.replaced_before_consume.wrapping_add(1); + self.snapshot.last_dropped_frame_id = frame_id.get(); + } + + fn record_consumed(&mut self, frame_id: FrameId) { + self.snapshot.consumed_submissions = self.snapshot.consumed_submissions.wrapping_add(1); + self.snapshot.last_consumed_frame_id = frame_id.get(); + } + + fn record_presented(&mut self, frame_id: FrameId) { + self.snapshot.presented_frames = self.snapshot.presented_frames.wrapping_add(1); + self.snapshot.last_presented_frame_id = frame_id.get(); + } + + fn record_repeated_present(&mut self, frame_id: FrameId) { + self.snapshot.repeated_presents = self.snapshot.repeated_presents.wrapping_add(1); + self.snapshot.last_presented_frame_id = frame_id.get(); + } + + fn record_render_error(&mut self, frame_id: FrameId) { + self.snapshot.render_errors = self.snapshot.render_errors.wrapping_add(1); + self.snapshot.last_error_frame_id = frame_id.get(); + } + + fn record_present_error(&mut self, frame_id: FrameId) { + self.snapshot.present_errors = self.snapshot.present_errors.wrapping_add(1); + self.snapshot.last_error_frame_id = frame_id.get(); + } + + fn record_stale_epoch_discard(&mut self, frame_id: FrameId) { + self.snapshot.stale_epoch_discards = self.snapshot.stale_epoch_discards.wrapping_add(1); + self.snapshot.last_dropped_frame_id = frame_id.get(); + } + + fn record_shutdown_discard(&mut self, frame_id: FrameId) { + self.snapshot.shutdown_discards = self.snapshot.shutdown_discards.wrapping_add(1); + self.snapshot.last_dropped_frame_id = frame_id.get(); + } +} + #[derive(Debug, Clone, Default)] pub struct RenderHandoff { pending: Option, @@ -125,6 +199,7 @@ pub struct RenderManager { active_policy: RenderPolicy, next_frame_id: FrameId, handoff: RenderHandoff, + telemetry: RenderTelemetry, transition_state: RenderTransitionState, } @@ -137,6 +212,7 @@ impl RenderManager { active_policy: RenderPolicy::for_app_mode(active_app_mode), next_frame_id: FrameId::ZERO, handoff: RenderHandoff::default(), + telemetry: RenderTelemetry::default(), transition_state: RenderTransitionState::Idle, } } @@ -182,7 +258,52 @@ impl RenderManager { } pub fn discard_pending_submission(&mut self) -> Option { - self.handoff.discard_pending() + let discarded = self.handoff.discard_pending(); + if let Some(submission) = discarded.as_ref() { + self.telemetry.record_shutdown_discard(submission.frame_id); + } + discarded + } + + pub fn render_telemetry(&self) -> RenderTelemetrySnapshot { + let mut snapshot = self.telemetry.snapshot; + snapshot.active_render_epoch = self.active_ownership.epoch; + snapshot + } + + pub fn sync_telemetry(&self, telemetry: &AtomicTelemetry) { + let snapshot = self.render_telemetry(); + telemetry.produced_submissions.store(snapshot.produced_submissions, Ordering::Relaxed); + telemetry + .replaced_before_consume + .store(snapshot.replaced_before_consume, Ordering::Relaxed); + telemetry.consumed_submissions.store(snapshot.consumed_submissions, Ordering::Relaxed); + telemetry.presented_frames.store(snapshot.presented_frames, Ordering::Relaxed); + telemetry.repeated_presents.store(snapshot.repeated_presents, Ordering::Relaxed); + telemetry.render_errors.store(snapshot.render_errors, Ordering::Relaxed); + telemetry.present_errors.store(snapshot.present_errors, Ordering::Relaxed); + telemetry.stale_epoch_discards.store(snapshot.stale_epoch_discards, Ordering::Relaxed); + telemetry.shutdown_discards.store(snapshot.shutdown_discards, Ordering::Relaxed); + telemetry.last_produced_frame_id.store(snapshot.last_produced_frame_id, Ordering::Relaxed); + telemetry.last_consumed_frame_id.store(snapshot.last_consumed_frame_id, Ordering::Relaxed); + telemetry + .last_presented_frame_id + .store(snapshot.last_presented_frame_id, Ordering::Relaxed); + telemetry.last_dropped_frame_id.store(snapshot.last_dropped_frame_id, Ordering::Relaxed); + telemetry.last_error_frame_id.store(snapshot.last_error_frame_id, Ordering::Relaxed); + telemetry.active_render_epoch.store(snapshot.active_render_epoch, Ordering::Relaxed); + } + + pub fn record_repeated_present(&mut self, frame_id: FrameId) { + self.telemetry.record_repeated_present(frame_id); + } + + pub fn record_render_error(&mut self, frame_id: FrameId) { + self.telemetry.record_render_error(frame_id); + } + + pub fn record_present_error(&mut self, frame_id: FrameId) { + self.telemetry.record_present_error(frame_id); } pub fn set_active_app_mode(&mut self, app_mode: AppMode) { @@ -226,6 +347,10 @@ impl RenderManager { }; self.next_frame_id = self.next_frame_id.next(); + if let Some(replaced) = self.handoff.pending_submission() { + self.telemetry.record_replaced_before_consume(replaced.frame_id); + } + self.telemetry.record_produced(frame_id); self.handoff.publish(submission); Ok(self.handoff.pending_submission().expect("submission was just stored")) } @@ -244,10 +369,13 @@ impl RenderManager { return false; }; if submission.ownership != self.active_ownership { + self.telemetry.record_stale_epoch_discard(submission.frame_id); self.handoff.record_stale_epoch_discard(submission); return false; } + self.telemetry.record_consumed(submission.frame_id); surface.consume_submission(&submission); + self.telemetry.record_presented(submission.frame_id); self.handoff.record_consumed(submission); true } @@ -262,6 +390,8 @@ impl Default for RenderManager { #[cfg(test)] mod tests { use super::*; + use std::sync::Arc; + use std::sync::atomic::AtomicU32; #[derive(Default)] struct RecordingSurface { @@ -460,4 +590,64 @@ mod tests { RenderOwnership::new(0, AppMode::Game, 0) ); } + + #[test] + fn render_telemetry_counts_produced_replaced_consumed_and_presented() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + manager.close_compat_frame(); + manager.close_compat_frame(); + assert!(manager.publish_latest(&mut surface)); + + let telemetry = manager.render_telemetry(); + assert_eq!(telemetry.produced_submissions, 2); + assert_eq!(telemetry.replaced_before_consume, 1); + assert_eq!(telemetry.consumed_submissions, 1); + assert_eq!(telemetry.presented_frames, 1); + assert_eq!(telemetry.last_produced_frame_id, 1); + assert_eq!(telemetry.last_dropped_frame_id, 0); + assert_eq!(telemetry.last_consumed_frame_id, 1); + assert_eq!(telemetry.last_presented_frame_id, 1); + } + + #[test] + fn render_telemetry_counts_stale_repeat_and_error_hooks() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + + manager.close_compat_frame(); + manager.transition_render_owner(AppMode::Shell, 1); + assert!(!manager.publish_latest(&mut surface)); + manager.record_repeated_present(FrameId::new(9)); + manager.record_render_error(FrameId::new(10)); + manager.record_present_error(FrameId::new(11)); + + let telemetry = manager.render_telemetry(); + assert_eq!(telemetry.stale_epoch_discards, 1); + assert_eq!(telemetry.repeated_presents, 1); + assert_eq!(telemetry.render_errors, 1); + assert_eq!(telemetry.present_errors, 1); + assert_eq!(telemetry.last_presented_frame_id, 9); + assert_eq!(telemetry.last_error_frame_id, 11); + } + + #[test] + fn render_telemetry_syncs_to_atomic_telemetry() { + let mut manager = RenderManager::new(AppMode::Game); + let mut surface = RecordingSurface::default(); + let atomic = AtomicTelemetry::new(Arc::new(AtomicU32::new(0))); + + manager.transition_render_owner(AppMode::Game, 5); + manager.close_compat_frame(); + assert!(manager.publish_latest(&mut surface)); + manager.sync_telemetry(&atomic); + + let snapshot = atomic.snapshot(); + assert_eq!(snapshot.produced_submissions, 1); + assert_eq!(snapshot.consumed_submissions, 1); + assert_eq!(snapshot.presented_frames, 1); + assert_eq!(snapshot.last_presented_frame_id, 0); + assert_eq!(snapshot.active_render_epoch, 1); + } } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs index 40d3782c..f4aae550 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tick.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tick.rs @@ -304,6 +304,7 @@ impl VirtualMachineRuntime { self.atomic_telemetry .host_cpu_time_us .store(start.elapsed().as_micros() as u64, Ordering::Relaxed); + self.render_manager.sync_telemetry(&self.atomic_telemetry); let current_frame_logs = self.atomic_telemetry.current_logs_count.load(Ordering::Relaxed); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 474f6f9e..88b7b37a 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,6 +1,6 @@ {"type":"meta","next_id":{"DSC":42,"AGD":42,"DEC":32,"PLN":98,"LSN":48,"CLSN":1}} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} -{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0040","status":"in_progress","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-05","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[{"id":"AGD-0040","file":"AGD-0040-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-04","updated_at":"2026-06-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-vm-and-render-parallel-execution-boundary.md","status":"accepted","created_at":"2026-06-05","updated_at":"2026-06-05","ref_agenda":"AGD-0040"}],"plans":[{"id":"PLN-0087","file":"PLN-0087-fix-game2d-packet-overlay-composition.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0088","file":"PLN-0088-define-read-only-render-resource-boundary.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0089","file":"PLN-0089-introduce-render-handoff-abstraction.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0090","file":"PLN-0090-add-render-frame-pacing-contract.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0091","file":"PLN-0091-implement-appmode-render-policy-matrix.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0092","file":"PLN-0092-add-render-epoch-ownership-transitions.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0093","file":"PLN-0093-add-render-telemetry-counters-and-events.md","status":"done","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0094","file":"PLN-0094-specify-async-render-boundary.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0095","file":"PLN-0095-prototype-local-render-worker-capability.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0096","file":"PLN-0096-define-render-shutdown-and-failure-handling.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0097","file":"PLN-0097-add-async-render-integration-test-matrix.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0041","status":"open","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-06-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[{"id":"AGD-0041","file":"AGD-0041-foreground-stack-game-pause-shell-vm-backed.md","status":"open","created_at":"2026-06-05","updated_at":"2026-06-05"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0038-render-frame-packet-boundary/LSN-0047-typed-render-submissions-preserve-domain-boundaries.md","status":"done","created_at":"2026-06-04","updated_at":"2026-06-04"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} diff --git a/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md b/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md index 54dba281..2fd67371 100644 --- a/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md +++ b/discussion/workflow/plans/PLN-0093-add-render-telemetry-counters-and-events.md @@ -2,7 +2,7 @@ id: PLN-0093 ticket: vm-render-parallel-execution-boundary title: Add Render Telemetry Counters and Events -status: open +status: done created: 2026-06-05 ref_decisions: [DEC-0031] tags: [runtime, renderer, telemetry, diagnostics]