Context
We're building a local daemon (Overfactor) that uses just-git v2 through durableFileSystemFromNodeFs to compute worktree diff stats (diff HEAD --numstat) for tracked repos. It works great on small/medium repos. On a large real-world repo, the performance profile vs system git is steep enough that we wanted to share concrete numbers, since the wall time and especially the peak RSS matter a lot for a long-lived embedding process.
Setup
- just-git
v2 branch @ 425ebbb4e2cb5a78f3fca740f6f97f3e974fa804 (package version 1.8.0), built with tsc -p tsconfig.build.json
- Node v22.22.0, macOS 26.6.1, Apple M3 Max
- system git 2.x for comparison (
git --version below in repro)
- Test repo: Arize-ai/phoenix @
3acffbdb3 — 7,222 tracked files, ~13 GB working copy including untracked/ignored build state (the tracked tree itself is far smaller)
Repro
git clone https://github.com/Arize-ai/phoenix
git clone -b v2 https://github.com/blindmansion/just-git && cd just-git && npm install && npm run build && cd ..
mkdir bench && cd bench && npm init -y && npm install ../just-git
cat > bench.mjs <<'JS'
import fsPromises from "node:fs/promises";
import { createGit, durableFileSystemFromNodeFs } from "just-git";
const cwd = process.argv[2];
const git = createGit({ fs: durableFileSystemFromNodeFs(fsPromises), cwd });
const t0 = performance.now();
const result = await git.exec("diff HEAD --numstat");
const files = result.stdout.trim().split("\n").filter(Boolean).length;
console.log(`exit=${result.exitCode} changedFiles=${files} wallMs=${Math.round(performance.now() - t0)}`);
JS
# optionally dirty one file: echo "// touch" >> ../phoenix/README.md
/usr/bin/time -l node bench.mjs ../phoenix # just-git
/usr/bin/time -l git -C ../phoenix diff HEAD --numstat # system git
Results (warm runs, two each)
| scenario |
just-git v2 wall |
just-git v2 peak RSS |
system git wall |
system git peak RSS |
| clean tree |
1.33–1.72 s |
660–670 MB |
0.03 s |
9.7 MB |
| 1 modified file |
1.46–1.60 s |
~662 MB |
0.03–0.04 s |
9.7 MB |
Roughly ~45× wall time and ~65× peak memory. The cost appears dominated by the size of the tracked tree rather than the size of the change (clean vs 1-file-dirty is within noise). For a one-shot CLI that's tolerable; embedded in a long-lived process the transient ~660 MB per diff is the painful part (V8 holds the RSS for a while afterwards).
No specific fix in mind — mostly sharing a real-world profile in case an index/stat-cache-style fast path (skip content reads for entries whose stat matches the index, like git's racy-clean handling) is on the roadmap for v2. Happy to run patches against this repo.
Side observation: committed symlinks report as modified
On this repo, a clean tree isn't reported clean: diff HEAD --numstat returns two entries that system git does not report, and both are committed symlinks (mode 120000):
$ node bench.mjs ../phoenix # clean tree
0 1 .cursor/skills/phoenix-skill-development
0 1 js/.prettierrc.json
$ git -C ../phoenix ls-files -s .cursor/skills/phoenix-skill-development js/.prettierrc.json
120000 2b6768158783515d16467b6e5fe92f019c0121a3 0 .cursor/skills/phoenix-skill-development
120000 a7d795af5ecc7eb816feb25db05c2253fd4b9f5e 0 js/.prettierrc.json
$ git -C ../phoenix diff HEAD --numstat | wc -l
0
Each symlink shows as a 1-line deletion, and the rendered patch shows the link target as removed content. Say the word and I'll split this into its own issue.
Context
We're building a local daemon (Overfactor) that uses just-git v2 through
durableFileSystemFromNodeFsto compute worktree diff stats (diff HEAD --numstat) for tracked repos. It works great on small/medium repos. On a large real-world repo, the performance profile vs system git is steep enough that we wanted to share concrete numbers, since the wall time and especially the peak RSS matter a lot for a long-lived embedding process.Setup
v2branch @425ebbb4e2cb5a78f3fca740f6f97f3e974fa804(package version 1.8.0), built withtsc -p tsconfig.build.jsongit --versionbelow in repro)3acffbdb3— 7,222 tracked files, ~13 GB working copy including untracked/ignored build state (the tracked tree itself is far smaller)Repro
Results (warm runs, two each)
Roughly ~45× wall time and ~65× peak memory. The cost appears dominated by the size of the tracked tree rather than the size of the change (clean vs 1-file-dirty is within noise). For a one-shot CLI that's tolerable; embedded in a long-lived process the transient ~660 MB per diff is the painful part (V8 holds the RSS for a while afterwards).
No specific fix in mind — mostly sharing a real-world profile in case an index/stat-cache-style fast path (skip content reads for entries whose stat matches the index, like git's racy-clean handling) is on the roadmap for v2. Happy to run patches against this repo.
Side observation: committed symlinks report as modified
On this repo, a clean tree isn't reported clean:
diff HEAD --numstatreturns two entries that system git does not report, and both are committed symlinks (mode120000):Each symlink shows as a 1-line deletion, and the rendered patch shows the link target as removed content. Say the word and I'll split this into its own issue.