//! Leak Tracker - Exit-time diagnostics for strong references still held //! //! Phase 285: Extended to report all global roots (modules, host_handles, plugin boxes). //! Phase 29y.1: Added root category summary (SSOT: 30-OBSERVABILITY-SSOT.md) //! //! ## Environment Variable //! //! - `NYASH_LEAK_LOG=1` - Summary counts only (with category breakdown) //! - `NYASH_LEAK_LOG=2` - Verbose (include names/entries, truncated to first 10) //! //! ## Output Format //! //! ```text //! [lifecycle/leak] Roots still held at exit: //! [lifecycle/leak] modules: 3 //! [lifecycle/leak] host_handles: 5 //! [lifecycle/leak] plugin_boxes: 2 //! [lifecycle/leak] Root categories: //! [lifecycle/leak] handles: 8 //! [lifecycle/leak] (Phase 1 limitation: locals/temps/heap_fields/singletons=0) //! ``` use crate::runtime::get_global_ring0; use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Mutex; /// Leak log level: 0 = off, 1 = summary, 2 = verbose static LEVEL: Lazy = Lazy::new(crate::config::env::leak_log_level); /// Backward compatibility: enabled if level >= 1 static ENABLED: Lazy = Lazy::new(|| *LEVEL >= 1); static LEAKS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); /// Phase 29y.1: Root category summary for observability /// /// Categories are defined in docs/development/current/main/phases/phase-29y/30-OBSERVABILITY-SSOT.md #[derive(Debug, Default, Clone)] pub struct RootSummary { /// host-visible registry/handle table pub handles: usize, /// runtime singleton/globals (Phase 1: always 0) pub singletons: usize, /// object strong-owned fields (Phase 1: always 0 - VM teardown) pub heap_fields: usize, /// temporary values/VM registers (Phase 1: always 0 - VM teardown) pub temps: usize, /// local variable bindings (Phase 1: always 0 - VM teardown) pub locals: usize, } /// Phase 29y.1: Collect root summary from available sources /// /// Phase 1 limitation: Only handles category is observable at exit time. /// locals/temps/heap_fields/singletons require VM state which is unavailable after teardown. fn collect_root_summary() -> RootSummary { let host_handles = crate::runtime::host_handles::snapshot(); let modules = crate::runtime::modules_registry::snapshot_names_and_strings(); RootSummary { handles: host_handles.len() + modules.len(), singletons: 0, // Phase 1 limitation: VM state not available heap_fields: 0, // Phase 1 limitation: VM state not available temps: 0, // Phase 1 limitation: VM state not available locals: 0, // Phase 1 limitation: VM state not available } } pub fn init() { let _ = &*REPORTER; } pub fn register_plugin(box_type: &str, instance_id: u32) { if !*ENABLED { return; } LEAKS .lock() .unwrap() .insert((box_type.to_string(), instance_id), "plugin"); } pub fn finalize_plugin(box_type: &str, instance_id: u32) { if !*ENABLED { return; } LEAKS .lock() .unwrap() .remove(&(box_type.to_string(), instance_id)); } struct Reporter; impl Drop for Reporter { fn drop(&mut self) { if !*ENABLED { return; } emit_leak_report(); } } static REPORTER: Lazy = Lazy::new(|| Reporter); /// Emit exit-time leak report (Phase 285) /// /// Called automatically on program exit via Reporter::drop. /// Can also be called manually for testing. pub fn emit_leak_report() { let level = *LEVEL; if level == 0 { return; } let ring0 = get_global_ring0(); // Collect root counts let modules = crate::runtime::modules_registry::snapshot_names_and_strings(); let host_handles = crate::runtime::host_handles::snapshot(); let plugin_boxes = LEAKS.lock().map(|m| m.len()).unwrap_or(0); let modules_count = modules.len(); let host_handles_count = host_handles.len(); // Only print if there's something to report if modules_count == 0 && host_handles_count == 0 && plugin_boxes == 0 { return; } // Summary header ring0 .log .warn("[lifecycle/leak] Roots still held at exit:"); // Summary counts if modules_count > 0 { ring0 .log .warn(&format!("[lifecycle/leak] modules: {}", modules_count)); } if host_handles_count > 0 { ring0.log.warn(&format!( "[lifecycle/leak] host_handles: {}", host_handles_count )); } if plugin_boxes > 0 { ring0 .log .warn(&format!("[lifecycle/leak] plugin_boxes: {}", plugin_boxes)); } // Phase 29y.1: Root category summary let summary = collect_root_summary(); ring0.log.warn("[lifecycle/leak] Root categories:"); ring0.log.warn(&format!("[lifecycle/leak] handles: {}", summary.handles)); ring0.log.warn("[lifecycle/leak] (Phase 1 limitation: locals/temps/heap_fields/singletons=0)"); // Verbose details (level 2) if level >= 2 { const MAX_ENTRIES: usize = 10; // Module names if !modules.is_empty() { ring0.log.warn("[lifecycle/leak] module names:"); for (i, (name, _value)) in modules.iter().take(MAX_ENTRIES).enumerate() { ring0 .log .warn(&format!("[lifecycle/leak] [{}] {}", i, name)); } if modules.len() > MAX_ENTRIES { ring0.log.warn(&format!( "[lifecycle/leak] ... and {} more", modules.len() - MAX_ENTRIES )); } } // Plugin box details if plugin_boxes > 0 { ring0.log.warn("[lifecycle/leak] plugin box details:"); if let Ok(m) = LEAKS.lock() { for (i, ((ty, id), _)) in m.iter().take(MAX_ENTRIES).enumerate() { ring0.log.warn(&format!( "[lifecycle/leak] [{}] {}(id={}) not finalized", i, ty, id )); } if m.len() > MAX_ENTRIES { ring0.log.warn(&format!( "[lifecycle/leak] ... and {} more", m.len() - MAX_ENTRIES )); } } } } }