| 1 |
# Overnight backlog 3 (from 2026-09-29): not worse than Git |
| 2 |
|
| 3 |
Andy's brief: there is no point making a tool that is worse than Git. |
| 4 |
Until oxy matches Git on a real, large tree, this is the only work: |
| 5 |
speed, memory and space at scale, with correctness never traded away. |
| 6 |
|
| 7 |
Work top to bottom. An item is done when: |
| 8 |
- its tests pass and the gate is clean; |
| 9 |
- it is measured before and after on Earth, and the numbers are in |
| 10 |
`docs/benchmarks.md`; |
| 11 |
- it is committed with `oxy` as Andy and pushed to Earth. |
| 12 |
|
| 13 |
If an item is too large, split it here and do the first part. Log each |
| 14 |
iteration at the bottom. |
| 15 |
|
| 16 |
## The yardstick |
| 17 |
|
| 18 |
The Linux tip on Earth: 95 943 files, 1.8 GB, on 4 cores with 31 GiB. |
| 19 |
Git 2.53 does this (from `~/bench/linux/results.txt`, 2026-09-29): |
| 20 |
|
| 21 |
|
| 22 |
|
| 23 |
| First commit of the tree | 33.7 s, 42 MB | at most 50 s, under 2 GB | |
| 24 |
| Clean `status` (warm) | 0.26 to 0.31 s, 31 MB | at most 0.4 s, under 300 MB | |
| 25 |
| `status`, one file edited | 0.25 s | at most 0.4 s | |
| 26 |
| `diff`, one file edited | 0.34 s | at most 0.5 s | |
| 27 |
| `commit`, one file edited | 0.43 s | at most 0.6 s | |
| 28 |
| `status`, 1 000 files edited | 0.26 s | at most 0.5 s | |
| 29 |
| `diff`, 1 000 files edited | 1.38 s | at most 2 s | |
| 30 |
| `commit`, 1 000 files edited | 0.76 s | at most 1.2 s | |
| 31 |
| `log -n 100` | 0.05 s | at most 0.1 s | |
| 32 |
| `blame MAINTAINERS` | 0.07 s | at most 0.2 s | |
| 33 |
| Checkout of another state and back | 0.49 s and 0.38 s | at most 1 s each | |
| 34 |
| Store after gc | 311 MB (`.git`) | at most 350 MB | |
| 35 |
| Everyday commands | about 31 MB | under 300 MB | |
| 36 |
|
| 37 |
The targets allow oxy a small margin, because it does more (conflicts |
| 38 |
as data, signatures, an operation log). A target that turns out to be |
| 39 |
out of reach for a structural reason is written up here, with the best |
| 40 |
number reached and why. It is never quietly dropped. |
| 41 |
|
| 42 |
## Rules for this loop |
| 43 |
|
| 44 |
- **Measure first.** Before changing code, measure where the time or |
| 45 |
memory goes on Earth: `/usr/bin/time -v`, `perf record` (installed on |
| 46 |
Earth), `OXY_TIMING` phases, or a counting allocator behind a feature. |
| 47 |
Record what the profile showed in the log. |
| 48 |
- **Heavy work only on Earth**, always under a cap: |
| 49 |
`systemd-run --user --scope -p MemoryMax=14G …`. Andy's workstation has |
| 50 |
crashed from memory before. Locally, run the gate and small cases only. |
| 51 |
- **Getting code to Earth.** `~/bench/oxygen` on Earth is a plain copy of |
| 52 |
the source. Sync it with `rsync -a --delete --exclude target --exclude |
| 53 |
.oxy -e "ssh -i ~/.ssh/earth_claude" ~/development/oxygen/ |
| 54 |
claude@earth:bench/oxygen/`. |
| 55 |
- `~/bench/linux-bench.sh` builds oxy from that copy and runs the whole |
| 56 |
comparison. `~/bench/first-commit.sh` runs only the first commit, |
| 57 |
status and fsck. |
| 58 |
- Run long jobs with Bash `run_in_background`, and wait for the |
| 59 |
completion notice rather than polling. |
| 60 |
- **Correctness is never traded.** Every change keeps these passing: |
| 61 |
`oxy fsck` on the Linux repository, the property tests, and |
| 62 |
`oxy git verify` on the Git imports. |
| 63 |
- **Format changes** (change, checkpoint, pack or wire) are allowed |
| 64 |
before 1.0, with these steps: |
| 65 |
- bump the spec draft and keep old objects readable with unchanged |
| 66 |
hashes; |
| 67 |
- bump `deps/oxygen` in oxygenflow with `make pins`, gate, commit, |
| 68 |
push and `make deploy-runner`; |
| 69 |
- then `make pins` in oxygenhub, gate, commit, push, `make deploy`, |
| 70 |
and check `/health`; |
| 71 |
- only then install the new oxy locally. Commit the format change |
| 72 |
itself with the old binary. |
| 73 |
- **Memory and pace.** Every workstation build and test runs under |
| 74 |
`systemd-run --user --scope -p MemoryMax=8G`, and |
| 75 |
`CARGO_INCREMENTAL=0` (the disk is nearly full). |
| 76 |
|
| 77 |
## Correctness first (from metalgear, 2026-09-29) |
| 78 |
|
| 79 |
A working copy that silently becomes the wrong tree is worse than any |
| 80 |
slowness, so these come before everything else. |
| 81 |
|
| 82 |
- [x] C1 **Working-copy writes hold the workspace lock** (oxygen#36). Checkout, bookmark switch (with --carry), pull's checkout, undo, discard and commit hold the lock from reading the head until their operation is recorded. `merge_views` never lets a working-copy change based on a replaced one win. Test: a snapshot concurrent with a checkout never records the checkout's writes as edits. |
| 83 |
- [x] C2 **`bookmark create NAME CHANGE` refuses a partial state** (oxygen#37). When CHANGE is one of the current state's heads but not all of them, it refuses and explains, unless given --only. `bookmark switch` summarises a wide tree difference. The guide explains that bookmarks name states, not ancestry. |
| 84 |
- [x] C3 **A parent's switch never rolls a subrepo's bookmark back** (oxygen#38). |
| 85 |
- [x] C4 **`pull` says "Already up to date" while one behind** (oxygen#29). Reproduce it with a test, then fix it. |
| 86 |
|
| 87 |
## Memory |
| 88 |
|
| 89 |
- [x] M0 **Tools to see memory.** Add a counting global allocator behind |
| 90 |
a cargo feature (peak bytes and allocation count per `OXY_TIMING` |
| 91 |
phase), so a profile names the structure that holds the memory. Then |
| 92 |
profile a clean `status` of the Linux repository, killed at 14 GB |
| 93 |
(oxygen#35), and the first commit (9.9 GB). Write down which |
| 94 |
structures hold what before changing anything. |
| 95 |
- [x] M1 **Status without every graph** (oxygen#35). A clean `status` |
| 96 |
must not build every file graph. Unchanged files are skipped by the |
| 97 |
manifest (spec §12.4), and the first status after a commit must not |
| 98 |
replay the whole change. Options: the commit writes a checkpoint; |
| 99 |
history-only states load graphs on demand (`State::load_graphs`). |
| 100 |
Target: under 300 MB and 0.4 s. |
| 101 |
- [x] M2 **Lines without an allocation each.** An insert's lines become |
| 102 |
one shared buffer with line offsets (for example `Arc<[u8]>` and |
| 103 |
ranges) in the change and in the file graph. The file graph shares |
| 104 |
that buffer and does not copy it. `AtomInfo` and the edge map shrink. |
| 105 |
Measure bytes per line before and after. |
| 106 |
- [ ] M3 **A commit that streams.** The snapshot builder stores each |
| 107 |
file's text blob as it reads it and keeps no content it no longer |
| 108 |
needs, and the state is built without a second copy. Target for the |
| 109 |
first commit: under 2 GB. |
| 110 |
- [ ] M4 **Everyday commands under 300 MB** on the Linux repository: |
| 111 |
status, diff, commit, log and blame, one file and 1 000 files edited. |
| 112 |
|
| 113 |
## Speed |
| 114 |
|
| 115 |
- [ ] S1 **First commit at most 50 s** (55.3 s now): profile with |
| 116 |
`perf`, and parallelise reading, hashing and diffing across the four |
| 117 |
cores where it is safe. |
| 118 |
- [ ] S2 **Status, diff and commit** with one and with 1 000 files |
| 119 |
edited, to the targets above. |
| 120 |
- [ ] S3 **log, blame and checkout** on the Linux repository, to the |
| 121 |
targets above. |
| 122 |
- [ ] S4 **fsck and gc** measured against Git's. Re-run Git's `fsck` |
| 123 |
with gc turned off: 451 s is suspect, because Git's own background gc |
| 124 |
raced it. |
| 125 |
|
| 126 |
## Space |
| 127 |
|
| 128 |
- [ ] P1 **Store at most 350 MB after gc** (442 MB now against Git's |
| 129 |
311 MB): measure what takes the space (changes, text blobs, graphs, |
| 130 |
checkpoints, indexes), then fix the largest. Compression level and |
| 131 |
dictionary, and packing order, are all candidates. |
| 132 |
|
| 133 |
## History at scale |
| 134 |
|
| 135 |
- [x] H1 **Importer name conflict** (oxygen#30): the 5 000-commit |
| 136 |
vscode import differs from Git in 51 files. Fix the cause and add a |
| 137 |
test. `oxy git verify` must be clean. |
| 138 |
- [ ] H2 **Import memory**: drop graphs untouched by recent commits and |
| 139 |
reload them on demand, so memory is bounded by the working set and not |
| 140 |
by the history. |
| 141 |
- [ ] H3 **Linux, stage 2**: import slices of recent history (2 000, |
| 142 |
10 000 and 50 000 commits), each verified, then time log, blame and |
| 143 |
status against Git on the same slice. |
| 144 |
- [ ] H4 **vscode's full history** (about 130 000 commits) into a scratch |
| 145 |
repository on Earth. It is not published. Verify it and time it. |
| 146 |
- [ ] H5 **Linux's full history** (about 1.3 million commits) if H3 shows |
| 147 |
it can finish overnight. Otherwise, write up what stands in the way. |
| 148 |
|
| 149 |
## Finally |
| 150 |
|
| 151 |
- [ ] Z1 Re-run the full `~/bench/linux-bench.sh` with the final build, |
| 152 |
and put the complete Git against oxy table in `docs/benchmarks.md`. |
| 153 |
Summarise, for Andy, where oxy now matches or beats Git and where it |
| 154 |
does not. |
| 155 |
|
| 156 |
## Log |
| 157 |
|
| 158 |
- 2026-09-29 Backlog written. Starting point: the Linux first commit |
| 159 |
succeeds (55.3 s, 9.9 GB, 442 MB store) with change format 2 |
| 160 |
(oxygen#34). A clean `status` is killed at 14 GB (oxygen#35). |
| 161 |
- 2026-09-29 **C1 to C4 done** (the metalgear findings in ~/oxygen_feedback.md, items 22 and 25 to 26). The gate passes: 205 tests, clippy clean. |
| 162 |
- **C1** (e8e51993, closes #36): the working-copy lock now covers checkout, switch (and --carry), merge, commit, amend and rewrites, discard and pull. It is re-entrant per thread, waits up to a minute, and a live process's lock is never taken over. A workspace changed by two concurrent operations is now an error. A race test runs twenty switches against a snapshot thread; without the lock it failed one run in three. |
| 163 |
- **C2** (bf423e78, closes #37): `bookmark create NAME CHANGE` refuses when it would leave out earlier work, unless given --upto or --only. `bookmark switch` says how many changes the new state lacks. The guide explains states for people coming from Git. |
| 164 |
- **C3** (6b582c4c, closes #38): a parent's checkout moves a subrepo's bookmark only forward, and the pin is read from the nested working copy. `--carry` leaves a subrepo whose pin is carried work as it is. |
| 165 |
- **C4** (5dfea7b7, closes #29): not a sync bug. rstar-api's operation log shows each such pull ran on a feature bookmark, while main, which the fetch had just updated, was the one behind. pull now names the bookmark it pulled and the others left behind. |
| 166 |
- **For Andy:** metalgear's working copy is sound, on develop and clean. `logo-25` holds just the logo change and its dependencies; delete it if it is not wanted. |
| 167 |
- 2026-09-30 **M0 and M1 done; M4, S2 and S3 part way** (oxygen#35 and oxygen#39). The tests and clippy pass after each step. Each step was measured on the Linux tip and is recorded in docs/benchmarks.md. |
| 168 |
- **M0** (f11e81a1): a counting allocator behind the `alloc-stats` feature; with `OXY_TIMINGS` it reports memory per phase. |
| 169 |
- **M1**, with these steps: |
| 170 |
- history-only loads read no text (22b70898), then read only a change's skeleton (39c6adea); |
| 171 |
- graph keys are written at commit (22b70898); |
| 172 |
- a clean status is answered from the manifest (b4fefa1e) with a parallel walk (e19aa6c5). |
| 173 |
- Clean status: killed at 14 GB before, now 0.44 s and 123 MB (Git: 0.22 s). |
| 174 |
- **History-only loads where no graph is needed** (26f81555): bookmark, merge, pull, tags, bisect, doctor and most history rewrites. |
| 175 |
- **Checkout**: a batch at a time (921fa890), without a snapshot when clean (d1b480f0), and against the manifest without the base state (2b7ce2c6). |
| 176 |
- One-file switch: killed before, now 2.1 s and 455 MB (Git: 0.2 s). |
| 177 |
- Local clone: killed before, now 110 s and 8 GB (Git: 9.6 s). |
| 178 |
- **log, blame and fsck** (34ea4ace), with metadata-only change reads: |
| 179 |
- log -n 100: 28 s and 4.7 GB before, now 2.0 s; |
| 180 |
- blame: killed before, now 1.1 s; |
| 181 |
- fsck: killed before, now 123 s and 4.5 GB. |
| 182 |
- **Snapshot**: a parallel walk with stats carried into the manifest (a578fa19), and a cached tree view (de3f07f8). Status with one file changed: 4.1 s before, now 2.3 s (Git: 0.23 s). |
| 183 |
- **Still far from Git**: one-file and 1 000-file status, diff and commit (2 to 5 s); switch (2 s); log (2 s); clone (110 s, 8 GB); first commit (56 s, 10 GB); fsck (4.5 GB). |
| 184 |
- The common cost is loading the first change's history (0.5 s) and building the tree view (0.4 s). |
| 185 |
- Next: a persistent per-state cache (tree view, graph keys and change set), Git's index in effect, then compact graphs (M2) and a streaming commit (M3). |
| 186 |
- 2026-09-30 **M2 done, M3 and S1 part way** (oxygen#35). The first commit of the Linux tip now takes 40.7 to 41.6 s on a quiet Earth (S1 target: 50 s; Git: 34 to 38 s) and 4.9 GB RSS (from 10 GB). |
| 187 |
- **Encoding** (b7103633): each change is encoded once, the working copy's edits are moved rather than copied, and text blobs and chunk compression run on every core. |
| 188 |
- **Storing** (b7a090c7): texts go to the store a batch at a time. |
| 189 |
- **M2, lines** (086641a8): an insert's lines are one shared buffer, into which graph atoms point. |
| 190 |
- **Clone** (5d62fa66, 11bf3b00, 397d9f98): a local clone takes 48 s and 3.9 GB (from 96 s and 5.2 GB, and killed before that; Git: 9.6 s). The changes behind it: |
| 191 |
- an unedited file's graph is read off as a chain; |
| 192 |
- a bundle carries its changes' hashes; |
| 193 |
- a large change is read once per checkout; |
| 194 |
- a local fetch moves changes as stored. |
| 195 |
- **Hub and runner**: re-pinned to oxygen 397d9f98 and deployed (oxygenhub#58, a6f006ba; oxygenflow#15, 630b7fd6). The hub reads metadata or skeletons where it lists changes. Checked: /health, and the log, file history, tree and file pages. |
| 196 |
- **Note**: my pushes start Flow jobs on Earth, so timings under load are marked in benchmarks.md. |
| 197 |
- 2026-09-30 **H1 done** (oxygen#30). Three causes, each from Git having no identity for a directory: |
| 198 |
- a directory deleted and made again became a second node; |
| 199 |
- a placement under a removed directory was out of reach; |
| 200 |
- orphans could not be seen. |
| 201 |
- The fix: an import revives the directory, and a merge retires a kept node's other placements and every orphan. There is a Git-built test for each cause; each reproduces the vscode symptom without its fix. Spec §23. |
| 202 |
- The vscode slice now verifies: all 1 747 commits and 3.3 million files. |
| 203 |
- `oxy git verify` is cached by graph key and blob: 115 s instead of 1 400 s. |
| 204 |
- 2026-09-30 **H2, first part**: an import builds graphs only for the files a commit may change, from history-only states. The first 5 000 vscode commits: 309 s and 2.3 GB (from 412 s and 6.3 GB), and all verified. Next: verify and import reuse each commit's history instead of loading it again, before H3 to H5. |
| 205 |
|