use std::fmt;
#[derive(Debug, Clone, Copy)]
pub struct MonitorConfig {
pub alpha: f64,
pub expected_lifetime_ns: u64,
pub min_observations: u64,
}
impl Default for MonitorConfig {
fn default() -> Self {
Self {
alpha: 0.01,
expected_lifetime_ns: 10_000_000, min_observations: 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlertState {
Clear,
Watching,
Alert,
}
impl fmt::Display for AlertState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Clear => f.write_str("clear"),
Self::Watching => f.write_str("watching"),
Self::Alert => f.write_str("ALERT"),
}
}
}
#[derive(Debug)]
pub struct LeakMonitor {
config: MonitorConfig,
e_value: f64,
threshold: f64,
observations: u64,
log_e_value: f64,
peak_e_value: f64,
alert_count: u64,
}
impl LeakMonitor {
#[must_use]
pub fn new(config: MonitorConfig) -> Self {
assert!(
config.alpha > 0.0 && config.alpha < 1.0,
"alpha must be in (0, 1), got {}",
config.alpha
);
assert!(
config.expected_lifetime_ns > 0,
"expected_lifetime_ns must be > 0"
);
let threshold = 1.0 / config.alpha;
Self {
config,
e_value: 1.0,
threshold,
observations: 0,
log_e_value: 0.0,
peak_e_value: 1.0,
alert_count: 0,
}
}
pub fn observe(&mut self, age_ns: u64) {
let was_alert = self.is_alert();
self.observations += 1;
#[allow(clippy::cast_precision_loss)]
let ratio = if self.config.expected_lifetime_ns == 0 {
0.0
} else {
age_ns as f64 / self.config.expected_lifetime_ns as f64
};
let normalizer = 1.0 + (-1.0_f64).exp(); let lr = ratio.max(1.0) / normalizer;
self.log_e_value += lr.ln();
self.e_value = self.log_e_value.exp();
if self.e_value > self.peak_e_value {
self.peak_e_value = self.e_value;
}
if !was_alert && self.is_alert() {
self.alert_count += 1;
}
}
#[must_use]
pub fn alert_state(&self) -> AlertState {
if self.observations < self.config.min_observations {
return AlertState::Clear;
}
if self.e_value >= self.threshold {
AlertState::Alert
} else if self.e_value > 1.0 {
AlertState::Watching
} else {
AlertState::Clear
}
}
#[must_use]
pub fn is_alert(&self) -> bool {
self.alert_state() == AlertState::Alert
}
#[must_use]
pub fn e_value(&self) -> f64 {
self.e_value
}
#[must_use]
pub fn threshold(&self) -> f64 {
self.threshold
}
#[must_use]
pub fn observations(&self) -> u64 {
self.observations
}
#[must_use]
pub fn peak_e_value(&self) -> f64 {
self.peak_e_value
}
#[must_use]
pub fn alert_count(&self) -> u64 {
self.alert_count
}
#[must_use]
pub fn config(&self) -> &MonitorConfig {
&self.config
}
pub fn reset(&mut self) {
self.e_value = 1.0;
self.log_e_value = 0.0;
self.peak_e_value = 1.0;
self.observations = 0;
self.alert_count = 0;
}
#[must_use]
pub fn snapshot(&self) -> MonitorSnapshot {
MonitorSnapshot {
e_value: self.e_value,
threshold: self.threshold,
observations: self.observations,
alert_state: self.alert_state(),
peak_e_value: self.peak_e_value,
alert_count: self.alert_count,
}
}
}
#[derive(Debug, Clone)]
pub struct MonitorSnapshot {
pub e_value: f64,
pub threshold: f64,
pub observations: u64,
pub alert_state: AlertState,
pub peak_e_value: f64,
pub alert_count: u64,
}
impl fmt::Display for MonitorSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"LeakMonitor[{}]: e={:.4} threshold={:.1} obs={} peak={:.4} alerts={}",
self.alert_state,
self.e_value,
self.threshold,
self.observations,
self.peak_e_value,
self.alert_count,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn init_test(name: &str) {
crate::test_utils::init_test_logging();
crate::test_phase!(name);
}
fn default_config() -> MonitorConfig {
MonitorConfig {
alpha: 0.01,
expected_lifetime_ns: 1_000_000, min_observations: 3,
}
}
#[test]
fn new_monitor_starts_clear() {
init_test("new_monitor_starts_clear");
let monitor = LeakMonitor::new(default_config());
crate::assert_with_log!(
monitor.alert_state() == AlertState::Clear,
"initial state",
AlertState::Clear,
monitor.alert_state()
);
let e = monitor.e_value();
crate::assert_with_log!((e - 1.0).abs() < f64::EPSILON, "initial e-value", 1.0, e);
crate::assert_with_log!(
monitor.observations() == 0,
"observations",
0,
monitor.observations()
);
crate::test_complete!("new_monitor_starts_clear");
}
#[test]
#[should_panic(expected = "alpha must be in (0, 1)")]
fn alpha_zero_panics() {
let config = MonitorConfig {
alpha: 0.0,
..default_config()
};
let _m = LeakMonitor::new(config);
}
#[test]
#[should_panic(expected = "alpha must be in (0, 1)")]
fn alpha_one_panics() {
let config = MonitorConfig {
alpha: 1.0,
..default_config()
};
let _m = LeakMonitor::new(config);
}
#[test]
#[should_panic(expected = "expected_lifetime_ns must be > 0")]
fn zero_lifetime_panics() {
let config = MonitorConfig {
expected_lifetime_ns: 0,
..default_config()
};
let _m = LeakMonitor::new(config);
}
#[test]
fn normal_observations_stay_clear() {
init_test("normal_observations_stay_clear");
let mut monitor = LeakMonitor::new(default_config());
for _ in 0..100 {
monitor.observe(500_000); }
let state = monitor.alert_state();
crate::assert_with_log!(
state == AlertState::Clear,
"state after normal",
AlertState::Clear,
state
);
crate::assert_with_log!(!monitor.is_alert(), "not alert", false, monitor.is_alert());
crate::test_complete!("normal_observations_stay_clear");
}
#[test]
fn leaked_obligations_trigger_alert() {
init_test("leaked_obligations_trigger_alert");
let mut monitor = LeakMonitor::new(MonitorConfig {
alpha: 0.01,
expected_lifetime_ns: 1_000_000, min_observations: 3,
});
for _ in 0..10 {
monitor.observe(100_000_000); }
let state = monitor.alert_state();
crate::assert_with_log!(
state == AlertState::Alert,
"alert",
AlertState::Alert,
state
);
crate::assert_with_log!(monitor.is_alert(), "is_alert", true, monitor.is_alert());
let alert_count = monitor.alert_count();
crate::assert_with_log!(alert_count > 0, "alert count > 0", true, alert_count > 0);
crate::test_complete!("leaked_obligations_trigger_alert");
}
#[test]
fn alert_count_tracks_threshold_crossings_not_samples() {
init_test("alert_count_tracks_threshold_crossings_not_samples");
let mut monitor = LeakMonitor::new(MonitorConfig {
alpha: 0.01,
expected_lifetime_ns: 1_000_000,
min_observations: 3,
});
for _ in 0..10 {
monitor.observe(100_000_000);
}
crate::assert_with_log!(
monitor.alert_count() == 1,
"first alert episode counted once",
1,
monitor.alert_count()
);
monitor.reset();
for _ in 0..5 {
monitor.observe(100_000_000);
}
crate::assert_with_log!(
monitor.alert_count() == 1,
"post-reset alert episode counted once",
1,
monitor.alert_count()
);
crate::test_complete!("alert_count_tracks_threshold_crossings_not_samples");
}
#[test]
fn alert_gated_by_min_observations() {
init_test("alert_gated_by_min_observations");
let mut monitor = LeakMonitor::new(MonitorConfig {
alpha: 0.01,
expected_lifetime_ns: 1_000,
min_observations: 5,
});
monitor.observe(1_000_000_000);
monitor.observe(1_000_000_000);
let state = monitor.alert_state();
crate::assert_with_log!(
state == AlertState::Clear,
"below min obs",
AlertState::Clear,
state
);
for _ in 0..5 {
monitor.observe(1_000_000_000);
}
let state = monitor.alert_state();
crate::assert_with_log!(
state == AlertState::Alert,
"above min obs",
AlertState::Alert,
state
);
crate::test_complete!("alert_gated_by_min_observations");
}
#[test]
fn reset_clears_state() {
init_test("reset_clears_state");
let mut monitor = LeakMonitor::new(default_config());
for _ in 0..10 {
monitor.observe(100_000_000);
}
crate::assert_with_log!(
monitor.is_alert(),
"alert before reset",
true,
monitor.is_alert()
);
monitor.reset();
crate::assert_with_log!(
!monitor.is_alert(),
"clear after reset",
false,
monitor.is_alert()
);
crate::assert_with_log!(
monitor.observations() == 0,
"obs after reset",
0,
monitor.observations()
);
let e = monitor.e_value();
crate::assert_with_log!(
(e - 1.0).abs() < f64::EPSILON,
"e-value after reset",
1.0,
e
);
crate::test_complete!("reset_clears_state");
}
#[test]
fn snapshot_captures_state() {
init_test("snapshot_captures_state");
let mut monitor = LeakMonitor::new(default_config());
monitor.observe(500_000);
let snap = monitor.snapshot();
crate::assert_with_log!(snap.observations == 1, "observations", 1, snap.observations);
let has_threshold = snap.threshold > 0.0;
crate::assert_with_log!(has_threshold, "threshold", true, has_threshold);
let display = format!("{snap}");
let has_leak = display.contains("LeakMonitor");
crate::assert_with_log!(has_leak, "display", true, has_leak);
crate::test_complete!("snapshot_captures_state");
}
#[test]
fn supermartingale_property_under_null() {
init_test("supermartingale_property_under_null");
let mut monitor = LeakMonitor::new(MonitorConfig {
alpha: 0.01,
expected_lifetime_ns: 1_000_000,
min_observations: 3,
});
for i in 0u64..1000 {
let age = ((i % 10) + 1) * 100_000; monitor.observe(age);
}
let e = monitor.e_value();
let bounded = e <= 2.0; crate::assert_with_log!(bounded, "e-value bounded", true, bounded);
crate::assert_with_log!(
!monitor.is_alert(),
"no alert under H0",
false,
monitor.is_alert()
);
crate::test_complete!("supermartingale_property_under_null");
}
#[test]
fn deterministic_across_runs() {
init_test("deterministic_across_runs");
let config = default_config();
let ages = [500_000u64, 1_000_000, 2_000_000, 100_000, 5_000_000];
let mut m1 = LeakMonitor::new(config);
let mut m2 = LeakMonitor::new(config);
for &age in &ages {
m1.observe(age);
m2.observe(age);
}
let e1 = m1.e_value();
let e2 = m2.e_value();
crate::assert_with_log!((e1 - e2).abs() < f64::EPSILON, "deterministic", e1, e2);
crate::test_complete!("deterministic_across_runs");
}
#[test]
fn alert_state_display() {
init_test("alert_state_display");
let clear = format!("{}", AlertState::Clear);
crate::assert_with_log!(clear == "clear", "clear display", "clear", clear);
let watching = format!("{}", AlertState::Watching);
crate::assert_with_log!(
watching == "watching",
"watching display",
"watching",
watching
);
let alert = format!("{}", AlertState::Alert);
crate::assert_with_log!(alert == "ALERT", "alert display", "ALERT", alert);
crate::test_complete!("alert_state_display");
}
#[test]
fn monitor_config_debug_clone_copy() {
let c = MonitorConfig::default();
let c2 = c; let c3 = c;
assert!((c2.alpha - 0.01).abs() < 1e-10);
assert_eq!(c3.min_observations, 3);
let dbg = format!("{c:?}");
assert!(dbg.contains("MonitorConfig"));
}
#[test]
fn alert_state_debug_clone_copy_eq() {
let s = AlertState::Clear;
let s2 = s; let s3 = s;
assert_eq!(s, s2);
assert_eq!(s2, s3);
assert_ne!(s, AlertState::Alert);
let dbg = format!("{s:?}");
assert!(dbg.contains("Clear"));
}
#[test]
fn monitor_snapshot_debug_clone() {
let ms = MonitorSnapshot {
e_value: 1.5,
threshold: 100.0,
observations: 10,
alert_state: AlertState::Watching,
peak_e_value: 2.0,
alert_count: 0,
};
let ms2 = ms;
assert_eq!(ms2.observations, 10);
assert_eq!(ms2.alert_state, AlertState::Watching);
let dbg = format!("{ms2:?}");
assert!(dbg.contains("MonitorSnapshot"));
}
}