[Bug]: unchanged `logs/HEAD` rewrites during `git reflog expire --all` cause repeated `fff-git-status` work (0.11.0)

Open
#985 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 3 days

@gustav-fff is already working on this.

Since Nov 4, 2026.

  • #986 by @gustav-fff — open

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

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

triaged
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

  1. Make a repo with 20,000 tracked files and two commits.
  2. Start fff-mcp in it, complete the MCP handshake (initialize, then notifications/initialized), and wait 15 s.
  3. Read the CPU time of the fff-git-status thread (/proc/<pid>/task/*/comm, then stat).
  4. Wait 30 s, then read it again.
  5. Run git reflog expire --all 30 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

  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.

More from dmtrKovalenko/fff

All issues in dmtrKovalenko/fff

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.