Commit Graph
10 Commits
Author SHA1 Message Date
Neil e13631ee53 Prioritize workspace opening over replacement checkout preparation (#23013)
* Prioritize workspace opening over replacement checkout preparation

* Preserve Git hook semantics and exercise preparation edge cases
2026-09-25 21:31:25 -07:00
Jinwoo Hong 52b6851b6e fix(worktree-create): prioritize creation Git and defer background preparation (#20722)
* fix(worktree-create): run create git commands at interactive tier, defer pool side jobs, bound queue wait by timeout

Creating a worktree on a busy machine stalled for minutes because the create's
own git competed for the same admission budget as everything else.

- The create path never set an admission tier, so it defaulted to 'status' and
  could never use the scheduler's headroom slots. It now tags the option objects
  that reach git directly: the add, the post-add listing, the base-ref probes and
  the prepared-checkout finalize. The speculative warm-up and the SSH path are
  unchanged.
- The prepared-pool re-arm is a full `reset --hard`; it ran mid-create and held a
  general slot. `consumePreparedWorktreeCreate` now returns it as a thunk the
  create runs after the startup terminal is spawned. Stale-preparation
  reclamation (`worktree unlock` / `worktree remove`) drops to 'background'.
- A command's timeout only armed once its child spawned, so a saturated queue
  could hold a 1s command indefinitely. Admission now takes the same deadline and
  raises GitCommandTimeoutError without spawning; a caller abort still reports as
  an abort.

The tier is kept off the `{ wslDistro }` routing objects: several callers test
those for emptiness to decide whether a repo has local git routing at all.

* fix(worktree-create): keep a bounded queue wait from reading as an absent base ref

The admission deadline added in the previous commit made every create-path probe's
15s/120s budget cover the queue wait. The default-base and worktree-base probes answer
`false`/`null` for any failure, so a saturated queue reported a repo that has origin/main
as having no default base and the create refused to start. Both probe families now let
`GitCommandTimeoutError` through, and the branch-name resolution loop, the push-target
configuration and the post-add listing run at the create's interactive tier so they reach
the headroom the rest of the create already uses.

Also: the deferred pool re-arm re-checks the pool inside the thunk, since `startPreparation`
replaces a map entry outright and would strand a prefetch's locked checkout with no owner;
the shared worktree scan keys on the tier so an interactive listing cannot inherit a queued
status scan's wait; and the deadline's microtask hop is gone, along with two fake-timer
`vi.waitFor` calls that jumped the clock past a 10ms budget before the grant settled.

* fix(worktree-create): preserve probe fallbacks and defer runtime replenishment

* fix(worktree-create): preserve interactive priority through prepared claims

* fix(worktree-create): prioritize CLI creation and preserve SHA probe timeouts

* fix(worktree-create): scope Git execution policy at creation boundaries

* fix(worktree-create): preserve inconclusive Git probe timeouts

* test(runtime): align creation fixtures with scoped Git execution

* test(native-chat): extract windowing layout fixture to satisfy file limit

* fix(git): restore execution-only timeouts while queued

* refactor(worktree-create): remove unrelated error-handling changes

* chore: narrow review scope and clarify preparation timing

* test(native-chat): restore fixture extraction to fix CI lint

* refactor(git): keep the admission scheduler in its original module

Reverts a move-only extraction. Inlines the single-use command-class
wrapper so the tier-resolution import fits the file's line budget.

* fix(worktrees): re-arm the prepared pool after CLI create launches terminals

The runtime create fired the pool re-arm right after materialization, so its
`reset --hard` competed with the startup agent's first git reads. Return the
thunk to the caller and fire it last, matching the desktop path.

* fix(worktrees): skip a preparation whose checkout is still running

An interactive create that claimed an in-flight preparation awaited a checkout
queued at background, so on a saturated budget it yielded to every arriving
status poller until aging promoted it. The create now misses with not_ready and
does its own add at interactive; the preparation stays armed for the next one.

Also drops the one-field policy object from the Git operation executor.

* fix(worktrees): report repo_mismatch before not_ready when selecting a preparation

The readiness filter ran before the same-repo check, so another repo's
in-flight preparation was labeled not_ready instead of repo_mismatch, hiding
the cap-thrash signal for multi-project users. The hit/miss decision is
unchanged.

* test(runtime): type the worktree-meta stub against WorktreeMeta

Main now rejects bare object parameters, and the merge picked that rule up.

* fix(worktrees): wait on in-flight preparations and re-arm the pool on failed creates

A create landing mid-checkout now claims the in-flight preparation and awaits it, as main
did. The `checkoutFinished` filter and its `not_ready` miss reason made the create skip a
prepared checkout that was seconds from done and pay a full cold add instead; on a 40k-file
repo that turned a 0.2-1.5s create into 2.4-4.3s. The preparation's own git also runs at
`status` again rather than `background`, so awaiting it does not park behind status pollers.
Only the stale reclaim stays `background`, which no create waits on.

The deferred pool re-arm now fires on every path, not just the success path. Main armed the
replacement synchronously inside the consume, so a later failure in include copy, push-target
setup, or terminal startup still left one warming. The thunk stays deferred until after
terminal startup for admission ordering, but a `finally` on the desktop create and matching
failure-path fires on the runtime create restore that guarantee. It fires exactly once.

* refactor(runtime): carry the pool re-arm in one holder

The runtime create used three mechanisms to guarantee the deferred pool re-arm fires: a
catch in the git create, a catch on materialization, and a holder fired in the managed
create's finally. The desktop create already used one holder for the same guarantee.

The holder now threads down through the create args, so the git create arms it at the point
it consumes a prepared checkout and nothing below has to handle the failure case. The thunk
already re-checks the pool before arming, so a single fire point in the outermost finally
covers every failure after the consume. Behavior is unchanged; both flipped failure-path
tests still assert exactly one fire, and each fails without the production change.
2026-09-15 19:02:50 -04:00
Neil a63a4579cf Let worktree creation proceed during stale preparation reclamation (#18967)
* Stop obsolete worktree preparations when evicted or expired

* Let worktree preparation proceed during stale reclamation

* Verify creation during stalled stale worktree reclamation

* Preserve preparation ownership until Git removal starts

* test: keep artifact share fixtures unexpired across calendar dates (#18955)
2026-09-05 22:30:01 -07:00
Neil e2270fe94d Stop evicted and expired worktree preparations (#18951)
* Stop obsolete worktree preparations when evicted or expired

* fix(worktree): skip discard retries for registrations an aborted checkout already removed

An evicted or expired preparation now aborts its checkout, which self-discards
the registration before the pool's own discard runs. That second discard failed
with "is not a working tree" and was enrolled for up to three retries on later
preparations for the same host, spawning Git only to fail again and warning that
the path stays registered when it was already gone.

Also accept fs.watch events without a filename in the abort real-Git test, and
add an opt-in bench (ORCA_WORKTREE_PREPARATION_CANCEL_BENCH=1) that measures a
fresh checkout's wall time with obsolete checkouts left running versus aborted.
2026-09-05 22:16:57 -07:00
Neil 6c8eea5ebe perf(worktree): fix the prepared-checkout hit rate and make misses visible (#17863) 2026-09-01 20:26:00 -07:00
Neil 3777070eaf fix(worktree): restore the stale-cleanup signal after the module split (#18058)
* fix(worktree): restore the stale-cleanup signal after the module split

Moving stale-preparation cleanup into its own module took
`staleCleanupInFlight` with it, but `hasPendingWorktreeCreatePreparations`
still read it directly. Both sides were green in isolation — the reference
arrived on main while the split was in review — so the break only appeared
once they merged, and it fails typecheck for every branch built on main.

Expose the predicate from the module that owns the map, and cover the
signal with a test so the idle gate's "a create is imminent" answer cannot
silently regress again.

* test(worktree): anchor the pending-signal test on the scan, not on await depth
2026-09-01 20:08:09 -07:00
Neil e89321192a perf(worktree): batch remote conflict probes, re-arm the prepared checkout (#17829)
* perf(worktree): batch remote conflict probes, re-arm the prepared checkout

A repo with many remotes paid one `git show-ref --verify` subprocess per
remote on every branch-conflict check during create. Ask one
`git cat-file --batch-check` over stdin instead; it reports a missing ref
as data rather than a failed exit, so a batch stays as decidable as the
per-ref probe. Hosts that cannot feed stdin, and undecided batches, still
fall back to the per-ref path.

The prepared checkout was single-use, so the second create in a row paid
the full cold `git worktree add`. Re-arm it in the background after one is
consumed; the existing TTL and preparation limit still bound it.

The create timing recorder existed but its phases were never emitted and
did not cover preflight, leaving a multi-second gap in the trace with no
attribution. Add `resolve_name`/`prepare_push_target` phases and record the
breakdown, plus the unattributed remainder, on the create span.

* fix(worktree): format the conflicting review number eagerly for the create error

* perf(worktree): re-arm a prepared checkout only for a burst of creates

Re-arming after every consumed preparation spends a full checkout and
~200MB of disk on a user who created one worktree and stopped, then pays
an unexplained delete when the TTL expires five minutes later. Track when
each preparation key was last consumed and only replace it when a second
create lands inside the burst window, so the warm second create is still
free and an isolated create costs nothing.

* fix(worktree): address review findings on the create-path batching

Three findings from PR review:

The `batched.found` fallback in the remote-conflict probe was unreachable
— a present ref is decisive, so `found` never survives with `unknown`
set, and the guard above already returns that case.

`rearmPreparation` checked for an existing preparation before recording
the consume, so a prefetch that re-armed the key while create finalized
swallowed the timestamp and made the next create look isolated when it
was really mid-burst.

Create runs some phases concurrently, so summing phase durations
double-counted overlap and understated `unattributed_ms` — the one
number that matters when a create is slow for no visible reason. Measure
the union of the phase intervals instead.

* refactor(worktree): move stale-preparation cleanup into its own module

The preparation module crossed the 300-line budget. Crash recovery is a
separate concern from the pool itself — it discards preparations another
process left registered, single-flighted per repo and runtime so a burst
of arming calls shares one worktree listing.

* test(worktree): make the re-arm test able to fail

The burst test armed a preparation manually after the second consume, so
the third checkout appeared whether or not the re-arm produced it — the
assertion passed with re-arming disabled. Drop that arming call so the
third checkout can only come from the re-arm, and assert the consume
results rather than discarding them.
2026-09-01 19:01:59 -07:00
Neil f2db24bca0 fix(worktree): reclaim a prepared checkout whose discard failed in-process (#17899)
Speculative create-preparation evicts entries past the 3-entry limit and the
5-minute TTL, and both paths swallowed a failed `discardPreparedWorktree`.
The only other reclaim path, `cleanupStalePreparations`, skips any preparation
whose lock-reason pid is still alive, so a discard that failed inside the
running app stranded its scratch checkout and its locked worktree registration
until restart.

Record the failed discard keyed by host (repo path + WSL distro) and prepared
path, and retry it the next time a fresh preparation starts for that host.
The retry is kicked off before `listWorktreeGraph` but never awaited, so it
runs alongside the stale scan and the `worktree add` instead of sitting in
front of the user's create. It is capped at 3 attempts and warns when it gives
up. Enrolment is unconditional: `prepareWorktreeCreateCheckout` self-discards
on failure, but only best-effort, so a checkout that failed on a busy handle
can strand the same registration.
2026-09-01 02:39:03 -07:00
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
Neil 3ab9766e38 perf(worktree): prepare checkouts while the composer is open
Squashed merge of PR #17290.
2026-08-30 12:12:04 -07:00