Performance watchdog: detect and attribute sustained idle main-thread work - #110
Open
daiverd wants to merge 2 commits into
Open
Performance watchdog: detect and attribute sustained idle main-thread work#110daiverd wants to merge 2 commits into
daiverd wants to merge 2 commits into
Conversation
A tab once burned ~18% of a core for three days before anyone noticed, and the condition stopped mid-investigation with no record of what had been running. This adds an always-on monitor that catches the onset, samples it while it is live, and names the culprit. Three cheap signals, no per-event instrumentation in the steady state: a longtask PerformanceObserver, a 500ms interval that measures its own scheduling delay, and a requestAnimationFrame wrapper that counts callers. The wrapper never schedules a frame of its own — installing a rAF loop to measure frame rate pins the page at 60fps and manufactures the load it claims to observe. Only trips while idle (no keydown or pointer activity for 10s) and visible, since background tabs throttle timers to ~1Hz and sustained work during active use is expected. Exactly one console warning per episode; the episode ends after the condition stays clear for 60s. On trip it records live subsystems, inbound message and output rates sampled from store entry-id deltas, rAF callers per second, and long-task totals, then ranks them so the warning names the top contributors. Records are JSON and go to a pluggable sink, defaulting to a bounded in-memory array, so the diagnostics buffer can be dropped in later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matches the identical hunk in the diagnostics branch (#111) so the two PRs merge cleanly in either order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #103
A tab once burned ~18% of a core for three days before anyone noticed, and the condition stopped mid-investigation with no record of what had been running. This adds an always-on monitor that catches the onset, samples it while it is live, and names the culprit in one copy-pasteable line.
Design
src/diagnostics/perfWatchdog.tsruns three cheap signals and no per-event instrumentation in the steady state:longtaskPerformanceObserver — native, free when nothing is slow, silently absent on Firefox/Safari (the lag sampler covers those).setIntervalmeasuring its own scheduling delay.requestAnimationFramewrapper — counts callers. It never schedules a frame of its own; the callback is passed through untouched and the native id is returned, socancelAnimationFramekeeps working. Installing a rAF loop to measure frame rate pins the page at 60 fps and manufactures the very load it claims to observe, which is the false positive that derailed the original investigation.Samples roll through a 10 s window (21 slots at 500 ms; the first is the baseline). The window is discarded outright on any tick that is not both idle and visible, so a trip means the whole window was quiet — background tabs throttle timers to ~1 Hz and would otherwise read as lag that is not there.
Attribution on trip:
AudioContext.stateviawindow.mudClient, connection state, open editor windows, LiveKit voice chat, inbound message and output rates sampled fromuseChannelHistoryStore/useOutputStoreentry-id deltas (O(1), no per-event hooks), rAF callers/sec, and long-task totals. Each becomes a scored contributor; the warning leads with the top three by name.Records are plain JSON and go to a pluggable sink (
type WatchdogSink = (record: WatchdogRecord) => void), defaulting to a bounded 20-entry array plus oneconsole.warn. The diagnostics buffer from #100 can be passed as the sink once both land — nothing here imports it.Wiring is one line in
App.tsx:useEffect(() => ensurePerfWatchdog(), []). Refcounted, so it starts once, is tied to the app rather than the connection (survives reconnects), and releases its interval, observer, three passive listeners, and the rAF wrapper on stop.EditorManagergains anopenEditorCountgetter for the attribution snapshot.Thresholds and why
longtaskonly counts tasks over 50 ms, so 20% of wall clock inside them while nobody is touching the client is unambiguous.Tests
src/diagnostics/perfWatchdog.test.ts, 10 cases on fake timers with an injected clock, long-task source, and probes: one warning per episode; a second episode only after the cooldown; silence while interacting; silence while hidden; the lag path tripping with zero long tasks; a record carrying subsystems, rates, ranked contributors and surviving a JSON round trip; the default sink plusconsole.warnfallback; and the rAF wrapper counting callers, passing through, adding zero calls of its own, and unwrapping cleanly on stop.npx vitest run— 1071 tests across 107 files, all green.npm run typecheckclean. Biome clean on the new files (the two warnings onApp.tsxare pre-existing, untouched lines).🤖 Generated with Claude Code