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