A shallow clone of a big repository works, and quickly (closes #137) 7bd805ffb418
A depth-0 clone now fetches the blobs of the state's binary files (or leaves them lazy under --max-blob): no change it takes names them. The checkpoint is stored in one batch instead of object by object.
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 175cad1d596c
- When
- · 2026-10-25 00:53 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
7bd805ffb418c08c7e31385c5823b239cf46df3191bf326652ae31fa403cff77- Depends on
- 2863456a0f6d 75167672ebcd 8fd78eb52ad2 982864068a59 d3b270909c78 e4595038502a ff0af1ee299d
- Depended on by
- 80b069facc69 b260109d27c5 f3811eb20bea 2aa1854b0d13
- In bookmarks
- identical-sides main push-notes subrepo-pin-state
- In tags
- v0.10.0 v0.11.0 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 8 atoms, 1 tree operation, 5 files
Browse the tree at this change
added docs/lazy-clone.md +172 −0
| 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, with the VS Code history (`~/bench/vscode-full9.oxy`, 166k |
|
| 140 | + | changes, served by `oxy serve` on the LAN so the network is the same for |
|
| 141 | + | every case), each timed cold (`echo 3 > drop_caches` not available: a |
|
| 142 | + | fresh clone directory each time, and the server warm), median of three: |
|
| 143 | + | ||
| 144 | + | | Step | git clone | oxy clone | oxy clone --depth 0 | oxy clone --lazy | |
|
| 145 | + | |---|---|---|---|---| |
|
| 146 | + | | clone | | | | | |
|
| 147 | + | | `ls -R \| wc -l` | | | | | |
|
| 148 | + | | read one file (`src/vs/code/electron-main/main.ts`) | | | | | |
|
| 149 | + | | edit it, `status` | | | | | |
|
| 150 | + | | `diff` | | | | | |
|
| 151 | + | | `commit` | | | | | |
|
| 152 | + | | `push` (to a fresh bare repo) | | | | | |
|
| 153 | + | | bytes over the wire | | | | | |
|
| 154 | + | | disk after | | | | | |
|
| 155 | + | ||
| 156 | + | Git is `git clone --no-local` from a `git daemon` of `~/bench/vscode.git` |
|
| 157 | + | (and `--filter=blob:none` as a second row, Git's own lazy clone). The |
|
| 158 | + | first three oxy columns can be measured now; they set the bar the |
|
| 159 | + | prototype must clear: the lazy clone should finish in seconds and its |
|
| 160 | + | first `status` in milliseconds, and nothing else may get slower. |
|
| 161 | + | ||
| 162 | + | ## 10. The prototype (L2) |
|
| 163 | + | ||
| 164 | + | 1. `checkpoint` with `graphs: false` in oxy-core and oxy-server, and a |
|
| 165 | + | repository flag for lazy graph objects (fetch on load, like lazy blobs). |
|
| 166 | + | 2. `oxy mount` (Linux, `fuser`, as flow-casfs) with the upper layer, in a |
|
| 167 | + | new crate `oxy-mount` beside the CLI, behind a cargo feature so builds |
|
| 168 | + | without FUSE are unaffected. |
|
| 169 | + | 3. `status`/`diff`/`commit` from the overlay when the working copy is |
|
| 170 | + | mounted. |
|
| 171 | + | 4. `oxy clone --lazy`, `oxy lazy fetch`, `oxy lazy materialize`. |
|
| 172 | + | 5. Measured as in §9; numbers on oxygen#132. |
modified CHANGELOG.md +1 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | - **A shallow clone of a big repository works, and quickly (#137).** `oxy clone --depth 0` of the VS Code history failed after eleven minutes ("object … not found"): the blobs of the state's binary files are named only by the changes that set them, which a depth-0 clone does not take, so nothing brought them. They are now fetched with the checkpoint (in parts), or left to be fetched on read under `--max-blob`. The checkpoint itself was stored object by object, each written loose and flushed (57,498 objects at 27 a second); it is now stored in one batch, as a fetch's objects are. |
|
| 5 | 6 | - **A push no longer passes a proxy's request limit (#133).** A push to a hub behind Cloudflare, which takes at most 100 MB in one request, was refused `HTTP 413` with Cloudflare's HTML page printed raw: parts of a long push were about 256 MiB, and one change's blobs went in one request however many there were. Parts are now about 64 MiB (`push.batch-bytes`), a part whose blobs pass that sends them first in parts of objects alone, an HTML error page is shown by its title, and a 413 says how to push in smaller parts. |
|
| 6 | 7 | - **A keyless agent's grant survives its owner's key rotation (#135).** Every delegation an owner adds or removes renews their identity, and a repository that names the newest key as `author.key` read every change their agent sealed as INVALID ("key … is neither the author nor a delegate"), and its pushes were refused, because the agent's grant was signed by the owner's earlier key. A grant by a key the author's identity was rotated or renewed from now counts when it was made before that rotation; one an old key made after it does not. |
|
| 7 | 8 | - **A tag can be withdrawn, by a signed withdrawal (oxygenhub#301).** There was no way to remove a tag anywhere: a mistaken release tag stayed for good. `oxy tag delete NAME [-r REASON]` records an `untag` attestation naming the tag's state, signed like a tag (a ten-minute keyless key too), and the next `oxy push` sends it to a remote that still has the tag; `oxy push --delete tags/NAME` does both at once. A receiver under a policy accepts it only from a key that may create the tag, keeps it beside the tag's creation, and lists withdrawn tags in its refs answer (key 8), so a `fetch` withdraws the tag there too (`tag v1 was withdrawn on origin, and here`). A withdrawn name is never used again, so a tag name never names two states. `oxy-server`'s `Backend` gains `vet_tag_withdrawal` (OxygenHub refuses while a release uses the tag), and `Repo::host_tag_create` and `host_tag_withdrawal` let a host make or withdraw a tag for a signed-in user. The wire change is additive, but an older server refuses a withdrawal (wrong attestation kind), and an older client keeps a withdrawn tag. |
|
| … | 258 unchanged lines |
modified crates/oxy-core/src/repo/sync.rs +52 −7
| … | 1849 unchanged lines |
||
| 1850 | 1850 | } |
|
| 1851 | 1851 | // Base: the remote's checkpoint of exactly these heads. |
|
| 1852 | 1852 | let cp_bundle = remote.checkpoint(&r.heads)?; |
|
| 1853 | - | let mut cp_ref = None; |
|
| 1854 | - | for (ty, body) in &cp_bundle.objects { |
|
| 1855 | - | let put = self.store.put(*ty, body)?; |
|
| 1856 | - | if *ty == ObjectType::Checkpoint { |
|
| 1857 | - | cp_ref = Some(put); |
|
| 1858 | - | } |
|
| 1859 | - | } |
|
| 1853 | + | // In one batch: a big state's checkpoint is tens of thousands of |
|
| 1854 | + | // graph blobs and chunks, which stored one by one were each written |
|
| 1855 | + | // loose and flushed (a VS Code clone ran at 27 objects a second). |
|
| 1856 | + | let objects: Vec<(ObjectType, &[u8])> = cp_bundle |
|
| 1857 | + | .objects |
|
| 1858 | + | .iter() |
|
| 1859 | + | .map(|(ty, body)| (*ty, body.as_slice())) |
|
| 1860 | + | .collect(); |
|
| 1861 | + | let batch = self.store.begin_batch()?; |
|
| 1862 | + | let stored = self.store.put_many(&objects); |
|
| 1863 | + | batch.finish()?; |
|
| 1864 | + | let cp_ref = objects |
|
| 1865 | + | .iter() |
|
| 1866 | + | .zip(stored?) |
|
| 1867 | + | .find(|((ty, _), _)| *ty == ObjectType::Checkpoint) |
|
| 1868 | + | .map(|(_, r)| r); |
|
| 1860 | 1869 | let Some(cp_ref) = cp_ref else { |
|
| 1861 | 1870 | return fail(format!("{remote_name} sent no checkpoint")); |
|
| 1862 | 1871 | }; |
|
| 1863 | 1872 | self.install_checkpoint(&bookmark, &cp_ref)?; |
|
| 1873 | + | // The blobs of the state's opaque files come with no change when the |
|
| 1874 | + | // window holds none of the changes that set them (#137): fetch them |
|
| 1875 | + | // now, or under `filter.max-blob` leave them to be fetched on read. |
|
| 1876 | + | let cp = crate::objects::Checkpoint::decode( |
|
| 1877 | + | &self.store.get_typed(&cp_ref, ObjectType::Checkpoint)?, |
|
| 1878 | + | )?; |
|
| 1879 | + | let base = State::from_checkpoint_history(&self.read_blob(&cp.payload)?)?; |
|
| 1880 | + | let mut missing: Vec<ObjectRef> = base |
|
| 1881 | + | .opaque_nodes() |
|
| 1882 | + | .iter() |
|
| 1883 | + | .flat_map(|n| base.live_blobs(n)) |
|
| 1884 | + | .filter(|b| !self.store.has(b)) |
|
| 1885 | + | .collect(); |
|
| 1886 | + | missing.sort(); |
|
| 1887 | + | missing.dedup(); |
|
| 1888 | + | if self.config.max_blob.is_some() { |
|
| 1889 | + | for b in &missing { |
|
| 1890 | + | self.register_lazy(b, remote_name)?; |
|
| 1891 | + | } |
|
| 1892 | + | } else { |
|
| 1893 | + | for part in missing.chunks(512) { |
|
| 1894 | + | let objects = remote.objects(part)?; |
|
| 1895 | + | let objects: Vec<(ObjectType, &[u8])> = objects |
|
| 1896 | + | .iter() |
|
| 1897 | + | .filter(|(ty, _)| matches!(ty, ObjectType::Blob | ObjectType::Chunk)) |
|
| 1898 | + | .map(|(ty, body)| (*ty, body.as_slice())) |
|
| 1899 | + | .collect(); |
|
| 1900 | + | let batch = self.store.begin_batch()?; |
|
| 1901 | + | let stored = self.store.put_many(&objects); |
|
| 1902 | + | batch.finish()?; |
|
| 1903 | + | stored?; |
|
| 1904 | + | } |
|
| 1905 | + | if let Some(b) = missing.iter().find(|b| !self.store.has(b)) { |
|
| 1906 | + | return fail(format!("{remote_name} did not send blob {}", b.short(12))); |
|
| 1907 | + | } |
|
| 1908 | + | } |
|
| 1864 | 1909 | // History window. |
|
| 1865 | 1910 | let wanted = remote.shallow(&r.heads, depth)?; |
|
| 1866 | 1911 | let bundle = remote.fetch(&wanted, self.config.max_blob)?; |
|
| … | 870 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +42 −0
| … | 3024 unchanged lines |
||
| 3025 | 3025 | let _ = h1; |
|
| 3026 | 3026 | } |
|
| 3027 | 3027 | ||
| 3028 | + | /// #137: a depth-0 clone takes no change, so the blobs of the state's |
|
| 3029 | + | /// binary files came with nothing and the checkout failed ("object … not |
|
| 3030 | + | /// found"); they are fetched with the checkpoint, or left lazy under |
|
| 3031 | + | /// `--max-blob`. |
|
| 3028 | 3032 | #[test] |
|
| 3033 | + | fn a_depth_0_clone_brings_its_binary_files() { |
|
| 3034 | + | hermetic(); |
|
| 3035 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 3036 | + | let root = dir.path(); |
|
| 3037 | + | let origin_dir = root.join("origin"); |
|
| 3038 | + | let origin = Repo::init(&origin_dir) |
|
| 3039 | + | .unwrap() |
|
| 3040 | + | .with_home(root.join("home")); |
|
| 3041 | + | let png: Vec<u8> = (0..4000u32) |
|
| 3042 | + | .map(|i| (i * 7 % 251) as u8) |
|
| 3043 | + | .chain([0, 0xff]) |
|
| 3044 | + | .collect(); |
|
| 3045 | + | // The logo comes with a.txt's first line, and the head edits that line, |
|
| 3046 | + | // so the change that set the logo is below the head, not a head itself. |
|
| 3047 | + | write(&origin_dir, "logo.png", &png); |
|
| 3048 | + | write(&origin_dir, "a.txt", b"first\n"); |
|
| 3049 | + | origin.commit("logo", &[]).unwrap(); |
|
| 3050 | + | write(&origin_dir, "a.txt", b"later\n"); |
|
| 3051 | + | origin.commit("later", &[]).unwrap(); |
|
| 3052 | + | assert_eq!(origin.bookmark_heads().unwrap().len(), 1); |
|
| 3053 | + | let url = origin_dir.to_string_lossy().to_string(); |
|
| 3054 | + | Repo::clone_shallow(&url, &root.join("a"), 0, None, &[]).unwrap(); |
|
| 3055 | + | assert_eq!(fs::read(root.join("a/logo.png")).unwrap(), png); |
|
| 3056 | + | let lazy = Repo::clone_shallow(&url, &root.join("b"), 0, Some(0), &[]).unwrap(); |
|
| 3057 | + | assert_eq!(fs::read(root.join("b/a.txt")).unwrap(), b"later\n"); |
|
| 3058 | + | let blob = { |
|
| 3059 | + | let heads = lazy.bookmark_heads().unwrap(); |
|
| 3060 | + | let st = lazy.load_state_history(&heads).unwrap(); |
|
| 3061 | + | st.opaque_nodes() |
|
| 3062 | + | .iter() |
|
| 3063 | + | .flat_map(|n| st.live_blobs(n)) |
|
| 3064 | + | .next() |
|
| 3065 | + | .unwrap() |
|
| 3066 | + | }; |
|
| 3067 | + | assert_eq!(lazy.read_blob(&blob).unwrap(), png, "fetched on read"); |
|
| 3068 | + | } |
|
| 3069 | + | ||
| 3070 | + | #[test] |
|
| 3029 | 3071 | fn shallow_clone_and_deepen() { |
|
| 3030 | 3072 | hermetic(); |
|
| 3031 | 3073 | let dir = tempfile::tempdir().unwrap(); |
|
| … | 6981 unchanged lines |
modified crates/oxy-core/src/state.rs +7 −0
| … | 1889 unchanged lines |
||
| 1890 | 1890 | .collect() |
|
| 1891 | 1891 | } |
|
| 1892 | 1892 | ||
| 1893 | + | /// The nodes with replace records: opaque files, as their blobs go. |
|
| 1894 | + | pub fn opaque_nodes(&self) -> Vec<NodeId> { |
|
| 1895 | + | let mut nodes: Vec<NodeId> = self.replaces.keys().copied().collect(); |
|
| 1896 | + | nodes.sort(); |
|
| 1897 | + | nodes |
|
| 1898 | + | } |
|
| 1899 | + | ||
| 1893 | 1900 | /// Live blobs of an opaque node: every `new` that no later `Replace` retired. |
|
| 1894 | 1901 | pub fn live_blobs(&self, node: &NodeId) -> Vec<ObjectRef> { |
|
| 1895 | 1902 | let mut live: Vec<ObjectRef> = self |
|
| … | 1048 unchanged lines |