use std::cell::RefCell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Instant, SystemTime};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClockLeak {
pub capability_id: String,
pub count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClockLeakScope(u64);
impl ClockLeakScope {
fn next() -> Self {
static NEXT_SCOPE_ID: AtomicU64 = AtomicU64::new(1);
Self(NEXT_SCOPE_ID.fetch_add(1, Ordering::Relaxed))
}
}
thread_local! {
static ACTIVE_SCOPE_STACK: RefCell<Vec<ClockLeakScope>> = const { RefCell::new(Vec::new()) };
}
pub struct ClockLeakScopeGuard {
scope: ClockLeakScope,
owner: bool,
}
impl ClockLeakScopeGuard {
pub fn scope(&self) -> ClockLeakScope {
self.scope
}
}
impl Drop for ClockLeakScopeGuard {
fn drop(&mut self) {
let _ = ACTIVE_SCOPE_STACK.try_with(|stack| {
let mut stack = stack.borrow_mut();
if stack.last().copied() == Some(self.scope) {
stack.pop();
} else if let Some(index) = stack.iter().rposition(|scope| *scope == self.scope) {
stack.remove(index);
}
});
if self.owner {
REGISTRY
.lock()
.expect("clock leak registry mutex poisoned")
.remove_scope(self.scope);
}
}
}
pub fn install_scope() -> ClockLeakScopeGuard {
let scope = ClockLeakScope::next();
ACTIVE_SCOPE_STACK.with(|stack| stack.borrow_mut().push(scope));
ClockLeakScopeGuard { scope, owner: true }
}
pub fn enter_scope(scope: ClockLeakScope) -> ClockLeakScopeGuard {
ACTIVE_SCOPE_STACK.with(|stack| stack.borrow_mut().push(scope));
ClockLeakScopeGuard {
scope,
owner: false,
}
}
fn active_scope() -> Option<ClockLeakScope> {
ACTIVE_SCOPE_STACK.with(|stack| stack.borrow().last().copied())
}
struct ScopedLeaks {
scope: ClockLeakScope,
entries: Vec<ClockLeak>,
}
struct LeakRegistry {
scopes: Vec<ScopedLeaks>,
}
impl LeakRegistry {
const fn new() -> Self {
Self { scopes: Vec::new() }
}
fn entries_mut(&mut self, scope: ClockLeakScope) -> &mut Vec<ClockLeak> {
if let Some(index) = self.scopes.iter().position(|entry| entry.scope == scope) {
return &mut self.scopes[index].entries;
}
self.scopes.push(ScopedLeaks {
scope,
entries: Vec::new(),
});
&mut self.scopes.last_mut().expect("scope just pushed").entries
}
fn record(&mut self, scope: ClockLeakScope, capability_id: &str) {
let entries = self.entries_mut(scope);
if let Some(entry) = entries
.iter_mut()
.find(|entry| entry.capability_id == capability_id)
{
entry.count = entry.count.saturating_add(1);
return;
}
entries.push(ClockLeak {
capability_id: capability_id.to_string(),
count: 1,
});
}
fn snapshot(&self, scope: ClockLeakScope) -> Vec<ClockLeak> {
self.scopes
.iter()
.find(|entry| entry.scope == scope)
.map(|entry| entry.entries.clone())
.unwrap_or_default()
}
fn drain(&mut self, scope: ClockLeakScope) -> Vec<ClockLeak> {
let Some(index) = self.scopes.iter().position(|entry| entry.scope == scope) else {
return Vec::new();
};
std::mem::take(&mut self.scopes[index].entries)
}
fn reset(&mut self, scope: ClockLeakScope) {
if let Some(entry) = self.scopes.iter_mut().find(|entry| entry.scope == scope) {
entry.entries.clear();
}
}
fn remove_scope(&mut self, scope: ClockLeakScope) {
self.scopes.retain(|entry| entry.scope != scope);
}
}
static REGISTRY: Mutex<LeakRegistry> = Mutex::new(LeakRegistry::new());
pub fn wall_now(capability_id: &str) -> SystemTime {
record_in_active_scope(capability_id);
SystemTime::now()
}
pub fn instant_now(capability_id: &str) -> Instant {
record_in_active_scope(capability_id);
Instant::now()
}
pub fn snapshot() -> Vec<ClockLeak> {
let Some(scope) = active_scope() else {
return Vec::new();
};
REGISTRY
.lock()
.expect("clock leak registry mutex poisoned")
.snapshot(scope)
}
pub fn drain() -> Vec<ClockLeak> {
let Some(scope) = active_scope() else {
return Vec::new();
};
REGISTRY
.lock()
.expect("clock leak registry mutex poisoned")
.drain(scope)
}
pub fn reset() {
let Some(scope) = active_scope() else {
return;
};
REGISTRY
.lock()
.expect("clock leak registry mutex poisoned")
.reset(scope);
}
fn record_in_active_scope(capability_id: &str) {
let Some(scope) = active_scope() else {
return;
};
REGISTRY
.lock()
.expect("clock leak registry mutex poisoned")
.record(scope, capability_id);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock_mock::{install_override, MockClock};
use std::sync::{Arc, Barrier};
use std::thread;
fn isolated_registry<F: FnOnce()>(f: F) {
let _scope = install_scope();
reset();
f();
reset();
}
#[test]
fn no_mock_no_leak() {
let _ = wall_now("test/cap");
let _ = instant_now("test/cap");
assert!(snapshot().is_empty());
}
#[test]
fn mock_present_records_leak() {
isolated_registry(|| {
let _guard = install_override(MockClock::at_wall_ms(1_000_000));
let _ = wall_now("test/cap");
let leaks = snapshot();
assert_eq!(leaks.len(), 1);
assert_eq!(leaks[0].capability_id, "test/cap");
assert_eq!(leaks[0].count, 1);
});
}
#[test]
fn duplicate_capability_increments_count() {
isolated_registry(|| {
let _guard = install_override(MockClock::at_wall_ms(1_000_000));
wall_now("test/cap");
wall_now("test/cap");
wall_now("test/cap");
let leaks = snapshot();
assert_eq!(leaks.len(), 1);
assert_eq!(leaks[0].count, 3);
});
}
#[test]
fn distinct_capabilities_kept_separate_in_insertion_order() {
isolated_registry(|| {
let _guard = install_override(MockClock::at_wall_ms(1_000_000));
wall_now("test/a");
wall_now("test/b");
wall_now("test/a");
let leaks = snapshot();
assert_eq!(leaks.len(), 2);
assert_eq!(leaks[0].capability_id, "test/a");
assert_eq!(leaks[0].count, 2);
assert_eq!(leaks[1].capability_id, "test/b");
assert_eq!(leaks[1].count, 1);
});
}
#[test]
fn drain_empties_registry() {
isolated_registry(|| {
let _guard = install_override(MockClock::at_wall_ms(1_000_000));
wall_now("test/cap");
let drained = drain();
assert_eq!(drained.len(), 1);
assert!(snapshot().is_empty());
});
}
#[test]
fn sibling_scope_reset_cannot_erase_active_scope() {
let alpha = install_scope();
let alpha_scope = alpha.scope();
let beta = install_scope();
let beta_scope = beta.scope();
let barrier = Arc::new(Barrier::new(2));
let worker_barrier = Arc::clone(&barrier);
let worker = thread::spawn(move || {
let _beta = enter_scope(beta_scope);
worker_barrier.wait();
crate::reset_thread_local_state();
wall_now("test/beta");
assert_eq!(snapshot().len(), 1);
});
let _alpha_thread = enter_scope(alpha_scope);
wall_now("test/alpha");
barrier.wait();
worker.join().expect("worker");
let leaks = snapshot();
assert_eq!(leaks.len(), 1);
assert_eq!(leaks[0].capability_id, "test/alpha");
assert_eq!(leaks[0].count, 1);
drop((alpha, beta));
}
}