Skip to content

diff-sync: stop reconcile churn spawning git diff back-to-back forever - #127

Open
danperks wants to merge 1 commit into
zeronsh:mainfrom
danperks:comet/diff-sync-churn-fix
Open

diff-sync: stop reconcile churn spawning git diff back-to-back forever#127
danperks wants to merge 1 commit into
zeronsh:mainfrom
danperks:comet/diff-sync-churn-fix

Conversation

@danperks

@danperks danperks commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

What happened

On an idle checkout, the engine spawned git diff back-to-back forever — each pass burning ~100% of a core for 1–3s, a new one starting the moment the last exited. Enough to starve the machine (WindowServer at 99%, UI hover lag). Nobody was editing files in the checkout.

Root cause

Two mechanisms combined into a self-sustaining loop:

  1. reconcile runs on every workspace chat row change — including the branch and checkoutId writes sync_entry itself makes — and resolved each chat's checkout identity by spawning git rev-parse twice. Every publish fanned out into a storm of git spawns.
  2. Under that fd pressure, one transient spawn failure (EMFILE) made the chat ungroupable, so reconcile tore its entry down and re-added it on the next pass. Every re-add kicks a full capture, whose row writes trigger the next reconcile.

Teardown discards the entry's checksum state, so the re-capture always republishes, which always triggers the next reconcile. On a large checkout each pass costs seconds of CPU, so the loop never runs out of work.

Separately, watcher setup ran inline on the runtime: the budget walk reads up to MAX_WATCH_DIRS directory entries and FSEvents registration stalls for seconds under contention, starving workspace watches and presence for every checkout whenever entries were rebuilt.

The fix

Damp reconcile rather than treat the symptom — the apply_numstat comment records a previous fix for the same runaway symptom, so treating symptoms here is a known failure mode.

  • Memoize identities per cwd. Chat-watch reconciles spawn no git at all; the repair tick revalidates. A failed fresh resolve keeps the memo unless the directory is actually gone.
  • Orphan grace period. An entry whose chats vanish is only marked; teardown happens after a full REPAIR_INTERVAL of continuous absence. A single flapping chat-watch emission can no longer destroy a live entry.
  • Serialize reconcile passes. Concurrent passes could both observe a checkout as missing and both add_entry it, the second insert silently discarding the first's checksum state.
  • Watcher setup moves to the blocking pool in both diff-sync and spaces, attaching to the entry when ready with a kick to close the capture→attach gap.

Repair tick, watch budget bail-out and checksum-suppressed publishing are unchanged. Nothing is special-cased by path or file count — a pathological checkout made this visible, but any checkout can trigger it.

Testing

crates/engine/tests/diff_sync_churn.rs covers all three dampers. Verified they actually pin the fix by mutating each damper away: with pre-fix behaviour restored, entry_survives_transient_identity_failure fails with exactly the incident signature (updated_at moves — the entry was torn down and re-captured), and chat_flap_keeps_entry_and_sustained_absence_removes_it fails on the entry being removed after a single missed pass. deleted_checkout_is_evicted_after_grace passes both ways by design: it guards that the dampers don't make legitimate eviction impossible.

Full cargo test -p zeron-engine is green (all suites, including 20/20 in m5_repos_diffs_terminals). cargo fmt --check clean; clippy adds no new warnings in the touched files.

One test deadline moved: diff_sync_publishes_and_updates_chat_branch's watcher-path wait went 20s → 150s. FSEvents is permitted to drop events, and a dropped one converges only on the 120s repair tick, so the old deadline read normal event loss as failure.

What this doesn't cover

  • A checkout whose capture legitimately costs seconds still costs that on every real change; this removes the spurious passes, not the cost of a genuine one.
  • Identity memoization means a checkout's identity changing underneath us (a worktree repointed at a different repo) is picked up on the repair tick rather than instantly.
  • The blocking-pool move bounds runtime starvation but the budget walk itself is unchanged.

Made with Cursor


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

An idle checkout could pin a core indefinitely: reconcile runs on every
workspace chat row change — including the branch/checkoutId writes
sync_entry itself makes — and resolved each chat's checkout identity by
spawning git. Under that fd pressure a transient spawn failure made the
chat ungroupable, so reconcile tore the entry down and re-added it next
pass, and every re-add kicks a full capture whose row writes trigger the
next reconcile. On a big checkout each pass costs seconds of CPU.

Damp reconcile rather than treat the symptom: memoize identities per cwd
(chat-watch passes spawn no git, the repair tick revalidates), tear an
entry down only after a full grace period of continuous absence, and
serialize passes so two of them can't both add the same entry.

Watcher setup also ran inline on the runtime, where the budget walk and
FSEvents registration block for seconds and starved workspace watches and
presence for everyone. It moves to the blocking pool in both diff-sync and
spaces, attaching to the entry when ready with a kick to close the gap.

Repair tick, watch budget bail-out and checksum-suppressed publishing are
unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
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