It froze on him this morning while learning recipes at a trainer, and he had to
kill it. Both logs end on an ordinary line; the fps journal's last sample is a
second earlier with normal numbers; `journalctl -k` holds nothing but firewall
noise between 07:30 and 08:20, so no GPU reset and no OOM kill; and the two crash
reports in Diagnostics are from the 11th and the 15th by their own embedded
server-time lines. A frozen frame writes no line of its own, and a force-kill
writes no crash report, so the session left no evidence of where it stopped.
There was a watchdog for exactly this, with a heartbeat, a threshold set, an
`_exit` backstop and two injectors to test it — registered
`#[cfg(all(feature = "dev", target_os = "macos"))]`, because its diagnosis is
`/usr/bin/sample`. On Linux nothing was watching at all.
So the sampler grows a Linux arm instead of a second watchdog beside it. `/proc`
already knows: each thread's scheduler state, the kernel function it sleeps in,
and its CPU time in clock ticks. That is less than a backtrace, but it settles
the first question — climbing ticks is a loop, `S` or `D` with flat ticks is a
wait and the wchan says what for — and it is free, where a real backtrace of our
own process needs ptrace, which yama's `ptrace_scope=1` refuses between siblings
and which this client has no business asking for on a player's machine. The
report says so, and names the pid for `sudo eu-stack -p <pid>` by hand.
Verified with the injector that was already there, `WOW_STALL_INJECT=20:14000`:
stall: no frame finished in 10072 ms — main thread tid 2793492 state S
wchan hrtimer_nanosleep cpu 667 ticks benilla; 39 thread(s) in all
`S` on `hrtimer_nanosleep` is the injected sleep, read correctly as a wait. The
full report lists all 39 threads by CPU. The in-frame verdict goes to the log as
well as the file, because the log is what gets read after a freeze and a file
nobody knows about is not evidence.
Per-platform where the platforms differ: Linux waits ten seconds rather than
600 ms, since this client has honest frames of several (a world entry's UI load,
a `/reload`), and takes more reports, since reading `/proc` suspends nothing —
where a `sample` suspends the audio thread, which is itself a crackle, and still
declines for that reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn