[Bug]: unchanged `logs/HEAD` rewrites during `git reflog expire --all` cause repeated `fff-git-status` work (0.11.0)
Maintainers usually reply within 3 days
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 25/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Stale
- Tech stack
- git, linux, rust
- Domain
- backend, performance
Research direction
Start with background_watcher.rs around the reported watcher match at line 1029, then trace how it sets need_full_git_rescan and reaches the handler around lines 447 and 794. Reproduce with repeated git reflog expire --all runs and compare CPU use while confirming that a real commit still triggers an update. Done means identical logs/HEAD rewrites avoid unnecessary full status work while real changes remain detected; pull request #986 is already open.
Written by the indexing model from the issue text.
Description
Which fff frontend?
MCP server (fff-mcp)
has logs
No log attached; the reproduction below measures per-thread CPU.
Description
Summary
git reflog expire --all replaces .git/logs/HEAD even when no entry expires, so the file keeps the same bytes. fff-mcp still reacts: its fff-git-status thread keeps working through these rewrites, although git status did not change.
Environment
fff-mcp 0.11.0 (95fd777c2529fc7b4d7572dabff64cc07268f2c5), Linux x86_64, git 2.53.0- Started by Claude Code as a stdio MCP server:
fff-mcp --log-level warn --max-cached-files 500
Steps to reproduce
- Make a repo with 20,000 tracked files and two commits.
- Start
fff-mcpin it, complete the MCP handshake (initialize, thennotifications/initialized), and wait 15 s. - Read the CPU time of the
fff-git-statusthread (/proc/<pid>/task/*/comm, thenstat). - Wait 30 s, then read it again.
- Run
git reflog expire --all30 times, sleeping 1 s after each run, then read it again.
Script (Python 3, Linux)
import hashlib, json, os, shutil, subprocess, tempfile, time
FFF = 'fff-mcp'
root = tempfile.mkdtemp(prefix='fff-reflog-')
repo = os.path.join(root, 'repo')
for i in range(20000):
d = os.path.join(repo, f'd{i // 100:03d}')
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, f'f{i}.txt'), 'w') as fh: fh.write(f'line {i}\n')
g = lambda *a: subprocess.run(['git', '-C', repo, '-c', 'user.email=t@t', '-c', 'user.name=t', *a], capture_output=True, text=True, check=True)
g('init', '-q'); g('add', '-A'); g('commit', '-qm', 'one')
with open(os.path.join(repo, 'd000', 'f0.txt'), 'a') as fh: fh.write('two\n')
g('commit', '-qam', 'two')
p = subprocess.Popen([FFF, '--no-update-check', '--frecency-db', f'{root}/f.db', '--history-db', f'{root}/h.db'],
cwd=repo, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
p.stdin.write(json.dumps({'jsonrpc': '2.0', 'id': 1, 'method': 'initialize', 'params': {'protocolVersion': '2025-06-18', 'capabilities': {}, 'clientInfo': {'name': 't', 'version': '0'}}}) + '\n'); p.stdin.flush()
p.stdout.readline()
p.stdin.write(json.dumps({'jsonrpc': '2.0', 'method': 'notifications/initialized'}) + '\n'); p.stdin.flush()
time.sleep(15)
def git_status_cpu():
for t in os.listdir(f'/proc/{p.pid}/task'):
if open(f'/proc/{p.pid}/task/{t}/comm').read().strip() == 'fff-git-status':
st = open(f'/proc/{p.pid}/task/{t}/stat').read().split(')')[1].split()
return (int(st[11]) + int(st[12])) / os.sysconf('SC_CLK_TCK')
lh = os.path.join(repo, '.git', 'logs', 'HEAD')
c0 = git_status_cpu(); time.sleep(30); c1 = git_status_cpu()
sha0 = hashlib.sha256(open(lh, 'rb').read()).hexdigest()
for _ in range(30):
g('reflog', 'expire', '--all'); time.sleep(1)
c2 = git_status_cpu()
sha1 = hashlib.sha256(open(lh, 'rb').read()).hexdigest()
print(f'idle 30 s: {c1 - c0:.2f} s; 30 reflog-expire runs: {c2 - c1:.2f} s; logs/HEAD unchanged: {sha0 == sha1}')
p.stdin.close(); p.wait(10); shutil.rmtree(root)
Expected
logs/HEAD has the same content before and after, so git status is unchanged and no refresh is needed.
Actual
| Phase | fff-git-status CPU |
|---|---|
| Idle, 30 s | 0.00 s |
| 30 reflog-expire runs, with 1 s sleeps | 2.97 s and 2.39 s (two runs) |
logs/HEAD had the same SHA-256 before and after the loop in both runs.
Impact we saw
On a monorepo checkout with 18,041 tracked files, 23 fff-mcp processes ran on the same base path. During a burst, sampled git commands in that checkout included fetch, maintenance run --auto, gc --auto and reflog expire --all. The 23 processes used 4.57 cores over 60 s. A later 30 s window measured 0.007 cores, with no observed logs/HEAD inode change. These are whole-process measurements; I did not isolate the share caused by the reflog rewrites.
Likely cause
On main (3004e2d), the watcher matches logs/HEAD without comparing its contents (background_watcher.rs:1029). A matching event sets need_full_git_rescan (:447), and the handler requests a full git rescan (:794).
I reproduced the CPU increase on 0.11.0. I have not measured refresh counts or tested a current-main build.
Could identical reflog rewrites avoid full status work while preserving updates after real commits?
Related: #942's October 16 comment describes full-refresh cost from unchanged index rewrites.
- Dominant language
- Rust
- Stars
- 11.2k
- Forks
- 477
- Avg merge
- 2d 8h
- Merged PRs (30d)
- 31
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dmtrKovalenko/fff
-
triaged
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
dmtrKovalenko/fff#842 · 2 comments ·
Maintainers usually reply within 3 days
-
[Bug]: zlob walker ignores `.git/info/exclude`Possibly taken @gustav-fff claimed this 4 days ago. Opentriaged
Difficulty 4/5 3-5 days Newbie friendliness 35/100
dmtrKovalenko/fff#982 · 1 comment ·
Maintainers usually reply within 3 days
-
triaged
Difficulty 3/5 1-2 days Newbie friendliness 68/100
dmtrKovalenko/fff#980 · 1 comment ·
Maintainers usually reply within 3 days
-
[Bug]: pi-fff blocks session startup while indexing filesPossibly taken @adonis0147 claimed this 7 days ago. Openduplicate triaged
Difficulty 3/5 1-2 days Newbie friendliness 70/100
dmtrKovalenko/fff#975 · 1 comment ·
Maintainers usually reply within 3 days
-
[Bug]: on a non-git base path, the watcher indexes new files in hidden folders that the scan skips (0.11.0)Possibly taken @gustav-fff claimed this 9 days ago. Opentriaged
Difficulty 3/5 1-2 days Newbie friendliness 35/100
dmtrKovalenko/fff#972 · 2 comments ·
Maintainers usually reply within 3 days
All issues in dmtrKovalenko/fff
Similar issues
-
Windows docs: pin a tested ConPTY version for Kitty graphics; 1.25.260930003 corrupts large uploadsOpen
Difficulty 1/5 1-3 hours Newbie friendliness 88/100
raphamorim/rio#2021 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
hydro-project/infinity#187 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
near/near-sdk-rs#1676 ·
Maintainers usually reply within 2 days
-
auto:producer auto:security
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
fritzprix/libr-agent#2595 ·
Maintainers usually reply within 1 day
-
area:fs type:fix
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Tripstack-Corp/spyc#1002 ·
Maintainers usually reply within 1 day