OXYGENHUB
# fa53a2849de8 oxygen / docs / lazy-clone.md
10.2 KiB · Markdown · last changed Depth 0 takes no change, and a checkpoint is gathered in parallel (refs #137) 11 days ago renderedsource blame history raw
1
# Instant clone: a lazy working copy (oxygen#132)
2
3
Status: design, 2026-10-25. Nothing here is built yet beyond what it names
4
as existing. An experimental `oxy clone --lazy` (Linux) follows if this
5
measures well (§9).
6
7
## 1. The aim
8
9
`oxy clone URL` of any repository, however big, gives a working copy in
10
about a second: `ls`, `cd`, editors and builds see every file at once, a
11
file's bytes arrive the first time something reads it, and `status`,
12
`diff`, `commit` and `push` work at once and cost what the edits cost, not
13
what the repository weighs. Git's partial clone and VFS for Git do part of
14
this (VFS needs Windows and a special server); Oxygen can do all of it
15
with what it already has, because every object is content addressed and
16
a state's tree is known without its file contents.
17
18
## 2. What exists
19
20
- **Shallow clones** (spec §17): `oxy clone --depth 0` takes a checkpoint
21
  of the bookmark's heads and no history. The `checkpoint` message brings
22
  the checkpoint, its payload blob and **every file's graph blob** (spec
23
  §11, payload version 2; `Repo::checkpoint_bundle_for`,
24
  crates/oxy-core/src/repo/checkpoint.rs). A file graph holds the file's
25
  current text and its tombstones, so this is the whole state's text and
26
  more: for VS Code most of the clone's bytes.
27
- **Lazy blobs** (spec §13.5, §17): `--max-blob N` leaves opaque blobs
28
  larger than N to be fetched with `objects` on first read; `status` and
29
  `commit` never need them.
30
- **Sparse filters** (`--sparse PATH`): only those paths are materialized.
31
- **flow-casfs** (OxygenFlow, `crates/flow-casfs`): a read-only FUSE mount
32
  of a state from a local repository. The tree comes from the state
33
  index; a text file is rendered from its graph on first read or stat and
34
  cached on disk by graph key; an opaque file is its blob.
35
- **Content addressing everywhere**: an object fetched later is checked
36
  against the hash that named it, so a lazily fetched byte is as
37
  trustworthy as one cloned eagerly (spec §13).
38
39
## 3. The design
40
41
`oxy clone --lazy URL DIR`:
42
43
1. **Refs and the checkpoint skeleton.** Fetch `refs` (with the bookmark's
44
   attestation, checked against the policy as any clone does), then the
45
   checkpoint of its heads **without its graph blobs**: a `checkpoint`
46
   request with a new `graphs: false` field (additive; an older server
47
   ignores it and sends them, so the clone is merely not lazy). The
48
   payload's `files` entries name each graph blob by ObjectRef (graphs
49
   under 2 KiB are inline and come anyway), and its `tree` gives every
50
   path, mode and node. Clone writes the repository as a shallow one at
51
   depth 0 whose missing graph blobs are **lazy objects**, like lazy blobs
52
   today.
53
2. **The mount.** DIR is a FUSE mount (lower layer) of that state, built on
54
   flow-casfs: the directory tree is answered from the payload's tree at
55
   once; `stat` of a text file needs its length, so the payload gains
56
   (payload version 3, writer-optional) each file's rendered length, and
57
   until a server writes it the length comes with the graph on first
58
   `stat`. A read of a file whose graph is not stored fetches it with
59
   `objects` (hash checked), renders it, keeps both, and answers. Opaque
60
   files fetch their blob the same way. A small daemon (`oxy mount`,
61
   started by the clone, restarted by any oxy command that finds it gone)
62
   serves the mount; the repository itself stays in `DIR/.oxy`, which is
63
   a real directory beside the mount (bind or an overlay entry), so oxy
64
   never reads through FUSE.
65
3. **The writable layer.** Writes go to an upper directory
66
   (`.oxy/overlay/`): a written file is copied up whole on first write
67
   (its bytes fetched first if they were never read), a new file is
68
   created there, a removal is a whiteout, a rename is a whiteout plus a
69
   copy up. This is the ordinary overlay model, done in the daemon so it
70
   needs no privileges (kernel overlayfs over FUSE needs a user namespace
71
   and is not available everywhere).
72
4. **Status, diff and commit read the overlay only.** The upper layer is
73
   exactly the set of paths that may differ from the base state, so
74
   `status` lists it (comparing each copied-up file's bytes with the
75
   base rendering, which is stored, since copying up read it) and never
76
   walks the tree. `diff` and `commit` do the same, and the changes they
77
   build need only the touched files' graphs, all stored by then.
78
   `commit` clears the overlay entries it recorded: the mount's base
79
   state moves to the new heads.
80
5. **Fetch, pull, checkout.** A fetch brings changes as today (their
81
   blobs lazy). Moving the working copy to another state swaps the
82
   mount's base: the new state's tree is known from the changes and the
83
   stored checkpoint, the overlay is kept when its paths are unchanged
84
   between the two states (the `--carry` rule, §10.1), and graphs needed
85
   to render the new state are lazy again.
86
6. **Push.** As today: the pushed changes and the blobs they bring. A
87
   change built on a lazy base never needs the base's unread files.
88
89
## 4. Offline and failures
90
91
A file read once is kept, so it reads offline. Reading a file never read
92
while the hub cannot be reached fails with `EIO`, and the daemon logs the
93
path and the reason; `oxy status` says how many files are not local yet.
94
`oxy lazy fetch [PATH..]` brings them in ahead (a directory, the whole
95
state, or the files a build reads, as a background job), and `oxy lazy
96
materialize` turns the working copy into an ordinary one (unmounted, every
97
file written), after which nothing is lazy. A hash that does not match is
98
an error for that read, never a wrong byte.
99
100
## 5. Without FUSE
101
102
Where FUSE is not available (macOS without macFUSE, Windows, a container
103
without /dev/fuse), `--lazy` falls back to what exists: a depth-0 clone
104
with lazy blobs, and `--sparse` paths when given, saying so. Windows
105
ProjFS and macOS File Provider are later backends behind the same daemon
106
interface (lookup, read, write-through to the upper layer).
107
108
## 6. The protocol
109
110
- `checkpoint` gains `graphs: false`: leave out graph blobs (and their
111
  chunks) the payload names by reference.
112
- `objects` already fetches any object by hash, with a blob's chunks; it
113
  needs batching limits only (as `push.batch-bytes` bounds pushes).
114
- Optional later: `render` by graph key (the rendered bytes rather than
115
  the graph with its tombstones), smaller for long-lived files; a client
116
  checks it by rendering the graph when it has it, so it is an
117
  optimization a client may refuse.
118
- A host should keep `objects` cheap: it is now on the read path of
119
  editors and builds. OxygenHub's state cache and bucket stores (Strata)
120
  already serve objects by hash.
121
122
## 7. Trust
123
124
Nothing is weaker than an eager clone. The clone checks the bookmark's
125
attestation and the checkpoint against its heads (a checkpoint is valid
126
only if it reproduces from its heads, spec §11, which a lazy client cannot
127
check without the graphs: it trusts the attested heads and checks each
128
graph it fetches against the checkpoint's reference). `oxy verify` on a
129
lazy clone checks what is stored and says how much is not.
130
131
## 8. Not in scope
132
133
History (`log`, `blame` beyond the checkpoint) on a lazy clone fetches
134
changes on demand later (`oxy deepen` exists); the first version answers
135
`log` from the checkpoint's change list as a shallow clone does.
136
137
## 9. Measurements
138
139
On Earth (4 cores, SSD), 2026-10-25, the VS Code history
140
(`~/bench/vscode-full9.oxy`, 166k changes; `~/bench/vscode.git` for Git)
141
served over loopback by `oxy serve` and `git daemon`, oxy at main
142
(0.5.1 + #137), Git 2.53; one run each, a fresh directory, the server
143
warm (its checkpoint of the heads already made). Seconds:
144
145
| Step | git clone | git clone --filter=blob:none | oxy clone --depth 0 | oxy clone --depth 0 --max-blob 0 |
146
|---|---|---|---|---|
147
| clone | 91.4 | 30.5 | 457.9 | 849.0 |
148
| `ls -R \| wc -l` | 0.11 | 0.11 | 0.54 | 0.11 |
149
| read `src/vs/code/electron-main/main.ts` | 0.01 | 0.01 | 0.01 | 0.01 |
150
| append a line, `status` | 0.18 | 0.72 | 8.07 | 8.50 |
151
| `diff` | 0.10 | 0.03 | 0.23 | 0.23 |
152
| `commit` | 0.12 | 0.05 | 0.98 | 0.92 |
153
| disk after | 1.7 GB | 693 MB | 977 MB | 979 MB |
154
155
Before #137 the depth-0 clone failed after 11 minutes (the state's binary
156
files' blobs came with nothing) and, before that fix's other half, wrote
157
the checkpoint object by object: stopped after 38 minutes with 57,498
158
loose objects. The full `oxy clone` (12 GB of history) was not run
159
tonight.
160
161
What this says:
162
163
- A depth-0 clone is five times slower than Git's full clone and fifteen
164
  times slower than its blobless one. Timed on both sides (`OXY_TIMINGS=1`,
165
  oxygen#137): the server spent 375 s gathering the checkpoint's 57,000
166
  graph blobs and chunks one read at a time (239 s once read in parallel),
167
  19 s encoding one 2 GB answer, and the client 120 s on checkout, 111 s
168
  of it rendering 19,545 files on one core. Depth 0 also took the 33,835
169
  head changes, which it no longer does (spec §17), though that was not
170
  where the time went. A lazy clone avoids all three: no graph blobs move
171
  at clone time, and nothing is rendered until read.
172
- `--max-blob 0` without a mount is slower, not faster: the checkout then
173
  fetches every binary file's blob one request at a time. Lazy contents
174
  only pay with a mount that reads them when asked.
175
- The first `status` after a clone is 8 s (it builds the manifest); Git's
176
  is 0.2 s. A lazy clone's `status` from the overlay (§3.4) would not
177
  depend on the tree at all.
178
179
So the prototype's bar: clone in seconds (refs, the checkpoint skeleton
180
without graphs, the mount), `ls -R` at once from the payload's tree, a
181
first read in tens of milliseconds, `status` in milliseconds; and,
182
whatever happens to `--lazy`, the depth-0 clone itself should come down
183
to Git's order (streamed checkpoint in parts, rendering on every core).
184
185
## 10. The prototype (L2)
186
187
1. `checkpoint` with `graphs: false` in oxy-core and oxy-server, and a
188
   repository flag for lazy graph objects (fetch on load, like lazy blobs).
189
2. `oxy mount` (Linux, `fuser`, as flow-casfs) with the upper layer, in a
190
   new crate `oxy-mount` beside the CLI, behind a cargo feature so builds
191
   without FUSE are unaffected.
192
3. `status`/`diff`/`commit` from the overlay when the working copy is
193
   mounted.
194
4. `oxy clone --lazy`, `oxy lazy fetch`, `oxy lazy materialize`.
195
5. Measured as in §9; numbers on oxygen#132.