# jj performance
Compare jj on girt with stock jj at the same base revision. Lower ratios are better. Failures and
missing results sort first, followed by the worst measured ratio across sizes. These are warm-cache
measurements; provenance below names the release of each cell.
π’ β€1.20Γ; π >1.20β2.00Γ; π΄ >2.00Γ; βͺ not like-for-like; β failure/refusal; β³ pending. `n` is
the number of successful girt samples; JSON records each tool separately.
| `git clone` / local-full-checkout | π’ 0.91Γ (n=6) (girt 0.13.1) | π 1.30Γ (n=3) (girt 0.14.0) [#218](https://github.com/joshka/girt/issues/218) | β timeout (girt 0.14.0) [#581](https://github.com/joshka/girt/issues/581) [#455](https://github.com/joshka/girt/issues/455) |
| `git export` / 100-pending-bookmarks | π 1.51Γ (n=6) [#216](https://github.com/joshka/girt/issues/216) | π΄ 2.77Γ (n=6) [#216](https://github.com/joshka/girt/issues/216) | π΄ 2.69Γ (n=6) [#216](https://github.com/joshka/girt/issues/216) |
| `log` / limited | π 1.98Γ (n=6) (girt 0.14.0) [#66](https://github.com/joshka/girt/issues/66) | π’ 1.09Γ (n=6) (girt 0.14.0) | π’ 1.13Γ (n=6) |
| `commit` / default | π’ 1.03Γ (n=6) (girt 0.13.1) | π’ 1.11Γ (n=6) (girt 0.13.1) | π 1.88Γ (n=3) [#453](https://github.com/joshka/girt/issues/453) |
| `duplicate` / default | π’ 1.15Γ (n=6) (girt 0.13.1) | π’ 1.17Γ (n=6) (girt 0.13.1) | π 1.86Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `op restore` / default | π’ 1.16Γ (n=6) (girt 0.13.1) | π 1.22Γ (n=6) (girt 0.13.1) [#453](https://github.com/joshka/girt/issues/453) | π 1.70Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `describe` / default | π’ 1.17Γ (n=6) (girt 0.13.1) | π 1.203Γ (n=6) (girt 0.13.1) [#453](https://github.com/joshka/girt/issues/453) | π 1.68Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `edit` / default | π’ 1.05Γ (n=6) (girt 0.13.1) | π’ 1.19Γ (n=6) (girt 0.13.1) | π 1.66Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `abandon` / default | π’ 1.08Γ (n=6) (girt 0.13.1) | π’ 1.16Γ (n=6) (girt 0.13.1) | π 1.65Γ (n=3) [#222](https://github.com/joshka/girt/issues/222) [#453](https://github.com/joshka/girt/issues/453) |
| `bookmark` / create | π’ 1.11Γ (n=6) (girt 0.13.1) | π’ 1.17Γ (n=6) (girt 0.13.1) | π 1.65Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `rebase` / 10 | π’ 1.14Γ (n=6) (girt 0.13.1) | π’ 1.17Γ (n=6) (girt 0.13.1) | π 1.64Γ (n=6) [#222](https://github.com/joshka/girt/issues/222) [#453](https://github.com/joshka/girt/issues/453) |
| `undo` / default | π’ 1.13Γ (n=6) (girt 0.13.1) | π 1.202Γ (n=6) (girt 0.13.1) [#453](https://github.com/joshka/girt/issues/453) | π 1.63Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `new` / default | π’ 1.06Γ (n=6) (girt 0.13.1) | π’ 1.18Γ (n=6) (girt 0.13.1) | π 1.61Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `status` / untracked | π’ 0.22Γ (n=6) (girt 0.13.1) | π’ 0.29Γ (n=6) (girt 0.13.1) | π 1.60Γ (n=3) [#66](https://github.com/joshka/girt/issues/66) |
| `restore` / default | π’ 1.10Γ (n=6) (girt 0.13.1) | π’ 1.04Γ (n=6) (girt 0.13.1) | π 1.59Γ (n=3) [#453](https://github.com/joshka/girt/issues/453) |
| `rebase` / 1 | π’ 1.15Γ (n=6) (girt 0.13.1) | π’ 1.16Γ (n=6) (girt 0.13.1) | π 1.59Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `split` / default | π’ 1.12Γ (n=6) (girt 0.13.1) | π’ 1.14Γ (n=6) (girt 0.13.1) | π 1.58Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `squash` / default | π’ 1.14Γ (n=6) (girt 0.13.1) | π’ 1.19Γ (n=6) (girt 0.13.1) | π 1.57Γ (n=3) [#453](https://github.com/joshka/girt/issues/453) |
| `status` / untracked-unique | π 1.24Γ (n=6) (girt 0.13.1) [#220](https://github.com/joshka/girt/issues/220) | π 1.37Γ (n=6) (girt 0.13.1) [#220](https://github.com/joshka/girt/issues/220) | π 1.55Γ (n=6) [#220](https://github.com/joshka/girt/issues/220) |
| `rebase` / 100 | π’ 0.84Γ (n=6) (girt 0.13.1) | π’ 1.15Γ (n=6) (girt 0.13.1) | π 1.51Γ (n=6) [#222](https://github.com/joshka/girt/issues/222) [#453](https://github.com/joshka/girt/issues/453) |
| `diff` / wc | π 1.26Γ (n=6) [#66](https://github.com/joshka/girt/issues/66) | π’ 1.14Γ (n=6) | π 1.50Γ (n=3) [#220](https://github.com/joshka/girt/issues/220) |
| `git import` / 100-external-refs | π’ 1.05Γ (n=6) (girt 0.13.1) | π 1.32Γ (n=6) (girt 0.13.1) [#66](https://github.com/joshka/girt/issues/66) | π 1.44Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `diff` / git | π’ 1.16Γ (n=6) | π 1.23Γ (n=6) [#66](https://github.com/joshka/girt/issues/66) | π 1.43Γ (n=3) [#220](https://github.com/joshka/girt/issues/220) |
| `status` / many-edits | π’ 1.16Γ (n=6) (girt 0.13.1) | π’ 1.10Γ (n=6) (girt 0.13.1) | π 1.43Γ (n=6) [#220](https://github.com/joshka/girt/issues/220) |
| `diff` / large | π’ 1.09Γ (n=6) | π’ 1.16Γ (n=6) | π 1.43Γ (n=6) [#66](https://github.com/joshka/girt/issues/66) |
| `bookmark` / delete | π’ 1.01Γ (n=6) (girt 0.13.1) | π’ 1.17Γ (n=6) (girt 0.13.1) | π 1.42Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `evolog` / default | π’ 1.05Γ (n=6) | π 1.40Γ (n=6) [#66](https://github.com/joshka/girt/issues/66) | π’ 0.94Γ (n=6) |
| `diff` / stat | π’ 1.12Γ (n=6) | π’ 1.16Γ (n=6) | π 1.37Γ (n=3) [#220](https://github.com/joshka/girt/issues/220) |
| `bookmark` / move | π’ 1.07Γ (n=6) (girt 0.13.1) | π’ 1.14Γ (n=6) (girt 0.13.1) | π 1.35Γ (n=6) [#453](https://github.com/joshka/girt/issues/453) |
| `log` / large | π’ 1.16Γ (n=6) | π 1.33Γ (n=6) [#222](https://github.com/joshka/girt/issues/222) | π 1.34Γ (n=3) [#222](https://github.com/joshka/girt/issues/222) |
| `status` / small-edit | π’ 1.18Γ (n=6) (girt 0.13.1) | π’ 1.03Γ (n=6) (girt 0.13.1) | π 1.33Γ (n=6) [#66](https://github.com/joshka/girt/issues/66) |
| `rebase` / bookmark | π’ 1.09Γ (n=6) (girt 0.13.1) | π’ 1.18Γ (n=6) (girt 0.13.1) | π 1.27Γ (n=6) [#216](https://github.com/joshka/girt/issues/216) [#453](https://github.com/joshka/girt/issues/453) |
| `file show` / large-old-file | π’ 1.18Γ (n=6) | π’ 1.06Γ (n=6) | π’ 0.94Γ (n=6) |
| `git init` / colocate | π’ 1.00Γ (n=6) (girt 0.13.1) | π’ 1.16Γ (n=6) (girt 0.14.0) | π’ 1.00Γ (n=3) (girt 0.14.0) |
| `file annotate` / large-old-file | π’ 0.96Γ (n=6) | π’ 0.86Γ (n=6) | π’ 1.14Γ (n=1) (girt 0.14.0) |
| `log` / default | π’ 1.11Γ (n=6) | π’ 1.12Γ (n=6) | π’ 0.97Γ (n=6) |
| `log` / no-graph | π’ 1.06Γ (n=6) | π’ 1.11Γ (n=6) | π’ 0.98Γ (n=6) |
| `git fetch` / tags | π’ 1.10Γ (n=6) | π’ 0.75Γ (n=6) | π’ 1.11Γ (n=6) |
| `show` / default | π’ 1.04Γ (n=6) | π’ 1.11Γ (n=6) | π’ 1.01Γ (n=6) |
| `status` / clean | π’ 1.03Γ (n=6) (girt 0.13.1) | π’ 1.10Γ (n=6) (girt 0.13.1) | π’ 1.08Γ (n=3) |
| `git fetch` / noop | π’ 0.55Γ (n=6) | π’ 0.83Γ (n=6) | π’ 1.08Γ (n=6) |
| `git push` / delete | π’ 1.04Γ (n=6) | π’ 0.44Γ (n=6) | π’ 1.08Γ (n=6) |
| `git push` / new | π’ 0.29Γ (n=6) | π’ 0.53Γ (n=6) | π’ 1.07Γ (n=6) |
| `op log` / default | π’ 1.05Γ (n=6) | π’ 0.97Γ (n=6) | π’ 0.98Γ (n=6) |
| `workspace add` / full-checkout | π’ 0.96Γ (n=6) (girt 0.13.1) | π’ 1.04Γ (n=6) (girt 0.13.1) | π’ 0.71Γ (n=3) (girt 0.14.0) |
| `git push` / update | π’ 0.35Γ (n=6) | π’ 0.75Γ (n=6) | π’ 1.02Γ (n=6) |
| `bookmark` / list | π’ 1.02Γ (n=6) | π’ 0.91Γ (n=6) (girt 0.14.0) | π’ 0.98Γ (n=6) |
| `log` / ancestry | π’ 0.84Γ (n=6) (girt 0.14.0) | π’ 0.63Γ (n=1) (girt 0.14.0) | π’ 1.01Γ (n=1) (girt 0.14.0) |
| `git fetch` / 100 | π’ 0.80Γ (n=6) | π’ 0.70Γ (n=6) | π’ 0.92Γ (n=6) |
| `git push` / stale | π’ 0.62Γ (n=6) | π’ 0.91Γ (n=6) | π’ 0.66Γ (n=6) |
| `git fetch` / 1 | π’ 0.76Γ (n=6) | π’ 0.53Γ (n=6) | π’ 0.86Γ (n=6) |
| `util gc` / clean | βͺ 0.18Γ (n=6) not like-for-like | βͺ 0.07Γ (n=6) not like-for-like | βͺ 0.01Γ (n=1) not like-for-like |
| `util gc` / garbage | βͺ 0.56Γ (n=6) not like-for-like | βͺ 1.18Γ (n=3) (girt 0.14.0) not like-for-like | βͺ 1.21Γ (n=1) (girt 0.14.0) not like-for-like [#66](https://github.com/joshka/girt/issues/66) |
Cells marked `(girt X)` were measured on that release rather than girt 0.11.1; each row records its
own `girt_commit` and `measured_on` date.
## Limits and interpretation
A timeout has a recorded limit and no successful girt ratio; a refusal has no timed girt sample.
Neither is presented as a fast result. Stock jj taking more than twice Git is flagged separately in
the JSON as `jj_level_gap`. Git equivalents sometimes perform different work; read each rowβs
workload notes. Git has no timed counterpart for colocated initialization (the input is already a
Git repository), import (Git already sees its own refs), or jj undo/operation restore (Git has no
equivalent repository-wide operation restoration). Reflogs approximate jj evolution and operation
history but contain different records. Ranges, peak RSS, sample counts, per-row load, issue
mappings, and limitations are in [the results JSON](benchmarks/jj-matrix.json). `util gc` rows are
not like-for-like: girt's gc packs loose objects and prunes, but doesn't rewrite existing packs as
`git gc` does. They stay timed as a collection-cost signal but are marked βͺ, are never shown as a
girt win, and carry `comparable: false`. Peak RSS counts resident pages of memory-mapped pack and
index files, so it is not heap use alone.
Machine load was not controlled. The one-minute load average at sample start ranged from 3.4 to 88.4
(median 21.5) on 12 cores, so other work competed with many samples. Tool order alternates within
each cell, which spreads that contention across tools but does not remove it. Treat ratios within
about 0.2 of a band boundary on sub-second cells as provisional.
## Provenance
- Measurement dates: 2026-10-17 (America/Los_Angeles).
- Machine: Apple M2 Max; macOS-26.6.2-arm64-arm-64bit-Mach-O.
- girt: release 0.11.1, `69e3985c68e488a5975f1ad634873106ec2db18a`.
- jj adapter: `88ed7e9e286cffeedacf910930792c9b8ef5a990`.
- Stock jj base: `35dbc3623b6dd5a23daf5b5ca09b8640694a72ae`.
- Git: git version 2.56.0; release commit `a018953688f1b10bddf91bff8747068f5f4746a4`.
- Build: release; debug=1; strip=none; codegen-units=1; rustc 1.99.0 (b940084d7 2026-09-28).
Note: The jj adapter commit predates 032e725a on the fork, which skips writing blobs the repository
already has during colocated init (#217); the Linux colocated-init timeout was measured without it.
Most cells were measured on girt 0.11.1; marked cells name their own release. Each row records the
girt release, girt commit, jj adapter commit, and date it was measured, so cells can be re-measured
selectively after a change. Binary fingerprints are per row;
[the JSON provenance block](benchmarks/jj-matrix.json) also holds per-repository object, ref, tag,
worktree, and pack facts. The Git commit identifies release source; distribution packaging may add
patches. Profiles and raw checkpoints are retained locally at the evidence paths; they are not
bundled in this repository. The [original 53 small rows](jj-performance-original-small.json) from
the first matrix are kept as a historical baseline.
| small | 18103 / 0 | 11 | 10 | 208 | 8560689 | 11 / 0 |
| medium | 421375 / 0 | 3100 | 1012 | 4857 | 323150867 | 0 / 3100 |
| large | 9103336 / 1000 | 4787 | 1770 | 78700 | 4409566247 | 16 / 4771 |
## Method
Build both binaries before taking the shared `/tmp/girt-timing.lock`. Prepare inputs outside the
lock. Use fresh APFS `cp -cR` copies for mutating samples, one large copy at a time; checkpoint
timing and validation before deleting it. Reuse clean read-only baselines and verify unchanged jj
operation heads. Automatic Git maintenance is disabled; explicit GC remains measured.
Linux has tracked paths differing only by case. On case-insensitive APFS, Git rebase setup verifies
that every unstaged path is such an alias and marks only those paths assume-unchanged before timing.
Other unstaged edits fail setup. This platform limitation means the checkout cannot represent both
versions of those files faithfully.
Full runs take six samples per tool on small and medium cells in Git, girt, stock, stock, girt, Git
blocks. Linux cells, and targeted refreshes that release the timing lock between samples, use
adaptive calibration in Git, stock, girt order: if the slowest first sample exceeds 60 seconds, take
one sample per tool; if it exceeds 10 seconds, take three; otherwise take six. Further samples
alternate girt and stock positions, so slow-cell ratios have lower precision. Counts in the JSON
describe the samples retained.
Kill each timed girt process group at `max(2 Γ first stock-jj seconds, 5 seconds)`. Stock jj and Git
each have a 600-second cap. Timeouts fail the cell and record the limit. Report medians, full
ranges, and peak RSS using `/usr/bin/time -l`. Release the lock between timed samples in
bounded-disk mode. Caches are warm and are not flushed.
Full `git fsck` follows the `operation-type` scope: each mutating operation type is checked once per
size, and clone and explicit GC are checked at each size. This run recorded 51 checks and 0
failures. Semantic checks cover imported/exported refs and actual garbage removal. Profiles diagnose
gaps without replacing timings; adaptive diagnostic recordings are capped at five seconds.
Fetch cells perform an untimed fetch before introducing the measured commit/tag delta. This imports
existing tags and makes noop a genuine second fetch with no remote changes. Stale-push setup
prepares remote-tracking refs with untimed Git fetch plus jj import. Limited log uses
`::@ --limit 100` against Git `-n 100`.
## Refresh before a release
On a quiet macOS machine, build the intended frozen jj/girt revisions and prepare all fixtures under
`~/tmp/girt-bench` or `~/tmp/girt-fixtures`. Follow
[the matrix runner instructions](../scripts/jj-e2e/README.md), recording exact commits, binary
hashes, fixture facts, failures, and issue links. Keep the raw checkpoint and logs. Publication is a
manual normalization step: the runner emits a timing list and raw checkpoint, not this publication
schema. Merge selected reruns by repository/operation, retain replaced records, classify statuses,
attach reviewed issue/profile mappings, and audit sample counts, ranges, fingerprints, and
validation epochs before updating the publication envelope. Update the results and provenance in
`docs/benchmarks/jj-matrix.json` from that run, then regenerate and check the page:
```sh
python3 scripts/jj-e2e/render-performance.py
python3 scripts/jj-e2e/render-performance.py --check
```
CI runs only the freshness check. It never runs benchmarks.