Skip to content

perf(git): only schedule the upstream-ref poll for the repo that has one - #16443

Merged
nwparker merged 3 commits into
mainfrom
perf/git-status-ref-poll-on-demand
Aug 26, 2026
Merged

nwparker merged 3 commits into
mainfrom
perf/git-status-ref-poll-on-demand

Conversation

@nwparker

@nwparker nwparker commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​73 $\color{#cf222e}{\Huge{\mathbf{−}}}$​11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​62
Prod 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​89 $\color{#cf222e}{\Huge{\mathbf{−}}}$​11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​78

Background: what "holding a ref" means

To show ahead/behind for a branch, Orca has to notice when the branch's upstream ref file changes — e.g. .git/refs/remotes/origin/main moving when someone else pushes. So it watches that one file.

It only does this for one worktree at a time: the one whose ahead/behind is actually on screen. That's activeGitStatusRefBinding — a single module-level value in worktree-git-status-ref-watch.ts, set as you move around the UI:

let activeBinding: ActiveGitStatusRefBinding | null = null

applyActiveGitStatusRefBinding() then gives that one worktree a one-element set of paths to watch, and clears it for everyone else:

watch.gitStatusRefPaths.clear()
if (activeBinding && watchMatchesBinding(watch, activeBinding)) {
  watch.gitStatusRefPaths.add(resolveRuntimePath(watch.path, activeBinding.upstreamRef))
}

So "repo N holds a ref" just means: N is the one worktree currently being shown, so it has one upstream ref file worth watching. Every other repo's set is empty.

The bug

Every repo scheduled its own 2-second poll over that set — including the repos whose set is empty. Those pollers woke twice a second, allocated a Set, awaited Promise.all([]), diffed two empty maps, and went back to sleep. With 100 repos open, 99 of them were doing this forever and could never find anything, because a repo only gets a path when it becomes the active one.

The fix

Only schedule the poll for the repo that actually has a path, and start/stop it from the rebinding event rather than from a timer.

This costs nothing in detection latency. Rebinding is synchronous and in-process — applyBindingToWatches() runs when the active worktree changes — so a newly selected ref starts its poll on the same tick it would previously have been noticed. The wake-ups weren't a safety margin; they were pure waste. The repo that does hold a ref polls exactly as before.

Measured

100 repos, none holding a ref, 20s idle window:

before (always scheduled)  0.811 CPU-ms/s
after  (demand driven)     0.320 CPU-ms/s

The 0.320 is the measurement harness itself, so the poll's own cost effectively goes to zero.

Test changes, and why

  • Two listener-count assertions drop by one in cases where no ref is selected. That decrease is the saving — one fewer poller parking on window visibility.
  • One re-arm assertion now waits for reconciliation to settle instead of assuming a fixed fake-clock advance. Removing a poller removed incidental async activity that the old bare assertion had been relying on to let real filesystem promises settle. The sibling test in the same file already used vi.waitFor for exactly this reason; this makes the two consistent.

Follows #16404. Independent of the metadata poll scheduler.

@coderabbitai

coderabbitai Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The status-ref watch now exposes binding-change subscriptions and notifies listeners after applying active bindings. The common primary watch tracks polling generations, rejects stale or invalid status-ref pollers, and unsubscribes during teardown. Tests cover repeated binding changes, updated visibility listener counts, disposal cleanup, and asynchronous filesystem reconciliation.

Merge Risk: 🔵 Low · up to 05c21

This change makes upstream-ref polling demand-driven, but an unresolved teardown path may leave a poller running after watcher failure. The PR is mergeable with explicit owner awareness or follow-up on that bounded runtime/resource risk.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the background, bug, fix, performance result, and test updates. However, it does not follow the required template and omits the Visual Proof, Testing, Review, Agent skill upst… Add the missing template sections. Include N/A with a reason for Visual Proof if there is no visual or interaction change. Document verification steps, tested platforms, and automated test results. Complete the Review, Agent skill upstrea…
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: making Git upstream-ref polling run only for the repository that has an active ref.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the background, bug, fix, performance result, and test updates. However, it does not follow the required template and omits the Visual Proof, Testing, Review, Agent skill upstream boundary, Notes, and Checklist sections.

Resolution

Add the missing template sections. Include N/A with a reason for Visual Proof if there is no visual or interaction change. Document verification steps, tested platforms, and automated test results. Complete the Review, Agent skill upstream boundary, Notes, and Checklist sections. Link the issue using the required issue-link format.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4494b0f8-c5fd-428b-b177-256576057bf6

📥 Commits

Reviewing files that changed from the base of the PR and between 4218d50 and 9bf9673.

📒 Files selected for processing (3)
  • src/main/ipc/worktree-git-common-primary-watch.ts
  • src/main/ipc/worktree-git-common-watch.test.ts
  • src/main/ipc/worktree-git-status-ref-watch.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/ipc/worktree-git-common-primary-watch.ts (1)

176-176: ⚠️ Potential issue | 🟠 Major

Serialize initial poller adoption with binding reconciliation.

The initial startStatusRefPollingIfSelected() call can overlap syncStatusRefPolling(). If bindings change during setup, Line 199 can overwrite a poller adopted by syncStatusRefPolling, or adopt a poller after selection becomes empty. The lost poller remains active after teardown. Route both paths through one reconciliation mechanism, or recheck selection and ownership before assigning statusRefPolling and unsubscribe the losing poller.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e6b6f09-68e7-4323-988c-1b2cfce4636b

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf9673 and 3ca0d5e.

📒 Files selected for processing (1)
  • src/main/ipc/worktree-git-common-primary-watch.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/ipc/worktree-git-common-primary-watch.ts (1)

46-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shorten the new explanatory comments.

Keep each comment to one short statement of intent.

  • src/main/ipc/worktree-git-common-primary-watch.ts#L46-L50: reduce the generation ownership explanation to one line.
  • src/main/ipc/worktree-git-common-primary-watch.ts#L89-L92: reduce the adoption rule explanation to one line.
  • src/main/ipc/worktree-git-common-primary-watch.ts#L205-L207: reduce the startup race explanation to one line.

As per coding guidelines, **/*.{ts,tsx,js,jsx} must “DO NOT: be verbose, explain the obvious, walk through the code.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 32f9bdbe-828e-41ae-8268-2e9d598a1d67

📥 Commits

Reviewing files that changed from the base of the PR and between 3ca0d5e and 8bddbd9120f4f6f9dbb7ed226a2251f3d03543e1.

📒 Files selected for processing (2)
  • src/main/ipc/worktree-git-common-primary-watch.ts
  • src/main/ipc/worktree-git-common-watch.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Comment on lines +217 to 218
await adoptStatusRefPolling(generation, nextStatusRefPolling)
backstopPolling = nextBackstop

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Assign the backstop before the awaited adoption.

If watcher teardown occurs during await adoptStatusRefPolling(...), teardown sees backstopPolling === null. Line 218 then stores nextBackstop after teardown, leaving its timer and visibility listener active.

Proposed fix
-        await adoptStatusRefPolling(generation, nextStatusRefPolling)
         backstopPolling = nextBackstop
+        await adoptStatusRefPolling(generation, nextStatusRefPolling)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await adoptStatusRefPolling(generation, nextStatusRefPolling)
backstopPolling = nextBackstop
backstopPolling = nextBackstop
await adoptStatusRefPolling(generation, nextStatusRefPolling)

A single global binding means at most one worktree holds a selected upstream
ref at a time, so every other repo's 2s poll woke only to stat an empty set.
Rebinding is synchronous and in-process, so reacting to it detects a newly
selected ref exactly as fast as polling did — the wake-ups were pure waste.

Measured at 100 repos with no selected ref: 0.811 -> 0.320 CPU-ms/s idle.
A rebind can flip back while the poller is being constructed. The concurrent
unbind sees statusRefPolling still null and correctly does nothing, so without
re-reading selection the in-flight build adopts a poller for a repo that no
longer holds a ref — reinstating the idle wake-ups this change removes.
…token

Startup and each rebind could be mid-build at once, and the startup path
adopted its poller unconditionally. A rebind that won the slot first was then
overwritten without being unsubscribed, stranding that poller's timer and
visibility listener for the process lifetime. The generation names which
attempt still owns the slot so every loser tears down what it built.

Adds a regression test driving the real binding path; it fails if the poller
is dropped without unsubscribing.

Reported by CodeRabbit on #16443.
@nwparker
nwparker force-pushed the perf/git-status-ref-poll-on-demand branch from 8bddbd9 to 05c21a1 Compare August 26, 2026 01:45
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/ipc/worktree-git-status-ref-watch.ts (1)

77-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shorten the rationale comment.

Keep this explanation to one line where possible.

Proposed revision
-// Why: at most one worktree holds a selected upstream ref at a time, so every
-// other repo's ref poll would wake only to stat nothing. Publishing the
-// transition lets those pollers stay unscheduled until they actually have a ref,
-// with no detection delay — rebinding is an in-process event, not a timer.
+// Notify pollers immediately because rebinding is synchronous and timer-free.

As per coding guidelines, **/*.{ts,tsx,js,jsx}: “DO NOT: be verbose; BE CONCISE. 1 LINE if possible.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ec371e4-1da7-4779-b218-85af61c6fea4

📥 Commits

Reviewing files that changed from the base of the PR and between 91a5007 and 05c21a1.

📒 Files selected for processing (3)
  • src/main/ipc/worktree-git-common-primary-watch.ts
  • src/main/ipc/worktree-git-common-watch.test.ts
  • src/main/ipc/worktree-git-status-ref-watch.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/ipc/worktree-git-common-primary-watch.ts
  • src/main/ipc/worktree-git-common-watch.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

@nwparker
nwparker merged commit 5a8b4af into main Aug 26, 2026
48 checks passed
dallascrilley pushed a commit to dallascrilley/orca that referenced this pull request Aug 27, 2026
…one (stablyai#16443)

* perf(git): only schedule the upstream-ref poll for the repo that has one

A single global binding means at most one worktree holds a selected upstream
ref at a time, so every other repo's 2s poll woke only to stat an empty set.
Rebinding is synchronous and in-process, so reacting to it detects a newly
selected ref exactly as fast as polling did — the wake-ups were pure waste.

Measured at 100 repos with no selected ref: 0.811 -> 0.320 CPU-ms/s idle.

* fix(git-watch): re-read ref selection after building the poller

A rebind can flip back while the poller is being constructed. The concurrent
unbind sees statusRefPolling still null and correctly does nothing, so without
re-reading selection the in-flight build adopts a poller for a repo that no
longer holds a ref — reinstating the idle wake-ups this change removes.

* fix(git-watch): fence concurrent ref-poller starts with a generation token

Startup and each rebind could be mid-build at once, and the startup path
adopted its poller unconditionally. A rebind that won the slot first was then
overwritten without being unsubscribed, stranding that poller's timer and
visibility listener for the process lifetime. The generation names which
attempt still owns the slot so every loser tears down what it built.

Adds a regression test driving the real binding path; it fails if the poller
is dropped without unsubscribing.

Reported by CodeRabbit on stablyai#16443.
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 28, 2026
…one (stablyai#16443)

* perf(git): only schedule the upstream-ref poll for the repo that has one

A single global binding means at most one worktree holds a selected upstream
ref at a time, so every other repo's 2s poll woke only to stat an empty set.
Rebinding is synchronous and in-process, so reacting to it detects a newly
selected ref exactly as fast as polling did — the wake-ups were pure waste.

Measured at 100 repos with no selected ref: 0.811 -> 0.320 CPU-ms/s idle.

* fix(git-watch): re-read ref selection after building the poller

A rebind can flip back while the poller is being constructed. The concurrent
unbind sees statusRefPolling still null and correctly does nothing, so without
re-reading selection the in-flight build adopts a poller for a repo that no
longer holds a ref — reinstating the idle wake-ups this change removes.

* fix(git-watch): fence concurrent ref-poller starts with a generation token

Startup and each rebind could be mid-build at once, and the startup path
adopted its poller unconditionally. A rebind that won the slot first was then
overwritten without being unsubscribed, stranding that poller's timer and
visibility listener for the process lifetime. The generation names which
attempt still owns the slot so every loser tears down what it built.

Adds a regression test driving the real binding path; it fails if the poller
is dropped without unsubscribing.

Reported by CodeRabbit on stablyai#16443.

(cherry picked from commit 5a8b4af)
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…one (stablyai#16443)

* perf(git): only schedule the upstream-ref poll for the repo that has one

A single global binding means at most one worktree holds a selected upstream
ref at a time, so every other repo's 2s poll woke only to stat an empty set.
Rebinding is synchronous and in-process, so reacting to it detects a newly
selected ref exactly as fast as polling did — the wake-ups were pure waste.

Measured at 100 repos with no selected ref: 0.811 -> 0.320 CPU-ms/s idle.

* fix(git-watch): re-read ref selection after building the poller

A rebind can flip back while the poller is being constructed. The concurrent
unbind sees statusRefPolling still null and correctly does nothing, so without
re-reading selection the in-flight build adopts a poller for a repo that no
longer holds a ref — reinstating the idle wake-ups this change removes.

* fix(git-watch): fence concurrent ref-poller starts with a generation token

Startup and each rebind could be mid-build at once, and the startup path
adopted its poller unconditionally. A rebind that won the slot first was then
overwritten without being unsubscribed, stranding that poller's timer and
visibility listener for the process lifetime. The generation names which
attempt still owns the slot so every loser tears down what it built.

Adds a regression test driving the real binding path; it fails if the poller
is dropped without unsubscribing.

Reported by CodeRabbit on stablyai#16443.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant