Introduction
mado is a lazy, content-addressed FUSE
virtual filesystem working copy for jj (Jujutsu).
It mounts a jj working copy through the kernel, serves committed file content
on demand over a content-addressed object store — local or networked — and
folds edits made through the mount back into jj history. It is the open-source
analog of Google Piper + CitC and Meta Mononoke + EdenFS, built on the released
jj-lib (no forks).
A 50k-file repository mounts instantly because nothing is checked out up front:
a file’s bytes are fetched the first time something reads them, cached on local
disk, and never re-fetched. Edits land in an overlay that is journaled durably
and auto-snapshotted into the working-copy commit (@) after a brief idle
period — so an agent’s work is captured in jj history without it running a
single command.
What mado is
- A jj working copy over FUSE.
mado mountruns a daemon that serves@through a mountpoint. Ordinary jj commands (log,describe,new,rebase,undo, …) work and reflect through the mount live. - Content-addressed and lazy. Every object is keyed by its BLAKE3 hash. Reads fault content in on demand; the on-disk blob cache is never invalidated (a content-addressed blob is never stale) and never evicted by default.
- Networked or local. With
MADO_REMOTE_ADDRset, object reads/writes go to amado serveserver over gRPC through a local caching client. Unset, mado works entirely against an on-disk store under.jj/repo/store. - Built for portable agent workspaces. A workspace can be paused on one
machine and resumed on another (
mado ws pause/mado ws resume), guarded by a server-side lease so two machines never fight over one working copy. A running mount auto-checkpoints on a cadence, so a crashed machine is resumable elsewhere up to the last checkpoint.
What mado is not
- Not a git-interop layer.
mado import-git/mado export-gitare a one-way boundary conversion at the edges of the system (with a memoized, resumable, round-trippable mapping), not live bidirectional git syncing. The online interop tasks onmado serveare opt-in mirroring, not a merge protocol. - Not a checkout. mado deliberately does not materialize the whole tree. The first read of an un-cached file pays a fetch; the design surfaces that cost honestly (see Performance) rather than hiding it behind an eager copy.
- Not cross-platform. mado is Linux-only (it depends on
/dev/fuseor virtiofs / vhost-user).
How the pieces fit
jj CLI ──► mado backend ──► content-addressed blob store
│
FUSE mount ◄── mado daemon ──────┤ (local .jj/repo/store, or
(reads fault content in, │ a remote `mado serve` over gRPC,
edits fold into @) │ fronted by a local disk cache)
│
locality packs (MDLP) ◄───────────┘ one ranged GET hydrates a whole
small-file subtree
The rest of this book is task-oriented:
- Getting started — init, mount, and daily jj use.
- Workspaces — pause / resume / checkpoint, and leases.
- Performance — locality packs, the disk cache, and honest cold-vs-warm expectations with the measured numbers.
- Operations — running a server, GC, recovery, and benchmarking.
- Reference — CLI verbs, environment variables, and on-disk file formats.
Getting started
mado is Linux-only and needs FUSE (/dev/fuse). Every command below is a
subcommand of the mado binary — a custom jj CLI: mado log, mado describe,
and every other jj verb work exactly as in upstream jj, backed by mado’s stores.
Local: a mounted jj working copy
mkdir myrepo && cd myrepo
mado init # a jj repo whose backend + working copy are mado
mado mount --mountpoint ~/mnt & # FUSE daemon: serves @, folds edits back into @
cd ~/mnt # work here: read lazily, edit freely
echo hello > hello.txt # ...idle edits auto-snapshot into @ (~1 s)
mado log # the full jj CLI, backed by mado stores
mado init creates a jj repository whose object backend and working copy are
mado. By default the local store uses the loose-file at-rest format (fs), one
object per file — the format that lets a mado mount daemon and a mado CLI
process share the same store side by side. (--store-format fjall selects the
embedded LSM format instead; it is held exclusively by one process at a time and
is meant for import/serve stores, not side-by-side local use.)
The mount daemon
mado mount --mountpoint <dir> runs a daemon that:
- serves the working-copy commit
@through the mountpoint (which must be a path other than the workspace root); - collects edits made through the kernel into a durable overlay journal
(
overlay.snapshot), restoring them on restart; - auto-snapshots those edits into
@after a short idle period — an in-process jj transaction, so an agent’s work is captured in jj history without it running any command. Pass--no-auto-snapshotto turn that off and rely on the overlay journal plus explicitmado snapshot.
mado snapshot is the manual, on-demand equivalent of the daemon’s idle fold:
it pulls the live overlay from the running daemon over the control socket and
amends @. It is quiet and idempotent.
To let another user (or a container / guest VM) read the mount, add
--allow-other (requires user_allow_other in /etc/fuse.conf). --read-only
mounts a reference checkout the kernel refuses writes to before they reach the
VFS.
Daily jj use
Once mounted, work inside the mountpoint and drive history with ordinary jj
verbs through the mado binary:
cd ~/mnt
mado new master # start a new change on top of master
$EDITOR src/lib.rs # edits fault content in on first read,
# land in the overlay, auto-snapshot into @
mado describe -m "fix the thing" # set @'s description
mado log
mado diff
mado rebase -d main
mado undo # jj's operation log works normally
Nothing needs pre-warming: a cold read inside the mount faults the blob in over whatever backend the workspace uses (local disk or a remote server) and caches it. See Performance for what “cold” costs and how to make a whole subtree warm in one fetch.
Networked: one server, many clients
A shared mado serve server lets many thin clients check out the same history
lazily. See Operations for running the server; the client
side is:
mkdir ~/client1 && cd ~/client1
export MADO_REMOTE_ADDR=http://server:50051 # object reads/writes go here
mado init --workspace alice # register a UNIQUE workspace name
mado new master # position @; objects fetch lazily
mado mount --mountpoint ~/mnt1 & # a 50k-file repo mounts instantly
Every client of a shared server must use a distinct
--workspacename. Two workspaces sharing one name fight over the same working-copy commit — each checkout makes the other stale and resets its mount. Against a remote, a name already registered on the server is refused (unless you are resuming — see Workspaces).
Clients share the op-log through the server (concurrent operations merge exactly
like jj’s --at-op), fetch blobs lazily through a local disk cache, and fold
their mount edits into their own @.
Scratch directories (build output)
Build output (target/, node_modules/, …) is large and write-hot, and you
almost never want it in the jj snapshot. Mount with a scratch backing
directory and redirect those paths to plain local disk:
mado mount --mountpoint ~/mnt \
--scratch ~/scratch-alice \
--redirect target --redirect node_modules
Paths listed with --redirect (merged with whatever the repo’s
.mado-redirections manifest declares) are served from disk under --scratch
instead of the virtual tree, so they never enter a jj snapshot. Give each agent
its own scratch directory so their build trees don’t collide. Scratch state
travels with mado ws pause / resume — see Workspaces.
Workspaces
A mado workspace is one client’s checkout: a workspace name, a working-copy
commit @, an overlay of uncommitted mount edits, and (optionally) scratch
redirections. Against a shared server, a workspace can be paused on one
machine and resumed on another, so an agent’s environment is portable. This
page covers the lifecycle verbs (mado ws …) and the lease that keeps two
machines from fighting over one workspace.
Checkpoints
A checkpoint is an explicit, moment-in-time record that binds three things into one durable blob:
- the current jj operation (what
@and the view are), - the scratch manifest (the redirected build-dir state), and
- the presence of the workspace’s attribute table.
mado ws checkpoint # no scratch
mado ws checkpoint --scratch ~/scratch-alice --redirect target
mado ws checkpoint folds pending mount edits into @, snapshots the scratch
redirections into the blobstore, and writes the checkpoint record (in the
Derived domain), with .jj/working_copy/scratch-head pointing at it (atomic
write). This is the record pause / resume travel on. The daemon’s background
scratch snapshotter keeps the manifest fresh continuously, but only this
command (or pause) binds scratch state to an operation.
Auto-checkpoint
A mount against a remote records a remote-durable, resumable checkpoint on a
cadence — under the lease it already holds, without releasing it — so a
lost or crashed machine is resumable elsewhere up to the last checkpoint, with
no human running mado ws pause.
- Default cadence: 30 seconds (
mado mount --checkpoint-interval <secs>). This is the bounded off-box loss window: a crash loses at most this much cross-machine durability. Local crash-safety stays continuous via the overlay journal regardless. --no-auto-checkpoint(or--checkpoint-interval 0) relies solely on explicitmado ws pausefor off-box durability.- Auto-checkpoint never runs on a local (no-remote) workspace — there is no server to make anything durable to.
- It also never runs under
mado serve-virtiofs: that daemon holds no workspace lease of its own (the FUSE mount / resume path owns the lease), and auto-checkpoint commits only under the daemon’s own held lease. The virtiofs flags exist for CLI symmetry only.
Pause and resume
mado ws pause moves a workspace off the current machine so it can resume
elsewhere. It is one blocking sequence with a single commit point:
mado ws pause # no scratch
mado ws pause --scratch ~/scratch-alice --redirect target
Pause folds pending mount edits into @ (driving a running daemon over the
control socket, exactly like mado snapshot; with no daemon it folds directly),
snapshots the scratch redirections, uploads the attribute table, writes the
checkpoint record, and finally CAS-swaps the server-side wshead/<workspace>
scope to it. The order is load-bearing: every blob is remote-durable
before the record naming it, and the CAS is the commit point — a crash
anywhere earlier leaves the previous pause fully intact. Pause blocks until
everything is remote-durable.
mado ws resume adopts a paused workspace into a fresh checkout on another
machine:
mkdir ~/machine-b && cd ~/machine-b
export MADO_REMOTE_ADDR=http://server:50051
mado init --workspace alice --resume # same server, SAME workspace name, --resume
mado ws resume --scratch ~/scratch-alice # restore into a local scratch dir
The resume target must be a fresh mado init --workspace <name> --resume
against the same server under the same workspace name (the --resume flag
skips the name-collision probe, because the name is expected to be registered
— it is the paused workspace being adopted). Resume:
- fetches the workspace head (exactly one record; zero means never paused, several means concurrent pauses forked it — both are typed errors),
- restores the attribute table and the scratch redirections eagerly (lazy scratch hydration is future work), and
- positions
@at the checkpoint’s exact operation (no new operation is written).
Resume refuses a dirty workspace unless you pass --force. A resumed tree is
byte-identical — content, exec bits, and file mtimes are restored — so a
resumed build is incremental, not cold (see the measured F2 numbers in
Performance).
Leases
The lease is what makes concurrent multi-machine use safe. A workspace head is
single-writer per scope: while one machine holds the lease on
wshead/<workspace>, a second acquire on that scope is refused
(FailedPrecondition). The mount daemon renews its lease well within the TTL;
the default lease TTL is 300 seconds (server clock).
- Normal
mado ws resumetakes the lease only if it is free or expired, and never steals it from a live holder. mado ws resume --stealforces a takeover: the steal succeeds only once the lease has expired on the server clock, and it bumps the epoch so the old holder’s late writes are fenced off. Use it when the prior machine is gone but its lease still looks live.
Because the head is single-writer per scope, “N machines writing one workspace scope” is not physically realizable — the lease serializing each scope is itself the guarantee that concurrent checkpoints don’t storm the shared op-heads journal (measured: CAS retries stay at zero as concurrency rises — see Performance).
Performance
mado’s performance story is a single trade: reads are lazy, so the first read of un-cached content pays a fetch, and everything after it is a local cache hit. This page explains the two mechanisms that make cold reads fast — locality packs and the disk cache — and gives honest cold-vs-warm expectations, citing the measured numbers by their source section and date.
All numbers below are copied verbatim from the “as measured” sections of
docs/design/performance-testing.mdanddocs/design/locality-pack-activation.md. They are point measurements on specific hardware (largely an Intel N100) with injected network latency, not guarantees. Where a number does not exist, this page says so rather than estimating.
The cost model
- Cold read — the first read of a file whose content is not in the local
disk cache. It costs one (or more) object-store round trips (GETs). Over a
remote backend with RTT
r, a naive per-file cold walk ofNfiles costs roughlyN × r. - Warm read — the content is already in the disk cache. No network; a local read.
The two levers below both attack the cold case: locality packs cut the GET count (many files in one ranged fetch), and the disk cache turns every cold read into a permanent warm one.
Locality packs
A locality pack (MDLP on disk) bundles a subtree’s small inline files into one
content-addressed blob. Faulting any file in that subtree fetches the whole
pack in one ranged GET and explodes it into the disk cache, instead of one GET
per file. This is the mechanism that makes a cold walk of a node_modules- or
nixpkgs-shaped tree fast.
Building packs
mado pack # per-directory (v1) packs for @'s tree
mado pack --recursive # recursive-subtree (v2) packs — the better default
mado pack -r <revset> # pack a specific revision's tree
mado pack(per-directory, v1) bundles each directory’s dense inline files. It wins on genuinely per-directory-dense shapes (vendornode_modules), but on thin, spread-out trees it can be worse than no packs (each dense subdir pays a pack GET on top of per-file faults — measured below).mado pack --recursive(v2, the covering index) bundles a whole thin subtree’s inline files across its many small directories into one pack root. This is the measured-best default for real repository shapes. The daemon loads either index version transparently.
Packs are a reclaimable cache, never a GC root: sweeping them only makes reads fall back to the per-file path. Building is idempotent — an unchanged tree re-uploads nothing.
Zero-config discovery (import-time publishing)
You usually do not have to run mado pack by hand. mado import-git builds
and publishes the recursive pack index for each imported tip by default
(--no-packs opts out). It does this without any extra remote traffic: the
import just wrote every tree and file object, so the pack builder reads that
content warm-local — it never issues a remote GET the pack exists to eliminate
(the “never build by re-fetching” invariant).
Discovery is content-addressed via a well-known derived pointer (MDPW, keyed
by the tree id), so a fresh mount finds the packs with no local file and no
manual step. A mado pack run publishes this pointer too (while still writing
a local pointer for offline use); the daemon attach path falls back from the
local pointer to the derived one.
As measured (
locality-pack-activation.md§“Follow-ups” item 2, 2026-07-05): import-time recursive-index build measured 0.15 s for a 2000-file tree — the import just wrote the content, so the builder reads warm-local. A fresh client holding only the tree id discovers and warms the subtree in ≤ 1 GET.
Observing pack state
An unpacked large tree silently falls back to per-file cold reads. mado pack-status makes that observable instead of surfacing only as mysterious
slowness:
mado pack-status
It queries the running mount daemon over the control socket and reports either
that a prefetcher is attached (mode per-dir / recursive, index version,
directory-entry count, and whether discovery came via local pointer or via derived pointer) or the distinct reason it is not (no index built, index for a
different tree, index swept, undecodable, …). At attach time, a large tree
served without packs also emits an advisory: “cold reads are per-file — run
mado pack --recursive.” mado pack-status requires a live
mado mount / serve-virtiofs in the workspace.
As measured
Recursive vs per-directory on nixpkgs (locality-pack-activation.md
§“Recursive-subtree packs as built”, 2026-07-04, cap 64 MiB): 10 roots → 10
packs, 72.7 MiB (the whole pkgs subtree — 46 134 members — fits ONE 62.4 MiB
pack). Modeled whole-tree cold walk 50 747 → 1 958 GETs (−96.1 %). Real FUSE
cells at RTT 20 ms, off / per-dir / recursive:
| Subtree | off | per-dir | recursive |
|---|---|---|---|
nixos/tests (dense, 1000-file walk) | 998 GETs / 24.9 s | 176 GETs / 5.5 s | 2 GETs / 1.4 s |
pkgs/development/python-modules (thin) | 1000 GETs / 25.4 s | 1936 GETs / 47.8 s | 2 GETs / 10.4 s |
The thin-subtree row is the key honesty point: per-directory packing is worse than no packs on a thin tree (dense subdirs each pay a pack GET on top of the per-file faults), while recursive wins by 2.4× even including the one-time 46k-member explode.
Real-tree census (performance-testing.md §“Real-tree study as measured”,
2026-07-04, full nixpkgs — 52 695 files / 37 086 dirs, avg 1.42 files/dir): only
76 dirs exceed the per-directory trigger, covering just 8.75 % of inline
files — the hard ceiling on what per-directory packing can do on a thin tree,
and the reason recursive packs exist.
Chunked-file coverage (locality-pack-activation.md §“Follow-ups” item 3,
2026-07-05): MDLP v2 packs a chunked file’s manifest and chunks as typed
members. On nixpkgs, 1 945 of 1 948 chunked files now pack (3 giant files stay
per-file), taking the whole-tree cold-walk model 1 958 → 787 GETs.
Streamed explode (locality-pack-activation.md §“Follow-ups” item 1,
2026-07-04): the first faulting read now waits only for the pack GET, not the
whole explode. Time-to-first-file dropped 954 → 19 ms (RTT 0), 7195 →
57 ms (RTT 20), 16222 → 83 ms (RTT 50), at an unchanged 1-GET budget.
Live FUSE trigger path (performance-testing.md §“Cold-fill fsync + explode
serialization levers”, 2026-07-04): a cold 300-file dense subtree walk through
the live daemon (trigger-driven prefetch, pack RTT inside the timed walk),
median of 5, after the fsync + explode-concurrency fixes:
| RTT (ms) | prefetch off, cold | trigger, cold |
|---|---|---|
| 0 | 703 ms | 222 ms |
| 20 | 7030 ms | 262 ms |
| 50 | 16068 ms | 283 ms |
The trigger path serves the whole cold walk in exactly 1 remote GET at every
RTT (asserted); its wall-clock is roughly flat in RTT (one round trip) while the
per-file path scales as N × RTT — ~60× faster than per-file at 50 ms.
The disk cache
Every workspace has a persistent, content-addressed on-disk blob cache at
.jj/working_copy/blob-cache/. Every process of the workspace — the CLI, the
mount daemon, across restarts — reads through it. It is:
- content-addressed (keyed by BLAKE3 hash), so a cached blob is never stale;
- never invalidated, and never evicted by default — it grows with everything materialized or read;
- safe to delete at any time (an evicted key simply re-fetches).
Cached fills for value-hashed content are written without a per-blob fsync,
protected instead by verify-on-read + self-heal: every local hit is checked
(blake3(value) == key.hash); a torn or bit-rotted file is deleted and the read
falls through to re-fetch a good copy. This is strictly stronger than fsync
(fsync never caught bit rot) — the one documented edge is that a local-only
exploded pack member torn by a power crash reads as absent until the next
mount’s trigger re-explodes the pack.
Bounding the cache
By default the cache is unbounded. Set MADO_DISK_CACHE_MAX_BYTES=<bytes>
to cap it; the tier then evicts least-recently-used blobs to stay at or below
the cap. The cap is read once at client-stack construction, so it applies
uniformly to the mount daemon, serve-virtiofs, and the CLI backends. 0,
empty, or unparseable keeps the unbounded default.
As measured (
performance-testing.md§“E3 cache-size curves”, 2026-07-04): the cache-size curve is a cliff, not a slope. Re-reading a working setWwith capC, pass-2 remote GETs are exactlyNatC = 0.5 W(classic LRU sequential-scan thrash) and exactly0atC ≥ W. A single oversized blob is stored regardless of the cap. Guidance: keep the default unbounded; when you do bound it, size the cap at or above the working set — a graceful sub-Wdefault would need a scan-resistant eviction policy, which is not warranted today.
Prefetching before you go offline
mado materialize [PATH] walks @’s tree (default: the whole tree; a directory
prefetches its whole subtree) and fetches every file’s blobs — manifests and
chunks — into the disk cache with bounded concurrency. It is the hermetic
alternative to faulting content in lazily: run it before going offline or before
a latency-sensitive build so no read of committed content ever waits on the
network.
mado materialize # prefetch the whole working-copy tree
mado materialize src/lib # prefetch just a subtree
mado materialize requires a remote-backed workspace (MADO_REMOTE_ADDR); on a
local workspace every blob is already on disk and it is a no-op.
Resume readiness
A mado ws resume restores a workspace’s tree with the reads parallelized, so a
resumed build is incremental rather than cold.
As measured (
performance-testing.md§“F2 resume TTFB”, 2026-07-04): resuming a 2000×1 KiB scratch shape over injected RTT, after the concurrency fix (16 in-flight ordered prefetches), took 2.1 s (RTT 0), 5.3 s (RTT 20), 9.7 s (RTT 50) — down from 90+ seconds when the restore was serial. GET and byte counts are identical before and after: the fix moves time, not requests. Restored tree facts (content, exec bits, file mtimes) are byte-identical, asserted — so the resumed build is incremental. Note that reads on the resume target are per-file, not per-unique-content (a fresh target has no cache tier yet).
Concurrent multi-agent load
The single-actor numbers above measure one reader or one writer in isolation.
The concurrent story (performance-testing.md §“Concurrent multi-agent
workload” / §“As measured”, 2026-07-03, concurrent_agents.rs, in-memory fast
tier) is a nightly trend tool, not a PR gate (latency percentiles and CAS
retry counts depend on thread interleaving and are inherently non-repeatable):
- Read fan-out: overlapping cold reads collapse to one key set at every concurrency (unique GETs identical at N ∈ {1,2,4,8}, asserted) — content addressing works under concurrent fan-out.
- Write dedup: cross-workspace dedup collapses file content (~70 % saved vs independent copies) but not per-workspace manifests — each agent still contributes a ~131 KiB manifest that does not dedup (the sole varying field is a directory-node mtime). Documented as a known gap.
- Checkpoint CAS: because the workspace lease is single-writer per scope, concurrent checkpoints on distinct scopes show CAS retries = 0 at every N by construction.
- Interference (the #21 thesis): one foreground reader’s p50 is essentially flat as background write+checkpoint load rises 0→8 agents; its p99 inflates only ~1.17–1.19× at RTT 20 ms and ~1.30–1.39× at RTT 50 ms — so background durability stays subordinate to foreground reads.
Operations
This page covers running a mado server, reclaiming disk with garbage collection, recovering from a lost op-heads database, and benchmarking.
Running a server
mado serve exposes this repo’s content-addressed blob store (and a SQLite
op-heads store) over gRPC, so other machines can use it as a networked mado
backend. It serves the repo’s own .jj/repo/store — the same store local mado
commands write through — so a remote reader sees exactly what was committed. The
call blocks until the process is killed.
# Production shape: seed a fjall store, then serve it.
mkdir server && cd server && mado init --store-format fjall
mado import-git --git-repo ~/src/nixpkgs --ref master \
--shallow-since 2024-01-01 --op-heads-db ../op_heads.sqlite
mado serve --addr 0.0.0.0:50051 --op-heads-db ../op_heads.sqlite
Key points:
--op-heads-db <file>persists the op-heads set to SQLite. Without it the op-heads set is ephemeral (in-memory) — fine for a throwaway server, but an ephemeral op-heads set is not a safe retention root, so scheduled GC and online git interop both require this flag.- A fjall store is held exclusively by one process. Bulk-import before
starting the server, and stop the server for offline
mado gc/mado export-git. The loose-file (fs) format is shareable but is the local default, not the production server format. --auth-token <token>requires every RPC to presentauthorization: Bearer <token>. Without it the server is open — suitable only for a trusted network. Clients setMADO_TOKENto match.--prod-blobstore/--blob-prefix <p>select the production at-rest composition (Prefix → Pack(zstd) → Multiplexed → Fs), giving at-rest compression and per-repo namespacing. This changes the at-rest format: a store created with these flags must always be served (and GC’d, exported, recovered) with the same flags, and vice-versa. A--store-format fjallstore implies the composition via its format marker, so the flags are unnecessary there.--stats-interval-secs(default 60) logs a periodic blob-traffic summary (ops, hit rate, bytes — deltas since the previous line) when there was activity;0disables it.
The server also derives each commit’s changed paths (so lazy clients’ path-scoped
revsets need not fetch whole trees) and, when given an op-heads db, maintains a
segmented graph store for the Index.GraphDelta RPC.
Online git interop (optional)
mado serve can periodically git fetch a source remote into a server-side
mirror, convert it, and publish the new tips (--git-ingest-url …), and/or
export the current view’s bookmarks and git push them to a target remote
(--git-export-url …). A named-remotes config file (--git-interop-config)
scales this to N sources and M targets. All of it requires --op-heads-db. See
the CLI reference for the full flag set and
docs/design/online-git-interop.md for the design.
mado sync-now (against MADO_REMOTE_ADDR) wakes a running server’s interop
tasks immediately instead of waiting for their poll interval:
mado sync-now # wake every interop task
mado sync-now --remote git-ingest[foo]
Garbage collection
mado gc collects blobs unreachable from any live op log. It reads the live
op-head set from the same SQLite database mado serve writes and this
workspace’s own local op log, walks both op DAGs to every commit any operation
can still see, expands to the full ancestor closure, marks the blobs that
closure needs, and reports — or, with --sweep, deletes — the rest.
mado gc --op-heads-db ../op_heads.sqlite # dry run (reports only)
mado gc --op-heads-db ../op_heads.sqlite --sweep # actually delete
--op-heads-dbis required. GC derives the retained set from these heads. If the head set is empty,mado gcrefuses to run — collecting against zero heads would mark the entire store garbage (almost always a wrong-database / wrong-scope mistake).--min-age-secs(default 3600) is an age grace period: an unreachable blob younger than this (or of unknown age) is deferred, never collected. This protects in-flight commits whose blobs land before their op-head publishes.0disables the window — only safe against a quiesced store.--scopereads live heads for a specific op-heads scope (defaults to the unscoped set).- Prod stores: pass the same
--prod-blobstore/--blob-prefixthe store is served with. GC decodes the object graph through the prod stack while sweeping the raw physical keys; a prod store read without these flags cannot decode a single commit.
Locality packs and other derived data are reclaimable cache, never GC roots — sweeping them only makes reads fall back to the per-file path.
Scheduled GC on the server
Instead of a cron’d mado gc --sweep, mado serve can sweep in-process:
mado serve --op-heads-db ../op_heads.sqlite \
--gc-interval-secs 3600 --gc-min-age-secs 3600
The scheduled sweep retains the ancestor closure of every live op-head across
all scopes (collecting against one scope would treat every other scope’s
history as garbage), plus the host workspace’s local op log. The first sweep
runs one full interval after startup; a GC failure is logged and retried next
interval — it never takes the serving path down. --gc-interval-secs 0 (the
default) disables it. Requires --op-heads-db.
Recovering op-heads
The op-heads database is the only pointer into the history of every scope: lose
it and the blobs survive but are anonymous. Every op-heads mutation is journaled
into the blobstore (the opjournal namespace, MDOJ records) before it
commits, so mado recover-op-heads can rebuild the database:
mado recover-op-heads --out-db ./recovered.sqlite
It scans the journal, verifies each scope’s hash chain, and replays the
verifiable prefix into a fresh database at --out-db (refusing to
overwrite an existing file — so you can diff the recovered database against a
suspect original before trusting it). It reports scopes recovered, the last
sequence per scope, and any chain breaks (truncation / corruption). Like
mado gc, pass matching --prod-blobstore / --blob-prefix if the store was
served with them.
Benchmarking
scripts/bench.sh runs the whole performance suite as one command and archives
the results:
nix develop --command scripts/bench.sh # every bench row
nix develop --command scripts/bench.sh load_smoke # a named subset
It executes the bench-corpus store-layer benches, load_smoke, and the FUSE
macro_bench (via the scripts/test.sh userns wrapper), archiving each bench’s
full output plus a summary.md (provenance: rustc, commit, CPU; per-bench
pass/fail + wall-clock) under bench-results/<UTC stamp>/. That directory is
git-ignored — curated numbers are hand-copied into
docs/design/performance-testing.md, and the raw dated archives stay untracked.
Positional names run a subset; --criterion opts into the micro-bench crates.
Exit is nonzero if any bench’s assertions fail, while a FUSE-mount
environment failure records the macro bench as SKIPPED rather than failing the
run.
As measured (
performance-testing.md§“Operationalization”, 2026-07-04, Intel N100): the first full run was 9/9 passed, ~26 min end-to-end — a viable nightly cadence.
The nightly timer
scripts/bench-nightly.sh wraps the runner with the trend policy — flag large
regressions for a human, never fail on jitter: after the run it compares each
bench’s wall-clock against the previous archived summary and flags any that is
both > 30 % and > 10 s slower (appended to the new summary.md, printed to
stderr, nonzero exit).
It is scheduled on the dev box as a systemd user timer:
systemctl --user status mado-bench # last run's status
systemctl --user list-timers # see the schedule
The unit (mado-bench.timer in ~/.config/systemd/user/) runs at 03:30 UTC
nightly with Persistent=true and linger enabled; archives land in
bench-results/ as usual.
Reference
This section is the exhaustive, verified reference for mado’s surface:
- CLI verbs — every
madosubcommand and its notable flags, verified againstcrates/mado/src/bin/mado.rs. - Environment variables — the
MADO_*(andMADO_TOKEN) variables mado reads, and what each controls. - File formats — the on-disk / on-the-wire format magics
(
MDLP,MDPI,MDSM,MDSP,MDFM,MDOJ,MDWL,MDPW,MDWC) with one-line descriptions and pointers to the owning module.
mado is one binary.
mado <verb>covers both mado-specific verbs (below) and the full jj CLI (mado log,mado describe,mado rebase, …), which behave exactly as in upstream jj. This reference documents only the mado-specific verbs. Runmado <verb> --helpfor the authoritative, always-current flag list.
CLI verbs
Every verb below is a subcommand of the single mado binary, verified against
crates/mado/src/bin/mado.rs. Flags shown are the notable ones; run
mado <verb> --help for the complete, authoritative list.
mado init
Initialize a new mado repository + workspace in the current directory.
| Flag | Default | Meaning |
|---|---|---|
--workspace <name> | default | Workspace name to register. Must be unique per shared server. |
--store-format <fs|fjall> | fs | At-rest blob format for a local init. fs: loose files, shareable by concurrent processes. fjall: embedded LSM, exclusive to one process, the production/import format. Rejected for remote inits. |
--resume | off | This init is the target of a mado ws resume: skip the workspace-name collision probe (the name is expected to be registered). |
mado mount
Mount the working copy through the FUSE VFS (a daemon).
| Flag | Default | Meaning |
|---|---|---|
--mountpoint <dir> | required | Where to mount. Must differ from the workspace root. |
--no-auto-snapshot | off | Disable folding idle mount edits into @. |
--no-auto-checkpoint | off | Disable the remote auto-checkpointer (no effect on a local workspace). |
--checkpoint-interval <secs> | 30 | Auto-checkpoint cadence — the bounded off-box loss window. 0 disables. |
--allow-other | off | -o allow_other: let other uids access the mount (needs user_allow_other in /etc/fuse.conf). Required to re-export into a guest VM. |
--read-only | off | -o ro: kernel refuses writes before they reach the VFS. |
--scratch <dir> | none | Local backing dir for scratch redirections (off-snapshot build dirs). |
--redirect <path> | none | Mount-relative path to redirect into --scratch (repeatable; merged with .mado-redirections). |
--hydration-timeout-secs <secs> | 30 | Per-attempt deadline for a content fetch inside a FUSE read. |
--hydration-retries <n> | 2 | Extra fetch attempts after the first failure/timeout, then EIO. |
mado serve-virtiofs
Serve the working copy over virtiofs (vhost-user-fs) on a socket — the
native second transport, no virtiofsd and no /dev/fuse.
| Flag | Default | Meaning |
|---|---|---|
--socket <path> | required | vhost-user unix socket qemu attaches to. |
--no-auto-snapshot | off | Disable idle edit folding. |
--no-auto-checkpoint / --checkpoint-interval <secs> | / 30 | Inert here (the daemon holds no lease); present for CLI symmetry. |
--cache-timeout-secs <secs> | 1 | Guest attr/entry cache timeout (virtiofs has no active invalidation). Larger = faster, staler. |
--fresh | off | No caching (--cache-timeout-secs 0): host-side checkouts reflect instantly. |
--scratch <dir> / --redirect <path> | Scratch redirections, as for mount. | |
--hydration-timeout-secs / --hydration-retries | 30 / 2 | As for mount. |
mado snapshot
Fold edits made through the mount into @. Pulls the live overlay from the
running daemon over the control socket and amends @. Quiet, idempotent — the
manual equivalent of the daemon’s idle auto-snapshot. No flags.
mado materialize [PATH]
Prefetch (hydrate) the working copy’s file contents into the disk blob cache.
Walks @’s tree under PATH (default: the whole tree; a directory prefetches
its subtree) and fetches every file’s blobs with bounded concurrency. Requires a
remote-backed workspace (MADO_REMOTE_ADDR); a no-op on a local workspace.
mado pack
Build locality packs for a commit’s dense small-file subtrees and record the pack index.
| Flag | Default | Meaning |
|---|---|---|
-r, --revision <revset> | @ | Revision whose tree to pack (must resolve to one commit). |
--recursive | off | Build recursive-subtree (v2) packs instead of per-directory (v1). The measured-best default for real trees. |
Packs are a reclaimable cache, never a GC root. Building is idempotent.
mado pack-status
Report the running mount daemon’s locality-pack attach state (attached mode /
version / entry count / discovery source, or the not-attached reason). Requires
a live mado mount / serve-virtiofs in the workspace. No flags.
mado ws checkpoint
Record a workspace checkpoint: fold pending mount edits into @, snapshot
scratch, and bind (operation, scratch manifest, attr-table presence) into one
durable record.
| Flag | Meaning |
|---|---|
--scratch <dir> | Local backing dir of the scratch redirections. Omit if the mount runs without scratch. |
--redirect <path> | Extra path redirected into --scratch (repeatable; merged with .mado-redirections). |
mado ws pause
Pause this workspace so it can resume on another machine. Folds edits, snapshots
scratch, uploads the attr table, writes the checkpoint, and CAS-swaps the
server-side workspace head to it (the commit point). Blocks until everything is
remote-durable. Flags: --scratch <dir>, --redirect <path> (as for
checkpoint).
mado ws resume
Resume a paused workspace into this workspace. The target must be a fresh
mado init --workspace <name> --resume against the same server.
| Flag | Meaning |
|---|---|
--scratch <dir> | Local backing dir to restore scratch redirections into. Required when the checkpoint carries scratch entries. |
--force | Overwrite existing local workspace state (dirty workspace). |
--steal | Steal the workspace lease from a prior holder — succeeds only once the lease has expired on the server clock; bumps the epoch to fence the old holder. |
mado serve
Serve this repo’s blob store + op-log over gRPC (the networked backend). Blocks until killed. Core flags:
| Flag | Default | Meaning |
|---|---|---|
--addr <host:port> | 127.0.0.1:50051 | Bind address (:0 lets the OS pick, printed on startup). |
--op-heads-db <file> | none | Persist op-heads to SQLite (else ephemeral in-memory). Required for scheduled GC and git interop. |
--auth-token <token> | none | Require authorization: Bearer <token> on every RPC. |
--prod-blobstore | off | Store through the production composition (Prefix → Pack(zstd) → Multiplexed → Fs). Changes the at-rest format. |
--blob-prefix <p> | none | Namespace blobs under this prefix (implies prod composition). |
--gc-interval-secs <secs> | 0 | Run a server-side GC sweep every N seconds (0 = never). Requires --op-heads-db. |
--gc-min-age-secs <secs> | 3600 | Scheduled GC age grace period. |
--stats-interval-secs <secs> | 60 | Blob-traffic summary interval (0 disables). |
Online git interop flags (all require --op-heads-db): --git-ingest-url,
--git-ingest-ref (default master), --git-ingest-interval-secs (300),
--git-ingest-mirror, --git-ingest-scope, --git-ingest-threads;
--git-export-url, --git-export-staging, --git-export-refspec,
--git-export-bookmark, --git-export-scope, --git-export-interval-secs
(300), --git-export-force, --git-export-on-reject (retry|rebase|surface,
default retry), --git-export-fidelity (any|roundtrip-only, default
any); --git-interop-config <file> (named remotes, additive to the
single-remote flags); throttling --git-interop-max-serve-ops (0 =
unthrottled); server-held credentials --git-credential-token /
--git-credential-file (preferred) / --git-credential-username
(default x-access-token); event webhook --git-webhook-addr /
--git-webhook-secret / --git-webhook-secret-file. See
Operations and docs/design/online-git-interop.md.
mado sync-now
Wake a running mado serve’s online git interop tasks now (against
MADO_REMOTE_ADDR) instead of waiting for their poll interval.
| Flag | Meaning |
|---|---|
--remote <name> | An interop task label to wake (e.g. git-ingest[foo]); repeatable. Omitted = wake them all. |
mado gc
Garbage-collect unreachable blobs from .jj/repo/store. Dry run by default.
| Flag | Default | Meaning |
|---|---|---|
--op-heads-db <file> | required | Live-heads database defining what to retain. |
--scope <scope> | "" | Op-heads scope to read heads for. |
--sweep | off | Actually delete (else report only). |
--min-age-secs <secs> | 3600 | Age grace period; 0 disables (only safe when quiesced). |
--prod-blobstore / --blob-prefix <p> | Must match how the store is served. |
mado recover-op-heads
Rebuild the op-heads database from the blobstore journal.
| Flag | Meaning |
|---|---|
--out-db <file> | Where to write the reconstructed database. Must not already exist. |
--prod-blobstore / --blob-prefix <p> | Must match how the store is served. |
mado import-git
Convert a git repository’s history into this repo’s mado store and (unless
--no-publish) publish the imported tips. One-way boundary conversion, memoized
in git-map.sqlite (resumable; incremental on re-run after git fetch).
| Flag | Default | Meaning |
|---|---|---|
--git-repo <path> | required | Git repo (work tree or bare .git) to import. |
--ref <ref> | all local branches | A git ref to import (repeatable). |
--depth <n> | none | Shallow graft by generation count (explodes on merge-dense history — prefer --shallow-since). |
--shallow-since <when> | none | Time-bound shallow graft (YYYY-MM-DD, ISO timestamp, or epoch seconds). |
--no-publish | off | Convert objects but publish no operation (imports stay unreferenced). |
--op-heads-db <file> | none | Database to publish into. Required unless --no-publish. |
--scope <scope> | "" | Op-heads scope to publish under. |
--progress-every <n> | 1000 | Progress line cadence (0 = silent). |
--threads <n> | 0 | Conversion worker threads (0 = one per core). |
--pack-order <auto|on|off> | auto | Convert in pack-offset order (faster for large shallow clones). |
--prod-blobstore / --blob-prefix <p> | Must match how the store is served. | |
--no-packs | off | Skip building + publishing locality packs for imported head trees (on by default; recursive index, warm-local). |
mado export-git
Export this repo’s history (the current view’s bookmarks) into a git repository.
Shares git-map.sqlite with import: exports back into the original repo reuse
objects byte-identically; fresh exports of unsigned, gitlink-free history
reproduce the original commit ids. Conflicted commits refuse to export.
| Flag | Default | Meaning |
|---|---|---|
--git-repo <path> | required | Git repo to export into (initialized non-bare if absent). |
--bookmark <name> | all local bookmarks | A bookmark to export as the same-named branch (repeatable). |
--op-heads-db <file> | required | Database naming the view to export. |
--scope <scope> | "" | Op-heads scope to read the view from. |
--force | off | Move existing branch refs that point elsewhere. |
--progress-every <n> | 1000 | Progress line cadence (0 = silent). |
--prod-blobstore / --blob-prefix <p> | Must match how the store is served. |
Environment variables
The MADO_* variables mado reads, found by grepping the crates and verified at
their read sites. Only the four in the first table are ordinary user-facing
configuration.
User-facing
| Variable | Read by | Meaning |
|---|---|---|
MADO_REMOTE_ADDR | crates/mado/src/bin/mado.rs (remote_addr) | Server address (e.g. http://server:50051). Set = the workspace’s object reads/writes go to a mado serve server over the cached client stack; unset/empty = a local on-disk store under .jj/repo/store. A remote-inited workspace that is loaded with this unset errors clearly. |
MADO_TOKEN | crates/mado-client/src/auth.rs (token_from_env) | Client bearer token attached as authorization: Bearer <token> to every RPC. Must match the server’s --auth-token. Unset or empty = no auth header (matches a server started without --auth-token). |
MADO_REMOTE_PREFIX | crates/mado/src/bin/mado.rs (remote_prefix) | The client’s repo prefix (blob namespace) for per-repo ACL authorization of op-heads / lease requests. Unset or empty = the default namespace (""), matching a single-tenant mado serve. |
MADO_DISK_CACHE_MAX_BYTES | crates/mado/src/lib.rs (with_disk_cache) | Byte cap on the persistent disk blob cache. Set = the tier evicts least-recently-used blobs to stay at or below the cap. Unset, empty, 0, or unparseable = unbounded (the default). Read once at client-stack construction, so it applies uniformly to the mount daemon, serve-virtiofs, and the CLI. |
Internal / not user-set
These appear in the codebase but are not ordinary configuration knobs:
| Variable | Where | Why it is not a config knob |
|---|---|---|
MADO_GIT_TOKEN, MADO_GIT_USERNAME | crates/mado/src/git_credential.rs | mado sets these itself in the child git process’s environment to feed a server-held credential to interop git over the GIT_ASKPASS pattern. You configure the credential with mado serve --git-credential-file (etc.), not by exporting these. |
MADO_BENCH_N | crates/mado-index/src/store.rs, crates/mado-index/src/tests.rs | Sizes the N of #[ignore]d benchmark tests only. No effect on the shipped binary. |
MADO_REAL_TREE | crates/mado/src/real_tree_study.rs | Points the env-gated real-tree study bench at a directory. Test-only; unset skips the bench cleanly. |
Note.
MADO_INDEX_TYPEshows up as a re-export alias incrates/mado-index/src/lib.rs(pub use store::INDEX_TYPE as MADO_INDEX_TYPE) for a compile-time constant string ("mado", the jj index type name). It is not read from the environment and is not a configuration variable — it is named here only because aMADO_grep surfaces it.
File formats
mado’s binary formats are self-describing: each begins with a 4-byte ASCII magic and a version byte, and every format is ossified (a reader rejects a higher-than-known version with a typed error; a newer reader can still read the old version). This table lists each magic, a one-line description, and the module that owns its encoder/decoder.
| Magic | Name | Owning module | One-line description |
|---|---|---|---|
MDLP | Mado Locality Pack | crates/mado-filestore/src/pack.rs | A content-addressed bundle of a subtree’s inline files (and, in v2, chunked-file manifests + chunks as typed members), so one ranged GET hydrates a whole small-file subtree instead of one GET per file. |
MDPI | Mado Pack Index | crates/mado/src/pack_build.rs | The directory → covering-pack index a mado pack build records; content-addressed by the tree it describes. v1 = per-directory (exact match); v2 = recursive-subtree (ancestor-walk covering lookup). The daemon loads either version. |
MDPW | Mado Pack Well-known (pointer) | crates/mado/src/pack_publish.rs | The well-known derived discovery pointer — a tiny blob at a key derived from the tree id — that points at the freshest MDPI index for a tree, enabling zero-config pack discovery with no local file and no new RPC. |
MDFM | Mado File Manifest | crates/mado-filestore/src/manifest.rs | A chunked file’s manifest: the ordered content-defined-chunk list that reassembles a large file, plus its size (so file_size can stat without fetching content). |
MDSM | Mado Scratch Manifest | crates/mado-filestore/src/scratch.rs | The snapshot of a workspace’s scratch (off-snapshot build-dir) tree — the redirected paths and their content, captured for a checkpoint / pause. |
MDSP | Mado Scratch (manifest) Pointer | crates/mado/src/scratch.rs | A pointer record naming the freshest MDSM scratch-manifest blob (distinct from the MDSM manifest it points at). |
MDWC | Mado Workspace Checkpoint | crates/mado/src/scratch.rs | The durable checkpoint record binding a jj operation id, the scratch-manifest hash, and attr-table presence into one blob — the record mado ws pause / resume travel on. |
MDWL | Mado Workspace Lease (handoff) | crates/mado/src/ws.rs | A local, single-machine lease-handoff file (.jj/working_copy/ws-lease) recording the lease held by this workspace so a daemon and a mado ws command on the same box rendezvous on it. |
MDOJ | Mado Op Journal | crates/mado-server/src/journal.rs | An op-heads journal record in the blobstore’s opjournal namespace: every op-heads mutation is chained here (hash chain per scope) before it commits, so mado recover-op-heads can rebuild the op-heads database from blobs alone. |
Notes
- The magics
MDLP,MDSM,MDWC, andMDPIshare a v1 header layout (magic | version | encryption byte | …); the encryption byte reserves space for a future key-wrapping scheme and is currently a fixed marker. MDLPis grouping-agnostic: per-directory (v1) and recursive-subtree (v2) packs use the identical pack format — only the builder grouping, theMDPIindex semantics, and the trigger gate differ. See Performance § Locality packs.- These magics are the wire/at-rest contract; the store’s own at-rest format
(loose files vs the fjall LSM engine, and whether the production
Pack(zstd)/Prefix/Multiplexedcomposition wraps it) is a separate, orthogonal concern selected atmado init/mado servetime. A store’s at-rest format is recorded in ablobs-formatmarker inside.jj/repo/store(absent = the original loose-file layout).