Skip to content

diff HEAD on a large worktree (7.2k files): ~45x slower, ~65x more peak memory than system git; committed symlinks reported as modified #4

Description

@cephalization

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions