1use std::{
4 sync::Arc,
5 thread::{self, JoinHandle},
6 time::{Duration, Instant},
7};
8
9use omp_core::Str;
10use parking_lot::{Condvar, Mutex};
11
12const STALL_THRESHOLD: Duration = Duration::from_millis(250);
13const SYSTEM_SLEEP_THRESHOLD: Duration = Duration::from_secs(60);
14
15#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct StallReport {
18 pub elapsed: Duration,
20 pub phase: Str,
22}
23
24#[derive(Debug)]
30pub struct LoopWatchdogCore {
31 last_tick: Duration,
32 phase: Str,
33 reported: bool,
34}
35
36impl LoopWatchdogCore {
37 #[must_use]
39 pub fn new(now: Duration) -> Self {
40 Self { last_tick: now, phase: "unknown".into(), reported: false }
41 }
42
43 pub const fn tick(&mut self, now: Duration) {
45 self.last_tick = now;
46 self.reported = false;
47 }
48
49 pub fn set_phase(&mut self, phase: impl Into<Str>) {
51 self.phase = phase.into();
52 }
53
54 #[must_use]
60 pub fn check(&mut self, now: Duration) -> Option<StallReport> {
61 let elapsed = now.saturating_sub(self.last_tick);
62 if elapsed > SYSTEM_SLEEP_THRESHOLD {
63 self.reported = false;
64 return None;
65 }
66 if elapsed <= STALL_THRESHOLD {
67 self.reported = false;
68 return None;
69 }
70 if self.reported {
71 return None;
72 }
73 self.reported = true;
74 Some(StallReport { elapsed, phase: self.phase.clone() })
75 }
76}
77
78struct Shared {
79 core: LoopWatchdogCore,
80 stopping: bool,
81}
82
83pub struct LoopWatchdog {
90 origin: Instant,
91 shared: Arc<(Mutex<Shared>, Condvar)>,
92 worker: Option<JoinHandle<()>>,
93}
94
95impl LoopWatchdog {
96 #[must_use]
98 pub fn new(report: impl Fn(Duration, &str) + Send + 'static) -> Self {
99 let origin = Instant::now();
100 let shared = Arc::new((
101 Mutex::new(Shared { core: LoopWatchdogCore::new(Duration::ZERO), stopping: false }),
102 Condvar::new(),
103 ));
104 let worker_shared = Arc::clone(&shared);
105 let worker = thread::spawn(move || {
106 loop {
107 let (lock, wake) = &*worker_shared;
108 let mut guard = lock.lock();
109 wake.wait_for(&mut guard, STALL_THRESHOLD);
110 if guard.stopping {
111 break;
112 }
113 let stall = guard.core.check(origin.elapsed());
114 drop(guard);
115 if let Some(stall) = stall {
116 report(stall.elapsed, &stall.phase);
117 }
118 }
119 });
120
121 Self { origin, shared, worker: Some(worker) }
122 }
123
124 pub fn tick(&self) {
126 let (lock, wake) = &*self.shared;
127 let mut shared = lock.lock();
128 shared.core.tick(self.origin.elapsed());
129 drop(shared);
130 wake.notify_one();
131 }
132
133 pub fn set_phase(&self, phase: impl Into<Str>) {
135 let (lock, _) = &*self.shared;
136 let mut shared = lock.lock();
137 shared.core.set_phase(phase);
138 }
139
140 pub fn stop(&mut self) {
142 let Some(worker) = self.worker.take() else {
143 return;
144 };
145 let (lock, wake) = &*self.shared;
146 let mut shared = lock.lock();
147 shared.stopping = true;
148 drop(shared);
149 wake.notify_one();
150 let _ = worker.join();
151 }
152}
153
154impl Drop for LoopWatchdog {
155 fn drop(&mut self) {
156 self.stop();
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use std::time::Duration;
163
164 use super::LoopWatchdogCore;
165
166 #[test]
167 fn reports_a_300ms_stall_with_phase() {
168 let mut watchdog = LoopWatchdogCore::new(Duration::ZERO);
169 watchdog.set_phase("render");
170 let report = watchdog
171 .check(Duration::from_millis(300))
172 .expect("300 ms should exceed the stall threshold");
173 assert_eq!(report.elapsed, Duration::from_millis(300));
174 assert_eq!(report.phase, "render");
175 assert_eq!(watchdog.check(Duration::from_millis(400)), None);
176 }
177
178 #[test]
179 fn ignores_a_90s_system_sleep_gap() {
180 let mut watchdog = LoopWatchdogCore::new(Duration::ZERO);
181 watchdog.set_phase("render");
182 assert_eq!(watchdog.check(Duration::from_secs(90)), None);
183 }
184
185 #[test]
186 fn stays_silent_under_normal_ticks() {
187 let mut watchdog = LoopWatchdogCore::new(Duration::ZERO);
188 for millis in [200, 400, 600, 800] {
189 let now = Duration::from_millis(millis);
190 assert_eq!(watchdog.check(now), None);
191 watchdog.tick(now);
192 }
193 }
194}