bigywater.tech
Git · 37

fsmonitor 与 untracked cache

大仓库里 git status 为什么慢,untracked cache 和 fsmonitor 又各自省了什么?这篇在 1 万和 10 万个文件的仓库上分别测:先看 lstat 次数,再看耗时,最后看守护进程的代价。结论有点出人意料:1 万个文件时它们几乎没用。

184 → 38 ms
10 万个已跟踪文件 + 2 万个未跟踪文件时 git status 的耗时:基线与 untracked cache + fsmonitor 叠加
10000 → 0
打开 fsmonitor 之后 git status 的 lstat 次数,但 1 万个文件时耗时没有变
UNTR / FSMN
两个加速分别在 index 里增加的扩展块

基线

实测 · 本机执行输出
  $ git status --short; git ls-files | wc -l; find build -type f | wc -l
  ?? build/
     10000
      3000
  git status 耗时(5 次中位数): 32.0 ms;lstat 次数(preload 与 refresh 之和): 10000

git status 要对每个已跟踪文件 lstat 一次(用 preload-index 并行),还要扫描目录找出未跟踪的文件。1 万个文件时整个 status 只要约 30 ms。

untracked cache 与 fsmonitor

实测 · 本机执行输出
  $ git update-index --test-untracked-cache 2>&1 | tail -1; git config core.untrackedCache true; git status --short
  Testing mtime in '/work/l33a' ...... OK
  ?? build/
  index 里的扩展块: ['TREE', 'UNTR']
  + untracked cache: 28.3 ms;lstat 次数 10000
  $ git config core.fsmonitor true; git status --short; sleep 1; git fsmonitor--daemon status 2>&1 | sed -E "s#'.*'#'<repo>'#"
  ?? build/
  fsmonitor-daemon is watching '<repo>'
  index 里的扩展块: ['TREE', 'UNTR', 'FSMN']
  + fsmonitor: 30.1 ms;lstat 次数 0
  (1 万个文件时 lstat 本来就很快:git 用 preload-index 多线程并行 lstat,1 万次只要约 9 毫秒)

core.untrackedCache 把每个目录上次的扫描结果(及目录 mtime)存进 index 的 UNTR 扩展,目录没变就不再扫描。core.fsmonitor 启动守护进程监听文件系统事件,index 的 FSMN 扩展记着上次查询的标记,status 只需问“之后哪些路径变了”,不再 lstat 全部文件。

规模决定收益

实测 · 本机执行输出
  $ git ls-files | wc -l; find build -type f | wc -l
    100000
     20000
  基线 git status: 184.4 ms;lstat 次数 100000
  + untracked cache: 102.8 ms
  + fsmonitor(两个都开): 38.3 ms;lstat 次数 0
  只开 fsmonitor(关掉 untracked cache): 88.3 ms

1 万个文件时 lstat 从 10000 次降到 0,耗时却没变(并行 lstat 本来就快)。10 万个文件时才显出差别:184 ms 降到 untracked cache 的 103 ms,两者叠加的 38 ms。仓库不大时不值得打开。

改动怎么被发现

实测 · 本机执行输出
  $ git status --short
   M src/d123/f061700.txt
  ?? build/
  git status: 89.9 ms(fsmonitor 开启)

守护进程持续接收文件系统事件。改一个文件、新建一个文件之后 status 立刻能看到,仍不需要全量扫描。

代价

实测 · 本机执行输出
  $ git fsmonitor--daemon status 2>&1 | sed -E "s#'.*'#'<repo>'#"; ls .git | grep fsmonitor | tr "
" " "; echo
  fsmonitor-daemon is watching '<repo>'
  fsmonitor--daemon fsmonitor--daemon.ipc
  $ git config --unset core.fsmonitor; git fsmonitor--daemon status 2>&1 | sed -E "s#'.*'#'<repo>'#"
  fsmonitor-daemon is watching '<repo>'
  $ git fsmonitor--daemon stop; git fsmonitor--daemon status 2>&1 | sed -E "s#'.*'#'<repo>'#"; ls .git | grep fsmonitor | tr "
" " "; echo
  fsmonitor-daemon is not watching '<repo>'
  fsmonitor--daemon
  全部关闭之后: 154.8 ms

每个仓库一个常驻进程,有 ipc socket。把 core.fsmonitor 配置关掉并不会停掉它,需要 git fsmonitor--daemon stop。

分步演示

untracked cache 与 fsmonitor
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。仓库由脚本生成(10,000 / 100,000 个很小的文本文件);耗时 5 次中位数,含进程启动;lstat 次数来自 GIT_TRACE2_PERF 的 sum_lstat;fsmonitor 用 git 自带的 fsmonitor--daemon(macOS 的 FSEvents);每次测量前先让 index 刷新(避开 racy 窗口);同一台机器上各轮的基线有波动(10 万文件的“全部关闭”是 154.8 ms,基线是 184.4 ms)
    • 没有验证:Linux 上的 inotify 实现;Watchman 等第三方 fsmonitor 钩子;网络文件系统与 fsmonitor 的兼容;守护进程占用的内存;git status 之外(git diff、git add)的收益;core.untrackedCache 在 mtime 精度不足的文件系统上的退化
    京ICP备2026067502号-1京公网安备11010502063487号