Files
hakorune/src/runtime/leak_tracker.rs

201 lines
6.3 KiB
Rust

//! 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<u8> = Lazy::new(crate::config::env::leak_log_level);
/// Backward compatibility: enabled if level >= 1
static ENABLED: Lazy<bool> = Lazy::new(|| *LEVEL >= 1);
static LEAKS: Lazy<Mutex<HashMap<(String, u32), &'static str>>> =
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<Reporter> = 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
));
}
}
}
}
}