Fix Windows slow startup and multi-minute UI freezes (#7225)#7266
Conversation
Extend startup-time-bench with an event-loop-stall metric (maxGapMs from the existing probe), a --github-repos fixture whose repos reach the gh login probe during hydration, and a --gh-hang-ms PATH shim that hangs like a blackholed api.github.com while its child holds the inherited stdio pipes past the probe's timeout kill.
The first getRepos() of a launch ran per-repo sync git config probes and an execSync 'gh api user' network call on the Electron main thread — whether triggered by attachMainWindowServices (warm daemon reattach) or the renderer's startup repos:list. On Windows a timeout-killed gh leaves a grandchild holding the stdio pipes, and the ETIMEDOUT-only check let a stuck first probe fall through to a second equally stuck probe, freezing the app for minutes on blackholed networks. Repo hydration now serves only the cached/persisted username, and a background enrichment (mirroring repo-git-remote-identity-enrichment) resolves usernames async via gitExecFileAsync/ghExecFileAsync with a hard wall, persists them, and notifies the renderer to re-list. All former getGitUsername callers await the async resolver.
new Tray() is a synchronous Shell_NotifyIcon call that can block for seconds when explorer.exe is busy, and setupAutoUpdater's first getAutoUpdater() synchronously require()s electron-updater in packaged builds — both sat between window-created and load-start. Move them to ready-to-show (with a 15s fallback so crash-looping renderers still get update checks) and add tray-created / updater-setup-done milestones so ORCA_STARTUP_DIAGNOSTICS pins any remaining pre-paint cost.
…7225) Cold start listed every repo's worktrees from three paths back-to-back (renderer worktrees fetch, lineage resolution, boot pty-registry hydration), each a git process spawn per repo — expensive on Windows. listWorktrees now shares one in-flight scan per repo, and the renderer runs the worktrees + lineage fetches concurrently so they coincide. The four independent cheap reads (ui, keybindings, browser profiles, onboarding) start early and are awaited in place. Also bound gitExecFileSync with a 15s default timeout: a repo path on a dead network drive could otherwise hang a main-thread git spawn for minutes with no bound at all.
- Restore the candidate-ordered GitHub gate for the gh login (a GitLab- primary repo with a secondary GitHub mirror must not pick up the gh account as its branch prefix) via an async port of the old logic. - Timed-out gh probes are non-authoritative: they retry after a 5-min cooldown instead of pinning '' for the session (WSL-only gh installs can exceed one probe timeout legitimately), and the wall now covers ghExecFileAsync's internal retries and WSL fallback. - Enrichment results carry an authoritative flag so a completed probe can clear a stale persisted username while a timed-out one never clobbers it; repos added during an in-flight pass queue a follow-up pass instead of being dropped. - Tray creation gains a 12s fallback so windows revealed through createMainWindow's win32 10s reveal path (ready-to-show never fires) still get their tray icon before close-to-tray can hide the window. - Manual update checks (app menu / updater:check IPC) force the pending deferred updater setup first, so they never run against an unconfigured electron-updater (listeners, autoDownload=false). - listWorktrees scan sharing is generation-gated: worktree add/remove/ move bumps the repo's generation so post-mutation callers never join a pre-mutation scan. - Browser session profiles are no longer fetched early in the renderer chain (remote-runtime RPC may not be connected yet; a failure clears the profile list). - Drop the now-dead getGitUsername mock wiring from dependent suites; suites that controlled usernames now mock resolveLocalGitUsername explicitly.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (34)
💤 Files with no reviewable changes (6)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (26)
📝 WalkthroughWalkthroughChangesThis PR replaces synchronous Sequence Diagram(s)Diagrams included in the hidden review stack artifact illustrate local git username resolution, background username enrichment, and worktree scan sharing. Compact metadata
Poem A rabbit hops through git's terrain, 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
src/main/git/git-username.ts (1)
204-235: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider parallelizing independent git reads in
localRepoHasEffectiveGitHubRemote.
remotes,defaultBaseRef(viaresolveDefaultBaseRefViaExec), andcurrentBranchare independent reads but are awaited sequentially, as are the twogetConfiguredBranchRemotecalls. This function is reachable fromresolveLocalGitUsername, which runs on the interactive worktree-create path (createLocalWorktree), not just the background enrichment pass — each sequential local-git round trip adds latency users can feel, especially on a slower filesystem.♻️ Suggested parallelization
- const remotes = (await readGitStdout(repoPath, ['remote'])).split('\n').filter(Boolean) - const defaultBaseRef = await resolveDefaultBaseRefViaExec((argv) => - gitExecFileAsync(argv, { cwd: repoPath, timeout: LOCAL_GIT_READ_TIMEOUT_MS }) - ) + const [remotesRaw, defaultBaseRef, currentBranch] = await Promise.all([ + readGitStdout(repoPath, ['remote']), + resolveDefaultBaseRefViaExec((argv) => + gitExecFileAsync(argv, { cwd: repoPath, timeout: LOCAL_GIT_READ_TIMEOUT_MS }) + ), + readGitStdout(repoPath, ['branch', '--show-current']) + ]) + const remotes = remotesRaw.split('\n').filter(Boolean) const defaultBaseRemote = defaultBaseRef ? getRemoteNameFromRef(defaultBaseRef, remotes) : '' const defaultBranch = defaultBaseRef ? getDefaultBranchName(defaultBaseRef, defaultBaseRemote) : null - const currentBranch = await readGitStdout(repoPath, ['branch', '--show-current']) - const candidateRemotes = [ - await getConfiguredBranchRemote(repoPath, currentBranch || null), - await getConfiguredBranchRemote(repoPath, defaultBranch), - defaultBaseRemote, - 'origin', - remotes.length === 1 ? remotes[0] : '' - ] + const [currentBranchRemote, defaultBranchRemote] = await Promise.all([ + getConfiguredBranchRemote(repoPath, currentBranch || null), + getConfiguredBranchRemote(repoPath, defaultBranch) + ]) + const candidateRemotes = [ + currentBranchRemote, + defaultBranchRemote, + defaultBaseRemote, + 'origin', + remotes.length === 1 ? remotes[0] : '' + ]src/main/git/repo-username.test.ts (1)
223-235: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd multi-account coverage for the
gh auth statusfallback parser.This test only exercises a single logged-in account. Given the forward-search regex issue flagged in
git-username.ts(lines 106-128), please add a test with two accounts in theauth statusoutput — including a case where the first listed account is the inactive one — to lock in correct behavior once fixed.src/main/ipc/worktree-remote.ts (1)
1876-1876: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winParallelize username and base-ref resolution.
resolveLocalGitUsernameis awaited sequentially beforebaseBranchresolution, but the two are independent —usernameisn't consumed until the branch-naming loop starts at Line 2007+. Whenargs.baseBranch/repo.worktreeBaseRefare unset,resolveDefaultBaseRefViaExecalso does async git I/O, so running both concurrently would shave real latency off worktree creation.⚡ Proposed parallelization
- const username = await resolveLocalGitUsername(repo.path) + const usernamePromise = resolveLocalGitUsername(repo.path) const requestedName = args.name const sanitizedName = sanitizeWorktreeName(args.name) const requestedDisplayName = args.displayName ? sanitizeWorktreeDisplayName(args.displayName) : undefined // Why: resolve the base before branch/path selection so remote-tracking bases // can be refreshed before `git worktree add`. Creating first and repairing // later races setup scripts, agents, and user edits. - const baseBranch = + const [username, baseBranch] = await Promise.all([ + usernamePromise, args.baseBranch || repo.worktreeBaseRef || (await resolveDefaultBaseRefViaExec((argv) => gitExecFileAsync(argv, localGitExecOptions))) + ]) if (!baseBranch) {Also applies to: 1886-1899
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8db9a903-55fa-4a98-9f0e-bc2e15b8453c
📒 Files selected for processing (39)
src/main/agent-hooks/first-work-branch-rename.test.tssrc/main/agent-hooks/first-work-branch-rename.tssrc/main/automations/service-precheck.test.tssrc/main/automations/service.test.tssrc/main/git/git-username.tssrc/main/git/repo-username.test.tssrc/main/git/repo.tssrc/main/git/runner.tssrc/main/git/worktree.test.tssrc/main/git/worktree.tssrc/main/index.tssrc/main/ipc/repos-create.test.tssrc/main/ipc/repos-picker.test.tssrc/main/ipc/repos-remote.test.tssrc/main/ipc/repos-sparse-presets.test.tssrc/main/ipc/repos.tssrc/main/ipc/worktree-remote.tssrc/main/ipc/worktrees-windows.test.tssrc/main/ipc/worktrees.test.tssrc/main/persistence.test.tssrc/main/persistence.tssrc/main/repo-git-username-enrichment.test.tssrc/main/repo-git-username-enrichment.tssrc/main/runtime/fit-override-integration.test.tssrc/main/runtime/mobile-presence-lock.test.tssrc/main/runtime/mobile-subscribe-integration.test.tssrc/main/runtime/orca-runtime.test.tssrc/main/runtime/orca-runtime.tssrc/main/startup/startup-diagnostics.tssrc/main/window/attach-main-window-services.test.tssrc/main/window/attach-main-window-services.tssrc/renderer/src/App.tsxtools/benchmarks/results/startup-post-fix-final-gh-hang-2026-07-03T20-43-26-589Z.jsontools/benchmarks/results/startup-post-fix-final-no-hang-2026-07-03T20-43-49-250Z.jsontools/benchmarks/results/startup-post-fix-gh-hang-2026-07-03T20-04-32-823Z.jsontools/benchmarks/results/startup-post-fix-no-hang-2026-07-03T20-04-55-906Z.jsontools/benchmarks/results/startup-pre-fix-gh-hang-2026-07-03T19-36-38-636Z.jsontools/benchmarks/results/startup-pre-fix-no-hang-2026-07-03T19-39-15-435Z.jsontools/benchmarks/startup-time-bench.mjs
💤 Files with no reviewable changes (6)
- src/main/ipc/repos-picker.test.ts
- src/main/ipc/repos-sparse-presets.test.ts
- src/main/ipc/repos-create.test.ts
- src/main/automations/service.test.ts
- src/main/ipc/repos-remote.test.ts
- src/main/automations/service-precheck.test.ts
- Raise the gh probe wall to 10s so it covers ghExecFileAsync's full worst-case retry envelope (3 x 2.5s + 1.25s backoff) — a slow-but- recovering gh no longer trips the retry cooldown. - Parse gh auth status block-wise: each account prints its login line before its 'Active account' marker, so the old cross-block regex could capture the NEXT account's login on multi-account outputs. - Keep home-anchored local paths out of committed benchmark results.
80c1cfc to
f9984b8
Compare
… (#7306) * perf: overlap sidebar-scope loads with worktree scan at startup (#7225) Continues the renderer-chain parallelization proposed in #7225 (proposal 2), on top of the merged #7266: - Run project-groups → folder-workspaces concurrently with the per-repo `git worktree list` fan-out. Neither reads the repos store nor worktrees, so a slow remote host's 15s scope RPCs no longer queue ahead of the scan. - Raise worktree refresh concurrency 5 → 8 so multi-core machines run fewer sequential scan batches, still bounded so one moment can't launch every git probe at once. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Test overlapping sidebar scope loads and worktree hydration at startup Verify that sidebar scope loads and worktree hydration operations run concurrently before session hydration. This protects against regressions in startup performance under the perf/startup-lag optimization. Additionally, document the rationale for the worktree refresh concurrency limit in the worktrees slice. * Clarify worktree refresh concurrency comment --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Jinjing <[email protected]>
Fixes #7225 (slow Windows startup + multi-minute "Not Responding" freezes).
Root cause
The reporter's
ORCA_STARTUP_DIAGNOSTICS=1timeline showed two distinct main-thread blocks: ~6.3s betweenwindow-createdandload-start, and one continuous 127s event-loop stall starting right at first window show. Investigation traced them to synchronous subprocess work on the Electron main thread:getRepos()of a launch spawned processes synchronously.hydrateRepocomputedgitUsernameper repo via syncgit configspawns plusexecSync('gh api user')— a network call on the main thread. It fires either insideattachMainWindowServices(warm daemon reattach, i.e. thewindow-created → load-startgap) or at the renderer's firstrepos:list(right at window show). Three compounding Windows defects:execSync's 2.5s timeout kills cmd.exe but an orphanedgh.exegrandchild keeps the inherited stdio pipes open, so the call can block until gh gives up — minutes on a network where api.github.com blackholes (the reporter is on a CN network; 127s matches DNS + multi-IP TCP connect retries).ETIMEDOUTon Windows, so the "skip the second probe" guard never fired andgh auth statusran into the same wall (reproduced: 5.5s stall = 2 × 2.5s + git spawns).gitExecFileSynchad no timeout at all, so a repo path on a dead network drive / cloud placeholder could hang a sync git spawn indefinitely.new Tray()(syncShell_NotifyIcon, win32) andsetupAutoUpdater's syncrequire('electron-updater')(packaged builds, cold disk + Defender) sat betweenwindow-createdandload-start.git worktree listprocess fan-out twice back-to-back (fetch-worktrees, thenworktrees:listLineagerecomputing the same scans), plus serialized four independent cheap reads.Fixes
repo-git-remote-identity-enrichment) resolves usernames via the async runners with a hard 8s wall, persists them, and notifies the renderer. Resolution outcomes carry anauthoritativeflag so a timed-out probe never clobbers a persisted username, while a completed probe can clear a stale one. Timed-out gh probes retry after a 5-minute cooldown instead of pinning''for the session. The GitHub gate keeps the old candidate-ordered semantics (GitLab-primary repos with a GitHub mirror do not pick up the gh login).killed/SIGTERMnow counts as timeout → single probe).gitExecFileSyncgets a 15s default timeout so no sync git spawn can hold the main thread for minutes.ready-to-show, each with a fallback (12s/15s) covering windows revealed via the win32 10s reveal fallback and crash-looping renderers; manual update checks (menu /updater:check) force the pending setup first so they always run configured. Newtray-created/updater-setup-donemilestones pin any remaining pre-paint cost in future diagnostics.listWorktreesshares one in-flight scan per repo (generation-gated: worktree add/remove/move bumps the generation so post-mutation callers never join a pre-mutation scan;AbortSignalcallers keep private scans), and the renderer runsfetch-worktrees+fetch-worktree-lineageconcurrently so the two fan-outs coincide and dedupe. The three independent local reads (ui:get, keybindings, onboarding) start early and are awaited in place.Evidence (Windows 11, medians of 3,
tools/benchmarks/startup-time-bench.mjs)Reproduction:
--github-repos 3seeds repos whose hydration reaches the gh probe;--gh-hang-ms 20000puts a hangingghshim on PATH whose child survives the timeout kill holding the stdio pipes.A 20s-hanging
ghnow has zero effect on startup (2.76s vs 2.86s healthy — within noise). End-to-end check: launching the built app against the fixture showstray-created/updater-setup-donelanding after first paint, and the background enrichment resolving + persisting the real gh login for all repos (branch-prefixing still works, warm on next launch).The remaining ~1.2s stall is bundle/require load at boot — pre-existing, tracked as a follow-up.
Not addressed (follow-ups)
showlistener still runs a synchronous codex-home FS sync (rate-limits/service.ts→runtime-home-service.prepareForRateLimitFetch, recursivecpSync); bounded and local, but worth making async separately.Verification
pnpm typecheck,pnpm lintclean; test sweep across all touched areas green (the only failures on this Windows machine —repo-detection.test.ts,clipboard-file-copy.test.ts— reproduce identically on unmodified main).