use std::{marker::PhantomData, panic::Location, path::PathBuf};
use kithara_platform::time::{Duration, Instant};
use super::{
platform::write_dump,
shared::{HangDump, NoContext},
};
pub struct HangDetector<C: HangDump = NoContext> {
label: &'static str,
timeout: Duration,
ctx: Option<C>,
deadline: Option<Instant>,
dump_dir: Option<PathBuf>,
last_progress: Option<(&'static str, u32)>,
last_tick: Option<(&'static str, u32)>,
_marker: PhantomData<C>,
fired: bool,
spins_since_progress: u32,
}
impl<C: HangDump> HangDetector<C> {
#[must_use]
pub fn new(label: &'static str, timeout: Duration) -> Self {
Self {
label,
timeout,
deadline: None,
ctx: None,
dump_dir: None,
fired: false,
_marker: PhantomData,
last_tick: None,
last_progress: None,
spins_since_progress: 0,
}
}
fn deadline(&mut self) -> Instant {
*self
.deadline
.get_or_insert_with(|| Instant::now() + self.timeout)
}
fn diagnostic(&self) -> String {
let fmt = |loc: Option<(&'static str, u32)>| {
loc.map_or_else(
|| "<unknown>".to_string(),
|(file, line)| format!("{file}:{line}"),
)
};
format!(
"stuck at {stuck} | last progress at {progress} | {spins} tick(s) since progress | timeout {timeout:?}",
stuck = fmt(self.last_tick),
progress = fmt(self.last_progress),
spins = self.spins_since_progress,
timeout = self.timeout,
)
}
fn fire_dump(&mut self) {
if self.fired {
return;
}
self.fired = true;
let diag = self.diagnostic();
match self.ctx.as_ref() {
Some(ctx) => write_dump(self.label, ctx, self.dump_dir.as_deref(), &diag),
None => write_dump(self.label, &NoContext, self.dump_dir.as_deref(), &diag),
}
}
#[must_use]
pub fn remaining(&mut self) -> Duration {
self.deadline().saturating_duration_since(Instant::now())
}
#[track_caller]
pub fn reset(&mut self) {
let loc = Location::caller();
self.reset_from(loc.file(), loc.line());
}
pub fn reset_from(&mut self, file: &'static str, line: u32) {
self.deadline = Some(Instant::now() + self.timeout);
self.fired = false;
self.last_progress = Some((file, line));
self.spins_since_progress = 0;
}
#[track_caller]
pub fn tick(&mut self) {
let loc = Location::caller();
self.tick_from(loc.file(), loc.line());
}
pub fn tick_from(&mut self, file: &'static str, line: u32) {
self.last_tick = Some((file, line));
if Instant::now() < self.deadline() {
self.spins_since_progress = self.spins_since_progress.saturating_add(1);
return;
}
self.fire_dump();
panic!(
"[HangDetector] `{label}` no progress for {timeout:?} — {diag}",
label = self.label,
timeout = self.timeout,
diag = self.diagnostic(),
);
}
#[track_caller]
pub fn tick_with(&mut self, ctx: C) {
self.ctx = Some(ctx);
let loc = Location::caller();
self.tick_from(loc.file(), loc.line());
}
pub fn tick_with_from(&mut self, ctx: C, file: &'static str, line: u32) {
self.ctx = Some(ctx);
self.tick_from(file, line);
}
#[must_use]
pub fn with_dump_dir(mut self, dir: PathBuf) -> Self {
self.dump_dir = Some(dir);
self
}
}