No more false stall alarms from the macOS app window

macOS's WebKit (the engine behind Obeya's app window) slows a page's timers to about once a second when the window is visible but not in front. The page took each slow tick for a freeze and logged a "stall" almost every second. It now checks against a heartbeat from a background thread (a worker) that WebKit doesn't slow down, so only real freezes count. When a page does freeze the same way over and over, the log shows the first entry and then one line with the count.

Before (current main)
192
false stall entries in 9½ minutes
from a page that nobody touched, plus the 2 real freezes
After (this change)
0
false stall entries in the same 9½ minutes
only the 2 real freezes, both logged (1.5 s and 1.4 s)

How it was measured

I ran two scratch Obeyas, one on the current main and one with this change. Each was open in a WebKit window like the app's (a WKWebView), side by side in a screen corner: visible and on top, but never focused. Nobody touched either page. Four minutes in, and again at eight minutes, both pages had their main thread blocked for 1.5 s on purpose.

BeforeAfter
Page's own 100 ms timer late by ≥ 0.5 s
(WebKit slowing it to about once a second; the page stayed "visible")
the same (WebKit slows both)161 times
Stall entries while nothing was blocked1920
Deliberate 1.5 s block at 4 minlogged, 1.5 slogged, 1.5 s
Deliberate 1.5 s block at 8 minlogged, 1.4 slogged, 1.4 s

stalls.log after the change

2026-10-30T08:55:53.011Z
Obeya: a page (this machine, canvas bakery-shop) stalled for 1.5 s; meanwhile it took no click or key

2026-10-30T08:59:53.907Z
Obeya: a page (this machine, canvas bakery-shop) stalled for 1.4 s; meanwhile it took no click or key

2026-10-30T09:03:53.208Z
Obeya: a page (this machine, canvas bakery-shop) stalled for 0.8 s; meanwhile it took no click or key

2026-10-30T09:05:18.481Z
Obeya: a page (this machine, canvas bakery-shop) stalled 9 more times in 25 s like the one logged at 2026-10-30T09:03:53.208Z, for 0.7–0.8 s each

The last two entries are a page blocked for 0.8 s every 2–3 seconds, ten times. The first block gets a full entry. The rest go on one line once a minute passes without another. If the page keeps freezing like that, a line follows every hour. A page that froze like this all night now leaves about a dozen lines instead of 39,000, so the server's own stalls stay in stalls.log.

Before: the old version's stalls.log (excerpt)

2026-10-30T08:47:08.709Z
Obeya: a page (this machine, canvas bakery-shop) stalled for 0.9 s; meanwhile it took no click or key

2026-10-30T08:47:09.711Z
Obeya: a page (this machine, canvas bakery-shop) stalled for 0.9 s; meanwhile it took no click or key

2026-10-30T08:47:10.708Z
Obeya: a page (this machine, canvas bakery-shop) stalled for 0.9 s; meanwhile it took no click or key
… 192 entries between 08:51:51 and 09:01:25, in bursts of one a second