201 lines
6.3 KiB
Rust
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
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|