use std::{marker::PhantomData, panic::Location, path::PathBuf};
use kithara_platform::time::{Duration, coarse_now_ms};
use super::{
platform::write_dump,
shared::{HangDump, NoContext},
};
pub struct HangDetector<C: HangDump = NoContext> {
label: &'static str,
timeout: Duration,
ctx: Option<C>,
dump_dir: Option<PathBuf>,
last_progress: Option<(&'static str, u32)>,
last_tick: Option<(&'static str, u32)>,
started_ms: Option<f64>,
_marker: PhantomData<C>,
fired: bool,
spins_since_progress: u32,
ticks: u32,
}
impl<C: HangDump> HangDetector<C> {
const CLOCK_SAMPLE_TICKS: u32 = 64;
const MS_PER_SECOND: f64 = 1000.0;
#[must_use]
pub fn new(label: &'static str, timeout: Duration) -> Self {
Self {
label,
timeout,
started_ms: None,
ticks: 0,
ctx: None,
dump_dir: None,
fired: false,
_marker: PhantomData,
last_tick: None,
last_progress: None,
spins_since_progress: 0,
}
}
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 {
let now = coarse_now_ms();
let start = *self.started_ms.get_or_insert(now);
let timeout_ms = self.timeout.as_secs_f64() * Self::MS_PER_SECOND;
let left_ms = (start + timeout_ms - now).max(0.0);
Duration::from_secs_f64(left_ms / Self::MS_PER_SECOND)
}
#[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.started_ms = None;
self.ticks = 0;
self.fired = false;
self.last_progress = Some((file, line));
self.spins_since_progress = 0;
}
#[track_caller]
pub fn reset_with<F: FnOnce() -> C>(&mut self, ctx_fn: F) {
self.ctx = Some(ctx_fn());
let loc = Location::caller();
self.reset_from(loc.file(), loc.line());
}
pub fn reset_with_from<F: FnOnce() -> C>(&mut self, ctx_fn: F, file: &'static str, line: u32) {
self.ctx = Some(ctx_fn());
self.reset_from(file, line);
}
#[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));
self.spins_since_progress = self.spins_since_progress.saturating_add(1);
self.ticks += 1;
if self.ticks < Self::CLOCK_SAMPLE_TICKS {
return;
}
self.ticks = 0;
let now = coarse_now_ms();
let Some(start) = self.started_ms else {
self.started_ms = Some(now);
return;
};
if now - start < self.timeout.as_secs_f64() * Self::MS_PER_SECOND {
return;
}
self.fire_dump();
self.started_ms = None;
}
#[track_caller]
pub fn tick_with<F: FnOnce() -> C>(&mut self, ctx_fn: F) {
self.ctx = Some(ctx_fn());
let loc = Location::caller();
self.tick_from(loc.file(), loc.line());
}
pub fn tick_with_from<F: FnOnce() -> C>(&mut self, ctx_fn: F, file: &'static str, line: u32) {
self.ctx = Some(ctx_fn());
self.tick_from(file, line);
}
#[must_use]
pub fn with_dump_dir(mut self, dir: PathBuf) -> Self {
self.dump_dir = Some(dir);
self
}
}