Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 mount runs 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_ADDR set, object reads/writes go to a mado serve server 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-git are 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 on mado serve are 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/fuse or 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-snapshot to turn that off and rely on the overlay journal plus explicit mado 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 --workspace name. 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 explicit mado ws pause for 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 resume takes the lease only if it is free or expired, and never steals it from a live holder.
  • mado ws resume --steal forces 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.md and docs/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 of N files costs roughly N × 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 (vendor node_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:

Subtreeoffper-dirrecursive
nixos/tests (dense, 1000-file walk)998 GETs / 24.9 s176 GETs / 5.5 s2 GETs / 1.4 s
pkgs/development/python-modules (thin)1000 GETs / 25.4 s1936 GETs / 47.8 s2 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, coldtrigger, cold
0703 ms222 ms
207030 ms262 ms
5016068 ms283 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 set W with cap C, pass-2 remote GETs are exactly N at C = 0.5 W (classic LRU sequential-scan thrash) and exactly 0 at C ≥ 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-W default 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 present authorization: Bearer <token>. Without it the server is open — suitable only for a trusted network. Clients set MADO_TOKEN to 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 fjall store 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; 0 disables 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-db is required. GC derives the retained set from these heads. If the head set is empty, mado gc refuses 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. 0 disables the window — only safe against a quiesced store.
  • --scope reads live heads for a specific op-heads scope (defaults to the unscoped set).
  • Prod stores: pass the same --prod-blobstore / --blob-prefix the 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 mado subcommand and its notable flags, verified against crates/mado/src/bin/mado.rs.
  • Environment variables — the MADO_* (and MADO_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. Run mado <verb> --help for 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.

FlagDefaultMeaning
--workspace <name>defaultWorkspace name to register. Must be unique per shared server.
--store-format <fs|fjall>fsAt-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.
--resumeoffThis 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).

FlagDefaultMeaning
--mountpoint <dir>requiredWhere to mount. Must differ from the workspace root.
--no-auto-snapshotoffDisable folding idle mount edits into @.
--no-auto-checkpointoffDisable the remote auto-checkpointer (no effect on a local workspace).
--checkpoint-interval <secs>30Auto-checkpoint cadence — the bounded off-box loss window. 0 disables.
--allow-otheroff-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-onlyoff-o ro: kernel refuses writes before they reach the VFS.
--scratch <dir>noneLocal backing dir for scratch redirections (off-snapshot build dirs).
--redirect <path>noneMount-relative path to redirect into --scratch (repeatable; merged with .mado-redirections).
--hydration-timeout-secs <secs>30Per-attempt deadline for a content fetch inside a FUSE read.
--hydration-retries <n>2Extra 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.

FlagDefaultMeaning
--socket <path>requiredvhost-user unix socket qemu attaches to.
--no-auto-snapshotoffDisable idle edit folding.
--no-auto-checkpoint / --checkpoint-interval <secs>/ 30Inert here (the daemon holds no lease); present for CLI symmetry.
--cache-timeout-secs <secs>1Guest attr/entry cache timeout (virtiofs has no active invalidation). Larger = faster, staler.
--freshoffNo caching (--cache-timeout-secs 0): host-side checkouts reflect instantly.
--scratch <dir> / --redirect <path>Scratch redirections, as for mount.
--hydration-timeout-secs / --hydration-retries30 / 2As 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.

FlagDefaultMeaning
-r, --revision <revset>@Revision whose tree to pack (must resolve to one commit).
--recursiveoffBuild 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.

FlagMeaning
--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.

FlagMeaning
--scratch <dir>Local backing dir to restore scratch redirections into. Required when the checkpoint carries scratch entries.
--forceOverwrite existing local workspace state (dirty workspace).
--stealSteal 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:

FlagDefaultMeaning
--addr <host:port>127.0.0.1:50051Bind address (:0 lets the OS pick, printed on startup).
--op-heads-db <file>nonePersist op-heads to SQLite (else ephemeral in-memory). Required for scheduled GC and git interop.
--auth-token <token>noneRequire authorization: Bearer <token> on every RPC.
--prod-blobstoreoffStore through the production composition (Prefix → Pack(zstd) → Multiplexed → Fs). Changes the at-rest format.
--blob-prefix <p>noneNamespace blobs under this prefix (implies prod composition).
--gc-interval-secs <secs>0Run a server-side GC sweep every N seconds (0 = never). Requires --op-heads-db.
--gc-min-age-secs <secs>3600Scheduled GC age grace period.
--stats-interval-secs <secs>60Blob-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.

FlagMeaning
--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.

FlagDefaultMeaning
--op-heads-db <file>requiredLive-heads database defining what to retain.
--scope <scope>""Op-heads scope to read heads for.
--sweepoffActually delete (else report only).
--min-age-secs <secs>3600Age 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.

FlagMeaning
--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).

FlagDefaultMeaning
--git-repo <path>requiredGit repo (work tree or bare .git) to import.
--ref <ref>all local branchesA git ref to import (repeatable).
--depth <n>noneShallow graft by generation count (explodes on merge-dense history — prefer --shallow-since).
--shallow-since <when>noneTime-bound shallow graft (YYYY-MM-DD, ISO timestamp, or epoch seconds).
--no-publishoffConvert objects but publish no operation (imports stay unreferenced).
--op-heads-db <file>noneDatabase to publish into. Required unless --no-publish.
--scope <scope>""Op-heads scope to publish under.
--progress-every <n>1000Progress line cadence (0 = silent).
--threads <n>0Conversion worker threads (0 = one per core).
--pack-order <auto|on|off>autoConvert in pack-offset order (faster for large shallow clones).
--prod-blobstore / --blob-prefix <p>Must match how the store is served.
--no-packsoffSkip 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.

FlagDefaultMeaning
--git-repo <path>requiredGit repo to export into (initialized non-bare if absent).
--bookmark <name>all local bookmarksA bookmark to export as the same-named branch (repeatable).
--op-heads-db <file>requiredDatabase naming the view to export.
--scope <scope>""Op-heads scope to read the view from.
--forceoffMove existing branch refs that point elsewhere.
--progress-every <n>1000Progress 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

VariableRead byMeaning
MADO_REMOTE_ADDRcrates/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_TOKENcrates/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_PREFIXcrates/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_BYTEScrates/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:

VariableWhereWhy it is not a config knob
MADO_GIT_TOKEN, MADO_GIT_USERNAMEcrates/mado/src/git_credential.rsmado 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_Ncrates/mado-index/src/store.rs, crates/mado-index/src/tests.rsSizes the N of #[ignore]d benchmark tests only. No effect on the shipped binary.
MADO_REAL_TREEcrates/mado/src/real_tree_study.rsPoints the env-gated real-tree study bench at a directory. Test-only; unset skips the bench cleanly.

Note. MADO_INDEX_TYPE shows up as a re-export alias in crates/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 a MADO_ 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.

MagicNameOwning moduleOne-line description
MDLPMado Locality Packcrates/mado-filestore/src/pack.rsA 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.
MDPIMado Pack Indexcrates/mado/src/pack_build.rsThe 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.
MDPWMado Pack Well-known (pointer)crates/mado/src/pack_publish.rsThe 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.
MDFMMado File Manifestcrates/mado-filestore/src/manifest.rsA 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).
MDSMMado Scratch Manifestcrates/mado-filestore/src/scratch.rsThe snapshot of a workspace’s scratch (off-snapshot build-dir) tree — the redirected paths and their content, captured for a checkpoint / pause.
MDSPMado Scratch (manifest) Pointercrates/mado/src/scratch.rsA pointer record naming the freshest MDSM scratch-manifest blob (distinct from the MDSM manifest it points at).
MDWCMado Workspace Checkpointcrates/mado/src/scratch.rsThe 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.
MDWLMado Workspace Lease (handoff)crates/mado/src/ws.rsA 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.
MDOJMado Op Journalcrates/mado-server/src/journal.rsAn 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, and MDPI share 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.
  • MDLP is grouping-agnostic: per-directory (v1) and recursive-subtree (v2) packs use the identical pack format — only the builder grouping, the MDPI index 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/Multiplexed composition wraps it) is a separate, orthogonal concern selected at mado init / mado serve time. A store’s at-rest format is recorded in a blobs-format marker inside .jj/repo/store (absent = the original loose-file layout).