880x: Trust the Log, Rebuild the Rest

880x: Trust the Log, Rebuild the Rest

2026-09-01

Published 2026-09-01, hours after the fix landed. Full methodology and reproduction steps in docs/performance.md; the change itself is commit c7ef766 on gethilo/hilo. All numbers: release binary, best of 3, 16-core/59GB Linux box, real corpora (ripgrep 110 files, clap 330, tokio 793).

880x: Trust the Log, Rebuild the Rest

Tonight we made Hilo's graph queries up to 880x faster. hilo graph related on the tokio corpus went from 12.78 seconds to 0.01. Every read-only command in the binary was paying a twelve-second tax — and the reason it was paying it is a story about two instincts that both felt right, and a third option that required throwing away the assumption underneath both of them.

The Highlights:

{"type":"bar","title":"PERF-001 speedups — tokio corpus (793 files)","labels":["graph related","graph search","graph untested","graph stats","graph impact","graph understand"],"datasets":[{"label":"speedup (x)","data":[880,727,692,497,28,10],"color":"#d19a66"}],"yFormat":"number","aspectRatio":2.2,"caption":"Before/after, release binary, best of 3. Full table below. Smaller repos: ripgrep 49x, clap 103x."}

The Design That Was Almost Right

Hilo is an agent-first metadata filesystem. Its graph — which files depend on which, what's related to what, what's untested — is stored in two places by design:

  1. edges.jsonl — an append-only log of edges. One JSON object per line. Human-readable. Git-friendly. Streamable. This is the source of truth.
  2. graph.db — a DuckDB database built from that log. This is the query cache. Rebuildable at any time. Never authoritative.

We chose this shape deliberately. An append-only log can't corrupt itself the way a mutated database can. It diffs cleanly in git. It can be replayed from any point to reconstruct any past state. And because the database is derived, you can delete it whenever you like and get it back by replaying the log. The truth is small and auditable; the expensive structure is disposable. We still think this is the right architecture.

The mistake was one line of diligence on top of it.

The Tax

Every CLI invocation opens the graph database. And every open, we decided, should verify the cache against the log before trusting it — because a cache that's silently out of sync with its source of truth is the worst failure mode a tool like this can have. An agent querying a stale graph doesn't get an error. It gets a confident, plausible, wrong answer. So: verify on open. Always. Correctness first.

The verification was a full replay — re-read every line of edges.jsonl and re-insert it into DuckDB. On tokio: 7,400 edges at roughly 1.6ms per row-by-row insert. About 12 seconds. Every command. Every time. Even graph stats, which reads a handful of aggregates. Even one-shot queries that touch nothing the replay would change.

Here's the arithmetic that should have embarrassed us sooner: the verification was the workload. Building the cache once costs one replay. Verifying the cache costs one replay. So "verify before every read" doesn't cost one replay plus a read — it costs a replay on every read, forever. We had built a system where the check cost more than the work it guaranteed, and then paid it on every invocation, to confirm something that was — almost always — already true.

The benchmark battery made it undeniable. Warm re-run with zero changes: 25.4 seconds to re-parse all 793 files and re-replay every edge. The fixed cost dominated everything: graph stats was 12.2s on tokio, 2.3s on clap, 1.1s on ripgrep — linear in edge count, charged to every query equally.

Neither Instinct Was Right

When something is slow, there are two classic moves, and we want to be honest that both of them were on the table, and both of them are wrong — not stupid, wrong:

Trust the cache blindly. Skip the check. Reads become instant. And then some day a writer crashes mid-append, or two processes race, or a restore leaves the DB a few hundred edges behind — and the tool starts confidently returning answers from a snapshot of the past with no timestamp on it. Fast, and lying. This is how agent tooling earns the exact distrust it can't afford.

Verify everything, always. Never trust. Correct, and catastrophic: you've made the cost of reading equal to the cost of building, which means the cache saves you nothing, which means why have a cache. That's not caution. That's paying insurance premiums larger than every loss you're insuring against, every single day.

Neither way was right and neither way was wrong. They fail at different things: one fails correctness, the other fails economics. The real mistake was deeper than either — the assumption they share is that the cache is a fragile thing that must be protected: guarded by verification, or guarded by faith.

The way out was to stop protecting it.

The Fix: A Stamp, Not a Guarantee

Commit c7ef766 replaces the dilemma with a fingerprint. After every successful full replay, Hilo writes a tiny stamp file — .vfs/graph/.last_reconcile — recording the log's fingerprint: mtime-nanoseconds and size of edges.jsonl. On the next open, instead of re-validating the cache, it compares the stamp to the current file:

That's the whole mechanism. Notice what it does not do: it does not trust. It does not skip correctness. Any writer — the write-through triggers, a graph warm, an external script appending edges — changes the file's mtime and size by the act of writing, the stamp auto-invalidates, and the next open pays the full reconcile. A deliberate drift test (restore an old DB behind a current log) catches it. The guarantee is exactly as strong as it was. Only the price of trust changed: from a full replay on every open to a single file-stat.

And when reconcile does run, it got cheaper too: the replay went from row-by-row inserts with schema re-checks every batch to one prepared statement in a single transaction with rollback-on-error. The rare path got an order of magnitude cheaper along with the common path becoming free.

Recompute More, Keep Less, Inspect Everything

This is the part we actually changed our minds about, and it generalizes past this fix.

The old instinct — ours, and the industry's default — says computed things are precious. You built it; you maintain it; you verify it; you never throw it away. But a cache you refuse to rebuild is a cache you must forever verify, and verification at full price is just recomputation with worse branding. We were spending our diligence budget defending a structure we could have regenerated in 42 seconds.

The shape that works — the one this sprint landed on — inverts the caring:

And here is the payoff nobody advertises, the thing that makes the recomputation philosophy more than a performance trick: because the truth is an append-only log, the past is inspectable. The database can only ever tell you now. The log can tell you when this edge first appeared, what the graph looked like before the refactor, what changed between this release and that one — replay it to any point and read history directly out of it. You get auditability of the past precisely because you were willing to recompute it instead of hoarding snapshots of it. The thing we stopped protecting turned out to be the thing we could finally see.

That's why "delete the cache" is a feature and not a failure mode. Tonight's fix also made the delete-then-rebuild workflow work again: previously, a missing graph.db hard-bailed understand, search, module, untested, and rule-check. Now, with a log present, they rebuild automatically. You can always burn the derived world down and watch it regrow from the record of what actually happened. That's not a cache strategy. That's a trust model.

The Numbers

Command (tokio, 793 files) Before After Speedup
hilo graph related <file> 12.78 s 0.01 s 880x
hilo graph search <query> 13.26 s 0.02 s 727x
hilo graph untested 12.00 s 0.02 s 692x
hilo graph stats 12.70 s 0.03 s 497x
hilo graph impact <file> 13.31 s 0.48 s 28x
hilo graph understand <task> 15.63 s 1.49 s 10x

Smaller repos were paying the same tax at smaller scale: graph stats fell from 1.08s to 0.02s on ripgrep (49x) and 2.29s to 0.02s on clap (103x). The old cost scaled with the size of your project; the new one is constant per invocation.

Two quieter fixes rode along, and they matter more than they look:

What's Next

On the board, honest about what's still slow:

Reproduce it yourself:

git clone --depth 1 https://github.com/tokio-rs/tokio /tmp/tokio
cd /tmp/tokio && hilo init && hilo graph warm
/usr/bin/time -f '%e s | %M KB' hilo graph stats

The closing thought is the one we'd tattoo on the repo if repos took ink: the log is the truth; the database is a rumor about the truth; and a rumor you can regenerate for the cost of one transaction doesn't need to be guarded — it needs to be believed exactly as long as the evidence holds. The evidence is one stat() away. Everything else is recomputation, and recomputation is cheap.


Sources: commit c7ef766 (PERF-001 stamp-gated reconcile), commit be50f46 (baseline battery + PERF-001..003 filed), docs/performance.md (methodology, corpora, full tables, cache-coherence design). Corpora: ripgrep, clap, tokio. Corrections: none yet — will update as PERF-002/003 land.