Skip to content

bench: asyncpipe@120 sits exactly on a nursery trigger boundary and is not a usable A/B target #7926

Closed

Description

@proggeramlug

Summary

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):

arm asyncpipe @120 copying minors
before — 0
after +4.7% 1

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.

It also interacts with two known findings:

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.

Activity

  1. added
    performanceRuntime, compile-time, build-size, or memory performance
    on Aug 12, 2026
  2. proggeramlug commented on Aug 12, 2026

    @proggeramlug
    ContributorAuthor

    Cliff located (measured on ebefba51a)

    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:

    1. 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.
    2. 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.

  3. proggeramlug commented on Aug 12, 2026

    @proggeramlug
    ContributorAuthor

    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:

    1. 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.
    2. ⚠️ These coordinates predate perf(runtime): answer the promise-assimilation then probe 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.
  4. proggeramlug commented on Aug 12, 2026

    @proggeramlug
    ContributorAuthor

    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).

    BATCHES minors inc. starts steps compl state
    60, 80, 90, 95 0 0 0 0 clean
    100 0 1 6 0 #7909 stall
    105 … 121 1 0 0 0 clean
    122 … 320 2 0 0 0 clean — widest plateau
    330 … 360 2 1 7–13 0 #7909 stall
    480 3 1 23 1 mixed

    (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

    boundary recorded on ebefba51a re-derived on b8c1f103e
    0 → 1 minor 90 / 95 95 / 105 — and 100 is a stall, not a clean 0
    1 → 2 minors 120 / 121 121 / 122
    2 → 3 minors 280 / 320 360 / 480

    None 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 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.

    Full working: gc-handoff/PACING-NOTES.md §3.

  5. proggeramlug commented on Aug 12, 2026

    @proggeramlug
    ContributorAuthor

    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].

    BATCHES minors fulls cycle_starts state
    60, 80, 90, 95, 96 0 0 0 clean
    98, 100, 102 0 0 1 #7909 stall
    104 … 198 1 0 0 clean — widest plateau, width 95
    200 … 280 1 0 1 #7909 stall
    300 1 1 2 mixed
    320 … 400 2 1 1 mixed
    480 3 1 1 mixed

    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

    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.

  6. proggeramlug commented on Aug 12, 2026

    @proggeramlug
    ContributorAuthor

    ⚠️ Correction: BATCHES = 200 — the standing recommendation — is INSIDE a stall band

    Re-derived on current main by the #7929 agent (gc-handoff/BUDGET-NOTES.md):

    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.

  7. proggeramlug commented on Aug 13, 2026

    @proggeramlug
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions