Skip to content

watch the project folder by folder on linux, so a clangd restart no longer freezes the backend - #161

Merged
irrld merged 2 commits into
mainfrom
agent/linux-watch
Oct 4, 2026
Merged

irrld merged 2 commits into
mainfrom
agent/linux-watch

Conversation

@cogwright

Copy link
Copy Markdown
Collaborator

On Linux the backend froze for up to half a minute. Both freezes in irrl's log follow a clangd restart:

  • 11:17, game_martyr, nothing built or run. A startup configure regenerated the compile commands, clangd restarted at 11:17:52, and the backend answered nothing until about 11:18:22 (gitStatus 34.9 s, readFile 29.3 s, ping 14.5 s).
  • 13:24, "test". Run built the project, clangd restarted with the new compile commands at 13:24:51, and the backend went silent from about 13:25:02 until Repair IDE killed it at 13:25:16.

#159 fixed a real stall when a program floods the Output panel, but it was not this one.

Why

The backend watches the project folder so the Commit view and the explorer follow changes on disk. It used fs.watch(root, { recursive: true }). On Linux, Node implements that in JavaScript (internal/fs/recursive_watch, unchanged in Electron 44's Node 24.21), synchronously on the event loop. It:

  • puts a watch and a statSync on every file;
  • reads a whole folder again with readdirSync on each change inside it;
  • on each deleted file, walks the map of every watched file.

clangd restarted with --background-index rewrites its index in _build/<type>/.cache/clangd/index, inside the watched tree. Real Glist projects have about 1,940 index files there (BouncingBalls 1,909, NeonDodger 1,942), and each is written as a temporary file renamed over the old one. The _build and .cache filter only ran on the events afterwards, so it saved none of this work.

What changed

  • src/tree-watch.ts: on Linux, one watch per folder, never per file. Ignored folders are never entered. New and deleted folders are found with async readdir/lstat. Symbolic links are not followed, so a link to a folder above can't loop.

    • macOS and Windows keep the system's own recursive watching, which is cheap there.
    • A full watch table (ENOSPC), or a folder that vanished, is skipped. The focus and save refreshes still work.
  • src/git-service.ts uses it. What is left out (ignoredInProject):

    • _build, node_modules and .cache at the project's top;
    • .git/objects and .git/logs.

    For a repository folder kept outside the project, its objects and logs are left out (ignoredInGitDir). Changes still arrive as one debounced git:changed, as before.

Measured

In a Linux container under Electron 44's Node 24.21, a separate process wrote into the project the way clangd and a file manager do:

Backend held, today With this
clangd rewrites 1,940 index files 12,100 ms 7 ms
2,000 files copied into assets/ 5,177 ms 29 ms
Kernel watches 2,560 to 6,500 9

With an engine-sized build folder (10,000 files) beside the index rewrite, today's watcher held the backend for 28.6 s, about what the log shows. It also slowed the writing process itself, from 0.1 s to 28.7 s, so builds and indexing were being slowed too.

I also tried Node's own ignore option for fs.watch. It fixes the index case (5 ms), but copying 2,000 assets still held the backend for 5.5 s, so a game project's asset folders needed the folder-by-folder watch anyway.

Tests

  • tests/tree-watch.test.mjs, on every OS:

    • the ignore rules;
    • changes reported by path, in folders made after the watch started too;
    • a folder deleted and made again is watched again;
    • nothing from _build, clangd's index or Git's objects;
    • nothing after closing.

    It runs both ways on macOS, by folder on Linux, and recursive on Windows, where a watched folder can't be deleted.

  • On Linux, the same test also measures the stall: with real inotify, it rewrites 1,940 index files and copies 2,000 assets from a separate process, and checks that the backend is never held for 500 ms or more and that there are at most 20 kernel watches. It passes in the container under both Electron's Node 24 and Node 22, as CI's Ubuntu runner will run it. Pointed at today's recursive watch, it fails: 1,962 watches, and the backend held for 16.7 s.

  • tsc, lint, npm test and all 25 e2e flows pass on macOS. The flows pass again with the folder-by-folder watcher forced on, which exercises it through the explorer and Commit view.

Not verified

  • The packaged Linux app on irrl's machine. The mechanism is measured in the same Node, and both freezes in the log fit it, but the fix only shows once it's run there.
  • The browser build and CI run Node 22. Its recursive watch is the same JavaScript, and this change covers it too.

🤖 Generated with Claude Code

@irrld
irrld merged commit 73e9792 into main Oct 4, 2026
4 checks passed
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.

2 participants