use std::{
sync::Arc,
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use omp_core::Str;
use parking_lot::{Condvar, Mutex};
const STALL_THRESHOLD: Duration = Duration::from_millis(250);
const SYSTEM_SLEEP_THRESHOLD: Duration = Duration::from_secs(60);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StallReport {
pub elapsed: Duration,
pub phase: Str,
}
#[derive(Debug)]
pub struct LoopWatchdogCore {
last_tick: Duration,
phase: Str,
reported: bool,
}
impl LoopWatchdogCore {
#[must_use]
pub fn new(now: Duration) -> Self {
Self { last_tick: now, phase: "unknown".into(), reported: false }
}
pub const fn tick(&mut self, now: Duration) {
self.last_tick = now;
self.reported = false;
}
pub fn set_phase(&mut self, phase: impl Into<Str>) {
self.phase = phase.into();
}
#[must_use]
pub fn check(&mut self, now: Duration) -> Option<StallReport> {
let elapsed = now.saturating_sub(self.last_tick);
if elapsed > SYSTEM_SLEEP_THRESHOLD {
self.reported = false;
return None;
}
if elapsed <= STALL_THRESHOLD {
self.reported = false;
return None;
}
if self.reported {
return None;
}
self.reported = true;
Some(StallReport { elapsed, phase: self.phase.clone() })
}
}
struct Shared {
core: LoopWatchdogCore,
stopping: bool,
}
pub struct LoopWatchdog {
origin: Instant,
shared: Arc<(Mutex<Shared>, Condvar)>,
worker: Option<JoinHandle<()>>,
}
impl LoopWatchdog {
#[must_use]
pub fn new(report: impl Fn(Duration, &str) + Send + 'static) -> Self {
let origin = Instant::now();
let shared = Arc::new((
Mutex::new(Shared { core: LoopWatchdogCore::new(Duration::ZERO), stopping: false }),
Condvar::new(),
));
let worker_shared = Arc::clone(&shared);
let worker = thread::spawn(move || {
loop {
let (lock, wake) = &*worker_shared;
let mut guard = lock.lock();
wake.wait_for(&mut guard, STALL_THRESHOLD);
if guard.stopping {
break;
}
let stall = guard.core.check(origin.elapsed());
drop(guard);
if let Some(stall) = stall {
report(stall.elapsed, &stall.phase);
}
}
});
Self { origin, shared, worker: Some(worker) }
}
pub fn tick(&self) {
let (lock, wake) = &*self.shared;
let mut shared = lock.lock();
shared.core.tick(self.origin.elapsed());
drop(shared);
wake.notify_one();
}
pub fn set_phase(&self, phase: impl Into<Str>) {
let (lock, _) = &*self.shared;
let mut shared = lock.lock();
shared.core.set_phase(phase);
}
pub fn stop(&mut self) {
let Some(worker) = self.worker.take() else {
return;
};
let (lock, wake) = &*self.shared;
let mut shared = lock.lock();
shared.stopping = true;
drop(shared);
wake.notify_one();
let _ = worker.join();
}
}
impl Drop for LoopWatchdog {
fn drop(&mut self) {
self.stop();
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::LoopWatchdogCore;
#[test]
fn reports_a_300ms_stall_with_phase() {
let mut watchdog = LoopWatchdogCore::new(Duration::ZERO);
watchdog.set_phase("render");
let report = watchdog
.check(Duration::from_millis(300))
.expect("300 ms should exceed the stall threshold");
assert_eq!(report.elapsed, Duration::from_millis(300));
assert_eq!(report.phase, "render");
assert_eq!(watchdog.check(Duration::from_millis(400)), None);
}
#[test]
fn ignores_a_90s_system_sleep_gap() {
let mut watchdog = LoopWatchdogCore::new(Duration::ZERO);
watchdog.set_phase("render");
assert_eq!(watchdog.check(Duration::from_secs(90)), None);
}
#[test]
fn stays_silent_under_normal_ticks() {
let mut watchdog = LoopWatchdogCore::new(Duration::ZERO);
for millis in [200, 400, 600, 800] {
let now = Duration::from_millis(millis);
assert_eq!(watchdog.check(now), None);
watchdog.tick(now);
}
}
}