Skip to content

Add the allocation gate: alloc-diff and assert --max-alloc-mb - #215

Merged
jantimon merged 1 commit into
mainfrom
feat/alloc-gate
Aug 2, 2026
Merged

Add the allocation gate: alloc-diff and assert --max-alloc-mb#215
jantimon merged 1 commit into
mainfrom
feat/alloc-gate

Conversation

@jantimon

@jantimon jantimon commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Stacked on #213 (assert --format json). Review only this PR's own diff; merge from the tip of the stack. The alloc gate is wired through #213's AssertView, hence the stack.

Problem

A GC-pressure regression that allocates hard but costs little CPU passed every wpd gate. In the dogfood, cpu-diff caught a +61% allocation regression only by accident (building the array also cost CPU); a pure large-buffer / retained-churn regression sails through counts, timing, slice, and CPU gates alike.

Change

Two owed surfaces, mirroring the existing CPU gate:

  • alloc-diff <baseline> <current> --fail-on-regression gates on net allocated bytes (the allocation analog of cpu-diff's net JS self-time), with per-package and per-function byte movers, a comparability refusal across an incompatible workload/lane/capture, and an AllocDiffResult under --format json|toon.
  • assert --max-alloc-mb <mb> gates the total sampled MB against a budget.

Both read a --target node --alloc recording's sidecar .alloc.json. A recording with no alloc model (a chrome or node-cpu capture) is a loud n/a-FAIL, never a silent pass; a run-group (no --alloc member exists) is an n/a-FAIL too.

The noise floor (measured, and consistent with the cpu-diff fix)

Sampled byte totals are the directional tier (~10-20%), so the gate floor is percentage-led, the same two-term shape as cpu-diff: net must clear max(--noise-floor MB, --noise-pct% of the baseline), default max(1 MB, 25%).

[measured, --target node --alloc, gate lock held] on a ~14 MB workload (examples/probes/allocates.mjs at --iterations 20, same module path both sides):

identical-code net +50% regression net
relative p50 6%, p95 15%, max 20% +33..78%

25% sits above the 20% identical-code ceiling with margin. Verified through the built CLI's exit code:

  • identical pairs: 0/90 false-red (100% green)
  • +50% regression pairs: 80/80 caught (100%)
  • 1 MB absolute floor covers a tiny-allocation workload where 25% would be a fraction of a sample; --noise-floor / --noise-pct tune each term.

Scope

  • src/commands/allocdiff.ts (new), src/model/query.ts (AllocDiffResult + delta types), src/cli.ts (alloc-diff + --max-alloc-mb), src/commands/assert.ts (the alloc axis, n/a-FAIL without a model), src/index.ts + README type table + public-types walker.
  • Docs: allocation-profiling.md gains a gating section with the measured floor; facts.md pins the 25% default; README CI-gating section gains the alloc gate + a baseline-management paragraph (where baselines live, what is lean to commit, the re-record-on-upgrade rule).
  • Tests: test/unit/alloc-diff.test.mjs (6: identical, scaling floor, +50% gate, absolute-floor, knob, comparability refusal) and 2 assert alloc-axis tests (gates the sibling total; n/a-FAIL without a model).

Gates: lint, format:check, build, unit (753), measurement (6), the 3 Chrome assert e2e tests all green.

Replaces #214, which was auto-closed during a botched restack after its base (#213) squash-merged; this is the same change rebased onto main with the README gating paragraph merged against #212's noise-floor text. Unit suite green on the restacked head (759 pass).

A GC-pressure regression that allocates hard but costs little CPU passed every
gate before -- cpu-diff caught the dogfood's +61% allocation only by accident,
because building the array also cost CPU. A pure large-buffer churn sails through.

alloc-diff <baseline> <current> --fail-on-regression gates on net allocated bytes,
mirroring cpu-diff (per-package + per-function byte movers, comparability refusal,
AllocDiffResult under --format json|toon). assert --max-alloc-mb <mb> gates the
total against a budget. Both read a --target node --alloc recording's sidecar
model; a recording with no alloc model is a loud n/a-FAIL, never a silent pass.

The gate floor scales with the workload, the same shape as cpu-diff:
max(--noise-floor MB, --noise-pct% of baseline), default max(1 MB, 25%). Sampled
byte totals are ~15-20% directional (measured p95 15%, max 20% on a 14 MB
workload), so 25% clears the noise ceiling: 0/90 identical pairs false-red,
80/80 +50% regression pairs caught. --noise-floor/--noise-pct tune each term.
@jantimon
jantimon merged commit 85054d1 into main Aug 2, 2026
4 checks passed
@jantimon
jantimon deleted the feat/alloc-gate branch August 2, 2026 15:25
@github-actions github-actions Bot mentioned this pull request Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant