runtime: allocation accounting — deterministic signal for complexity gating #131

Merged
will.anderson merged 1 commits from feat/alloc-accounting into dev 2026-08-16 02:22:14 +00:00
Owner

Implements the three primitives the test-framework design (§6.5) needs to gate on growth curves: el_alloc_count, el_alloc_bytes, el_peak_rss. Registered in builtin_arity, wrapped in el_seed.c.

Why counts, not wall-clock. A growth-curve gate must be a hard build failure, so the signal can't flake. Wall-clock needs warmup, statistics, and a quiet machine — unusable as a CI gate. Allocation counts are perfectly deterministic: same input, same number, every machine, every run.

Why bytes as well as count — this is the whole gate, not redundancy. Measured with two El programs, one allocating once per item, one rebuilding its accumulator each iteration:

n linear allocs / bytes quadratic allocs / bytes
100 100 / 290 100 / 5,150
200 200 / 690 200 / 20,300
400 400 / 1,490 400 / 80,600
800 800 / 3,090 800 / 321,200

The quadratic program's allocation count is exactly linear — identical to the healthy one. Counting allocations alone would have missed it entirely. Bytes catch it: each doubling of n quadruples bytes (3.94 → 3.97 → 3.99, converging on 4.0 = O(n²)) while the linear case converges on 2.0.

That shape — count linear, per-allocation size growing — is the classic accidental quadratic, and it's exactly elc's defect: quadratic allocation volume, paid as 27 GB RSS by the old binary and as 42s of malloc/free churn by the rebuilt one. Volume was the invariant; RSS and wall-clock were just how it surfaced.

el_peak_rss is exported for context and explicitly not a gating signal — perturbed by allocator internals, page cache, and OS.

Counters are unsynchronised by design: this is measurement, and a lock would change what's being measured. Exact on the single-threaded compile path.

Implements the three primitives the test-framework design (§6.5) needs to gate on growth curves: `el_alloc_count`, `el_alloc_bytes`, `el_peak_rss`. Registered in `builtin_arity`, wrapped in `el_seed.c`. **Why counts, not wall-clock.** A growth-curve gate must be a hard build failure, so the signal can't flake. Wall-clock needs warmup, statistics, and a quiet machine — unusable as a CI gate. Allocation counts are perfectly deterministic: same input, same number, every machine, every run. **Why bytes as well as count — this is the whole gate, not redundancy.** Measured with two El programs, one allocating once per item, one rebuilding its accumulator each iteration: | n | linear allocs / bytes | quadratic allocs / bytes | |---|---|---| | 100 | 100 / 290 | 100 / 5,150 | | 200 | 200 / 690 | 200 / 20,300 | | 400 | 400 / 1,490 | 400 / 80,600 | | 800 | 800 / 3,090 | 800 / 321,200 | **The quadratic program's allocation count is exactly linear — identical to the healthy one.** Counting allocations alone would have missed it entirely. Bytes catch it: each doubling of n *quadruples* bytes (3.94 → 3.97 → 3.99, converging on 4.0 = O(n²)) while the linear case converges on 2.0. That shape — count linear, per-allocation size growing — is the classic accidental quadratic, and it's exactly `elc`'s defect: quadratic allocation **volume**, paid as 27 GB RSS by the old binary and as 42s of malloc/free churn by the rebuilt one. Volume was the invariant; RSS and wall-clock were just how it surfaced. `el_peak_rss` is exported for context and explicitly **not** a gating signal — perturbed by allocator internals, page cache, and OS. Counters are unsynchronised by design: this is measurement, and a lock would change what's being measured. Exact on the single-threaded compile path.
will.anderson added 1 commit 2026-08-16 02:22:00 +00:00
runtime: allocation accounting — the deterministic signal for complexity gating
El SDK CI - dev / build-and-test (pull_request) Failing after 12m7s
37bcf7eb74
Implements the three primitives the test-framework design (DESIGN.md §6.5)
requires for gating on growth curves: el_alloc_count, el_alloc_bytes,
el_peak_rss. Registered in codegen's builtin_arity and wrapped in el_seed.c per
the project's C-builtin recipe.

WHY COUNTS AND NOT WALL-CLOCK: a growth-curve gate has to be a hard build
failure, which means the signal cannot flake. Wall-clock needs warmup,
statistics, and a quiet machine; on shared CI it is unusable as a gate.
Allocation counts are perfectly deterministic — same input, same number, every
machine, every run. Fit them against n and a complexity regression becomes a
build failure with zero noise.

All four runtime string allocators (el_strdup, el_strbuf, and their _persist
variants) funnel every allocation the language performs, so instrumenting there
counts everything.

WHY BYTES AS WELL AS COUNT — this is not redundancy, it is the whole gate.
Measured with two El programs, one allocating once per item, one rebuilding its
accumulator each iteration:

    n     linear allocs / bytes      quadratic allocs / bytes
    100        100 /    290               100 /   5,150
    200        200 /    690               200 /  20,300
    400        400 /  1,490               400 /  80,600
    800        800 /  3,090               800 / 321,200

The quadratic program's allocation COUNT is exactly linear — identical to the
healthy one. Counting allocations alone would have missed it completely. Bytes
catch it: each doubling of n quadruples bytes (ratios 3.94, 3.97, 3.99 ->
converging on 4.0, i.e. O(n^2)), while the linear case converges on 2.0.

That shape — count linear, per-allocation size growing — is the classic
accidental quadratic, and it is exactly elc's defect: quadratic allocation
VOLUME, which the old shipped compiler paid in RSS (27 GB, OOM) and the rebuilt
one pays in malloc/free churn (42s on 1.4 MB). Volume was the invariant across
both; RSS and wall-clock were just the two ways it surfaced.

el_peak_rss is exported for context and is explicitly NOT a gating signal — it
is perturbed by allocator internals, the page cache, and the OS. Gate on the
deterministic numbers; report the physical one.

Counters are unsynchronised by design: this is measurement, and a lock would
change the thing being measured. Exact on the single-threaded compile path,
approximate under threads.
will.anderson merged commit cb1f2a74af into dev 2026-08-16 02:22:14 +00:00
Sign in to join this conversation.