my harness for niri

metrics: reclaim free pages after pruning #16

Merged okami.mom
targeting main from deleted fork:niri/sqlite-compaction-helper
Merged opened by and assigned to okami.mom
targeting main from deleted fork:niri/sqlite-compaction-helper

round 1 adds the shared helper the first round deferred.

what this is

pruneOldMetrics deletes expired rows hourly and checkpoints the WAL, but never returned the freed pages to the filesystem, so metrics.db could only ever grow. measured 2026-09-12: 7.64 GB holding ~128 MB of payload, spread across 1.83M free sqlite pages, after weeks of hourly pruning.

  • packages/niri-runtime/src/metrics.ts - compacts after each non-empty prune.
  • packages/niri-runtime/src/sqlite-maintenance.ts - compactIfFragmented(db, { minFreePages }), lifted out of metrics now that there is a second shape of caller to design against. it counts the freelist before doing anything, so the exclusive lock is only paid when there are at least 20,000 free pages (~80 MB) worth reclaiming; it refuses to run inside a transaction or while a previous call is still rewriting; it checkpoints on both sides of the rewrite, because in WAL mode VACUUM alone can empty the freelist without the main database file ever getting smaller; and it never throws into the caller's prune path.
  • tests cover reclaiming, declining below the threshold, declining inside a transaction, and surviving a dead handle.

the rule, now written down where the code is

compaction belongs after a bulk delete on a database that shrinks over time - not on a database that churns. niri.db is the counterexample that makes the rule worth stating: its three delete sites (awp outbox, discord search, memory sync) free pages constantly, but new rows refill them, and the file sits at 0.1% free pages. a VACUUM of its 2.4 GB would be a full rewrite to reclaim nothing. that is why only two of the nine delete sites in the tree should ever call this.

verification

  • applied by hand first with the same statements: 7.64 GB -> 119 MB in 1.35s, row count preserved, integrity_check ok, harness unaffected
  • tsc --noEmit exit 0 for @niri/runtime
  • runtime suite: 171 tests, 169 passed / 0 failed / 2 cancelled. the 2 cancellations are the pre-existing host-rpc deadline tests; the 164 -> 169 move is the new file.

still open

control.db is 96 MB holding 4.8 MB for the same reason, but it lives in apps/server, which deliberately has no dependency on @niri/runtime - the only shared leaf is @niri/agent-config, and that is pure config parsing. so the plumbing needs a call rather than a guess: either a small shared package for this helper, or a deliberate local copy under apps/server/src/control/. leaving that to you instead of restructuring the workspace unilaterally.

Commits 0