fix: prevent re-entrant event loop hang in main process (18s freeze)#388
Open
monotykamary wants to merge 3 commits into
Open
fix: prevent re-entrant event loop hang in main process (18s freeze)#388monotykamary wants to merge 3 commits into
monotykamary wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a6ef7ea68
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The main process became unresponsive for ~18s when the Electron main thread entered a re-entrant event loop: V8 compilation -> tick_callback -> nested uv_run processed fs.stat completions -> JS rejection handlers fired -> more compilation, blocking the Cocoa event loop indefinitely. Three-layer fix: 1. Re-entrancy guard in getExtensionBundle (extension-runner.ts): - enterBundleGuard/exitBundleGuard prevents nested uv_run - yieldToEventLoop on conflict drains pending I/O callbacks - 30s raceWithTimeout prevents indefinite hangs 2. Yield points in command discovery (commands.ts): - setImmediate yield before discoverAndBuildCommands - setImmediate yield after each batch in discoverApplications - setImmediate yield after each batch in discoverSystemSettings - setImmediate yield between discovery phases 3. Yield before extension execution (main.ts): - setImmediate yield before buildLaunchBundle in runCommandById Added vitest test suite (15 tests) covering: - Re-entrancy guard enter/exit/blocking semantics - Event loop yielding behavior - Timeout wrapper for slow operations - Simulated re-entrant loop patterns - Integration-level bundle load scenarios
5a6ef7e to
4ee731a
Compare
The rejection timer in raceWithTimeout used setTimeout(..., 0), causing every bundle load that didn't complete in one event-loop tick to be reported as a timeout immediately instead of after BUNDLE_LOAD_GRACE_MS. Removed the broken placeholder-cleanup timer pattern in favor of a single setTimeout(..., ms) that correctly rejects at the right delay.
8f5f5af to
08fa5a2
Compare
Move esbuild builds (on-demand + full extension rebuild) to a forked child process (build-worker.ts) so that V8 compilation, file resolution, and plugin I/O cannot re-enter the main process's Cocoa event loop. - build-worker.ts: receives build options over IPC from the main process, runs esbuild.build(), returns success/failure + missing bare imports. Follows the same pattern as window-manager-worker. - Remove requireEsbuild() and extractMissingBareImports() from the main process — they live in the worker now. - Rewrite runEsbuildBuild() to delegate to callBuildWorker(). Retry logic (missing packages -> install -> rebuild) stays in main. - Remove all 5 setImmediate yield points from commands.ts and main.ts — the worker replaces the band-aid fix with a proper architecture. - Keep re-entrancy guard + 30s timeout as defense-in-depth.
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.
Problem
A macOS hang report (
crash.dump) showed SuperCmd becoming unresponsive for 18.24 seconds. The Electron main thread entered a re-entrant event loop — V8 compilation → Node.js tick callback → nested libuvuv_runprocessedfs.statcompletions, triggering JS rejection handlers that scheduled more compilation, blocking the Cocoa event loop indefinitely.The critical stack chain from the dump:
This happens when:
discoverInstalledExtensionCommands()does heavy syncfs.readdirSync/fs.statSynccallsfs.stat(ENOENT for missing source files or incompletenode_modules) re-enters JSFix (3 layers)
1. Re-entrancy Guard + 30s Timeout in
getExtensionBundle(extension-runner.ts)enterBundleGuard/exitBundleGuard— per-label depth counter prevents nested calls from creating a nesteduv_runyieldToEventLoop()on conflict — drains pending I/O callbacks before retryingraceWithTimeout()— 30-second hard timeout prevents indefinite hangs2. Yield Points in Command Discovery (
commands.ts)setImmediateyield at the start ofdiscoverAndBuildCommands()— drains pending I/O before heavy sync worksetImmediateyield after every batch indiscoverApplications()— breaks long app-bundle scanning loopssetImmediateyield after every batch indiscoverSystemSettings()— breaks appex + prefPane scanning loopssetImmediateyield between discovery phases — before batched NSWorkspace icon extraction3. Yield Before Extension Execution (
main.ts)setImmediateyield beforebuildLaunchBundle()inrunCommandById— drains pending I/O before entering bundle loadingTesting
Added vitest (new dev dependency) with 15 tests across 4 suites in
src/main/__tests__/re-entrancy-guard.test.ts:Run tests:
npm test