docs/vfs: the --writable design, the commit-per-write trade, and the eager/lazy choice - #12
Merged
Conversation
`--writable` is the reserved name for a mount backed by a real jj workspace. The obvious reading is a commit per write, and one number from the `--scratch` measurements rules it out: a single `bun install` through the mount is 290,879 writes, so a commit per write is 290,879 commits and an operation log of the same order, with `jj undo` stepping back one `write(2)` at a time. Batching does not rescue it, because any batch boundary is a guess about when a tool finished writing and a wrong guess splits a file across two revisions. The alternative is that the mount becomes the workspace's working copy. jj already has the seam: `WorkingCopy`, `LockedWorkingCopy` and `WorkingCopyFactory` in `lib/src/working_copy.rs`, selected by a string in `.jj/working_copy/type`. Upstream anticipated this case in as many words, in the `set_sparse_patterns` TODO about working copies that use a virtual file system. Records four conclusions that were not obvious going in. The tracking rule is jj's, from `SnapshotOptions`, not a gitignored-versus-not rule of our own. The argument is agreement rather than taste: two definitions of "tracked" in one filesystem means `jj status` run inside the mount reports a different set of files from the one the mount serves. It also settles three cases a gitignore rule leaves open, including a new source file that is untracked and not ignored, which `--scratch` currently throws away. The mount is not "never dirty". It is dirty between a write and the next snapshot, exactly as a local working copy is. What is cheap is measuring the dirt, because the server handled every write and knows the changed set, so producing the tree costs writes-since-snapshot rather than the 38,517 entries in the tree. That comes from the bookkeeping and not from the transport, and the doc says so, so nobody later credits NFS or FSKit for it. The missing NFSv3 `close` does not stop being a problem, it becomes a smaller one. There is no commit boundary to infer, but jj commands are separate processes, so the server has to publish tree state for them to read, and that needs a flush rule. Named it and named its cost: a `jj status` inside the window reads a tree a few hundred milliseconds stale and the next flush corrects it. No data lost and no wrong commit. Whiteouts do not carry over. They exist because `--scratch` sits on a revision that cannot be edited; under `@` a deleted path is a path absent from the next tree. Recorded as a rejected alternative with the reason, and with the one condition that brings it back, because an alternative rejected silently gets re-proposed. Also records the risk worth watching, that three pieces of scratch-layer state are sound only because one process holds the `flock` and step 3 adds a fourth that another process is meant to read; what was not verified; and a size, which is several days.
… writing Two decisions were being made in conversation rather than in the repo, and both have a cost that only reads as a cost with a number beside it. The first is whether a commit object is written per write. The previous text argued against it from volume alone, which is arguable. Adds the argument that is not: a writer emits a file in chunks, so a commit taken after each `write(2)` holds a half-written file, and almost none of the 290,879 commits would be a state anyone could return to. Also states what the alternative gives up, which the previous text did not: between a write and the next fold into `@`, bytes live in the server's storage rather than the object store, so a crash in that window depends on the server rather than on jj. Proposes the middle. Fold writes into `@` on the same flush rule the state file already needs, so jj amends the working-copy commit instead of adding to it. `jj log` still shows one commit, durability lands within about a second and in the object store, and the count is one amend per flush rather than one per write. Ends with the question that has to be answered before the write path is built: is durability within about a second enough, or does something need each individual write recoverable. The second is when the changed set is computed. Records that both readings maintain the changed path set eagerly, because the server learns the path on every write for free and any design that skipped it would have to walk the tree, which is the cost the approach exists to avoid. The real choice is whether content is compared eagerly too, and it is written out as three options with their costs: eager paths with content compared at snapshot, which reports a file written back to its original bytes as modified until the next snapshot; eager paths with content hashed per write, which re-hashes a whole file once per append and is quadratic in file size against a 290,879-write workload; and lazy, which is exact and stale. Recommends the first, on the grounds that the false positive is narrow, visible only between a write and the next snapshot, and clears itself.
--writable design, and why it is not a commit per write--writable design, the commit-per-write trade, and the eager/lazy choice
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Design only. No code, and nothing here is measured against a prototype.
--writableis the namedocs/vfs.mdalready reserves for a mount backed by a real jj workspace. The obvious reading is a commit per write, and one number from the existing--scratchmeasurements disqualifies it: a singlebun installthrough the mount is 290,879 writes, so a commit per write is 290,879 commits and an operation log of the same order. Batching does not help, because every batch boundary is a guess about when a tool stopped writing and a wrong guess splits a file across two revisions.The alternative is that the mount becomes the workspace's working copy, and jj already has the seam.
WorkingCopy,LockedWorkingCopyandWorkingCopyFactoryinlib/src/working_copy.rs, chosen by a string in.jj/working_copy/type. Upstream anticipated this exact case: theset_sparse_patternsTODO mentions "working copies that don't support sparse checkouts (e.g. because they use a virtual file system)".Four conclusions in the doc that were not obvious at the start:
The tracking rule is jj's, from
SnapshotOptions, not a gitignored-versus-not rule of our own. The argument is agreement. Two definitions of "tracked" in one filesystem meansjj statusrun inside the mount reports a different file set from the one the mount is serving, with no way for the user to tell which is right. Adopting jj's rule also settles a case a gitignore rule gets wrong: a new source file that is untracked and not ignored, which--scratchsends to the disposable layer and throws away.The mount is not "never dirty". It is dirty between a write and the next snapshot, like any working copy. What is cheap is measuring it: the server handled every write, so producing the tree costs writes-since-snapshot rather than walking 38,517 entries. That is the bookkeeping, not the transport, and the doc says so.
The missing NFSv3
closeshrinks rather than disappears. No commit boundary to infer, but jj commands are separate processes, so the server must publish tree state for them and that needs a flush rule. The rule and its cost are both named: ajj statuslanding inside the window reads a tree a few hundred milliseconds stale, corrected on the next flush. No data lost, no wrong commit.Whiteouts do not carry over, recorded as a rejected alternative with its reasoning and with the one condition that would bring it back.
The doc also carries the risk most likely to bite (three pieces of scratch state are sound only because one process holds the
flock, and the design adds a fourth that another process is meant to read), an explicit list of what was not verified, and a size of several days broken into four steps.All nine code citations were checked against the files at this revision.
mkdocs build --strictandcodespellpass locally.Note
Document the
--writableVFS design, commit-per-write tradeoffs, and flush strategyAdds a design section to docs/vfs.md covering a proposed
--writablemode for a VFS backed by a real jj workspace.bun install) and proposes accumulating writes then snapshotting into@at flush-like boundaries (idle timeout, NFS COMMIT, hard cap).WorkingCopy/LockedWorkingCopy/WorkingCopyFactorytraits and workspace loader, usingSnapshotOptionsfor tracking rules.--writable.Macroscope summarized 5b9722d.