Skip to content

Incremental rebuild at the envelope is 2.8x over its gate, while fingerprinting is well inside #245

Description

@burakdede

Measured while writing #83's envelope benchmarks. Reported, not a release blocker yet: the gate
is enforced in Phase 7 (#101) against a reference machine that is not yet defined. But the
distance is large enough that it should be known now rather than discovered at the gate.

The measurement

pnpm bench:envelope, at the file envelope, on AMD Ryzen 9 3900X, 24 cores, 31 GB, Node
24.18.1, Linux x64. Committed as benchmarks/envelope/reference-2026-08-05.json.

"corpus": { "files": 2500, "totalGb": 0.096, "chunks": 39010 },
"measurements": {
  "fullBuildMs": 52337,
  "noopRebuildP50Ms": 857.76,
  "noopRebuildP95Ms": 915.68,
  "incrementalRebuildP50Ms": 4561.24,
  "incrementalRebuildP95Ms": 5631.62
},
"referenceGates": { "fingerprintP95Ms": 4000, "incrementalRebuildMs": 2000 }
Gate (section 5.5) Target Measured p95
Fingerprint at 2,500 files < 4,000 ms 916 ms comfortably inside
Single-document incremental rebuild < 2,000 ms 5,632 ms 2.8x over

The fingerprint half is healthy, and that is worth saying: deciding what changed across 2,500
files costs under a second. What costs is everything after that decision.

The machine above is a fast desktop. A reference machine is likely slower, so the real distance
is probably larger, not smaller.

What it is not

  • Not the corpus. 2,500 files producing 39,010 chunks is close to section 5.4's 50,000-chunk
    figure, so this is the shape the envelope describes.
  • Not a cold build. A no-op rebuild of the same corpus is 916 ms, so discovery, fingerprinting
    and the cache lookup are not where the time goes. What separates the two runs is one document
    reparsed and the index rewritten.
  • Not caching failing. The benchmark warns if fewer than files - 1 artifacts are reused, and
    it did not warn.

So the cost is in what a rebuild does after deciding only one artifact changed. Sealing writes
a whole new build database, because a build is immutable and content-addressed; at 39,010 chunks
that is 39,010 rows and an FTS5 index rewritten to change one document's worth.

Why this is a design question, not a slow function

Immutability is invariant 4, and rewriting the catalog is how a build stays a complete,
self-contained, content-addressed artifact. Making an incremental rebuild fast by mutating the
previous build in place would trade the invariant for the number, which is the wrong trade.

So the options are narrower than "optimise it", and the decision belongs on a ticket rather than
in a profiler:

  1. Copy the previous build database and patch it, rather than building it from nothing. The
    result must still be byte-identical for identical inputs, which is what makes this delicate:
    identity is logical and excludes physical bytes (build-identity.md), so a copied-and-patched
    database is legitimate only if every canonical root is recomputed rather than inherited.
  2. Accept the number and change the gate, if 2,000 ms was chosen before the sealing cost was
    understood. Section 5.5 predates the measurement.
  3. Narrow the gate's claim to a corpus size it holds for, and say what that size is.

Acceptance criteria

  • The decision above is recorded before any optimisation is attempted.
  • If the gate stands, an incremental rebuild at the envelope meets it, and the determinism
    suite still passes: same id from two paths, on Windows and POSIX, with shuffled enumeration.
  • If the gate moves, docs/ says the new number and why, and section 5.5 is amended rather
    than quietly diverged from.
  • benchmarks/envelope/ gains a run at the byte envelope too (1 GB), which this one is
    not: atByteEnvelope is false at 0.096 GB.

Also worth knowing

Chunks do not merge across headings, so a corpus's chunk count tracks its heading count
rather than its byte count. A first version of this generator wrote 40 KB as ~230 tiny sections
and produced 583,620 chunks from 2,500 files: eleven times the envelope's chunk figure at a tenth
of its byte figure. Anyone writing a corpus generator should set the section length deliberately,
and bench-envelope.mjs records why in a comment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions