From 4393bde4ec2ed20cce35410e2a31de10032c209b Mon Sep 17 00:00:00 2001 From: grom Date: Tue, 20 Oct 2026 19:17:44 -0600 Subject: [PATCH] sys-timing: a stall report names the thread and how long it was stuck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two durations were being conflated. How long the thread has not moved is the defect; how long the span it is inside has been open is only where it is stuck, and for an enclosing span that is the whole session — which is why the live log said "has been running 360380 ms" and then "was in there at least 360406 ms" about a 150 ms hiccup. So both, labelled, and the thread's name beside them: the span it is stuck in may be unnamed, since an enclosing `info_span!` carries no `name` field for `on_new_span` to record, and then the thread is the only thing the report can name. Before this, such a stall printed `span #2473901162497` and nothing else. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-world/src/sys_timing.rs | 51 +++++++++++++++++++------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/crates/benilla-world/src/sys_timing.rs b/crates/benilla-world/src/sys_timing.rs index 7fe95f78..022afa07 100644 --- a/crates/benilla-world/src/sys_timing.rs +++ b/crates/benilla-world/src/sys_timing.rs @@ -46,6 +46,11 @@ struct Slot { /// slot shows a span that has been open since the app started whenever an inner one returns. /// A thread that is actually wedged stops moving this. beat: AtomicU64, + /// Which thread took this slot, written once when it claims one. The span it is stuck inside + /// may be unnamed — an enclosing `info_span!` carries no `name` field for `on_new_span` to + /// record — and then the thread is the only thing a report can name. Behind a lock because it + /// is read when printing and never while detecting. + thread: Mutex>, } /// The process clock every [`Slot::since_ns`] is measured against. @@ -136,6 +141,10 @@ impl SystemTiming { } let taken = self.next_slot.fetch_add(1, Ordering::Relaxed); local.slot = if taken < SLOTS { taken } else { usize::MAX }; + if let Some(slot) = self.slots.get(local.slot) { + *slot.thread.lock().unwrap_or_else(|e| e.into_inner()) = + std::thread::current().name().map(str::to_string); + } } /// Fold this thread's tally into the shared one, dropping spans that are not systems, and @@ -426,10 +435,10 @@ fn spawn_watchdog(names: &Arc>>, slots: &Arc = HashMap::new(); + // Slot → who was reported, so a stall is announced once and its end once. Keyed by + // slot, not span: what is stalled is the thread, and the span it is stuck in is only + // how we name it. + let mut reported: HashMap = HashMap::new(); // Per slot, the beat last seen and when it last moved. See [`Slot::beat`]. let mut beats: Vec<(u64, Instant)> = vec![(0, Instant::now()); slots.len()]; let mut mem_at = Instant::now(); @@ -456,17 +465,17 @@ fn spawn_watchdog(names: &Arc>>, slots: &Arc = Vec::new(); + let mut fresh: Vec = Vec::new(); let mut done: Vec<(String, f64)> = Vec::new(); for (i, slot) in slots.iter().enumerate() { let beat = slot.beat.load(Ordering::Relaxed); // The thread entered or exited something since the last poll, so whatever it // is inside now, it is not stuck in it. if beat != beats[i].0 { + let stuck_ms = beats[i].1.elapsed().as_secs_f64() * 1000.0; beats[i] = (beat, Instant::now()); - if let Some((name, since)) = reported.remove(&i) { - #[allow(clippy::cast_precision_loss)] - done.push((name, now_ns.saturating_sub(since) as f64 / 1.0e6)); + if let Some(who) = reported.remove(&i) { + done.push((who, stuck_ms)); } continue; } @@ -493,24 +502,38 @@ fn spawn_watchdog(names: &Arc>>, slots: &Arc