anomalyco / anomalyco/opencode

fff watcher: GitStatusCache loops even after file changes stop, resulting in 100% cpu usage

Open
#42,557 0 comments 1 reaction 1 assignee View on GitHub

@rekram1-node is already working on this.

Since Aug 14, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
Avg merge
7h 2m
Merged PRs (30d)
384

Description

Description
Summary

An idle opencode session pins one core at ~100% for many minutes. The hot thread is fff-watcher-own. For every batch of file-watcher events it opens the git repository, computes git status, and frees the repository again. A fresh Repository::open per batch means libgit2 re-reads and re-parses the whole 6.3 MB index every cycle — including a SHA-1 hash over the full file — so each cycle costs hundreds of milliseconds. The event queue drains slower than build tools produce events, and the thread keeps burning CPU long after all file writes stop.

The events that trigger this are all gitignored paths: a Gradle daemon writing .gradle/9.6.0/fileHashes/fileHashes.bin, lock files, and executionHistory.bin. A normal Gradle build writes thousands of files under build/ and .gradle/, so on Android/JVM projects every build re-pins the core.

Observed
  • Process at ~100% CPU from launch: 4:30 CPU time in 4:37 elapsed. ps -M shows one thread at 98.6%; all others idle.
  • Verified with fd --changed-within 60s: zero file modifications in the repo in the previous minute. A fresh sample in the same minute still shows the watcher thread at full load in the same stacks, still inside FilePicker::handle_file_modify — it is draining a backlog, not reacting to live events.
  • .git/index mtime unchanged throughout, so this is not a feedback loop through the index file.
Stack evidence

5-second sample(1) of the process, fff-watcher-own thread (4159 samples total, 3433 in the watcher loop; trimmed, full files attached):

4159 Thread: fff-watcher-own
  3433 thread main loop
  ├─ 1917 fff_search::git::GitStatusCache::git_status_for_paths (+432)
  │    └─ 1916 git2::repo::Repository::status_file
  │         └─ 1670 git_status_file
  │              └─ 1383 git_repository_index__weakptr → git_index__open → git_index_read
  │                   ├─ 504 parse_index → git_hash_buf → SHA1DCUpdate      ← hashes the whole 6.3 MB index
  │                   ├─ 492 parse_index → git_index_entrymap_put           ← mostly __tolower in put/resize
  │                   └─ 313 parse_index → index_entry_dup → git_path_str_is_valid (next_hfs_char, strncasecmp)
  ├─ 1516 fff_search::git::GitStatusCache::git_status_for_paths (+1384)
  │    └─ read_status_impl → git2::repo::Repository::statuses → git_status_list_new
  ├─  459 git_repository_free
  ├─  182 git2::repo::Repository::open
  ├─   39 fff_search::file_picker::FilePicker::handle_file_modify
  └─   32 git2::repo::Repository::is_path_ignored

Reading of the numbers:

  • Repository::open (182) and git_repository_free (459) both show up hot → the repository handle is not kept alive between event batches.
  • Because of that, git_status_file hits git_index__opengit_index_readparse_index on nearly every call: 1383 of 1670 samples inside git_status_file are index re-parsing, dominated by SHA1DC hashing of the index file and per-character tolower while building the case-insensitive entry map.
  • On top of the per-path status_file calls, read_status_impl also runs a full git_status_list_new worktree scan (1516 samples).
Why this hurts

Cost per event batch on this repo: full index parse (SHA-1 over 6.3 MB + 28k-entry map build) + per-path status + a full status list scan. That is hundreds of milliseconds per batch. Gradle touches thousands of gitignored files per build, so the watcher falls behind and pins a core for the whole build plus many minutes afterwards, while the session looks idle.

Configuration that should prevent this, but does not

Global opencode.jsonc already excludes every directory involved, and snapshots are off:

"snapshot": false,
"watcher": {
  "ignore": ["node_modules/**", "dist/**", ".git/**", "**/build/**", ".kotlin/**", ".gradle/**", ".venv/**", "build/**"]
}

The events driving the CPU burn are all under .gradle/, which this config explicitly ignores. So watcher.ignore has no effect on the fff watcher — consistent with #37740. snapshot: false rules snapshots out as the trigger (also confirmed by .git/index mtime staying unchanged during the burn)

Expected
  • The watcher keeps one open Repository (or at least the parsed index) per repo and invalidates it on .git/index change, instead of open → status → free per batch.
  • Gitignored paths are dropped before the git-status pipeline runs. is_path_ignored appears in the samples, but the expensive status work runs for batches whose only content is gitignored build output.
  • One batched statuses() call per debounced event group instead of per-path status_file.
Related
  • #37740 — watcher.ignore never reaches the embedded fff engine; a root .ignore file is the only channel fff honors. This report is a concrete consequence: the config above ignores .gradle/**, yet fff still runs its git-status pipeline for .gradle events.

Local workaround

Per #37740, a root .ignore file is the only ignore channel fff honors.

  • Root .ignore with .gradle/, .git/, **/build/**
  • .ignore added to .git/info/exclude so it stays out of the repo
Plugins
OpenCode version

1.18.18

Steps to reproduce
  1. Open an opencode session in a large repo (tens of thousands of tracked files; a multi-MB .git/index).
  2. Run any tool that churns gitignored files in the workspace (a Gradle build is a reliable source; anything writing thousands of files under an ignored directory should do).
  3. Watch the process: one thread (fff-watcher-own) sits at ~100% during the churn and long after it stops. sample <pid> shows the stacks above.
Screenshot and/or share link
Operating System

macOS (Darwin 25.6.0), Apple Silicon

Terminal

iTerm2, IntelliJ embedded terminal

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.