zit — optimization machinery

Disposable by Construction

Nine caching layers sit between a zit repository and an answer. Deleting any of them changes no answer — that constraint is what makes the rest of the design possible.

The cost of one listing, three ways

Measured on the same store — tcl, 300 MB, 2,193 paths at tip — on one machine, through -R, in the mode zit ships (--release=fast). These are the same work; only which layer answered differs.

29.63s Full log derive
Every frame read and CRC’d, all derived state rebuilt. What --no-cache forces.
0.34s Record-body replay
A validated cache hit that still reconstructs ~14 MB of state on every process start. Where branch and tag still land.
0.04s Zero-parse image
Columns viewed in place. No hashmap fill, no propagation, no adjacency walk.

Two different gaps, worth separating. Having a cache at all is worth 87× — that is the top rung against the middle, and it is the one that would hurt to lose. The image is a further 8.5× on top. Both are cache hits; a hit that still rebuilds something is a different animal from one that does not, and ls took the middle rung for years because the verb asked for it, not because it needed to.

An earlier revision of this page reported 641.9s / 58.3s / 0.31s

Those were Debug numbers — bare zig build, which is what Zig gives you without --release. On this workload Debug is roughly 150× slower than the shipped binary, so it inflates every rung and, worse, inflates them unevenly: the record-body rung is dominated by parsing and allocation, which Debug punishes hardest, while the image rung barely moves because it does almost no work either way. That is what turned a real 8.5× into an apparent 188×. The page also carried a footnote about a “separate environment” reporting 0.38s for the same corpus, unreconciled — it was not a separate environment. It was the same machine with an optimized build. See build.zig’s bench notes for the measurement rules this cost us.

The rule every layer obeys

These layers share one contract, and it is unusually strict: a cache may make an answer faster, never different. Every tier below can be deleted at any moment, mid-operation, and the only cost is time.

Why that is not the normal arrangement

Most version control systems make derived state load-bearing. Fossil keeps mlink, plink, event and tagxref as persistent SQL tables and reconstructs them only under an explicit fossil rebuild; Git’s index and packfiles are part of the repository you are expected to keep. In zit the append-only log is the whole repository, and everything else is a memo of a pure function over it. That is what lets a corrupt, truncated, stale or half-written cache be treated as a miss rather than as damage.

The validation is uniform. Every derived tier is keyed on the log’s own identity — end offset, frame count, last frame offset, last header CRC — plus a self-checksum over its own bytes. Any mismatch, wrong magic, wrong version, or failed checksum is a miss. Nothing is trusted because it was found.

Memos of derived state

These answer “what does history mean” — branches, tags, leaves, ancestry, name resolution. They live under .zit-cache/, the advisory tier beside the store.

The zero-parse image

.zit-cache/repo · image sectionwarm path

The hot query answers of a fully-derived state, laid out as dense integer-addressed columns and reloaded as offset-addressed windows into the cache buffer. Nothing is parsed, allocated or relocated on load — the columns are the loaded form.

Holds
Per rid: event mtime, flags (has-event / leaf / closed), active branch id with the main-branch fallback already resolved, and the artifact hash. Plus the leaf list, the resolved symbol table, and the full parent and child adjacency with the primary-link flag.
Validated by
Log-tail key, self CRC, a layout version, and per-column structural checks: every span inside the image, every CSR non-decreasing and closing at its pool length, every stored rid naming a real slot.
On mismatch
Falls through to the record body below — still a validated hit, just a slower one.

Its one allocation is widening the leaf rid list. Everything else is a window.

The crosslink record body

.zit-cache/repo · record sectionmiddle rung

The inputs to derivation, stored in persisted order at a named log prefix, so a read verb replays records instead of re-reading and re-CRC’ing the whole log.

Holds
The crosslink input records — what the derive consumed, not what it produced.
Validated by
The same log-tail key and self-checksum as the image; the two travel in one file.
On mismatch
Full rebuild from the log, then the cache is rewritten atomically.

This is the fallback that makes the image safe to change. A layout bump costs one slow open, never a wrong answer — which is why adding adjacency to the image needed no migration path at all.

The rebuild checkpoint

.zit-cache/checkpointwarm path

A prefix-anchored claim: as of check-in C, whose frame closes the log prefix, the fully-verified file set fingerprints to this value — plus the record body for that prefix.

Holds
One anchor and the crosslink records up to it, so a from-scratch derive can resume from the prefix instead of re-parsing a permanently flat segment.
Validated by
Its own prefix key; zit checkpoint verify re-derives everything it claims and compares byte for byte, trusting nothing.
On mismatch
Ignored. The first full derivation plants a fresh one at no extra cost.

Sidecar only, deliberately. An earlier design pinned the anchor’s file set as stored sub-manifest artifacts; it was retired because minting artifacts early created permanent, unreferenced objects — and everything it bought was available for free elsewhere. A cache that writes history is no longer disposable.

Memos of log structure

The log-scan index

.zit-cache/indexwarm path

Frame locations, so opening the store does not mean walking every frame header.

Holds
The serialized scan of the log’s frame headers, adopted by the lazy content channel on open.
Validated by
Itself — its own magic, CRC and log key, checked by the adopter. The cache module is a dumb byte carrier for it and never inspects the contents.
On mismatch
Re-walk the headers. Absence and unreadability are the same case: a miss.

Memos of content bytes

These are in-process, not on disk. They exist because reconstructing one blob and reconstructing a thousand share almost all of their work.

The plaintext memo

logstore.PlaintextCachein process

A delta family’s hot bases, kept resident for the span of one sweep. Without it, a walk over delta chains re-reconstructs each base once per referencing delta.

Holds
Reconstructed plaintexts keyed by frame offset. A resolved delta’s own plaintext is memoized so a deeper delta built on it hits; raw and zlib top-level frames are not, so a delta-free repository pays nothing.
Bounded by
64 MiB resident, with an entry cap derived from the byte budget so the two cannot disagree about which is doing the bounding.
On pressure
Eviction. It changes the work, never the answer — a miss simply reconstructs.

Turned a projected 8.5× read amplification on depth-16 chains into roughly two reconstructions per frame regardless of depth. The verb that most recently needed it was log, which walked history without it and paid for the omission on every repository large enough to notice.

The sub-manifest memo

repostate.SubManifestMemoin process

The same idea one layer up: tree nodes shared between check-ins, held while a partition walk runs.

Bounded by
64 MiB, chosen against a measurement rather than a guess — a pkgsrc-scale tree of ~123k files puts the working set near 30 MB, so the budget holds one comfortably while staying a statable cost.
Character
A memory-versus-hit-rate dial, not a correctness or capacity bound. Too small costs a little speed and nothing else.

Delta base selection

logstore · packwrite path

Not a cache — the reason the caches above matter. Stored artifacts are delta-compressed against a chosen base, so reading one blob may mean walking a chain.

Character
A base is a hint, never a requirement. Ineligible or unreadable candidates are silently skipped; there is no error path for a bad candidate.

One consequence worth knowing: a blob’s logical size never requires walking its chain. A zlib container states its uncompressed length in its first four bytes, and a delta carries its target size in its own header — so zit ls --size costs a header read per file, while the text/binary verdict, which needs every byte, does not.

Memos of the working tree

The materialization plan

.zit-cache/vstatwarm path

Which working files are already correct, so update does not read every file to find out.

Trusted only when
The row’s digest is the one the current baseline expects and inode, size and mtime all still match the file on disk. Anything else falls back to a full content compare, which remains the authority.
Known trade
An edit that restores mtime, size and inode exactly is invisible to the stat check — fossil’s own vfile trade, taken knowingly rather than inherited.

The store cone

.zit-cache/store-conepolicy

The odd one out: policy, not memo. It records which subtree a sparse store is meant to hold.

Why it lives here
Losing it heals toward dense — elided content reclassifies as transit, so the cost of loss is bandwidth, never data. That loss rule is what makes it safe to co-locate with the disposable tier despite not being a memo.

The worktree registry

.zit-cache/worktreesadvisory

Which working trees a store serves. Trees stamp themselves on open and update.

Self-healing
The listing lazily prunes entries whose path is gone or whose checkout re-pointed elsewhere. Membership is verified by the store file’s inode, so a same-named impostor never counts.

Where the technique comes from

The image’s column layout is borrowed deliberately, and the borrowing is recorded in the source rather than absorbed silently:

The CRC-32 implementation underneath all the checksums is slicing-by-N, from Kounavis & Berry at Intel, generalized by Kadatch & Jenkins. It is vendored rather than taken from the standard library because the standard one walks a byte at a time, which measured as ~93% of a repository open.

What the image still does not carry

The tag tables

tagxref is (rid, tag) → value with mtimes, and it is not in the image. So branch and tag still take the record-body rung — measurably: 0.33–0.36s against ls and info at 0.03–0.04s on the same store.

info was on that list too, and for a real reason: amend --comment stores its override as a tag, so on an early fast path it printed the manifest’s original comment instead. An existing test caught it the moment the verb moved. The override is now resolved on the image path and info reads it — which is the argument for having had the test, not an argument that the hazard was imaginary.

The sharper lesson from this work is about a failure mode rather than a missing column. Adjacency landed in the image, and the branch walk still answered wrongly — because a branch value was read from the empty tag table beside the correct edges, so every check-in looked like it sat on the main branch. The walk never left the branch and resolved start:feature to a check-in on a different branch, with exit 0 and no diagnostic.

Which is why the guarantee is phrased the way it is

“A cache may make an answer faster, never different” is not a slogan — it is the property that has to be tested, tier by tier, because a cache that is merely absent degrades gracefully while a cache that is partial answers confidently and wrongly. Every tier here is compared against the path it replaces, and the comparisons carry a guard that fails when the fixture had nothing to compare.

The layers at a glance

LayerLivesMemoizesLoss costs
Zero-parse image.zit-cache/repoDerived query answersOne record-body replay
Record body.zit-cache/repoDerivation inputsOne full log derive
Rebuild checkpoint.zit-cache/checkpointA verified log prefixRe-parsing the flat prefix
Log-scan index.zit-cache/indexFrame locationsA header walk on open
Plaintext memoin processReconstructed delta basesRe-reconstruction
Sub-manifest memoin processShared tree nodesRe-walking the partition
Materialization plan.zit-cache/vstatWhich files are currentA full content compare
Store cone.zit-cache/store-coneSparse policy (not a memo)Bandwidth — heals to dense
Worktree registry.zit-cache/worktreesTrees served by a storeSelf-heals on next open