Skip to content

fix(git): ignore read events in the git watcher (Linux refresh loop) - #375

Open
ik0zy wants to merge 1 commit into
pacifio:0.4.1from
ik0zy:ik0zy/368-git-watcher-read-loop
Open

ik0zy wants to merge 1 commit into
pacifio:0.4.1from
ik0zy:ik0zy/368-git-watcher-read-loop

Conversation

@ik0zy

@ik0zy ik0zy commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What?

The git metadata watcher (src-tauri/src/commands/git_watcher.rs) now ignores read-only filesystem events. A debounced batch that holds nothing but opens/closes/reads no longer flushes the refs cache, queues the capture commit walk, or emits atlas:git-changed.

Why?

On Linux this watcher drives a refresh loop that never settles.

  • notify 8 watches IN_OPEN on Linux and reports opens and closes as EventKind::Access(_). The callback (Ok(_events) => …) emitted atlas:git-changed for any event.
  • Every listener of that event runs git: the git store (status, log -100, diff HEAD, for-each-ref), the graph and diff panels, and every open editor tab (git diff for the gutter and git blame, editor-panel.tsx).
  • Each of those git processes opens .git/HEAD, .git/index and refs → access events → 200 ms later atlas:git-changed again → round and round.

macOS FSEvents doesn't report reads, which is why it never showed there.

On an idle Linux install with this repo open and no editor tabs, Atlas spawns a steady ~6–7 git processes a second, indefinitely. With editor tabs restored it becomes the blame storm in #368: the same file blamed ~20× concurrently, children respawning as soon as they're killed. The loop is what keeps respawning them. Every refresh also queued a capture commit walk, so the capture worker churned with it.

This is the same bug atlas-theme already fixed for theme files (is_theme_change: "Reads are not changes. On Linux inotify reports every open and close…"), and the fix follows the same rule.

No new dependencies, no telemetry changes.

How?

  • is_git_change(&notify::Event): EventKind::Access(_) → not a change, except Access(Close(Write)) (closing a file opened for writing), which is kept as belt and braces; nothing that only reads produces it. Everything else is a change, exactly as before.
  • The debouncer callback returns early unless at least one event in the batch is a change.
  • Real repository changes are unaffected. Commit, checkout, reset, merge, stage/unstage, fetch and pack-refs all write: a .lock file renamed into place, or a modify/remove of HEAD, index, packed-refs or a ref. So atlas:git-changed, the refs cache flush and capture's commit walk still fire for them. Capture's open-time backfill and the walk queued on bind don't depend on the watcher at all.
  • Working-tree edits never touched .git. Those reach the UI through atlas:explorer:changed (the fileindex watcher, which already filters event kinds), as on macOS.
  • Tests: reads_are_not_git_changes covers what git status produces on Linux (Open, Close(Read), Read, Any access). writes_are_git_changes covers create, modify (data, rename, any), remove, close-after-write and Any. I compiled both against notify 8.2.0 in isolation: both pass, and reads_are_not_git_changes fails with the filter removed.

CI can't exercise the loop itself: the app tests run on macOS, where no read events exist, and the Linux app job is clippy only. I'm checking it on Linux with a release build of this branch: git spawn rate when idle, panels still refreshing after commit / stage / checkout, and capture linking a new commit. I'll post the numbers here.

Refs #368. This removes the loop that keeps re-triggering the blame and diff refreshes. #368's other asks (dedupe blame per file, cancel on close, cap concurrent git children) are still worth doing as hardening, so I'm leaving the issue open.

Checklist

  • Targets the current version branch, unless it's a small standalone fix
  • New behaviour has a test; a bug fix has a test that fails without it
  • You've run the app and used the change in a window (Linux 0.4.1 build with this PR: lag gone, capture walk verified - see comments)

Copilot AI lite review requested due to automatic review settings October 7, 2026 04:07
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6b3f159f-fe79-4359-a8c8-d42bcf99ebe6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

No unresolved issues were identified, and the change includes focused test coverage.

Review effort: Lite
Findings: None

What changed in this PR

Updates the Linux Git watcher to ignore read-only filesystem events, preventing refresh loops and unnecessary Git work.

Changes:

  • Filters read-only access events.
  • Preserves write-related events.
  • Adds event classification tests.
File Description
src-tauri/​src/​commands/​git_watcher.rs Filters Git watcher events and adds unit tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ik0zy
ik0zy force-pushed the ik0zy/368-git-watcher-read-loop branch from fb4ff22 to dbe9b25 Compare October 7, 2026 13:47
Copilot AI lite review requested due to automatic review settings October 7, 2026 13:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

On Linux, inotify reports every open and close of the watched git
metadata (notify 8 watches IN_OPEN), and the watcher callback emitted
atlas:git-changed for any event. Every refresh that event triggers
(git status, log, diff HEAD, for-each-ref, plus diff and blame per open
editor tab) opens HEAD, index and refs, which fired the watcher again:
a git process storm that never settles. macOS FSEvents reports no reads,
so it never showed there.

Skip a debounced batch that holds nothing but access events, before the
refs cache flush, the capture commit walk and the emit. Real changes
(create, modify, rename, remove, close-after-write) behave as before.
Same rule atlas-theme's is_theme_change applies for the same loop.

Refs pacifio#368
@ik0zy
ik0zy force-pushed the ik0zy/368-git-watcher-read-loop branch from dbe9b25 to 6ec7cc3 Compare October 8, 2026 12:28
@ik0zy
ik0zy changed the base branch from 0.4.0 to 0.4.1 October 8, 2026 12:28
@ik0zy

ik0zy commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Verified on Linux (tryatlas-bin 0.4.1-1 built from 0.4.1 + #366 + this PR, Arch, 4 cores / 3.7GB RAM), this repo open with editor tabs:

Lag: new git children of tryatlas went from ~6-7/sec sustained (status + log -100 + diff HEAD + for-each-ref, plus blame/diff per editor tab) to ~0.3/sec, and the remainder is only git status (the 30s panel tick). No log/diff/blame/for-each-ref churn. Load average 14-24 -> ~3-4; tryatlas RSS 400MB+ -> ~70MB. The loop signature is gone.

Capture path: made a shell commit while capturing; the watcher fired, WalkCommits ran, found no linkable agent touches (correct - it was a human commit, and checkpoints require agent file touches), created no checkpoint, and advanced workspace_cursor to the probe commit with recovered=0. Commit then undone; the cursor re-baselines via the designed re-scan path. Full end-to-end checkpoint linkage needs an agent-made commit, which is unchanged code.

@drmikecrowe

Copy link
Copy Markdown

I hit the same loop on Linux, and this PR fixes the git half. There's a second watcher feeding it, though.

fileindex.rs filters event kinds in summarise_events, but the debouncer callback still emits atlas:explorer:changed for a batch of nothing but reads, just with dirs: []. On the frontend, project-git-store.ts treats empty dirs as "refresh every cached project" (runs git), and editor-panel.tsx revalidates every open tab on every event. Both read the tree, so the loop keeps going even with this PR applied.

Numbers from an idle window, counted at runCallback over 10s: 438 explorer:changed, 168 git-changed, 33 capture-changed. WebKit sat at 100-146% CPU and grew to 13.5 GB RSS before Atlas died.

My fix is the same rule as your is_git_change: an is_file_change predicate in the fileindex debouncer filter, plus reads_are_not_file_changes / writes_are_file_changes tests. With both fixes on a dev build, WebKit drops to about 10% CPU, RSS holds flat around 600 MB, and load went from 19 to 4.

Would you take a PR into your branch so both halves land together? If you'd rather keep this one scoped to git_watcher.rs, I'll open it separately against 0.4.1 and link it here.

@ik0zy

ik0zy commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

@drmikecrowe I also found the same issue in a recent profiling and wanted to fix it after this PR gets merged. Separate PR would be preferrable if you want to fix it.

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.

3 participants