Skip to content

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

Closed as not planned

Description

@lwasyl

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__open → git_index_read → parse_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


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

Activity

  1. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions