Renderer stall profiler (what the app was doing when it froze)
When the app window freezes for a second or more, Omniscio records what its code was doing: a .cpuprofile you open in Chrome DevTools plus one log line naming the heaviest functions. It watches only while the window struggles, never changes how the app behaves, keeps the newest 25 recordings under a size cap, and says why whenever it could not record instead of staying silent. Desktop main window only; turn it off with AMC_DISABLE_RENDERER_STALL_PROFILER=1.
What it is
What it answers
"The app just froze for three seconds — what was it actually doing?"
The main window draws the app. When that drawing is slow — a long chat that re-renders itself on every update is the usual cause — you feel it as typing and clicking hanging. The app has always been able to tell you that a redraw was slow. It could not tell you which part of the code was slow, because nothing was recording it.
This does. It watches the main window only while the window is struggling, and when a pause lasts a second or more it saves a recording of what the code was doing, plus one line summarising the heaviest parts.
Why you would look here
- The app felt frozen and you want to know why, rather than guess.
- Someone asked you for evidence about a slow redraw.
- You want to check the recorder is actually working (it is quiet when the app is calm — see below).
Where to find it
Turning it on and off
It is on by default. Nothing to enable, no setting to find, no restart needed for the default.
To turn it off, set the environment variable AMC_DISABLE_RENDERER_STALL_PROFILER=1 before starting
the app. To check whether it is currently running, run:
npm run perf:status # find the "stall profiler" row
npm run perf:control # lists it as a registered governor, with its switch name
Where the files go
Everything lands in the app's own logs folder — the same one that holds renderer-diag.log:
<app data>/logs/renderer-profiles/<timestamp>-<what>.cpuprofile
Only the newest 25 are kept, and there is a total size cap, so this cannot fill a disk.
How it behaves
Reading one
Open the .cpuprofile file in Chrome DevTools → Performance → Load profile. It shows a flame
chart of what ran and how long each part took.
The names will look short and strange — something like nr (index-abc123.js:4567) rather than
renderTurnGroup. That is normal and not a fault: the app ships with its internal names shortened
to keep it small and fast, and that happens to every finished app. What you do get is the exact
file, the exact line, and which chunk of the app the code came from — and the chunk alone answers
the question this was built for (is this React's own machinery, or is it our code?). If you need
the readable name, the build also keeps a map file that turns that line number back into a name.
Reading the log line
Each capture writes one line into renderer-diag.log, tagged [renderer-stall-profile]:
[renderer-stall-profile] trigger=redraw durationMs=3821 samples=920 window=45000ms file=...cpuprofile top: TurnGroup (index-abc.js:4568) 97% | ...
trigger— what noticed the stall (redraw,longtask, orfreeze).durationMs— how long the pause lasted.samples— how many snapshots the recording holds.window— how long the recorder had been watching when the pause hit.top— the five heaviest things it saw.
Why the recorder says it could not measure something
If a pause happens while the recorder was not watching, you still get a line — it just says so:
[renderer-stall-profile] trigger=longtask durationMs=1500 samples=0 window=0ms unavailable=disarmed-at-trigger top: (profiler dark — not armed when the stall hit)
That is deliberate. A recorder that quietly stops working looks exactly like an app that stopped being slow, and those are very different facts. The reasons you may see are:
unavailable= |
What it means |
|---|---|
kill-switch |
Somebody turned it off with the environment variable. |
self-defeated |
Starting it took 500 ms or more (the window freezes while it starts), so it switched itself off until the app restarts. |
no-window |
There is no main window to watch right now. |
debugger-owned-by-devtools |
You have DevTools open on the app window; close it and it resumes. |
attach-failed |
The window could not be attached to this time. |
arm-failed |
It attached, but sampling would not start. |
disarmed-at-trigger |
The pause landed before the recorder had started watching. |
capture-failed |
The recording came back empty. |
write-failed |
The recording could not be saved (usually disk). |
What starting it costs, and why it may switch itself off
Starting a recording is not free. Before it can take its first snapshot, the recorder has to list every piece of code the window has loaded, and the window is frozen while it does. On a small window that takes a few hundredths of a second. On a busy one it can take seconds — measured on a user's app in October 2026: 1-3 s at a 125-215 MB window, every time it started.
So the recorder now times every start and protects you from it:
- A start that takes 500 ms or more switches it off until the app restarts. You get one
[renderer-stall-profiler] self-defeated startCostMs=<n>line inrenderer-diag.log, and theperf:statusrow readsDARK — profiler off for this run. Paying seconds of freeze to explain freezes would make things worse, so it stops. - It never counts its own freeze as a stall. A pause that overlaps one of its own starts is ignored, so it can no longer record itself and restart in a loop.
- Its own cost is on the scorecard. The
stall profilerrow onnpm run perf:statusends withown start freeze <seconds> over <n> start(s)— the one place this freeze shows up, because the app's usual long-task counters cannot see it.
What it does NOT do
- It never changes how the app behaves. It only watches. If anything about it fails, the app
carries on exactly as if it were not there. The one cost it does impose — the short freeze each
start causes — is measured, shown on
perf:status, and capped (see above). - It only watches the main desktop window. The same chat shown on the phone/web view is a different window, drawn by different code, and is deliberately out of scope — a slow phone view is not evidence about the desktop window, so mixing the two would produce confidently wrong answers.
- It does not work on the main process. That is a separate instrument with its own tape lines
(
[stall-stack]); this one is only about the app window's own drawing.
Related
- Perf status — the scorecard this one reports into.
- Startup trace — the same idea for startup, step by step.
- Logs and debugging — where the logs live when you need the raw detail.
Last verified 2026-10-01