You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bench: asyncpipe@120 sits exactly on a nursery trigger boundary and is not a usable A/B target #7926
asyncpipe at its shipped size (120 batches) sits exactly on a nursery collection trigger boundary, which makes it unusable as an A/B target and makes any change touching allocation volume look arbitrarily good or bad.
Measured while landing #7924 (Promise.all fast arm):
The fast arm allocates strictly less, and the entire regression is one extra copying minor, decided by a 444 KB window on 17.5 MB of arena in-use. With the nursery cap inactive the same change on the same program is −9.0%.
At larger sizes the change is unambiguous: @240 −33.9%, @360 −34.3%, @480 −30.7%, with identical GC-cycle counts in both arms.
Why this is a benchmark defect and not a #7924 defect
A workload poised on a trigger boundary measures the boundary, not the program. Any future change that moves allocation volume by a fraction of a percent will flip the cycle count and swamp its real effect — in either direction. That is the same hazard as the constant-band pacing bugs (#7592, #7690): a threshold that a workload sits exactly on.
So the trigger this benchmark straddles is one we already believe is mis-paced here.
Requested
Establish where the cliff is (bisect batch count against copying_minors), and record it.
Either move the corpus asyncpipe off the boundary, or add a second size that is provably clear of it, so allocation-affecting changes have an honest target. Do not simply re-scale silently — whichever is chosen, the standings table must note that the program changed, or the round-over-round deltas become meaningless.
Until then: asyncpipe@120 must not be used to size Promise/async allocation work. Use @240 or larger, and report the cycle counts of both arms alongside any number.
One build per size, asyncpipe.ts with only const BATCHES changed, counting [gc-copy-minor] ran under PERRY_GC_TRACE=1 PERRY_GC_DIAG=1. Every cell exit 0,
printed totals monotone in batch count.
batches
40
60
70
80
85
90
95
100
110
115
118
120
121
122
125
130
140
160
180
200
240
280
320
360
480
copying minors
0
0
0
0
0
0
1
1
1
1
1
1
2
2
2
2
2
2
2
2
2
2
3
3
3
step
between
plateau it opens
width
0 → 1
90 / 95
1 minor: 95…120
26
1 → 2
120 / 121
2 minors: 121…280
160
2 → 3
280 / 320
3 minors: 320…480+
≥160
The shipped size 120 is the LAST batch of the 1-minor plateau; the step is at 121.
A change that moves this program's allocation volume by 0.8 % flips a whole
collection.
Note the defect has re-formed at a different step: this issue recorded a 0→1
flip at 120, and on ebefba51a (with #7924 landed) 120 sits on the 1→2 edge
instead. That is the argument for moving the program rather than re-tuning the
trigger — it will keep drifting back onto one.
Proposal (the corpus owner decides)
BATCHES = 200 — the centre of the widest plateau (121…280): 79 batches above
the lower step, 80 below the upper, i.e. a ±40 % allocation-volume margin
before the cycle count can change. 120 has a 0.8 % margin.
Either form is fine, with one condition:
Re-scale to 200 — keeps 19 programs. The standings table must say the
program changed, and asyncpipe's vs base column must be blanked rather
than carried, or every later delta silently compares two different programs.
Keep @120, add asyncpipe200 — no history break, costs a 20th program. @120 then stays as a known-poised cell and must be annotated as unusable for
sizing allocation work.
⚠️Re-derive before deciding.#7927 landed after this sweep and is an
allocation-volume change on exactly this program, so the plateau coordinates may
have moved. The method (centre of the widest plateau) is what transfers.
Reproduction: gc-handoff/roots/run_ap.sh + ap_<N>.ts; write-up in gc-handoff/ROOTS-NOTES.md §3.
Cliff located (measured on ebefba51a, per #7931 / gc-handoff/ROOTS-NOTES.md):
transition
batch range
0 → 1 copying minor
between 90 / 95
1 → 2
between 120 / 121
2 → 3
between 280 / 320
The shipped size 120 is the LAST batch of its plateau. A 0.8% change in allocation volume flips a collection — which is why two separate merged PRs (#7924 at +4.7%, #7927 at +35%) were scored by the boundary rather than by their own effect, while both are large wins at larger sizes.
Proposal: BATCHES = 200 — the centre of the widest plateau (121–280), giving ±40% margin in both directions.
Two conditions on acting:
If the corpus program is re-scaled, the standings table must say so and blank asyncpipe's round-over-round delta, or every historical comparison silently becomes meaningless. Adding a second size instead avoids that.
Re-derived on current main (b8c1f103e) — all three boundaries moved
Built clean in an isolated target dir; output byte-checked against m0810/expected/asyncpipe.txt at every size, exit 0 at every size. minors = [gc-copy-minor] ran under PERRY_GC_TRACE=1 PERRY_GC_DIAG=1; starts/steps/compl = the [gc-incremental] line (which did not exist when
this issue was filed).
It is 2 batches from the 1→2 flip at 122 — 1.67 % of allocation. The
qualitative problem this issue reports is unchanged; only the identity of the
adjacent flip changed.
★ A second hazard the original bisection could not see
Counting incremental cycles as well as copying minors shows two bands in the #7909 "started a budgeted cycle that never completes" state: 100, and 330–360.
There the program pays the SATB incremental mark barrier for the rest of the run
while the moving minor stays locked out at its budgeted entry guard — strictly
worse than sitting on a boundary.
Critically, 330–360 has the same minors value (2) as the clean plateau below
it. A bisection on copying_minors alone reads 122–360 as one flat plateau and
would recommend 350. Any future re-derivation must gate on cycle_starts too.
Recommendation (not applied — leaving the corpus decision to the maintainer)
BATCHES = 200 — the proposal already on the table — is confirmed by
re-derivation. It sits in the clean 122–320 plateau with 64 % headroom below
and 60 % above, cycle_starts=0, and 2 copying minors, so an
allocation-affecting change must move volume ~40 % before it can flip a cycle
count. 240 is also clean but less centred (49 % / 34 %).
Per the issue's own condition: if the program is re-scaled, the standings table
must record that it changed and asyncpipe's round-over-round delta must be
blanked. The table above is itself the argument — three of three boundaries
moved in one round, so a cross-round comparison of a boundary-straddling program
compares boundaries rather than programs.
Re-derived on current main (ac52a5c38) — and BATCHES = 200 is now inside a stall band
One build per size, asyncpipe.ts with only const BATCHES changed, under PERRY_GC_TRACE=1 PERRY_GC_DIAG=1. Exit 0 at every size, totals monotone in batch
count, errors=0 and seenUsers=6 everywhere, and byte-exact against node --experimental-strip-types (v26.5.1) spot-checked at 104, 150 and 198 —
the two plateau edges and its centre.
minors = [gc-copy-minor] ran; starts = cycle_starts from [gc-incremental].
All three boundaries moved again — third consecutive round
boundary
ebefba51a
b8c1f103e
ac52a5c38 (now)
0 → 1 minor
90 / 95
95 / 105
96 / 104
1 → 2 minors
120 / 121
121 / 122
300 / 320
stall bands
(not measured)
100, 330–360
98–102, 200–280
★★★ The standing recommendation is now the worst cell in the neighbourhood
Both prior comments recommended 200 as "the centre of the widest plateau
(121–280)". On current main 200 is the first size of the 200–280 #7909 stall
band: a budgeted incremental cycle starts, never completes, and the program pays
the SATB mark barrier for the rest of the run while the moving minor stays locked
out at its budgeted entry guard.
This is exactly the hazard the previous re-derivation flagged, now realised. A
bisection on copying_minors alone reads 104–280 as one flat plateau (minors = 1
throughout) and picks its centre — ~192 or 200 — landing in the stall. cycle_starts is the only quantity that separates them, and any future
re-derivation must gate on it.
Recommendation (not applied — the corpus is the maintainer's call)
BATCHES = 150. Centre of the clean 104–198 plateau: 46 batches above the
lower edge (−31 %) and 48 below the upper (+32 %), cycle_starts = 0,
1 copying minor, 0 fulls. An allocation-affecting change must move this program's
volume ~30 % in either direction before it can flip a cycle count.
The shipped 120 is now inside the clean plateau (last round it was 2 batches
from a flip) but is still asymmetric at −13 % / +65 %. It is usable today; 150 is
centred, and the qualitative problem this issue reports — the size drifts back
onto a boundary every round — is unchanged.
Per the issue's own condition: if the program is re-scaled, the standings table
must record that it changed and asyncpipe's round-over-round delta must be
blanked. Three of three boundaries moved in each of the last two rounds, so a
cross-round comparison of a boundary-straddling program compares boundaries
rather than programs.
Reproduction: gc-handoff/roots/run_ap.sh <arm-release-dir> <sizes...>; ap_<N>.ts
variants for every sampled size are now present. Full working: gc-handoff/BUDGET-NOTES.md §5.
So the recommendation posted twice in this thread would have moved the corpus program from one hazard onto another. This is the third time these coordinates have shifted under a merge, and the second time a recommendation was wrong — which is the argument that the fix is not a better constant.
Revised recommendation: BATCHES = 150 (−31% / +32% margin). Verified byte-exact vs node at 104 / 150 / 198.
For reference, 120 is now inside the clean plateau but asymmetric (−13% / +65%), so the original size is no longer acutely broken — it is simply badly centred.
Still not applied; the corpus belongs to the measurement session and any change must be annotated in the standings table with asyncpipe's round-over-round delta blanked, or every historical comparison for that row silently becomes false.
★ Method note for whoever re-derives this next: gate on cycle_starts as well as copying_minors. A stall band has the same minor count as the clean plateau below it, so bisecting on minors alone reads 104–360 as one plateau — which is exactly how 200 came to be recommended.
Completed after a fresh re-derivation on current main fe0d4979204dfd6b8b166320e1ebdd2318f30518 (after #8044).
Build/measurement controls:
isolated release target;
perry, perry-runtime-static, and perry-stdlib-static rebuilt together;
PERRY_RUNTIME_DIR pinned to those fresh archives;
one compile per size with only BATCHES changed;
both PERRY_GC_TRACE=1 and PERRY_GC_DIAG=1 enabled;
every Perry run exited 0 and was byte-identical to node --experimental-strip-types.
Current coordinates:
BATCHES
copying minors
starts / steps / completions
state
60..96 (sampled)
0
0 / 0 / 0
clean
97..191 (sampled densely, exact edges)
1
0 / 0 / 0
clean
192, 195, 198, 199, 200, 220, 240
2
0 / 0 / 0
clean
260, 280, 300, 320
3
0 / 0 / 0
clean
360, 400
4
0 / 0 / 0
clean
480
5
0 / 0 / 0
clean
The exact one-minor plateau is 97..191, with transitions at 96/97 and 191/192. 144 is its exact midpoint, 47 batches from either edge (~33% allocation-volume margin in both directions). Sizes 97, 144, and 191 were each repeated five times: all 15 runs remained byte-exact and reported 1 minor, 0 starts, 0 steps, 0 completions.
Corpus decision applied:
kept historical gc-handoff/apps/asyncpipe.ts at 120 so prior standings remain truthful;
added gc-handoff/apps/asyncpipe144.ts as a new twentieth stable-size arm for allocation-affecting A/B work;
annotated gc-handoff/CLOSING-STANDINGS.md that the new arm has no carried vs base value until its first quiet-mini measurement;
recorded full working in gc-handoff/ASYNCPIPE144-NOTES.md.
The materialized corpus arm itself was compiled and checked once more after creation: output 4702683 539699 6 4702683 0, byte-identical to Node, 1 copying minor, 0 incremental starts/steps/completions.
Activity
proggeramlug commentedon Aug 12, 2026
Cliff located (measured on
ebefba51a)One build per size,
asyncpipe.tswith onlyconst BATCHESchanged, counting[gc-copy-minor] ranunderPERRY_GC_TRACE=1 PERRY_GC_DIAG=1. Every cell exit 0,printed totals monotone in batch count.
The shipped size 120 is the LAST batch of the 1-minor plateau; the step is at 121.
A change that moves this program's allocation volume by 0.8 % flips a whole
collection.
Note the defect has re-formed at a different step: this issue recorded a 0→1
flip at 120, and on
ebefba51a(with #7924 landed) 120 sits on the 1→2 edgeinstead. That is the argument for moving the program rather than re-tuning the
trigger — it will keep drifting back onto one.
Proposal (the corpus owner decides)
BATCHES = 200— the centre of the widest plateau (121…280): 79 batches abovethe lower step, 80 below the upper, i.e. a ±40 % allocation-volume margin
before the cycle count can change. 120 has a 0.8 % margin.
Either form is fine, with one condition:
program changed, and
asyncpipe'svs basecolumn must be blanked ratherthan carried, or every later delta silently compares two different programs.
asyncpipe200— no history break, costs a 20th program.@120 then stays as a known-poised cell and must be annotated as unusable for
sizing allocation work.
allocation-volume change on exactly this program, so the plateau coordinates may
have moved. The method (centre of the widest plateau) is what transfers.
Reproduction:
gc-handoff/roots/run_ap.sh+ap_<N>.ts; write-up ingc-handoff/ROOTS-NOTES.md§3.proggeramlug commentedon Aug 12, 2026
Cliff located (measured on
ebefba51a, per #7931 /gc-handoff/ROOTS-NOTES.md):The shipped size 120 is the LAST batch of its plateau. A 0.8% change in allocation volume flips a collection — which is why two separate merged PRs (#7924 at +4.7%, #7927 at +35%) were scored by the boundary rather than by their own effect, while both are large wins at larger sizes.
Proposal:
BATCHES = 200— the centre of the widest plateau (121–280), giving ±40% margin in both directions.Two conditions on acting:
asyncpipe's round-over-round delta, or every historical comparison silently becomes meaningless. Adding a second size instead avoids that.thenprobe without a property lookup #7927, which is itself an allocation-volume change on this exact program. The method transfers but the numbers must be re-derived on current main before anything is changed.proggeramlug commentedon Aug 12, 2026
Re-derived on current
main(b8c1f103e) — all three boundaries movedBuilt clean in an isolated target dir; output byte-checked against
m0810/expected/asyncpipe.txtat every size, exit 0 at every size.minors=[gc-copy-minor] ranunderPERRY_GC_TRACE=1 PERRY_GC_DIAG=1;starts/steps/compl= the[gc-incremental]line (which did not exist whenthis issue was filed).
(sampled at 60, 80, 90, 95, 100, 105, 110, 115, 118, 119, 120, 121, 122, 125,
130, 140, 160, 180, 190, 195, 200, 205, 210, 220, 240, 280, 300, 320, 330, 340,
350, 355, 360, 480)
Against the coordinates in the issue body
ebefba51ab8c1f103eNone of the three survived #7927. Sizing async work against the old numbers
would have put you in a different plateau than you thought you were in.
@120 is still on the edge
It is 2 batches from the 1→2 flip at 122 — 1.67 % of allocation. The
qualitative problem this issue reports is unchanged; only the identity of the
adjacent flip changed.
★ A second hazard the original bisection could not see
Counting incremental cycles as well as copying minors shows two bands in the
#7909 "started a budgeted cycle that never completes" state: 100, and 330–360.
There the program pays the SATB incremental mark barrier for the rest of the run
while the moving minor stays locked out at its
budgetedentry guard — strictlyworse than sitting on a boundary.
Critically, 330–360 has the same
minorsvalue (2) as the clean plateau belowit. A bisection on
copying_minorsalone reads 122–360 as one flat plateau andwould recommend 350. Any future re-derivation must gate on
cycle_startstoo.Recommendation (not applied — leaving the corpus decision to the maintainer)
BATCHES = 200— the proposal already on the table — is confirmed byre-derivation. It sits in the clean 122–320 plateau with 64 % headroom below
and 60 % above,
cycle_starts=0, and 2 copying minors, so anallocation-affecting change must move volume ~40 % before it can flip a cycle
count.
240is also clean but less centred (49 % / 34 %).Per the issue's own condition: if the program is re-scaled, the standings table
must record that it changed and
asyncpipe's round-over-round delta must beblanked. The table above is itself the argument — three of three boundaries
moved in one round, so a cross-round comparison of a boundary-straddling program
compares boundaries rather than programs.
Full working:
gc-handoff/PACING-NOTES.md§3.proggeramlug commentedon Aug 12, 2026
Re-derived on current
main(ac52a5c38) — andBATCHES = 200is now inside a stall bandOne build per size,
asyncpipe.tswith onlyconst BATCHESchanged, underPERRY_GC_TRACE=1 PERRY_GC_DIAG=1. Exit 0 at every size, totals monotone in batchcount,
errors=0andseenUsers=6everywhere, and byte-exact againstnode --experimental-strip-types(v26.5.1) spot-checked at 104, 150 and 198 —the two plateau edges and its centre.
minors=[gc-copy-minor] ran;starts=cycle_startsfrom[gc-incremental].sampled at 60, 80, 90, 95, 96, 98, 100, 102, 104, 105, 110, 115, 118, 119, 120,
121, 122, 125, 130, 140, 145, 150, 155, 160, 165, 170, 175, 180, 185, 190, 192,
195, 198, 200, 205, 210, 220, 240, 260, 280, 300, 320, 330, 340, 350, 355, 360,
400, 480.
All three boundaries moved again — third consecutive round
ebefba51ab8c1f103eac52a5c38(now)★★★ The standing recommendation is now the worst cell in the neighbourhood
Both prior comments recommended 200 as "the centre of the widest plateau
(121–280)". On current main 200 is the first size of the 200–280 #7909 stall
band: a budgeted incremental cycle starts, never completes, and the program pays
the SATB mark barrier for the rest of the run while the moving minor stays locked
out at its
budgetedentry guard.This is exactly the hazard the previous re-derivation flagged, now realised. A
bisection on
copying_minorsalone reads 104–280 as one flat plateau (minors = 1throughout) and picks its centre — ~192 or 200 — landing in the stall.
cycle_startsis the only quantity that separates them, and any futurere-derivation must gate on it.
Recommendation (not applied — the corpus is the maintainer's call)
BATCHES = 150. Centre of the clean 104–198 plateau: 46 batches above thelower edge (−31 %) and 48 below the upper (+32 %),
cycle_starts = 0,1 copying minor, 0 fulls. An allocation-affecting change must move this program's
volume ~30 % in either direction before it can flip a cycle count.
The shipped 120 is now inside the clean plateau (last round it was 2 batches
from a flip) but is still asymmetric at −13 % / +65 %. It is usable today; 150 is
centred, and the qualitative problem this issue reports — the size drifts back
onto a boundary every round — is unchanged.
Per the issue's own condition: if the program is re-scaled, the standings table
must record that it changed and
asyncpipe's round-over-round delta must beblanked. Three of three boundaries moved in each of the last two rounds, so a
cross-round comparison of a boundary-straddling program compares boundaries
rather than programs.
Reproduction:
gc-handoff/roots/run_ap.sh <arm-release-dir> <sizes...>;ap_<N>.tsvariants for every sampled size are now present. Full working:
gc-handoff/BUDGET-NOTES.md§5.proggeramlug commentedon Aug 12, 2026
BATCHES = 200— the standing recommendation — is INSIDE a stall bandRe-derived on current main by the #7929 agent (
gc-handoff/BUDGET-NOTES.md):minors=1,cycle_starts=0, 0 fulls) is 104…198.So the recommendation posted twice in this thread would have moved the corpus program from one hazard onto another. This is the third time these coordinates have shifted under a merge, and the second time a recommendation was wrong — which is the argument that the fix is not a better constant.
Revised recommendation:
BATCHES = 150(−31% / +32% margin). Verified byte-exact vs node at 104 / 150 / 198.For reference, 120 is now inside the clean plateau but asymmetric (−13% / +65%), so the original size is no longer acutely broken — it is simply badly centred.
Still not applied; the corpus belongs to the measurement session and any change must be annotated in the standings table with
asyncpipe's round-over-round delta blanked, or every historical comparison for that row silently becomes false.★ Method note for whoever re-derives this next: gate on
cycle_startsas well ascopying_minors. A stall band has the same minor count as the clean plateau below it, so bisecting on minors alone reads 104–360 as one plateau — which is exactly how 200 came to be recommended.proggeramlug commentedon Aug 13, 2026
Completed after a fresh re-derivation on current main
fe0d4979204dfd6b8b166320e1ebdd2318f30518(after #8044).Build/measurement controls:
perry,perry-runtime-static, andperry-stdlib-staticrebuilt together;PERRY_RUNTIME_DIRpinned to those fresh archives;BATCHESchanged;PERRY_GC_TRACE=1andPERRY_GC_DIAG=1enabled;node --experimental-strip-types.Current coordinates:
The exact one-minor plateau is 97..191, with transitions at 96/97 and 191/192. 144 is its exact midpoint, 47 batches from either edge (~33% allocation-volume margin in both directions). Sizes 97, 144, and 191 were each repeated five times: all 15 runs remained byte-exact and reported 1 minor, 0 starts, 0 steps, 0 completions.
Corpus decision applied:
gc-handoff/apps/asyncpipe.tsat 120 so prior standings remain truthful;gc-handoff/apps/asyncpipe144.tsas a new twentieth stable-size arm for allocation-affecting A/B work;gc-handoff/CLOSING-STANDINGS.mdthat the new arm has no carriedvs basevalue until its first quiet-mini measurement;gc-handoff/ASYNCPIPE144-NOTES.md.The materialized corpus arm itself was compiled and checked once more after creation: output
4702683 539699 6 4702683 0, byte-identical to Node, 1 copying minor, 0 incremental starts/steps/completions.