perf(box): speed cold docker builds and fix update --check estimates - #271
perf(box): speed cold docker builds and fix update --check estimates#271danielrmerskine wants to merge 5 commits into
Conversation
Replace apt nodejs meta-packages with the official Node binary, bump cryptography to a cp312 wheel, and hash the target-ref Dockerfile so forward version jumps no longer report a false ~90s cached estimate. Also surface live docker-build detail on the progress bar and use algorithms.AES for BluFi so the crypto bump stays compatible. Co-authored-by: Cursor <cursoragent@cursor.com>
The at-ref hasher used bash pattern substitution and `printf -v`, both of which are silently wrong under dash — the digest would never match the stored working-tree hash on a box whose login shell is /bin/sh, making every --check report a spurious rebuild. Rewrite with `git show | sha256sum | sed` and the same `out=$(...)` + `echo "$out" | sha256sum` composition as the working-tree hasher, so the two are byte-identical by construction. Add tests that execute both snippets under `sh` against a fake box layout rather than asserting on substrings. Co-authored-by: Cursor <cursoragent@cursor.com>
Main's build-hash now walks ~/box/lager; keep the equivalence tests and preview assertions aligned so a Dockerfile-only at-ref hasher cannot regress silently. Co-authored-by: Cursor <cursoragent@cursor.com>
Two corrections to the same `lager update --check` estimate landed in the same week against the same code. main extracted `_deps_preview()`, giving it the `needs_flatten` rebuild trigger and a measured/unmeasurable distinction; the three commits below this one added target-ref build hashing so that a forward jump stops reporting `~90s (cached build)` off a pre-pull tree. Neither was written with knowledge of the other, and each shipped its own helper for the job. Keeping `_preview_deps_status` would have quietly reverted the flatten fix. It never reads `needs_flatten`, so a box still on the `box/` subdir layout that is also behind its target would go back to printing "cache valid" immediately before a ten-minute rebuild -- precisely the case the flatten fix was written for. `_deps_preview()` therefore stays as the single helper and gains `target_hash` and `needs_pull`. A successfully measured target ref now replaces the working tree as the basis for the whole preview, which also converts the old "unknown until pull (older ref may differ)" guess on rollbacks and branch switches into a measured answer. `needs_flatten` still forces a rebuild whatever that digest says, and a target that could not be measured reports unknown rather than falling back to the pre-pull tree and calling the cache valid. `_preview_deps_status` is removed; its four tests move onto `_deps_preview`, joined by a fifth that pins the flatten-beats-matching-target case this reconciliation could have regressed unnoticed. Also in this commit: - `test/COVERAGE.md` counts regenerated: unit (cli) 1258 -> 1274, total gated 3342 -> 3358. - The new Dockerfile comment claimed flex/bison/ccache/ninja were "build-only for uldaq (next stage)". Only flex and bison are, and they do move to that stage; nothing in this repo invokes ccache or ninja-build anywhere, so they are dropped outright rather than relocated. The comment now says that. - CHANGELOG entries for the target-ref hashing, the cold-build speedups, and the live build detail on the progress bar -- including the operational note that a box carrying globally-installed npm packages needs one `--force` update, because Node 18 -> 20 is an ABI break and only `--force` wipes the `lager-npm-global` volume that survives an ordinary rebuild. - Two test comments now describe the forward-jump condition they cover, which is what tells a reader when the case applies.
Hardware verification (real box, cold build)Every claim in this PR is now backed by a measurement rather than reasoning. Cold build: 20m14s, from Before / after on the same box
Read out of the running container, not from the build log. The claims, each with its evidence
|
| Scenario | Reported |
|---|---|
| Forward jump whose target ref changes the Dockerfile | will trigger fresh build (target Dockerfile, requirements or box source changed) / ~6 min |
| In sync with the target | cache valid (no rebuild) / ~5s / Nothing to do. |
The target Dockerfile wording is emitted only by the at-ref branch, so this confirms
the box really did read the Dockerfile at the target ref out of its object database and
compare digests — which also exercises the POSIX-safe hasher against a real box's login
shell, the failure mode that would otherwise have been invisible until every --check
started predicting a rebuild.
One honest qualification: the box used here was 1 ahead / 8 behind, a shape that would
have hit the pessimistic ~6 min branch under the old code too, via commits_ahead > 0.
So the estimate happens to coincide; what this proves is that the measurement path runs
correctly on real hardware and discriminates in both directions. The behind-only case,
where the old code said ~90s, is covered by the unit tests that execute both hashers
under sh against a fixture repo and assert the digests agree.
Not covered here
The Node 18 -> 20 ABI note in the CHANGELOG remains reasoned rather than measured: this
box has no globally-installed npm packages (.npm-global/lib does not exist,
npm_packages.txt is comments only), so there was nothing for the major-version jump to
break. The caution stands for a box that does carry them.
The comment claimed the build digest covers the image recipe but not every COPY layer. It covers both: the hash walks all of ~/box/lager, and every COPY source in box.Dockerfile resolves inside that tree. The floor is still correct, for a different reason. A matching digest proves the build inputs are unchanged, not that Docker still holds the layers built from them -- a `docker builder prune` between updates rebuilds everything with the digest identical.
Summary
Speeds up cold box image builds, and fixes a
lager update --checkestimate thatcould be wrong by an order of magnitude.
Authored by @juul-charles-w. Supersedes #260 — same commits, rebased onto current
main, plus one reconciliation commit described below.What changed
Cold build time (
box.Dockerfile):SHASUMS256.txtbefore extraction, instead of Debian's
nodejs npmmeta-packages — which pull inroughly 400
node-*packages the box never touches.cryptography38.0.4 -> 43.0.3. The old pin has no cp312 wheel, so every cold piplayer compiled it from Rust source.
flex/bisonmove into the uldaq stage, the only one whoseautoreconfneedsthem.
ccache/ninja-buildare dropped outright — nothing in this repo invokeseither.
algorithms.AES128toalgorithms.AES, required by the cryptobump and byte-identical for BluFi's fixed 16-byte key.
--checkaccuracy. The preview hashed the box's current Dockerfile,requirements and source — but an update pulls first. A box a long way behind, whose
current tree still matched
/etc/lager/build-hash, was told~90s (cached build)andthen waited six minutes.
--checknow hashes those same inputs at the target ref,read straight out of the object DB with no checkout (one extra SSH round-trip, on the
--checkpath only).Progress bar shows live BuildKit detail during the container build instead of one
frozen label.
Reconciliation with main (
6865111)b5ccab7clanded onmainafter this branch was cut and refactored the samefunction, extracting
_deps_preview()with theneeds_flattenrebuild trigger and ameasured/unmeasurable distinction. This branch had independently introduced
_preview_deps_status()for the same job, with neither.Taking the branch's helper would have silently reverted that fix. It never reads
needs_flatten, so a box still on thebox/subdir layout that is also behind wouldgo back to printing "cache valid" in front of a ten-minute rebuild — precisely the
case the flatten fix was written for.
_deps_preview()is kept as the single helper and gainstarget_hash/needs_pull.A measured target ref replaces the working tree as the basis for the whole preview,
which also turns the old "unknown until pull (older ref may differ)" guess into a
measured answer;
needs_flattenstill forces a rebuild whatever that digest says._preview_deps_statusis removed, its four tests move onto_deps_preview, and afifth pins the flatten-beats-matching-target case this reconciliation could have
regressed unnoticed.
Test status
Verified:
unit (cli)1274,unit (box)1596)bash -n, compileall,MCP collect, ruff, zizmor, actionlint, COVERAGE counts
test/COVERAGE.mdregenerated —unit (cli)1258 -> 1274, total gated3342 -> 3358
Not yet verified — no Docker image has been built from this branch. Everything
above exercises Python and linters; none of it builds an image, so the three Dockerfile
claims are still unproven:
node --versionis v20.18.1,cryptographyis43.0.3,
uldaqstill imports after the flex/bison move--checkagainst a forward jump that changes the Dockerfile reports a freshbuild, not
~90s (cached build)Operational note
A box carrying globally-installed npm packages should be updated once with
--forceafter this ships. Node's major version moves 18 -> 20, and the
lager-npm-globalvolume holding those packages survives an ordinary image rebuild — only
--forcewipes it. Any package with a compiled native module needs reinstalling under the new
ABI. This is recorded in the CHANGELOG entry.