OXYGENHUB
# b5e98094eb8f oxygen / docs / benchmarks.md
43.0 KiB · Markdown · last changed A verified clean check restamps the manifest; checkout keeps directory times (refs #41) 1 month ago renderedsource blame history raw
1
# Benchmarks
2
3
The harness is `crates/oxy-core/tests/bench.rs` (spec §25). It builds a
4
repository through the library, one commit at a time, then times the
5
everyday operations against thresholds:
6
7
    OXY_BENCH_DIR=~/.cache/oxy-bench \
8
      cargo test --release -p oxy-core --test bench -- --ignored --nocapture
9
10
`OXY_BENCH_FILES` (default 10 000) and `OXY_BENCH_CHANGES` (default
11
5 000) set the size. The data lands under `OXY_BENCH_DIR`; `/tmp` is often
12
a RAM disk, which hides the cost of `fsync`, so measure on a real disk.
13
14
## 2026-09-28: first run
15
16
Machine: 8 cores, x86-64, NVMe (ext4 on LVM), release build, warm cache.
17
Files are 21 lines each in 100 directories; one change in ten appends a
18
line to `hot.txt`, the rest rewrite one ordinary file.
19
20
| Operation (10 000 files, 5 000 changes) | Time | Threshold | |
21
|---|---:|---:|---|
22
| commit: import 10 000 files | 1 031 ms | 60 000 ms | ok |
23
| commit: one file (mean of 4 999) | 3 761 ms | 250 ms | **over** |
24
| status: clean working copy (stat crawl) | 2 977 ms | 2 000 ms | **over** |
25
| status: 10 files modified | 3 062 ms | 2 000 ms | **over** |
26
| commit: 10 files | 9 089 ms | 500 ms | **over** |
27
| log: whole bookmark | 408 ms | 2 000 ms | ok |
28
| blame: hot file (500 lines) | 2 920 ms | 2 000 ms | **over** |
29
| blame: ordinary file | 2 927 ms | 1 000 ms | **over** |
30
| checkpoint | 596 ms | 10 000 ms | ok |
31
| status after checkpoint | 2 873 ms | 2 000 ms | **over** |
32
| push: everything to an empty remote | 782 ms | 60 000 ms | ok |
33
| push: up to date | 4 ms | 1 000 ms | ok |
34
| clone: everything, with checkout | 3 378 ms | 60 000 ms | ok |
35
| fsck | 7 955 ms | 60 000 ms | ok |
36
| gc | 9 489 ms | 60 000 ms | ok |
37
| status after gc | 2 829 ms | 2 000 ms | **over** |
38
39
Peak resident memory over the whole run: 1 405 MiB. The run took 5 h 14 min,
40
almost all of it building the history, because each commit costs as much
41
as a status.
42
43
Smaller runs, same machine:
44
45
| Files, changes | Mean commit | Clean status | Blame (hot) | Peak memory |
46
|---|---:|---:|---:|---:|
47
| 1 000, 200 | 90 ms | 62 ms | 62 ms | 70 MiB |
48
| 1 000, 800 | 166 ms | 130 ms | 119 ms | 118 MiB |
49
| 4 000, 200 | 343 ms | 235 ms | 239 ms | 254 MiB |
50
| 10 000, 5 000 | 3 761 ms | 2 977 ms | 2 920 ms | 1 405 MiB |
51
52
### What it shows
53
54
Sync, log, checkpoints and repository maintenance are fine. Everything
55
that snapshots the working copy (status, commit, blame of the working
56
copy's base) grows with both the number of files and the length of the
57
history. Timing the phases of one snapshot at 4 000 files and 300 changes:
58
59
| Phase | Time |
60
|---|---:|
61
| load the base state (checkpoint plus the changes since) | 75–130 ms |
62
| materialize every file of the base state | 75 ms |
63
| walk the working copy against the manifest | 50 ms |
64
| build the working-copy change | 8 ms |
65
66
A clean status should be a stat walk and nothing else. The fix is backlog
67
item O10: skip loading and materializing when every file's stat matches a
68
manifest written for the same base, make the base state lazy for the
69
callers that need it, and cache materialized snapshots per state. The
70
thresholds above stay as they are until then, so the harness reports the
71
gap rather than hiding it.
72
73
## 2026-09-28: after O10a
74
75
`State::maximal` compared every pair of heads, and the benchmark's
76
bookmark has about 4 500 of them (changes to different files are
77
independent). It now walks once; commit also stops loading the base state
78
twice. Same machine and size:
79
80
| Operation (10 000 files, 5 000 changes) | Before | After | Threshold | |
81
|---|---:|---:|---:|---|
82
| commit: one file (mean of 4 999) | 3 761 ms | 861 ms | 250 ms | **over** |
83
| status: clean working copy | 2 977 ms | 803 ms | 2 000 ms | ok |
84
| status: 10 files modified | 3 062 ms | 864 ms | 2 000 ms | ok |
85
| commit: 10 files | 9 089 ms | 861 ms | 500 ms | **over** |
86
| log: whole bookmark | 408 ms | 400 ms | 2 000 ms | ok |
87
| blame: hot file | 2 920 ms | 809 ms | 2 000 ms | ok |
88
| blame: ordinary file | 2 927 ms | 828 ms | 1 000 ms | ok |
89
| status after checkpoint | 2 873 ms | 770 ms | 2 000 ms | ok |
90
| push: everything | 782 ms | 742 ms | 60 000 ms | ok |
91
| clone: everything | 3 378 ms | 1 664 ms | 60 000 ms | ok |
92
| fsck | 7 955 ms | 6 872 ms | 60 000 ms | ok |
93
| gc | 9 489 ms | 9 526 ms | 60 000 ms | ok |
94
95
Peak resident memory: 1 145 MiB (was 1 405). The run took 1 h 12 min
96
(was 5 h 14 min). Commit time no longer grows with history; what remains
97
grows with the number of files: at 10 000 files a snapshot spends about
98
320 ms loading the base state, 190 ms materializing it, 130 ms walking the
99
working copy and 40 ms building the change. That is O10b.
100
101
## 2026-09-28: after O10b
102
103
Rendering the base state, building file graphs from a checkpoint and
104
reading the working copy now run on all cores. Measured at 10 000 files
105
with 60 and 300 changes (commit time no longer depends on the length of
106
history, so the full 5 000 was not rerun):
107
108
| Phase of one snapshot, 10 000 files | After O10a | After O10b |
109
|---|---:|---:|
110
| load the base state | 320 ms | 180–280 ms |
111
| materialize the base state | 190 ms | 75 ms |
112
| walk and read the working copy | 130 ms | 80 ms |
113
| build the working-copy change | 40 ms | 40 ms |
114
| write the manifest | – | 46 ms |
115
116
| Operation, 10 000 files | After O10a | After O10b | Threshold |
117
|---|---:|---:|---:|
118
| status: clean working copy | 803 ms | 464–581 ms | 2 000 ms |
119
| commit: one file (mean) | 861 ms | 550–605 ms | 250 ms |
120
| commit: 10 files | 861 ms | 583–635 ms | 500 ms |
121
122
Loading the whole base state is now the largest cost. O10c removes it: a
123
snapshot should load and render only the files whose stat changed, which
124
needs checkpoints that can be read per file.
125
126
## 2026-09-28: O10c, first part: the in-memory state
127
128
Profiling a clean snapshot with `OXY_TIMINGS=1` (10 000 files of 40 lines,
129
200 changes) showed loading the base state dominating it. Applying the
130
changes to the in-memory graphs cost 650 ms and reading them 160 ms. A
131
checkpoint did not help: decoding one took 680 ms, as long as replaying
132
every change, because both build the same graphs for 400 000 lines. So the
133
cost was in the graph structure, not in the format:
134
135
- **Hashing.** The graph maps used SipHash over 36-byte keys, three to
136
  five times per line. The keys begin with BLAKE3 hashes, so they now use
137
  a seeded multiply-rotate hasher (`fasthash.rs`). The seed is random per
138
  process, so keys chosen by someone else cannot be aimed at one bucket.
139
- **The predecessor map.** It was written for every line and read nowhere,
140
  so it is gone.
141
- **Successor sets.** A `BTreeSet` per vertex became a sorted edge list
142
  that keeps its single successor inline. Nearly every line has one.
143
- **Line bytes.** Loading moves them into the graph instead of copying
144
  them, and chains each new line with one lookup instead of three.
145
146
Both binaries were built from the same tree and run alternately, nine
147
times each, medians shown:
148
149
| 10 000 files, 200 changes | Before | After |
150
|---|---:|---:|
151
| status: clean working copy | 0.96 s | 0.52 s |
152
| commit: one file | 0.99 s | 0.51 s |
153
| peak memory (either) | 657 MB | 239 MB |
154
155
The checkpoint format is unchanged. What remains of O10c is split into
156
O10d and O10e in the backlog:
157
158
- **O10d.** Skip reading text files whose stat matches the manifest, when
159
  the manifest recorded them equal to the base.
160
- **O10e.** Checkpoints readable per file, so a snapshot loads only the
161
  graphs it needs. This one changes the checkpoint format in spec §25.
162
163
## 2026-09-28: O10g, first part: status snapshots skip unchanged files
164
165
The manifest now records, per text file equal to the base, the key of the
166
base graph it matched: the file's node, its checkpoint graph (blob,
167
inline value or none) and the set of later changes that edited it
168
(spec §12.4). When a file's stat and key both still match, the snapshot
169
neither reads nor renders it.
170
171
`status`, `log`, `show`, `tag` and `bookmark` use a partial snapshot. It
172
loads the base for its history and builds only the graphs of files that
173
are not known unchanged. `commit`, `diff` and the rest still build the
174
whole base.
175
176
10 000 files of 40 lines, 22 changes, a version 2 checkpoint after 21 of
177
them; `OXY_TIMINGS=1`, clean working copy:
178
179
| Operation | Before | After |
180
|---|---:|---:|
181
| status | 0.55 s | 0.22 s |
182
| status peak memory | 259 MB | 51 MB |
183
| log | 0.7–0.8 s | 0.38 s |
184
| diff (full snapshot) | 0.56 s | 0.57 s |
185
186
| Phase of a clean status snapshot | Before | After |
187
|---|---:|---:|
188
| load base (whole / history only) | 317 ms | 31 ms |
189
| render base | 91 ms | 25 ms |
190
| read working copy | 68 ms | 50 ms |
191
| diff against base | 41 ms | 28 ms |
192
| write manifest | 33 ms | 35 ms |
193
194
The same for `commit` and `diff` comes next (O10h). There, only the graphs
195
the change touches are needed, and materialized states can be cached.
196
197
## 2026-09-29: O10h, commit, diff and show build only what they touch
198
199
Commit, working-copy diff, hunks and discard use the partial snapshot of
200
O10g. Files known unchanged are neither read nor rendered, and only the
201
graphs of the files the working copy changes are built. Showing a
202
committed change (`oxy show`, `oxy diff CHANGE`) builds the graphs of the
203
files that change edits and leaves the rest unrendered.
204
205
Same repository as O10g (10 000 files of 40 lines):
206
207
| Operation | After O10g | After O10h |
208
|---|---:|---:|
209
| diff (working copy) | 0.57 s, 244 MB | 0.22 s, 52 MB |
210
| commit (one file) | 0.58 s, 245 MB | 0.23 s, 52 MB |
211
| show (a one-file change) | not measured | 0.53 s, 145 MB |
212
213
Caching materialized states, planned alongside, is not needed now that
214
these commands render almost nothing.
215
216
## 2026-09-29: P5c, importing microsoft/vscode, first part
217
218
`oxy git import` on slices of microsoft/vscode's `main` (166 412 commits in
219
total), taken in the importer's own order (`git log --reverse
220
--topo-order`). The first 5 000 commits reach May 2016 with about 4 700
221
files.
222
223
The importer did work proportional to the whole tree for every commit:
224
- It cloned the previous state.
225
- It rebuilt a merge's first parent from history.
226
- It rendered, read and compared every file.
227
- It scanned every entry, with an allocation each, to decide whether each
228
  directory was still needed.
229
230
Now:
231
- Files with the same Git blob and mode as in the first parent's tree are
232
  marked unchanged and skipped, as a clean working copy's are. Paths that a
233
  merge's other parents touched are excluded.
234
- A commit's state is kept while first-parent children remain, up to 8,
235
  and the last child takes it without a clone.
236
- Needed directories come from one set of ancestors. That code is shared
237
  by snapshot and commit.
238
239
| Slice | Before | After |
240
|---|---:|---:|
241
| 300 commits (35 merges) | 65.1 s | 13.6 s |
242
| 1 000 commits (95 merges) | 271 s | 120 s, 3.3 GB |
243
| 5 000 commits (360 merges) | not finished after 80 min | 412 s, 6.3 GB, 17 MB store |
244
245
Where the 5 000-commit import spends its time (summed per phase): render
246
base 172 s, build change 99 s, bases 65 s, `git ls-tree` 56 s, read blobs
247
10 s, store 5 s.
248
249
Correctness: `oxy git verify` reproduces every commit of the 300 and
250
1 000-commit imports (1.9 million file comparisons for 1 000). A test
251
covers the case that must not be skipped: a `-s ours` merge whose other
252
side edited a file.
253
254
Next: rendering the base is still O(files) per commit, and memory grows
255
with the kept states. The full import follows.
256
257
## Linux, stage 1: the tip as one commit (2026-09-29)
258
259
The Linux tree at its tip: 95 943 files, 1.8 GB, on Earth (4 cores,
260
31 GiB), each run capped at 14 GB.
261
262
| | Git | oxy before | oxy, change format 2 |
263
|---|---:|---:|---:|
264
| First commit | 33.7 s, 42 MB | failed, 10.9 GB | 55.3 s, 9.9 GB |
265
| Store | | unreadable | 442 MB |
266
| Clean `status` | 0.26 to 0.31 s | | killed at 14 GB |
267
268
Before format 2 the commit made one change of 1.8 GB. That was over the
269
1 GiB an object may be, and the repository would not open afterwards
270
(oxygen#33, oxygen#34). Format 2 stores each insert of 4 KiB or more as
271
a text blob, so the change stays small.
272
273
Memory is the next problem. Every line is kept as its own allocation,
274
once in the change and again in the file graph, and `status` builds
275
every graph (L1b).
276
277
### Status after the commit (2026-09-29, oxygen#35)
278
279
A profile with the counting allocator (`--features alloc-stats`,
280
`OXY_TIMINGS=1`) of the first clean `status`, before it was killed:
281
282
| Phase | Time | Live after | Allocated |
283
|---|---:|---:|---:|
284
| Load base history | 17.5 s | 3.2 GB | 16 GB in 96 M allocations |
285
| Read working copy | 79.5 s | 4.9 GB | 1.8 GB |
286
| Load and render base | killed at 14 GB | | |
287
288
It had two causes, and both are fixed:
289
- **History held every line.** A history-only state now reads a stored
290
  insert's text blob only when it builds that file's graph, and a load no
291
  longer hashes each change again, which for format 2 means chunking all
292
  its text.
293
- **The manifest had no graph keys.** They are now recorded at commit, so
294
  the next status neither reads nor renders an unchanged file.
295
296
| Clean `status` on the Linux tip | Time | Peak RSS |
297
|---|---:|---:|
298
| Before | killed at 14 GB | |
299
| Lazy text and keys at commit | 3.7 s | 768 MB |
300
| And answered from the manifest | 0.72 s | 120 MB |
301
| And the manifest checked by a parallel walk | 0.44 s | 123 MB |
302
| Git | 0.22 s | 31 MB |
303
304
A clean status is now answered from the manifest alone, loading no
305
history. The manifest also records the executable bit, since a mode
306
change alters neither size nor time. Any change, a manifest older than
307
the racy window, or subrepos send it back to a full snapshot.
308
309
Still to do: status to the 0.4 s target (the walk and reading the
310
manifest); a status without a usable manifest still builds every graph
311
and is killed at 14 GB; `fsck` is killed at
312
14 GB; the first commit takes 57 s and 10.2 GB.
313
314
### Checkout and clone (2026-09-29, oxygen#35)
315
316
A checkout built and rendered every file of the target, read the whole
317
working copy, and after writing walked and hashed it all again for the
318
manifest. Switching bookmarks or cloning the Linux tree was killed at
319
14 GB. Now:
320
- a file is skipped when the manifest shows it unchanged and its graph is
321
  the same in both states;
322
- the rest are rendered 2 048 at a time, and each batch's graphs are
323
  dropped once written;
324
- the manifest is built from what the checkout already knows.
325
326
| Linux tip | oxy before | oxy now | Git |
327
|---|---:|---:|---:|
328
| Switch bookmark (one file differs) | killed at 14 GB | 7.1 to 7.4 s, 1.3 GB | 0.18 to 0.22 s, 39 MB |
329
| Local clone | killed at 14 GB | 98 s, 7.9 GB | 9.8 s, 469 MB |
330
331
A history-only load now reads a change's skeleton: the inline lines of
332
its inserts are stepped over, not decoded, and are read from the change
333
only for a graph that is built. A one-file switch then takes 5.4 to 5.6 s
334
and 0.8 GB, and the status after it 3.3 s and 600 MB (from 3.8 s).
335
336
A checkout of a working copy that is clean by the manifest now skips the
337
status snapshot, and a manifest entry inside the racy window is checked
338
by hashing the file rather than distrusting the whole manifest. A
339
one-file switch then takes 3.7 to 3.9 s and 705 MB. Where it goes:
340
checking the manifest (a walk of 96 000 files) 0.75 s, the base's and
341
the target's history 0.5 s each, their tree views 0.3 s each, planning
342
0.2 s, and writing the manifest 0.6 s.
343
344
A checkout now needs no base state at all: after the manifest check or a
345
snapshot, the manifest says which graph each file on disk renders. A
346
file is skipped when its manifest key is the target's, and a file left
347
as it was keeps its stat in the new manifest instead of being statted
348
again. What the new state lacks is found by a walk from the old heads
349
that stops at the target. A one-file switch takes 2.1 to 2.2 s and 455
350
MB: the manifest check 0.4 s, the target's history 0.5 s and tree view
351
0.3 s, planning 0.16 s, and writing the manifest 0.4 s.
352
353
Before that, most of a switch was loading history: the base's and the target's
354
history each decode the one huge first change (1.2 s and 364 MB each).
355
A clone holds the whole bundle in memory.
356
357
### log, blame and fsck (2026-09-30, oxygen#35)
358
359
The first full run of `~/bench/linux-bench.sh` against this work found
360
`log -n 100` taking 28 s and 4.7 GB, and `blame` killed at 14 GB:
361
- **log.** Checking each change's signature read the whole change, and
362
  for the first change that meant all 1.8 GB of its text. Signature
363
  checks, the dependency walks of fetch and push, and the prompt now
364
  read a change's metadata from its skeleton, and log answers the
365
  working copy's state from the manifest when it proves it clean.
366
- **blame.** It took a full snapshot, building every graph, to blame one
367
  file; it now loads the base's history and builds that file's graph.
368
- **fsck.** A bookmark's state was checked by loading it whole. It is
369
  now checked a batch of graphs at a time, deleted files' graphs
370
  included.
371
372
| Linux tip | Before | Now | Git |
373
|---|---:|---:|---:|
374
| `log -n 100` | 28 s, 4.7 GB | 2.0 s, 435 MB | 0.16 s |
375
| `blame MAINTAINERS` | killed at 14 GB | 1.1 s, 315 MB | 0.31 s |
376
| `fsck` | killed at 14 GB | 123 s, 4.5 GB | to measure again |
377
378
### One file changed (2026-09-30, oxygen#35)
379
380
A status with one file changed (MAINTAINERS) took 4.1 s: the walk ran
381
on one thread (0.83 s), and the manifest was written by statting all
382
96 000 files again (0.57 s). The walk now runs in parallel and each
383
file's stat is carried from the walk into the manifest. It now takes
384
2.5 s: the base's history 0.5 s, its tree view and graph keys 0.38 s,
385
the walk 0.35 s, rendering the changed file 0.47 s, the diff 0.18 s,
386
and the manifest 0.14 s. (Git: 0.23 s.) The tree view is now computed once per
387
state and cloned for each use, which brings rendering to 0.2 s and the
388
status to 2.3 s.
389
390
### The first commit (2026-09-30, oxygen#35)
391
392
A profile of the first commit of the Linux tip (61 s, 10 GB RSS): read
393
the working copy 2.3 s (1.6 GB of content), diff against the empty base
394
6.2 s (4.2 GB live, a Vec per line), hash and record 18.8 s, then the
395
commit itself about 33 s. The text of the one huge change was chunked
396
and hashed three or four times on one thread (hashing the working-copy
397
change, storing it, storing the commit, adding it to the state), the
398
commit copied every line of the working copy, and the chunks were
399
compressed on one thread.
400
401
Now:
402
- a snapshot encodes the working copy once, and hashes and stores that
403
  one encoding;
404
- a whole commit moves the working copy's lines rather than copying
405
  them, and joins the state under the hash it was stored by;
406
- text blobs are made on every core, 256 inserts at a time;
407
- the chunks of a large change are compressed on every core
408
  (`Store::put_many`).
409
410
| First commit, Linux tip | Time | Peak (counted) | RSS |
411
|---|---:|---:|---:|
412
| Before | 61 s | 7.1 GB | 10.1 GB |
413
| After | 40.7 s | 7.4 GB | 8.7 GB |
414
| Texts stored a batch at a time while encoding | 41.6 s | 4.8 GB | 5.8 GB |
415
| Lines in one shared buffer per insert (Earth under load) | 50.1 s | 4.3 GB | 4.9 GB |
416
| Git | 33.7 to 37.9 s | | 42 MB |
417
418
Storing a change's texts a batch at a time as they are made (the
419
encoder hands each batch of 256 to the store) removed the storing peak.
420
The peak was then the working copy's content (1.6 GB) and its lines, a
421
Vec each (2.6 GB). An insert's lines are now one buffer with line ends
422
(`Lines`), shared by the change and by the file graph, whose atoms point
423
into it: the diff of the whole tree takes 3.3 s and 1.1 million
424
allocations (from 6.2 s and 45 million), and the peak is 4.3 GB (4.9 GB
425
RSS). That run shared Earth with Flow jobs (load average 6.5 on 4 cores),
426
so its total time is not comparable with the rows above.
427
428
### Clone (2026-09-30, oxygen#35)
429
430
A local clone of the Linux tip took 96 s and 5.2 GB RSS. Its checkout
431
spent most of its time rendering: working out each file's line order
432
allocated three sets per line (the contraction of the graph), 93 GB in
433
all, although every file of a fresh tree is one chain of lines. And the
434
import hashed each change three times, for the huge first change
435
chunking its 1.8 GB of text each time.
436
437
- A graph that is one chain from start to end with nothing deleted (an
438
  edge to End besides the next line orders nothing) is read off in
439
  order; any other graph takes the general algorithm, which also skips
440
  its contraction where a line's successors are all kept. A test checks
441
  the two agree.
442
- A bundle carries each change's hash when its maker knows it (the store
443
  it came from, or the received bytes); never from the wire. An import
444
  hashes each change once, by storing it, and checks it is the named one.
445
446
| Local clone, Linux tip | Time | RSS |
447
|---|---:|---:|
448
| Before | 96 s (81.6 s in a quieter run) | 5.2 to 5.4 GB |
449
| After | 61.0 s | 4.1 GB |
450
| The change read once for all batches | 56.8 s | 4.7 GB |
451
| Changes moved as stored (load average 5.2) | 48.2 s | 3.9 GB |
452
| Git | 9.6 s | 469 MB |
453
454
A fetch between repositories on one machine now moves changes as they
455
are stored, their text blobs and chunks among the objects, instead of
456
decoding every change's text on one side and chunking, hashing and
457
compressing it again on the other; the import stores all the objects at
458
once, compressed on every core, and refuses a change whose text blob did
459
not come.
460
461
### Git import: the vscode slice (2026-09-30, oxygen#30)
462
463
The 1 747-commit vscode slice (to merge 0babb4299131) had one file as a
464
name conflict at every commit from that merge on: 51 verify differences.
465
There were three causes, all from Git having no identity for a directory:
466
- a directory deleted on one line and made again got a second node for
467
  the same path;
468
- a placement left under a directory another line removed was out of
469
  sight, so no merge could retire it;
470
- a file whose only placements were under removed directories (an
471
  orphan) could not be seen either.
472
473
Now an import brings a directory back rather than making a second node,
474
and a merge retires a kept node's other placements and every orphan.
475
476
`oxy git verify` built and rendered every file of every commit and read
477
every Git blob; it now loads each commit's history and builds only the
478
files whose graph (by key) or blob it has not already found equal to
479
that Git blob.
480
481
| vscode slice, 1 747 commits | Before | After |
482
|---|---:|---:|
483
| Import | 128.6 s | 132.1 s |
484
| Differences | 51 | none: 3 327 291 files compared |
485
| Verify | 1 369 to 1 444 s, 832 MB | 114.6 s, 441 MB |
486
487
### Git import memory (2026-09-30, backlog H2)
488
489
An import kept up to eight full states (every file's graph) for commits
490
whose children were still to come. It now keeps history-only states and
491
builds the graphs of only the files a commit may change; unchanged ones
492
were already neither read nor rendered. An imported change joins its
493
base under the hash it was stored by, instead of being encoded again.
494
495
| vscode, first 5 000 commits | Import | Peak RSS | Verify |
496
|---|---:|---:|---:|
497
| Before (2026-09-29) | 412 s | 6.3 GB | |
498
| After | 309 s | 2.3 GB | every commit (13.4 million files) in 978 s |
499
500
Verify loaded each commit's history from scratch. It now keeps the
501
state of a commit whose children are still to come, as the import does,
502
and a child adds only the changes its parent lacks. Those states stay
503
history only: graphs are built beside them (`State::build_graphs_detached`)
504
and dropped after rendering, so changes are added without their text and
505
every file keeps its graph key, which the cache of verified files needs.
506
The same 5 000 commits verify in 166.5 s (from 978 s), 1.3 GB.
507
508
### Git import at scale (2026-09-30, oxygen#25)
509
510
An imported commit cost what the whole tree is: `git ls-tree` of every
511
path, a tree view worked out again, every path compared, and every
512
changed file rendered from its whole history. Phases timed over the
513
first 5 000 vscode commits (`OXY_TIMINGS`, repeated phases summed) led
514
the work, one cost at a time:
515
516
| Change | 5 000 commits |
517
|---|---:|
518
| Before | 312 s |
519
| Each commit's tree is its parent's plus the diff, from one `git diff-tree --stdin` | 245 s |
520
| A parent not kept has its tree listed, so its child renders only what changed | 225 s |
521
| An ordinary commit is built from the paths it changes alone | 178 s |
522
| The diff sets aside lines found on one side only, as Git's xdiff does | 91 s |
523
| It diffs from the base's rendering rather than rendering again | 81 s |
524
| Tree views updated in place; merges add the other side without text | 56 s |
525
| File graphs keep their order as edits splice it | 38.8 s |
526
527
Twenty text diffs took 92 of the 103 s the builder spent: files
528
rewritten whole (line endings changed), on which Myers' algorithm is
529
quadratic. Rendering a long-lived file walked every line it ever had;
530
it now follows its live lines. Every step verified all 5 000 commits
531
(13.4 million files) clean.
532
533
| vscode slice | Before | After |
534
|---|---:|---:|
535
| 5 000 commits | 312 s, 2.1 GB | 38.8 s, 1.5 GB |
536
| 20 000 commits | 4 530 s, 6.7 GB | 925 s, 6.4 GB |
537
538
### Store size (2026-09-30, backlog P1)
539
540
The Linux tip's store was 448 MB, and 390 MB after `oxy gc`, against
541
311 MB for Git after `git gc`. The file contents cut into the store's
542
chunks compress, at zstd level 3, to 312 MB, level with Git; at level 9
543
to 284 MB (12.9 s on four cores), at level 19 to 270 MB (314 s). The
544
rest was two things:
545
- `oxy gc` left the first pack as written: packs merge only above 16.
546
- A text blob holds its lines as a CBOR array, so every line has a
547
  length header: 62 MB more raw, 35 MB more compressed for the tree's
548
  46 308 files of 4 KiB or more.
549
550
`oxy gc` now repacks everything into one pack at zstd level 9, compressed
551
on every core: 357 MB (349 MB pack) in 52 s. Change format 3 then stores
552
a text blob as the lines' bytes:
553
554
| Linux tip | Store after gc | First commit |
555
|---|---:|---:|
556
| Format 2, gc at level 3 | 390 MB | 41 s |
557
| Format 2, gc repacks at level 9 | 357 MB | |
558
| Format 3 (Earth at load average 11) | 324 MB (objects 306 MB) | 32.6 s, 4.4 GB |
559
| Git | 311 MB (.git) | 33.7 to 37.9 s, 42 MB |
560
561
### fsck (2026-09-30)
562
563
On the Linux tip in change format 3, `oxy fsck` took 24.7 s and 2.8 GB:
564
its references pass read every change whole, the first one's 1.8 GB of
565
text into memory, to check dependencies, authors and blobs. It now reads
566
a change's skeleton and checks its text as the blobs it is stored in (the
567
objects pass has checked every object already): 17.2 s and 984 MB, the
568
references pass 1.7 s. Phases are timed (`OXY_TIMINGS`): objects 5.1 s,
569
references 1.7 s, states and checkpoints 10.4 s.
570
571
572
### The full run (2026-09-30 06:24, main-0ba9b2e4)
573
574
`~/bench/linux-bench.sh` on Earth, at load average 3.8 to 4.5 (flow jobs
575
were running). Times are wall clock; memory is peak resident size.
576
577
| Operation | oxy | Git |
578
|---|---:|---:|
579
| init and commit the tip | 38.50 s, 4.4 GB | 38.77 s, 42 MB |
580
| status, clean (three runs) | 0.68 to 0.76 s, 123 MB | 0.35 to 0.76 s, 31 MB |
581
| status, one file | 3.32 s, 641 MB | 0.59 s |
582
| diff, one file | 2.93 s | 0.24 s |
583
| commit, one file | 4.74 s, 752 MB | 0.45 s |
584
| status, 1 000 files | 3.88 s, 811 MB | 0.41 s |
585
| diff, 1 000 files | 2.83 s | 0.95 s |
586
| commit, 1 000 files | 4.85 s, 922 MB | 0.57 s |
587
| log -n 100 | 1.39 s, 291 MB | 0.01 s |
588
| blame MAINTAINERS | 1.18 s, 335 MB | 0.09 s |
589
| checkout the first state (run by hand) | 2.84 s | 0.42 s |
590
| checkout back | 2.22 to 2.72 s, 491 MB | 0.47 s |
591
| fsck | 22.10 s, 1.07 GB | 13.09 s |
592
| gc | 39.66 s, 2.8 GB | 102.79 s |
593
| store before gc | 414 MB | 648 MB |
594
| store after gc | 324 MB | 295 MB |
595
596
The script's own oxy checkout of the first state failed; its hash lacked
597
the `#`, and the script is fixed. The time in the table is from running
598
it by hand.
599
600
Commit, one file: its graph keys now come from the state the commit
601
extended rather than a reload of the new base (main-eff93613), which
602
takes it to 3.05 to 3.27 s; the status after it takes 0.43 s.
603
604
What is left for the everyday commands is mostly loading the base's
605
history and its tree view (about 1 s of each) and rendering what
606
changed. oxygen#41 proposes a base index that would make those reads
607
proportional to what changed.
608
609
### A state index for the working copy's base (2026-09-30, oxygen#41)
610
611
A snapshot replayed the base's history (0.70 s) and worked out its tree
612
view (0.31 s) every time. Each workspace now keeps its base as a local
613
state index: change metadata, the view, the introducers, every file's
614
deferred history with its graph key, and the tree's maps, decoded only
615
when a command needs them. The index is hashed, and a test build checks
616
every read against a full load. The rest of a status that grew with
617
the tree now follows the change: the manifest is parsed once and
618
rewritten only when that helps; graph keys are looked up only for files
619
the manifest knows; and the change is built from the changed paths.
620
621
Linux tip, MAINTAINERS edited, oxy and Git alternating under the same
622
load (Earth at load average 4.5, other jobs running):
623
624
| | oxy (06:24) | oxy now | Git |
625
|---|---:|---:|---:|
626
| status | 3.32 s, 641 MB | 1.26 to 1.39 s, 370 MB | 0.26 to 0.29 s |
627
| diff | 2.93 s | 1.19 to 1.45 s | 0.16 to 0.25 s |
628
| status, clean | 0.68 to 0.76 s | 0.41 to 0.43 s | 0.26 to 0.32 s |
629
630
### Baseline for backlog 4 (2026-09-30 21:43, oxygen#41)
631
632
`~/bench/linux-bench.sh`, now with three runs of each everyday command,
633
oxy and Git alternating, and a local clone. Earth's load average was 7.2
634
at the start (no Flow jobs running), falling during the run; the first run
635
of each set is the noisiest.
636
637
| Operation | oxy | Git |
638
|---|---:|---:|
639
| init and commit the tip | **33.3 s**, 3.1 GB | 38.2 s, 42 MB |
640
| status, clean | 0.43 to 0.53 s, 75 MB | 0.34 to 0.46 s |
641
| status, one file | 1.73 to 1.81 s (3.36 s first), 375 MB | 0.36 to 0.65 s |
642
| diff, one file | 1.77 to 2.24 s | 0.24 to 0.29 s |
643
| commit, one file | 3.10 s | 0.41 s |
644
| status, 1 000 files | 1.62 s (4.21 s first), 573 MB | 0.28 to 0.38 s |
645
| diff, 1 000 files | 1.54 to 2.40 s | 0.59 to 0.81 s |
646
| commit, 1 000 files | 2.47 s | 0.53 s |
647
| log -n 100 | 0.86 to 1.03 s | 0.00 s |
648
| blame MAINTAINERS | 0.99 to 1.04 s | 0.07 s |
649
| clone, local path | 56.3 s, 3.3 GB | 18.4 s |
650
| checkout the first state, and back | 2.86 s, 3.00 s | 0.39 s, 0.50 s |
651
| fsck | 24.4 s, 773 MB | 12.8 s |
652
| gc | **48.1 s** | 100.5 s |
653
| store before and after gc | 465 MB, 375 MB | 648 MB, 295 MB |
654
655
oxy is now ahead of Git on the first commit (33 s against 38 s) and on gc
656
(48 s against 100 s). The store after gc grew from 324 MB because it now
657
includes the workspace's state index.
658
659
660
### S1, first part: walk, manifest, lazy index, mimalloc (2026-09-30 22:39, oxygen#41)
661
662
The same script after the first round of S1: a lister of our own in place
663
of the ignore crate's walker, the manifest as a flat binary file with
664
hashes as bytes, each file's history in the state index decoded only when
665
used, maps built in path order, and mimalloc as the allocator. Earth's
666
load average was 4 to 5 (no Flow jobs); runs alternate with Git.
667
668
| Operation | oxy | before (21:43) | Git |
669
|---|---:|---:|---:|
670
| init and commit the tip | **24.4 s**, 3.2 GB | 33.3 s, 3.1 GB | 36.2 s |
671
| status, clean | **0.17 to 0.18 s**, 130 to 150 MB | 0.43 to 0.53 s | 0.22 to 0.23 s |
672
| status, one file | 0.47 s (1.47 s first) | 1.73 to 1.81 s | 0.22 to 0.23 s |
673
| diff, one file | 0.47 to 0.51 s | 1.77 to 2.24 s | 0.11 to 0.14 s |
674
| commit, one file | 0.96 s | 3.10 s | 0.17 s |
675
| status, 1 000 files | 0.65 s (1.99 s first) | 1.62 s | 0.23 to 0.24 s |
676
| diff, 1 000 files | 0.65 s | 1.54 to 2.40 s | 0.46 to 0.48 s |
677
| commit, 1 000 files | 1.20 s | 2.47 s | 0.50 s |
678
| log -n 100 | 0.48 to 0.53 s | 0.86 to 1.03 s | 0.00 s |
679
| blame MAINTAINERS | 0.59 s | 0.99 to 1.04 s | 0.07 s |
680
| clone, local path | 36.7 s, 3.6 GB | 56.3 s | 18.0 s |
681
| checkout the first state, and back | 1.48 s, 1.37 s | 2.86 s, 3.00 s | 0.35 s, 0.34 s |
682
| fsck | 14.0 s, 1.0 GB | 24.4 s | 12.6 s |
683
| gc | **29.9 s** | 48.1 s | 70.3 s |
684
| store after gc | 369 MB | 375 MB | 295 MB |
685
686
mimalloc alone took one-file status from 0.78 to 0.47 s and clean status
687
from 0.26 to 0.18 s: status allocates hundreds of thousands of small
688
values from every thread, and glibc's arena locks were the largest single
689
cost. The first status after a commit (1.47 s) rebuilds the state index.
690
691
Afterwards (22:55), log reads change metadata from the state index and a
692
metadata cache in the store, and its signature marks no longer read each
693
change whole: `log -n 100` takes 0.19 to 0.20 s, of which 9 ms is the log
694
itself and the rest the check that the working copy is clean.
695
696
Where one-file status goes now (0.47 s): listing the working copy about
697
150 ms (the syscalls alone: `find` with a stat of each file takes 0.37 s
698
on one core), loading the state index about 60 ms, reading the manifest
699
about 40 ms, rendering and diffing the changed file about 100 ms.
700
701
### S1 to S5, second round (2026-09-30 23:54, oxygen#41)
702
703
After the second round: merge joins over whole-tree maps, a commit that
704
works out keys only for what it changed and brings the state index up to
705
date by replaying rather than rewriting it, the manifest checksummed and
706
written without fsync, log and blame from the state index, checkout
707
through the index with two indexes kept. Load average 4.2 at the start,
708
no Flow jobs.
709
710
| Operation | oxy | 22:39 | Git |
711
|---|---:|---:|---:|
712
| init and commit the tip | **21.9 s**, 3.2 GB | 24.4 s | 33.7 s |
713
| status, clean | **0.17 to 0.19 s** | 0.17 to 0.18 s | 0.22 s |
714
| status, one file | 0.40 to 0.41 s (1.10 s first) | 0.47 s | 0.22 to 0.24 s |
715
| diff, one file | 0.39 to 0.48 s | 0.47 to 0.51 s | 0.10 to 0.11 s |
716
| commit, one file | 0.67 s | 0.96 s | 0.21 s |
717
| status, 1 000 files | 0.58 to 0.65 s | 0.65 s | 0.23 to 0.24 s |
718
| diff, 1 000 files | 0.59 to 0.62 s | 0.65 s | 0.46 to 0.48 s |
719
| commit, 1 000 files | 0.88 s | 1.20 s | 0.48 s |
720
| log -n 100 | 0.17 to 0.19 s | 0.48 to 0.53 s | 0.00 s |
721
| blame MAINTAINERS | 0.16 s | 0.59 s | 0.07 to 0.16 s |
722
| clone, local path | 32.2 s, 3.6 GB | 36.7 s | 17.0 s |
723
| checkout the first state, and back | 0.74 s, 0.74 s | 1.48 s, 1.37 s | 0.37 s, 0.33 s |
724
| fsck | 13.6 s | 14.0 s | 12.6 s |
725
| gc | **29.4 s** | 29.9 s | 70.2 s |
726
| store after gc | 369 MB | 369 MB | 295 MB |
727
728
The first one-file status after the first commit (1.10 s) builds the
729
state index for the first time. Most of what remains in status is listing
730
the tree (about 135 ms), loading the index (about 60 ms) and starting up;
731
commit adds storing and recording with fsync (about 100 ms).
732
733
### P5a: flow-casfs, a read-only FUSE mount of a state (2026-10-01, oxygenflow#21)
734
735
The first flow-casfs (single-threaded, fuser 0.16, no read-ahead) on the
736
Linux repository on Earth, with the H4 import and Flow jobs running (load
737
average 10 to 11), so the times are high:
738
739
| | |
740
|---|---:|
741
| mount (state from the index 284 ms, tree of 102 327 entries 40 ms) | 0.52 s |
742
| first read of MAINTAINERS (rendered) | 0.10 s |
743
| `ls -lR`, rendering every file one at a time | 201 s |
744
| full read afterwards (bytes in memory) | 0.77 s |
745
| full read, new mount, renderings cached on disk | 20.8 s |
746
| render cache on disk | 1.8 GB |
747
748
`diff -r` between the mount and the working copy finds no difference. To
749
reach the spec's targets: render in parallel and ahead of reads, take a
750
file added once and never edited straight from its text blob, keep sizes
751
in the render cache so a stat does not render, serve FUSE on several
752
threads, and read the state index lazily for a mount under 100 ms.
753
754
### Status with work in progress (2026-10-01, oxygen#41)
755
756
`MAINTAINERS` edited on the Linux tree on Earth (load average 2.2, one
757
Flow job running), oxy and Git alternating, three runs each:
758
759
| | before | after | Git |
760
|---|---:|---:|---:|
761
| status, one file edited (after a first status) | 0.42 to 0.46 s, 330 MB | **0.16 to 0.17 s**, 120 to 155 MB | 0.20 to 0.21 s |
762
| status, clean | 0.18 to 0.20 s | **0.15 to 0.16 s** | 0.20 to 0.24 s |
763
| diff, one file | 0.43 s | 0.40 to 0.41 s | 0.08 to 0.09 s |
764
765
Before, the manifest only proved a clean working copy. With uncommitted
766
work every status listed and snapshotted the tree again, loaded the state
767
index and rendered the base (listing 164 ms, index 60 ms, base 33 ms). Now
768
a snapshot leaves a status cache keyed by the working-copy change and the
769
manifest's checksum, and keeps the manifest current for the edited files.
770
A later status that finds every file as the manifest says answers from
771
the cache. Test builds check that answer against a snapshot. Diff still
772
renders the base; it is next.
773
774
Then diff (same day, load average 4.2, Git 0.10 s throughout): while the
775
status cache holds, `oxy diff` renders the stored working-copy change
776
against its base loaded from the state index, rather than snapshotting.
777
0.34 to 0.35 s and 250 MB (from 0.41 s and 360 MB). Rendering it through
778
`change_base`, which reads the base's changes rather than the index, was
779
slower than a snapshot (0.9 s) and was not kept. What remains is the
780
status check's listing (about 130 ms), the index (60 ms) and rendering.
781
782
Then directory times (same evening, load average 3.1): the manifest
783
(format 3) keeps every directory the walk listed with its time, read
784
before the directory was. While no directory's time has moved, the check
785
stats the tracked files and does not list the tree; a directory whose
786
entries changed sends it back to a listing.
787
788
| | before | after | Git |
789
|---|---:|---:|---:|
790
| status, one file edited | 0.16 to 0.20 s, 120 to 155 MB | **0.10 to 0.12 s, 80 MB** | 0.22 s |
791
| status, clean | 0.15 to 0.21 s | **0.09 to 0.12 s, 80 MB** | 0.20 to 0.26 s |
792
| diff, one file | 0.34 s | 0.24 to 0.25 s | 0.09 to 0.13 s |
793
794
Diff, last round (load average 2.7): a change's base renders only its
795
edited files without first building a set of every other file to leave
796
out, and `oxy diff` no longer frees its structures just before the
797
process ends. 0.19 to 0.21 s (from 0.25 s; Git 0.08 s). What remains:
798
the stat check 78 ms (Git stats the same files), the state index 58 ms
799
(39 of them building the tree view), the base's full file map 21 ms.
800
801
### Commit, one file (2026-10-01, oxygen#41)
802
803
`~/bench/commit-vs-git.sh`: commits go forward and both repositories are
804
put back at the end (an `oxy undo` between runs dropped the manifest, and
805
its rehash of the tree evicted the page cache Git's runs relied on, which
806
made both sides' numbers meaningless). Load average 3 to 4.
807
808
| | before | after | Git `commit -am` |
809
|---|---:|---:|---:|
810
| commit after a status | 0.68 s | **0.36 to 0.44 s** | 0.14 to 0.17 s |
811
| commit straight after the edit | 0.68 s | 0.62 to 0.69 s | 0.14 to 0.17 s |
812
813
After a status, the commit takes the status's snapshot from the cache.
814
Straight after an edit, the stat check now hands its stats to the
815
snapshot as its listing (8 ms instead of 122 ms), but checks every file
816
rather than stopping at the first difference, so the gain is small. What
817
remains is mostly durability: 18 fsyncs take 210 ms of the commit (one
818
alone 120 ms). Git does not sync loose objects by default.
819
820
Fewer waits on the disk (same evening): a batch's loose objects are
821
written together, synced in parallel, renamed and their directories
822
synced once each; a commit runs in one batch; packing keeps the emptied
823
fan-out directories, and a missing one has all 256 created with one sync
824
of `loose` (that sync alone took 130 ms on a busy disk). Serial syncs per
825
commit fall from 18 to about 7; each still waits on the same ext4 journal
826
commit, so the gain is modest. Old and new builds alternating, five
827
commits after a status each: median 0.39 s before, 0.34 s after (Git
828
0.14 to 0.16 s).
829
830
A commit's own snapshot (straight after an edit) no longer stores its
831
working-copy change or records an operation for it: the commit's
832
operation, recorded straight after, replaces the working copy. Old and
833
new builds alternating, six direct commits each (load average 4.4): median
834
0.61 s before, 0.58 s after (Git 0.16 to 0.27 s at that load).
835
836
### log -n 100 (2026-10-02, oxygen#41)
837
838
`oxy log` no longer checks the files on disk: its working-copy row
839
describes the working copy as last recorded (with edits, or not; the
840
manifest's conflict flag), as Git's log never looks at the working tree.
841
The entries come from the state index's change metadata.
842
843
| repository | before | after | Git |
844
|---|---:|---:|---:|
845
| Linux tree (Earth) | 0.17 s | **0.03 s** | 0.00 s |
846
| oxygen, 388 changes (workstation) | 0.06 s, 79 MB | **0.03 s, 22 MB** | |
847
| vscode slice, 10 000 commits (Earth, warm) | | **0.02 s** | |
848
849
What remains on oxygen is checking each row's signature (30 ms for 100).
850
851
### Against Git tuned for large repositories (2026-10-02)
852
853
Git as it comes compares an untuned Git with an oxy that uses every
854
cache it has. `~/bench/vs-git-tuned.sh` adds a copy of the Git repository
855
tuned as Git recommends for large ones: `feature.manyFiles` (index v4 and
856
more), `core.untrackedCache`, and a commit-graph. Not `core.fsmonitor`:
857
Git 2.53 has no file-watcher daemon on Linux, and Watchman is not
858
installed. Earth, load average 1.6, three runs each, alternating.
859
860
| | oxy | Git | Git, tuned |
861
|---|---:|---:|---:|
862
| status, clean | 0.16 s | 0.20 to 0.21 s | **0.10 s** |
863
| status, one file edited | 0.12 to 0.13 s | 0.21 to 0.22 s | **0.10 to 0.11 s** |
864
| diff, one file | 0.22 to 0.23 s | 0.09 s | **0.08 to 0.09 s** |
865
| log -n 100 | 0.03 s | **0.00 s** | **0.00 s** |
866
| blame MAINTAINERS | 0.16 s | **0.06 s** | **0.06 s** |
867
868
Tuned Git is ahead of oxy on all five. Its untracked cache is what oxy's
869
directory times do, and with it Git's status is a little faster than
870
oxy's. oxy beats Git as it comes on status only. Claims of speed against
871
Git should say which Git.
872
873
Then the stat check by directory (same night): tracked files are grouped
874
by directory, each directory opened once, and each file statted relative
875
to it (`fstatat`), so a stat resolves one name rather than every component
876
from the root (the kernel's path lookup was most of a clean status). The
877
stat check went from 84 ms to 73 ms; a clean status is 0.10 to 0.12 s,
878
level with tuned Git (0.10 to 0.11 s). What is left is the 96 000 stat
879
calls themselves, the same work Git does; doing less needs a file watcher
880
(Git's fsmonitor), which oxy does not have.
881
882
### Blame, and the tree view (2026-10-02, oxygen#41)
883
884
`oxy blame` writes through one buffered lock on stdout and formats each
885
change's id, author and age once (30 219 lines of MAINTAINERS share a few
886
changes); printing went to 4 ms. The state index's tree view is built in
887
one step from its sorted paths rather than by an insert per path: 37 ms
888
to 21 ms, for every command that loads the index. Quiet Earth (load
889
average 0.4), three runs each, alternating:
890
891
| | oxy | Git | Git, tuned |
892
|---|---:|---:|---:|
893
| status, clean | 0.10 s | 0.19 to 0.23 s | 0.09 to 0.10 s |
894
| status, one file edited | 0.10 s | 0.19 s | 0.09 s |
895
| diff, one file | 0.17 to 0.19 s | 0.08 s | 0.08 s |
896
| log -n 100 | 0.03 s | 0.00 s | 0.00 s |
897
| blame MAINTAINERS | **0.08 s** (from 0.16) | 0.06 s | 0.06 s |
898
899
### Checkout, first measurement against Git (2026-10-02, oxygen#41)
900
901
`~/bench/checkout-vs-git.sh`: to the first state (1 001 files differ) and
902
back, oxy, Git and tuned Git alternating, three each (load average 1.6).
903
904
| | oxy | Git | Git, tuned |
905
|---|---:|---:|---:|
906
| to the first state | 0.65 to 0.71 s | 0.26 to 0.27 s | 0.24 to 0.26 s |
907
| and back | 0.71 to 0.73 s | 0.23 to 0.26 s | 0.23 to 0.24 s |
908
909
Where a checkout's time goes: the manifest check 134 ms (listing: see
910
below), planning 67 ms, the 1 001 texts read from the store (134 ms) or
911
their graphs built (141 ms) going back, writing 65 to 82 ms, recording
912
115 to 143 ms, the manifest 45 ms.
913
914
Found on the way, now fixed: a manifest written by a checkout kept no
915
directory times, and directories a checkout had just written stayed
916
within the racy window of the manifest's time for good, so every status
917
after a checkout listed the tree. A clean check that verified the tree
918
now restamps the manifest with the time it began, keeping the
919
directories it listed (Git restamps its index the same way): the first
920
status after a checkout takes 0.15 s, the ones after it 0.10 s again.
921