use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Default)]
pub struct EmbedderStallTracker {
last_ok_unix_secs: AtomicU64,
recent_timeout_count: AtomicU32,
total_ok_count: AtomicU64,
total_timeout_count: AtomicU64,
}
impl EmbedderStallTracker {
pub fn new() -> Self {
Self::default()
}
pub fn record_success(&self) {
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
self.last_ok_unix_secs.store(now_secs, Ordering::Relaxed);
self.recent_timeout_count.store(0, Ordering::Relaxed);
self.total_ok_count.fetch_add(1, Ordering::Relaxed);
}
pub fn record_timeout(&self) {
self.recent_timeout_count.fetch_add(1, Ordering::Relaxed);
self.total_timeout_count.fetch_add(1, Ordering::Relaxed);
}
pub fn last_ok_secs_ago(&self) -> Option<u64> {
let stored = self.last_ok_unix_secs.load(Ordering::Relaxed);
if stored == 0 {
return None;
}
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(stored);
Some(now_secs.saturating_sub(stored))
}
pub fn recent_timeout_count(&self) -> u32 {
self.recent_timeout_count.load(Ordering::Relaxed)
}
}
impl Clone for EmbedderStallTracker {
fn clone(&self) -> Self {
Self {
last_ok_unix_secs: AtomicU64::new(self.last_ok_unix_secs.load(Ordering::Relaxed)),
recent_timeout_count: AtomicU32::new(self.recent_timeout_count.load(Ordering::Relaxed)),
total_ok_count: AtomicU64::new(self.total_ok_count.load(Ordering::Relaxed)),
total_timeout_count: AtomicU64::new(self.total_timeout_count.load(Ordering::Relaxed)),
}
}
}
pub type StallTrackerArc = Arc<EmbedderStallTracker>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stall_tracker_records_success_and_timeout() {
let t = EmbedderStallTracker::new();
assert_eq!(t.recent_timeout_count(), 0);
assert!(t.last_ok_secs_ago().is_none(), "never succeeded yet");
t.record_timeout();
t.record_timeout();
assert_eq!(t.recent_timeout_count(), 2);
assert!(t.last_ok_secs_ago().is_none(), "still no success");
t.record_success();
assert_eq!(t.recent_timeout_count(), 0, "reset on success");
let ago = t.last_ok_secs_ago().expect("success should set timestamp");
assert!(ago < 5, "last_ok_secs_ago should be near-zero; got {ago}");
}
#[test]
fn stall_tracker_reset_on_success() {
let t = EmbedderStallTracker::new();
for _ in 0..5 {
t.record_timeout();
}
assert_eq!(t.recent_timeout_count(), 5);
t.record_success();
assert_eq!(t.recent_timeout_count(), 0, "must reset on first success");
}
#[test]
fn stall_tracker_timeout_does_not_set_ok_timestamp() {
let t = EmbedderStallTracker::new();
t.record_timeout();
t.record_timeout();
assert!(
t.last_ok_secs_ago().is_none(),
"timeout must not set the ok timestamp"
);
}
}