Skip to content

tests: assert growth, not wall-clock ceilings - #54

Closed
twmb wants to merge 12 commits into
mainfrom
wallclock-ratio
Closed

tests: assert growth, not wall-clock ceilings#54
twmb wants to merge 12 commits into
mainfrom
wallclock-ratio

Conversation

@twmb

@twmb twmb commented Aug 13, 2026

Copy link
Copy Markdown
Owner

The DoS and cost batteries bound absolute wall-clock time, so they red under
machine load rather than on regression. Three different tests failed that way
during recent work, all on correct code, each costing time to dismiss:

  • TestDoSBattery_C9_CustomTypeParseCost — 614ms against a 400ms bound under
    parallel package execution; 0.22s total when run alone.
  • TestDoSBattery_C10b_FieldLookupBreadth — 882ms against 500ms, at a baseline
    measurement before any edit.
  • TestInvariant_SharedSchemaNodeWalkedOnce — 675ms at factor 26 vs 1.49ms at
    13, traced to memory pressure from unrelated processes.

go test ./... runs packages concurrently up to GOMAXPROCS, so the root
package competes with ocf. CI is a 2-core runner, which is exactly that case.

What changed

These tests exist to catch a cost going superlinear. An absolute ceiling is a
poor instrument for that and a good instrument for measuring machine load. A
ratio between two problem sizes is immune to contention, because load inflates
both measurements together.

C9 already drove two sizes for this reason — its comment says so: "Doubling
separates them — linear doubles, quadratic quadruples." The ratio was already
the thing being reasoned about; the assertion just wasn't written that way.

68 of 72 assertions converted to growth ratios. 4 kept as absolutes, each
documented at its site: dosBudget, hangDeadline, the infinite-recursive-
default cell (no magnitude exists — the recursion is infinite by construction),
and the hostile-extras hang probe. A test measuring "does this finish at all"
is a legitimate absolute; one measuring "is this linear" is not.

The two sides of each ratio are measured in alternating rounds, so drift during
the measurement cancels rather than accumulating on one side.

wantAcceptUnder has no callers left and is gone. Its name stays in the two
source-scan vocabularies, so reintroducing it is still classified as a timing
cell.

No bound was raised

Raising bounds is the band-aid these tests were written to avoid, and it makes
them measure less. 21 neuters cover all 26 cells; every converted cell reds,
16x–3478x. The neuters were re-run after each estimator change, which caught
two cells whose limit their own regression could not reach — both retuned.

A timeout hazard found and fixed

Measuring at multiple problem sizes makes these tests slower. An earlier form
of this change took the raced root package to 598.3s against Go's default
600s timeout
, which CI relies on implicitly — a flakiness fix that would have
become a hard CI failure. Dropping the round floor to 2 and widening the
breadth span to 8x took the converted batteries from 98s to 32s raced.

Where this does not reach

Green under CPU contention: 6x oversubscription at GOMAXPROCS=2, and 3/3 on
repeated package-parallel runs.

NOT green under sustained memory-bandwidth starvation. The two sides hold
working sets differing by the magnitude, cache is not shared proportionally,
and a 64-MiB-per-core streaming antagonist inflates breadth ratios from 9.6–12
to 22–38. Widening the span does not escape it — it is a factor, not an offset.
Under that same antagonist the old ceilings fail harder (5–7 cells vs 2–6), and
fail as a bare "took 1.5s" where a ratio prints both measurements. Recorded in
the harness comment and the ledger rather than tuned until green.

Gates

Source files byte-identical to base — test files and BUG_AUDIT.md only.
Test-name set identical in all three build configurations. Suite green.
-race 318.7s, zero races. gofmt, vet clean.

twmb added 12 commits August 13, 2026 09:52
A cell that claims a cost is linear, or flat, or anything but the quadratic
its bound exists to catch, has been stating that claim as an absolute
wall-clock ceiling. A ceiling measures two things at once — the cost and the
machine — and cannot tell them apart. `go test ./...` runs packages
concurrently up to GOMAXPROCS, so this package's timed cells share the host
with ocf's work, and the two-core CI runner is exactly that case; it has been
lucky, not immune. Three cells have reported red on correct code that way:
614ms against a 400ms bound, 882ms against 500ms, and 675ms out of a memo
that was working while unrelated processes took the machine's memory. Every
one of them cost the time to prove it false.

A ratio between two problem sizes does not have that failure mode, because
contention inflates both measurements together and divides out. It is also
the claim actually being made. C9's own comment already says so — "One value
cannot tell the memo from its absence... Doubling separates them: linear
doubles, quadratic quadruples" — and then asserts a ceiling anyway. A
ceiling generous enough for the linear cost at n is generous enough for the
QUADRATIC cost at some smaller n, so one point only ever asks whether that
one size finishes.

costScale is a column's claim: two magnitudes and the largest ratio between
them that leaves the claim standing. Their SEPARATION is what buys margin —
at hi = 4*lo a linear pass lands at 4 and a quadratic one at 16, so a
tolerance between them is two-sided by a factor of two, where at hi = 2*lo
the same classes are 2 and 4 and every choice is within 1.4x of being wrong.
Each size is measured best-of-three, because contention and GC can only
inflate a sample and never deflate one, so the minimum converges on the real
cost from above while a mean chases the load. The floor below which a ratio
is not believed is the one wall-clock number left, and it can only ever
ADMIT a cell, never fail one; it is inflated proportionally under -race for
the reason raceInflated already states.

wantAcceptScales and wantRejectScales carry the verdict as well as the cost,
so a converted cell keeps asserting what it asserted before. build takes the
magnitude and returns the thunk to time, which is what keeps schema
generation and parsing out of the timed region — the same split
wantCostDoesNotScale documents, and for the same reason.

Two absolutes stay, and say why at their sites: dosBudget and hangDeadline
ask whether work RETURNS, and a hang has no second measurement to take a
ratio against.
Every cell of the schema-breadth battery drove ONE sibling count, 20000, and
compared the result to an absolute ceiling. The claim each of them makes is
that a pass over the siblings stays LINEAR in their count, which is a
statement about how cost responds to that count and which one size cannot
make: a ceiling generous enough for the linear cost at 20000 is generous
enough for the quadratic cost at 5000.

The column's own header said ratios were the wrong instrument — "a ratio
between two sizes is noise-sensitive on a loaded host". It is the other way
round. TestDoSBattery_C10b_FieldLookupBreadth reported 882ms against its
500ms ceiling on correct code, during a run that shared the machine with
package ocf; the ratio it would have reported instead is 4.35x, because
whatever slowed the 20000 measurement slowed the 5000 one with it.

So all 33 cells now drive 5000 and 20000 and assert the ratio between them.
Measured, every cell that is linear in the sibling count lands between 3.83
and 4.78 against a permitted 8, and every cell that does not touch the
siblings at all lands at 1.0 — a separation the absolute ceiling could not
show, since both classes simply sat under it.

The second ceiling constant is gone rather than retuned. breadthParseBound
existed because the two text-parsing cells cost ~140ms and ~300ms of honest
linear work at this size and a 500ms ceiling left them inside 1.7x of it, so
a busy host crossed the line; their growth is 3.97 to 4.68 whatever the host
is doing, so one scale now serves the whole column.

Two mechanical consequences. The cells that used to build one fixture and
time eleven or seventeen calls against it now need one per size, so the
fixture is a memoized accessor — the parse, the datum and the three wire
forms are what the magnitude needs and not what any of these bounds owns,
and they stay outside the timed closures where they were. And the twin's
canonical inequality, which keeps the resolve cells off Resolve's
short-circuit, is checked at BOTH sizes: a twin that diverges only at the
larger one would leave the smaller cell timing the short-circuit, and that
cell is the denominator of every ratio the kind reports.

The two source-derived guards learn the new vocabulary: the entry-point
regex so the breadth column is still visible to the coverage invariant, and
the cost-cell harness list so an exemption cannot sit on a timing cell by
preferring the ratio form.
C9's comment already contained the argument. "One value cannot tell the memo
from its absence: the cost this bound caps is quadratic without the memo, so
a single length only asks whether that length finishes... Doubling separates
them — linear doubles, quadratic quadruples." It then compared each length to
its own absolute ceiling and never to the other. The ratio was the thing
being reasoned about and was the one thing not asserted; this cell was also
one of the three that reported red on correct code, at 614ms against a 400ms
ceiling while the machine was busy.

The chain arms now read the ratio. Their two lengths move from 3000/6000 to
1500/6000, because a 2x separation was enough when it only had to make two
ceilings differ and is the worst usable one when it IS the instrument: at 2x
a linear cost lands at 2 and a quadratic at 4, so every tolerance is within
1.4x of being wrong. At 4x they are 4 and 16. Measured 3.85 to 4.63 across
runs, and 4.50 to 4.65 under -race, against a permitted 8.

C9's third arm needed its magnitude rethought rather than doubled. It drives
references to a large inherited type, and the shapes it separates are
O(refs + nodes) with per-parse sharing and O(refs x nodes) without. Growing
the reference count alone makes both of those linear in the driven
magnitude, so their ratios are identical and the comparison measures
nothing; the same is true of growing the type's size alone. Only growing
them together separates the classes, so the arm drives one magnitude across
both axes and says why. The cache warm-up that puts the inherited type in
place moved out of the timed region while it was being rewritten: what this
bound owns is the per-reference walk of an already-cached subtree.

C1's stray-key cells pinned depth 1000 under a 300ms ceiling, for a defect
whose shape is O(2^depth) and whose near miss is a quadratic re-validation.
They drive 125 and 1000. Eight and not four because these calls run in
hundreds of microseconds, and at that scale a linear cost does not land on
its nominal ratio — allocation and GC grow with the tree and put the honest
number up to 1.7x above it, reproducibly, on an idle machine. At 4x that
band reached 6.9 against a nominal 4 and left nowhere to put a tolerance
below the quadratic's 16. At 8x it is 7.8 to 12.1 against a nominal 8, and
the quadratic is 64.

Two supporting changes. measureCost samples adaptively — at least three, then
while the size has cost under 20ms, up to twenty-five — because the number
of samples a minimum needs depends on how expensive the call is, and it was
the sub-millisecond cells whose ratios wandered. And costScaleFor turns a
registry row into a growth claim, so a cell asserting its own ratio reads the
same magnitudes and the same tolerance as one handing the measurement to
wantCostDoesNotScale.
TestRegression_DeepValidSchemaParsesLinear and
TestRegression_RootSchemaEmitterLinearOnDeepNesting each pinned ONE depth,
900, against an absolute ceiling. Linear is a claim about how cost responds
to the depth, and a ceiling at one depth cannot make it — a quadratic parse
of a shallower schema passes the same 200ms. The suite already states this
rule for its reaching-path cells, in the doc comment of the invariant that
enforces it: "One value asks only 'does this finish?', which a cost merely
linear in the factor also answers." These two were outside its reach because
their magnitudes are inline string builders rather than named generators.

Both now drive 100 and 800, and both stay under the build's maxDepth, which
is what keeps them on the accept path they are about. The parse cell measures
7.66 to 8.85 for the 8x depth increase and the canonical cell 9.18 to 9.23,
against a quadratic's 64.

The emitter cell's two classes needed measuring rather than deriving, so
they were measured. Restoring the eager per-named-type snapshot — the
regression the cell commemorates — takes it from 1.9ms/8.3ms to 53ms/1.14s,
which is a ratio of 21.7 against the single-pass 4.2 to 4.9. It is 4.2 and
not 8 because the leaf's fixed 256 KiB doc is a large constant in one
emission; the tolerance is the geometric mean of the measured pair, so each
is about a factor of two away.

Both cells got their floors sized against their own cost rather than the
battery's. These calls are cheap — 125us and 9.5us at the shallow depth — and
a floor sized for the heavier cells would sit above the ratio's limit and
decide the cell by itself, which is a wall-clock ceiling wearing the floor's
name. At 200us and 5ms the ratio decides both, with the healthy cost 2.8x
under its limit.

The fixture each cell does not own moved out of its timed call while it was
being rewritten: the parse feeding Canonical, and the parse plus Root() walk
feeding the emitter. Both are driven at both depths by the cells that do own
them.
Nine cells here timed one magnitude against a wall-clock ceiling, and every one
of them makes a claim about how cost RESPONDS to a magnitude: O(n) byte scan,
O(M(scale)) derivation, a length cap applied before the expensive call,
"linear-ish" parse. One point cannot make that claim. It is worse than
uninformative — a ceiling that clears the honest cost at the top of a range
clears the amplified cost somewhere below it, and for these caps every value
below is reachable input, so the ceiling admits exactly the attack the cell is
named for.

All nine now drive two magnitudes and assert the ratio, and where the classes
being separated were not obvious the numbers are MEASURED rather than reasoned
about, by restoring the regression:

  - finiteScale's BitLen short-circuit costs 3.8us at scale 1024 and 963us at
    the cap, a ratio of 251 to 298; the divide-by-5 loop it replaced costs
    223us and 777ms, a ratio of 3478.
  - truncRatForError's guard costs 1.9us and 8.9us across its range; removing
    it to let RatString materialize the value costs 3.5ms and 89ms. That
    comparison also showed the message-length assertion alone cannot catch it,
    since the amplification is in CPU and allocation and the message stays
    bounded either way — the second magnitude is what makes the cell able to
    fail.

Three consequences worth naming.

Two cells SKIPPED their cost assertion entirely under -race, each saying a
fixed budget is not deterministic there and that relaxing it is "a band-aid
that still flakes under contention". That is right about a budget, and is the
reason both are ratios now; a ratio holds in both modes, so both cells assert
cost in every configuration rather than only where a wall-clock number
happened to behave. The race-relaxation registry records the two consults
going away.

The CPU-cost sentinels also assert their VERDICT now. Every one of the six
discarded its error, so a cell that started failing instantly for an unrelated
reason read as a pass. Two of them turned out to sit on a boundary worth
writing down: the JSON exponent cell accepts at the cap and rejects one past
it, and the big-decimal JSON reject arm never reaches the rendering it was
grouped with at all, because DecodeJSON caps the exponent before any *big.Rat
exists. Both are now stated at the site rather than implied by a discarded
error.

One cell needed a wider range than the rest, and it is the one place a ratio is
not automatically load-immune. The over-cap denominator cells hold working sets
that grow with the magnitude, and cache is not shared proportionally: at an 8x
range the honest ratio is 4.8 idle and reached 11 during a full-suite run,
against a regression at 16. At 64x they are 46 (54 under deliberate
whole-machine load) and 660. Several other big-integer cells took the same
treatment for the same reason — their honest cost is superlinear by
construction, so the class they separate is superlinear against quadratic, and
only a wide range puts distance between those.

Every ratio above was re-measured with all cores saturated by a mixed
CPU-and-memory-bandwidth load, which is what package-parallel `go test ./...`
looks like to a timed cell. The largest movement in any of them was 1.2x.
Three cells, three shapes, and the third one shows why the rule is worth
applying even where a cell looks like it already followed it.

The deep-nesting reject pinned depth 50000 under a 2s ceiling that was skipped
under -race, so in that mode nothing measured its cost at all. Driven at two
depths it reports something a single depth cannot show: the ratio is 1, not 8.
The linear pre-scan stops at bracket 4001, so depth stops mattering entirely
once it is past the limit — which is the property, stated exactly. The
quadratic unmarshal it replaced ran over the whole input before the guard
fired, and would be 64 across the same range.

The fixed-logical probe pinned one size, the top of the int64 range, under a
100ms ceiling. Two sizes make its claim — "must not allocate proportional to
that size" — directly, and the answer is again flat: the probe is capped at
maxFixedLogicalLen+1 whatever the schema declares, so 2^50 and 2^62 cost the
same (0.98). Twelve doublings rather than the usual three, because an
allocation that did follow the size would be 4096x across them. The panic
probe beside it stays absolute and says why: make([]byte, 9e18) either returns
or takes the process down, and neither has a second measurement to divide by.

The bare-union backtrack cell was already driving two depths, 20 and 200, and
comparing each of them to the SAME 500ms ceiling — never to each other. That
is two point budgets, not a scaling guard, whatever the second one was called,
and it is the exact shape the task of this branch is about: the ratio was
sitting right there unasserted. It reports 4.9 and 8.1 for an 8x depth
increase, against a backtracking decoder that is 2^200 at the high end and
would never return from it.
Six cells across four files, and the last of them is the one the whole rule is
for.

The hostile-SIZE rejection matrix pinned 1 MiB across fifteen encode arms and
claimed "no superlinear work before the type check" — about the size, which was
the one thing it never varied. Every arm now builds its value from a
caller-chosen size. One consequence had to be designed around: both sizes must
sit ABOVE every length cap the arms can reach, because straddling a cap does not
measure a complexity class, it measures the cap. At 128 KiB the decimal arm is
under boundedRatFromString's limit and pays a 29ms conversion; at 1 MiB it is
over and rejects in a microsecond; the "ratio" between those is 0.00 and the
limit it computes is 731ms, which is an arm asserting nothing. Above every cap
the fifteen divide cleanly at 1.0, 8.2, and a hypothetical 64.

The escaped-length scan cell had proven its property deterministically — the
returned value is identical across a 8192x input range — and then timed one 32
MiB string under a 2s ceiling beside it. Driven at two sizes the walk reports
1.02: past the budget it abandons at the same byte whatever it was handed, so
the input stops mattering entirely. That is a stronger statement than the
ceiling made and it is the property's actual shape.

The embed-diamond cell had the ratio written down and unasserted. Its registry
row has always carried a scaleTol; the cell logged the measured ratio, asserted
a 2s absolute ceiling, and left the declared tolerance enforcing nothing. Asking
it exposed that the depths were too close together to ask with: at 8 and 12 the
honest ratio wanders between 9.7 and 25 against a nominal 16, because a walk
this allocation-heavy is judged partly by the garbage collector, and a tolerance
with room for that band has none left under a 3^depth degradation. At depths 4
and 12 the nominal is 256, the measurement is 205 to 255 across runs, and the
degradation is 6561.

That instability also produced a harness fix worth having: the two sides of a
ratio are now sampled the SAME number of times, taken from the expensive side. A
minimum converges from above as samples accumulate, so 25 samples on the cheap
side against 5 on the dear one inflates the ratio by the difference rather than
by anything the code did.

The OCF empty-block cell pinned 10,000 blocks under a 10s ceiling, which is a
hang detector wearing a cost assertion's clothes: a reader that fails to advance
past a count-0 block never returns and is caught by the test binary's own
timeout, while a reader that rescanned from the file's start per block — the
quadratic near miss — walks 10,000 blocks well inside 10s and passed. It reports
7.81 for an 8x block count. The growth harness is copied into package ocf rather
than bridged, for the same reason dosRun already is: the core package's copy
lives in its own _test.go files and no import reaches it from another test
binary.

And the infinitely-recursive-default cell KEEPS its absolute bound, with the
reason written at the site. It has no magnitude to drive: its schemas are fixed
text, the recursion in each is infinite by construction, and the bound under
test is production's own maxDepth rather than anything the input carries. There
is no bigger version of "does not terminate" to measure a second time. The clock
asks whether Parse returned, which is the whole property, and the two outcomes
it separates are milliseconds and a dead process.
Both came out of running the neuter — removing the bound the cell guards and
requiring it to red — which is the only way to find a cell whose limit is
above what its regression costs.

The fixed-logical probe cell drove 2^50 and 2^62 under a floor of 500us. With
the clamp removed the probe at 64 MiB costs 240us, so the ratio it computed
was 9.9 and its limit was the floor, and the only thing that failed was the
panic probe beside it. Two changes make the ratio itself able to fail: the
sizes move to a range an unclamped probe can actually ALLOCATE (64 KiB to 64
MiB, where the removal measures 31x against a permitted 4), and the floor
moves to 50us, which is ten times the healthy cost rather than a hundred times
it. The growth half now runs first, so a build with the clamp removed reports
the cost it can still measure before the other half reports the panic; at 2^63
an unclamped make() does not run slowly, it panics, and that half of the claim
is what the panic probe was always for.

The hostile map-key cell had a tolerance of 25 against a claim of O(n). Its
measured answer is better than the claim — a hostile key is rejected on its
FIRST byte, so cost is FLAT in the key's length, 1.0 to 1.04 across runs — and
25 would have admitted an implementation that walked the whole megabyte before
deciding, which is the regression the cell is about. At 6 it admits the
measured spread and nothing else; making isJSONNumber walk the key before its
first-byte check now reds it at 7.9.
A ratio between two problem sizes is immune to host load because contention
inflates both measurements and divides out. That is true of the QUANTITY and
not automatically of the way it is measured, and running the converted battery
against a deliberately oversubscribed host — twenty-four spinning and
memory-streaming workers, GOMAXPROCS=2 — found three ways the measurement gave
the property away. All three are sampling defects, none is visible on an idle
machine, and none would have been found by reasoning about it.

Sampling the two sides in PHASES. A quiet window on a contended machine is a
stretch of TIME, so measuring one side to completion before starting the other
lets a quiet stretch fall entirely inside one side's phase and lower that
side's minimum alone. It put a 4x linear cost ratio at 24.

Combining two independently-taken minima. Interleaving alone is not enough,
because the sides do not suffer contention equally: a working set that grows
with the magnitude does not share a cache proportionally, so under
memory-bandwidth pressure the hi side inflates more, and pairing each side's
best-ever sample pairs a lucky low with an unlucky high that never coexisted.

Unequal sample counts. A minimum converges from above as samples accumulate,
so twenty-five samples against five inflates the ratio by convergence alone —
the SchemaFor depth cell moved between 14 and 20 for a cost that had not
changed.

measureCostPair answers all three: it alternates hi and lo in ROUNDS and
reports the pair from the round whose ratio was smallest, which is two
measurements made microseconds apart under one machine state. The downward
bias is the safe direction and not a loophole — the growth being hunted is a
property of the code, present in every round, and the neuters measure 16x to
3478x in the quiet rounds too. Under the same oversubscribed host the whole
converted battery is now green, with every breadth-column ratio at or under
9.7.

Re-running all twenty-one neuters against the new estimator caught the one
cell it did weaken. The hostile map-key cell is FLAT (1.0), its neuter
measures 7.9, and at a tolerance of 6 a single round whose lo side caught a GC
pause was enough to bring the neutered build in under the limit. At 4 it reds
three times out of three.

wantAcceptUnder is gone with its last caller. Every one of its thirty-eight
call sites was a cell pinning a complexity class on the accept path, which is
the one thing an absolute ceiling cannot express; the absolutes that remain
are watchdogs asking whether work RETURNS, plus one cell with no magnitude to
drive, and none of them is an accept-under-a-ceiling. The NAME stays in the
two source-derived vocabularies that classify timing cells, so a helper
reappearing under it is recognised as one immediately rather than a round
later.
Three superseded ledger lines, verbatim, plus the round's full narrative.

AUDIT_CORE.md stood at 54,620 bytes against its 55,000 bound — under by 380,
which this round's own ledger line would have crossed on the spot. That is the
case §Feedback loop names when it requires a distillation pass to "leave room
for the round's OWN ledger line, or it re-crosses on the spot", so the pass ran
first: the three lines superseded by the 2026-08-06 convergence anchor compress
to one, their text moving here unchanged.

The narrative is here rather than in the ledger because the format says so —
"a few wrapped lines at most", full narrative verbatim to the archive. It
records what the ceilings had been hiding, which is the part a later round
could not reconstruct: several cells turned out to be STRONGER than their
comments claimed, and only a second problem size could show it. The
deep-nesting reject is flat rather than linear, because the pre-scan stops at
bracket 4001 and depth stops mattering past the limit. The escaped-length walk
is flat past its budget. The fixed-logical probe is flat across a thousandfold
size range. A hostile map key is rejected on its first byte rather than
scanned. Each of those tolerances was then tightened to the measured answer,
which is what makes them able to fail at all.
`go test -race ./...` gets Go's default 600s per-package timeout — CI passes no
-timeout — and the root package already ran 522s there before any of this. The
converted battery took it to 598s, which is a CI failure with 1.7s to spare and
would have been one on the two-core runner it is aimed at. The suite's own
ledger has been carrying this as an open gap since a round that hit the
timeout, passed on the retry at 586s, and wrote it down.

Two changes, and the second is the better instrument as well as the cheaper one.

The round floor drops from three to two. Two adjacent rounds already give the
minimum-of-rounds its meaning, and cheap cells still collect up to twenty-five
from the wall-clock budget, which is where more rounds actually change the
number — an expensive cell's own size averages the noise out. Under -race the
budget is spent by the first round anyway, so the floor WAS the sample count
there, and a third of every growth cell's raced time went with it.

The breadth column's span widens from 4x to 8x. That halves its lo-side cost,
since the small call is now an eighth of the large one rather than a quarter,
and it separates the classes better: at 4x a linear pass is 4 and a quadratic
one 16, leaving a tolerance a factor of two on each side, where at 8x they are
8 and 64. The measured numbers say the same thing. Healthy cells land at 9.6 to
12.1 against a permitted 22, and restoring the quadratic union-tag ambiguity
scan measures 43.5 — where at the 4x span the same neuter measured 11.9 against
a permitted 8, which is 1.5x of margin on the side that matters.

The hostile-size matrix moves from 1-8 MiB to 0.5-4 MiB, still above every
length cap its arms can reach, which is the only property those two magnitudes
have to have.

Raced, the five converted batteries go from 98s to 32s against a 24s baseline.
Unraced the whole suite is 44.6s against 39.0s. All nine neuters whose
tolerances moved were re-run and all nine still red.
The claim this branch rests on is that a growth ratio divides host load out.
That is true of CPU contention and it is the case the change exists for:
pinned to GOMAXPROCS=2 with the machine six times oversubscribed, every
converted cell holds, and repeated package-parallel runs at GOMAXPROCS=2 —
the shape of a two-core runner executing `go test ./...` — hold too.

It is not true of memory-bandwidth starvation, and pretending otherwise would
leave the next person to discover it during an investigation, which is the
failure mode this whole branch is about. The two sides of a growth cell hold
working sets differing by the magnitude itself, and cache is not shared
proportionally, so an antagonist streaming 64 MiB buffers on every core
inflates the measured ratio by about 3x: the breadth column's 9.6-12 becomes
22-38. Widening the span does not escape it — the inflation is a factor, not
an offset, and at a 16x span the same antagonist turns 19-23 into 67-74 — so
no choice of two magnitudes makes it go away.

The comment says so, with the numbers and the two reasons it is still the
better instrument: the ceilings fail harder under the same antagonist (five to
seven cells against two to six, same code), and they fail as a bare "took
1.5s" where a growth cell prints both measurements and the sizes they were
taken at.
@twmb twmb closed this Aug 13, 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