Files
orca/src/main/worktree-root-preparation.ts
T
Neil e6257b6e32 perf(worktree): resolve the WSL workspace root off the main thread when preparing (#17792)
`computeWorkspaceRoot` resolves a WSL repo's mirror root through `getWslHome`,
which is a synchronous `execFileSync('wsl.exe', ...)` with a 5s timeout. Two
worktree preparation paths ran it on the Electron main thread:
`prepareLocalWorktreeRootForRepo` (repo registration, clone completion, repo
update, project host setup, folder->git upgrade) and `prepareWorktreeCreateForRepo`
(the speculative checkout started while the create composer is open). On a stopped
or cold distro that froze every window for up to 5s. Being fire-and-forget did not
help: only 3 of the 16 `prepareLocalWorktreeRootForRepo` call sites are `void`-ed,
the other 13 are awaited inside IPC handlers, and the sync probe blocks the main
thread either way. `prepareLocalWorktreeRootsForRepos` runs the same probe for
every repo from the settings-save handler.

Adopt the existing `computeWorkspaceRootAsync` (now exported) at those two call
sites, and give the two resolvers a shared mirror-distro decision and shared
root-from-home layout so they cannot drift apart.

Also thread the mirror distro into the prepare-side path settings.
`createLocalWorktree` passes `getWorktreeMirrorDistro(store, repo)` and
`prepareWorktreeCreateForRepo` did not, so a `C:\` repo on a WSL project runtime
prepared under `C:\workspaces` while the create click looked under the mirrored
WSL root: the keys never matched and every prepared checkout was discarded, after
paying for a full checkout that sat until the 5min TTL. Pre-existing on main;
included because it is the same line and the same resolver.

Scope of the win, stated precisely: only those two preparation paths stop
blocking. On a reachable distro `getWslHome` caches on success, so before this
change the first repo paid one blocking probe and the rest were cache hits -- the
change makes that one probe non-blocking, it does not remove N probes. Failed
probes are never cached, so on a stopped distro N repos did pay N sequential 5s
blocking probes and now share one in-flight async probe.

Costs: five sync `computeWorkspaceRoot` callers remain (allowed-roots resolution,
the create click in worktree-remote, CLI create, watch targets, worktree trash),
and `getWslHome` reads only `wslHomeCache` -- it cannot join an in-flight async
probe. The guaranteed synchronous cache warm-up therefore becomes a window in
which one of those callers can still block and can spawn a second concurrent
`wsl.exe`. Concretely: opening the create composer and clicking Create within a
few hundred ms on a cold distro now pays the freeze on the click instead of on the
background prep. Separately, the mirror-distro fix makes prepare spawn an async
`wsl.exe` home probe for `C:\` repos on a WSL runtime, where it previously spawned
none.

No race added: `prepareWorktreeCreateForRepo` computes the preparation key and
inserts the registry entry in one synchronous run after the await, so two
concurrent creates still dedupe to a single prepared checkout.

`worktree-create-preparation-wsl-root.test.ts` runs the real resolver through
prepare and then claims the entry with the production consume-side call shape
(including the mirror distro), so a divergence between the two resolvers fails a
test instead of silently discarding every prepared checkout.
2026-08-31 21:11:09 -07:00

47 lines
2.0 KiB
TypeScript

import { mkdir } from 'node:fs/promises'
import type { GlobalSettings } from '../shared/global-settings-types'
import type { Repo } from '../shared/repo-types'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host'
import { isFolderRepo } from '../shared/repo-kind'
import { computeWorkspaceRootAsync, getWorktreePathSettings } from './ipc/worktree-logic'
import { getWorktreeMirrorDistro } from './project-runtime-git-options'
import type { ProjectRuntimeResolutionStore } from './local-project-runtime-resolution'
// `localWindowsRuntimeDefault` is listed because the mirror distro is resolved
// from it plus the project catalog: a store that omits it prepares a different
// root than the create path uses.
type WorktreeRootPreparationSettings = Pick<GlobalSettings, 'workspaceDir' | 'nestWorkspaces'> &
Partial<Pick<GlobalSettings, 'localWindowsRuntimeDefault'>>
type WorktreeRootPreparationStore = {
getSettings: () => WorktreeRootPreparationSettings
getRepos: () => Repo[]
getProjects?: ProjectRuntimeResolutionStore['getProjects']
}
export async function prepareLocalWorktreeRootForRepo(
store: Pick<WorktreeRootPreparationStore, 'getSettings' | 'getProjects'>,
repo: Repo
): Promise<void> {
if (getRepoExecutionHostId(repo) !== LOCAL_EXECUTION_HOST_ID || isFolderRepo(repo)) {
return
}
try {
const root = await computeWorkspaceRootAsync(
repo.path,
getWorktreePathSettings(repo, store.getSettings(), getWorktreeMirrorDistro(store, repo))
)
// Why: mkdir touches the current root to preflight macOS TCC, while
// access remains scoped by recomputed settings instead of a permanent grant.
await mkdir(root, { recursive: true })
} catch (error) {
console.warn(`[worktree-root] failed to prepare worktree root for ${repo.path}:`, error)
}
}
export async function prepareLocalWorktreeRootsForRepos(
store: WorktreeRootPreparationStore
): Promise<void> {
await Promise.all(store.getRepos().map((repo) => prepareLocalWorktreeRootForRepo(store, repo)))
}