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.