perf(cache): warm no-op checks stop re-proving the cache (draft — review before merge)#4101
perf(cache): warm no-op checks stop re-proving the cache (draft — review before merge)#4101hellovai wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
⏭️ Performance benchmarks were skippedPerf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to To run them on this PR, do any of the following, then push a commit (or re-run CI):
|
Binary size checks passed✅ 7 passed
Generated by |
…nifest fragments, not unit payloads The warm path spent most of a no-op invocation re-proving its own cache validity: the serve-time throws gate re-derived package_items (re-parsing every file) to convert types for a comparison, and plan_reuse read + hashed + decoded every clean unit payload just to extract the tiny callable-throws fragments it seeds from. At a 295k LOC project that was ~7.2s to conclude "nothing changed" — the same wall time as recompiling from scratch. - No-delta gate skip: when the dirty partition finds no changed, added, or removed files, the gate is provably a tautology — the seeds it would validate are byte-for-byte the values the manifest was stored with (store persists file_throw_facts verbatim; units are content-addressed and written before the manifest), and the solve + runtime conversion are deterministic pure functions of those inputs. Any real delta still takes the full gate. - Manifest-resident fragments: ManifestFile now carries a verbatim copy of each unit's CallableThrowsFragment blob, and the callable_throws seeds project from that copy — so a no-delta invocation opens zero unit payloads. Units are loaded (now in parallel, manifest order preserved) only when a delta means something will be re-emitted or relinked. Both verify oracles (the BAML_CACHE_VERIFY full compare and the 1-in-32 sampled re-derive) now check the manifest copy — exactly the artifact that is served. - The gate path (and every warm collection) primes per-file semantic indexes across workers first, so the package_items derivation the gate and package-level diagnostics demand is a parallel-fed fold instead of a serial parse of the project. - bex_cache: unit_key is derived from the payload's integrity digest, which entry validation already computes — load_unit_shared now reuses that digest instead of hashing the payload a second time. Behavior change, deliberate: a unit file deleted out from under a warm cache is no longer detected on a no-op invocation (nothing reads it); the next compile that needs it falls back to a byte-identical full compile. Slower once, never wrong. Old manifests (without the fragment field) fail borsh decode and are treated as absent — one full recompile re-seeds them. Measured (Apple Silicon): warm no-op check at 295k LOC 7.2s -> 2.0s (zero unit reads, gate skipped); at 59k LOC 0.54s -> 0.30s. Warm leaf-edit check is emit-bound (it seeds the cache for later run/pack hits) and unchanged. Verified: 66 cache tests, BAML_CACHE_VERIFY honest-recompile compare (no-op + after-edit), forced sampled verify across 7 warm invocations, diagnostics byte-identity cold + warm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4oee6wrCHzXH6mM2h8eJo
c2da018 to
f214619
Compare
dc5828e to
5573885
Compare
Draft on purpose — this touches cache-soundness invariants; walk through the risk section below before merging. Stacked on #4100.
The problem
A warm no-op
baml checkat 295k LOC cost 7.2s — identical to a full recompute — because the warm path re-proved its own cache validity from scratch every run: the serve-time throws gate re-parsed every file (viapackage_items) just to convert types for a comparison, andplan_reuseread+hashed+decoded 78MB of unit payloads just to extract the tiny throws fragments it seeds from. Profiled: 70% gate, 20% unit loads.The fix (adversarially reviewed design)
The naive fix (compare throws in "fact space") was killed in adversarial review — it was either circular or unsound (alias-retarget hole). What's implemented is the reviewed survivor:
ManifestFilecarries a verbatim copy of each unit's throws-fragment blob; seeding reads that. A no-op invocation opens zero unit files. Units load (in parallel, order preserved) only when a delta means relink/re-emit will need them.package_itemsderivation (still needed on edits and for package-level diagnostics) is a parallel fold, not a serial parse.bex_cache: unit content-key is derived from the integrity digest entry-validation already computed — one hash pass per unit instead of two.Measured
run/packthen hit in ~0.7 s at 295k LOC)Risk analysis (what to scrutinize)
dirty == added == removed == ∅) reuses the same dirty-partition logic every warm compile already trusts; a bug there was already a soundness bug before this PR. The new failure surface is manifest-fragment drift from the unit copy — mitigated by copying from the same in-memory bytes at store time, and by re-pointing both verify oracles (fullBAML_CACHE_VERIFYcompare + always-on 1-in-32 sampled re-derive) at the manifest copy, i.e. exactly what is served.Verification run
BAML_CACHE_VERIFY=1honest-recompile byte-compare: PASS on no-op and after-edit.BAML_CACHE_SAMPLED_VERIFY=1forced on 7 consecutive warm invocations across edits: PASS.🤖 Generated with Claude Code
https://claude.ai/code/session_01V4oee6wrCHzXH6mM2h8eJo