How do you diagnose Electron main process hangs in VS Code-like apps? #3109
Replies: 2 comments 1 reply
|
For intermittent main-process hangs, I would treat this as an observability problem first rather than relying on attaching a debugger after the fact. A workflow I've found useful is to instrument the application so that a hang produces artifacts even when the normal IPC/UI path is no longer responsive.
Keep a timer/heartbeat in the main process and record event-loop delay rather than only checking whether the process is alive. A process can have a valid PID and still be effectively dead because the main JS thread is blocked. Record at least:
Node's diagnostic report is particularly useful for this type of failure because it can contain both JavaScript and native stack information, V8/heap information, libuv handles, resource usage, and platform information. For example, Node supports: --report-on-signal with SIGUSR2 as the default report signal on supported POSIX platforms. That gives you a useful "snapshot while the process is still alive" rather than waiting for a crash. One caveat is important: Node documents signal-triggered reports as unsupported on Windows, so I would provide an OS-specific collection mechanism rather than assuming SIGUSR2 works everywhere.
For a genuinely hung main process, I would not make the watchdog part of the same event loop that it is trying to diagnose. Have a small external process/service periodically check a heartbeat emitted by the Electron main process. If the heartbeat stops progressing, the watchdog can record the PID and collect an OS-level process dump/minidump or other platform-specific diagnostic artifact. That is much more useful than an in-process timeout when the main thread itself is blocked.
Electron supports --inspect / --inspect-brk for debugging the main process. With the inspector available, CPU profiling can help distinguish a JavaScript hot loop from time spent waiting elsewhere. For an intermittent production hang, however, I would treat inspector profiling as a reproduction tool rather than the primary incident-capture mechanism.
This distinction is important when interpreting the artifacts:
VS Code itself has a useful performance-debugging workflow worth studying. In particular, its performance guidance recommends collecting traces/profiles for renderer hangs and using verbose logging when investigating process/resource issues. The VS Code source is also worth looking at around its diagnostics services. The Electron main-process startup code wires in So for a production application I'd aim for this incident bundle: The key is to design the diagnostics before the hang occurs. Once the main event loop is blocked, anything that depends on that same event loop is inherently unreliable. References:
|
|
The governing fact: a hung main process cannot report on itself. Everything below I verified against Electron 44.4.3 (Node v24.21.0) on macOS by actually wedging a main process, plus reading Node/Electron/VS Code source. Where I could only verify from source or docs, I say so. 1. Triage: which process is wedgedCPU posture plus the process tree settles most cases in seconds. The main process is the one with no
2. JS stacks from a hung main:
|
| Concern | File |
|---|---|
argv, crashReporter.start, --crash-reporter-directory, --trace→contentTracing, --js-flags, enableSandbox() |
src/main.ts |
enableInspectPort() via process._debugProcess; child-process-gone telemetry |
src/vs/platform/utilityProcess/electron-main/utilityProcess.ts |
| Shared process / extension host / pty host as utility processes | sharedProcess.ts, extensionHostStarter.ts, electronPtyHostStarter.ts |
| Heartbeat watchdog: 5 s beats, two-stage timeouts, capped auto-restart | src/vs/platform/terminal/node/heartbeatService.ts + ptyHostService.ts |
Renderer-unresponsive JS-stack sampling (1000 ms interval / 15000 ms period, >20% duplicate stacks → telemetry) via frame.collectJavaScriptCallStack() |
src/vs/platform/windows/electron-main/windowImpl.ts |
--prof-startup: relaunches with three --inspect-brk ports and drives CDP profiles |
src/vs/code/node/cli.ts |
| Every diagnostic flag | src/vs/platform/environment/node/argv.ts |
The architectural point: VS Code's main process rarely hangs because it barely does anything. Windows, menus, dialogs, lifecycle, supervision — that's the job. Extensions, file watching, search and terminals all run in utilityProcess children (the API landed in Electron 22), extension host per window. A wedged extension stalls its own host, not the app; a crashed utility process is caught and restarted; the pty host additionally gets an application-level heartbeat.
If your main process hosts business logic today, moving it to utility processes and keeping main a thin supervisor is the highest-leverage change available — it converts "app hangs" into "one service hangs, detected by heartbeat, auto-restarted." Pair it with an out-of-process watchdog that pings main every N seconds and, on missed pings, auto-collects evidence (_debugProcess + CDP on POSIX, procdump -ma on Windows, sample on macOS) before restarting. VS Code doesn't watchdog its own main — nothing left inside main could report it — but you can, because you own the child.
6. Runbook
0. Posture (any OS, zero risk). Find the pid with no --type= and read its CPU. Pegged → step 2. Idle → step 1 then 3.
1. OS sampling — works even when the thread is natively blocked:
sample <pid> 5 -file /tmp/hang.txt # macOS
gdb -p <pid> -batch -ex 'thread apply all bt' # Linux
procdump.exe -accepteula -h -ma YourApp.exe C:\dumps # Windows (auto on 5 s hang)2. JS stacks via inspector (POSIX; needs JS executing or the loop ticking; needs the nodeCliInspect fuse):
kill -USR1 <pid>
curl -s http://127.0.0.1:9229/json/list # target appears within ~1 s
# attach → Pause → exact stack; optionally Profiler.start for a live profileWindows, or scripted anywhere: process._debugProcess(<pid>) from a separate Node process (equivalently node inspect -p <pid>).
3. Native attach when step 2 stays dark, or for module attribution: lldb -p / gdb -p / WinDbg on the procdump .dmp. Read CrBrowserMain: V8/JIT frames → JS problem (symbolize via step 2); read/wait4/mach_msg/kernel frames → identify the resource; frames inside *.node or a filter driver → addon or AV.
4. Pre-arm for next time: an out-of-process watchdog; an in-main lag monitor (perf_hooks.monitorEventLoopDelay()); crashReporter.start() on main's first line; a --trace switch wired to contentTracing; lint-ban *Sync APIs in main-process code.
If the frozen app is VS Code itself: code --status and Help → Open Process Explorer (both hang too if main is wedged — itself evidence); --crash-reporter-directory /tmp/x for local dumps; --trace --trace-category-filter="renderer,blink,v8,disabled-by-default-v8.cpu_profiler" then "Developer: Stop Tracing" for unresponsive windows; --inspect-extensions=5870, --inspect-sharedprocess (5879), --inspect-ptyhost (5877); --prof-startup for startup profiles. Recipes: https://github.com/microsoft/vscode/wiki/Runtime-debugging
Runtime claims were demonstrated on macOS (arm64), Electron 44.4.3 / Node v24.21.0, against a minimal app wedged with while(true){}. Windows and Linux command syntax is quoted from Microsoft/GNU docs and Node/Electron source but was not executed here. VS Code claims were read at microsoft/vscode main @ aca2ff4f.
Uh oh!
There was an error while loading. Please reload this page.
Hi VS Code community,
I am not reporting a VS Code bug here. I am working on an Electron-based desktop application, and I would like to learn from the practices used by projects like VS Code when diagnosing main process hangs/freezes.
In our case, the hard part is that when the Electron main process is stuck, the usual debugging channels can become unavailable or too late to attach. The renderer may still show the last frame, but IPC, menus, window management, and app-level actions stop responding. These cases are often intermittent and difficult to reproduce locally.
For people who have worked on VS Code, Electron apps, or similar desktop applications:
I am especially interested in practical workflows: what to instrument in advance, what users can provide after a hang, and how maintainers turn those artifacts into a likely root cause.
I realize this Discussions repo is primarily for extension development Q&A. If this question is off-topic here, I would appreciate a pointer to the right VS Code/Electron community channel.
Thanks!
All reactions