chore: merge train 253 — ten PRs (v0.5.1633) - #10918
Merged
Merged
Conversation
…nted (#10868) Perry mints 2,287,869 ShapeIds per `ts.transpileModule` of a 1,201-line file to describe 10,867 distinct layouts, and that layout count does not grow with the work. The band is 2^30 and exhaustion is `shape_id_exhausted_abort()` -> `std::process::abort()`, so it is ~476 transpiles to a hard abort. Counting mints does not say why; splitting them by cause does. `PERRY_SHAPE_MINT_DIAG=<path|1>` labels every mint by which of `shape_descriptor_ensure_with_holes`' six identity facts moved against the family already indexed under the same keys-array ADDRESS, taking the cheapest explanation the family offers rather than the worst sibling in the list. It hashes the ordered key-NAME list separately, so a new layout (`fresh_keys_new_list`) and the same layout at a new array address (`fresh_keys_known_list`) are different rows -- the distinction that turns this from a budget problem into a defect. `identical` is the row that would mean the `facts_key` memo failed; it reads 0 on every workload measured, and it is listed so that is a measurement rather than an assumption. Alongside: memo hits, retirement count and retirement AGE, a family-size histogram, `#[track_caller]` call sites, and the transition cache's own outcomes -- where one split carries the diagnosis. `TRANSITION_CACHE_SIZE` is 16384 and DIRECT-MAPPED, so `miss_empty` (a cold edge) and `miss_COLLIDE` (the slot holds a different live edge) are different findings. On tsc that reads 8.4 % hits, `miss_COLLIDE` 1,221,842 against `miss_empty` 60,186, and 96.1 % evicting inserts. The control that says the machinery works: `pool` -- N objects given the same eight field NAMES through a dynamic key, so HIR cannot fold them into one closed-shape literal -- mints 4,534 at N=1,000 and 4,534 at N=10,000, constant, with cache hits rising 84.6 % -> 98.2 %. Any fix has to keep that row flat. Costs nothing when off. Every call site is `#[cfg(feature = shape-mint-diag)]` and the feature is not in `default`, so the shipped runtime is unchanged; the module itself always compiles so its tests cannot bit-rot behind a feature nobody builds. Two trees at 89dd494, fixtures compiled by each arm's own perry, output byte-identical to node on every row, per-iteration fitted N=2,000 -> 20,000, min of 3: same 93.59 / 93.59 pool 15571.81 / 15571.92 del 21328.62 / 21334.33 accd 16110.85 / 16113.09 acc 19142.89 / 19145.02 diff 220295.71 / 220302.70 i.e. unchanged to the method's resolution (the `same` row resolves to 0.01 %). The gate is there because an earlier form was not free: with the hooks unconditional the same rows read +0.15 % to +0.55 %, and the residual tracked MINT VOLUME rather than cache traffic. Tests: 8 unit tests over the classifier, which always compile. Must-fail, two sabotages applied to the tree and reverted: making the FIRST sibling win instead of the cheapest turns `the_cheapest_explanation_wins_regardless_of_ family_order` and `an_exact_sibling_outranks_a_one_fact_sibling` red; reporting an exact sibling as `Mixed` instead of `Identical` turns `a_family_member_matching_every_fact_is_a_memo_failure` and `an_exact_sibling_outranks_a_one_fact_sibling` red. Full suite on both arms, skipping the one test that aborts the process on main (`bun_compat::plugin::tests::calls_setup_for_objects_and_functions_without_ running_hooks`, a non-unwinding panic, pre-existing): this branch 4,194 passed / 9 failed, control 4,186 passed / the SAME 9 failed. Net: 8 passing tests, no new failure. Review follow-ups (#10885): The `Cargo.toml` comment claimed the mint-side counters stay unconditional so a stock build could still take a before/after of mint volume. It cannot: every call site is gated, including `note_mint`, `note_retire`, `note_memo_hit` and `key_list_content_hash`, so an unfeatured build measures NOTHING. The comment was stale from the design before the staging measurement — and the durable version is that we gated the mint side BECAUSE the residual tracked mint volume. Gating `#[track_caller]`, then the transition-cache probes that LOOK hot (1.4 M lookups per transpile), still left +0.32 % / +0.35 % on `accd` / `acc`; those fixtures mint 3 and 4 ids per object while `pool` and `same` mint almost none and were already at zero. The comment now says what the code does, which matters here because the opposite error — being told data exists when it does not — is exactly what the dump's "the zeros below are absence of measurement, not absence of collisions" line exists to prevent. `TcMiss` also recorded one counter for two different refusals: the target array refusing the `GC_FLAG_SHAPE_SHARED` stamp, and its length/capacity no longer matching the edge. Split into `Unshared` and `TargetLen`, for the same reason `Empty` and `Collide` are separate. (cherry picked from commit edef434)
(cherry picked from commit 8a61284)
Baseline measured on f88acda: megamorphic=0 / armed=100% on an overflow hot key reproduces exactly. The armed way block costs a DIRECTLY MEASURED 12.95 instr/read (m64arm 772.92 vs m64noarm 759.97 - the same program with and without a shape that arms the site), which is 1.7% of a 773-instr read and not the +37% #7753 quoted against a cheaper read. Predicted counts: megamorphic 0 -> ~97.7%, armed 100% -> ~0.76%, in_ways 0, encoded-slot-in-a-way 0. Predicted cost: m64arm -11.4..-13.4; m64 only -2..-10 because that fixture's armed way genuinely serves 1/64 of its reads and the latch zeroes it. (cherry picked from commit accafd9)
A read site whose hot key lives in the overflow (spill) region could never
latch megamorphic, however many shapes it rotated. It kept the `armed` state
it earned during warm-up for the life of the process and the emitted gate kept
running four dependent loads and a compare tree on every read, all of which
could only ever miss. Measured on the issue fixture: megamorphic=0 across
16.8M primes, every one of them `armed`.
`pic_prime_get` computed one predicate for two different questions:
* may the displaced MRU entry be absorbed INTO a way? No, if its slot is
overflow-encoded -- the emitted way path computes obj + header + slot*8
straight from the slot word, so an encoded slot there is a wild load.
That is #9287, it is correct, and this commit does not touch it;
* did a DIFFERENT shape just displace the MRU entry? That is the evidence
the latch counts, and on such a site it is true on every read.
One `return` served both, so the consecutive-eviction run never advanced.
Split them: `evicted` records the displacement, `cascade` still decides
absorption, and a suppressed cascade on an ARMED site (`state > 0`, the exact
predicate the emitted gate evaluates) advances the run and latches at
PIC_MEGAMORPHIC_EVICTIONS. Nothing is written to a way on that path; only the
gate the ways sit behind turns itself off. A rotation in which no shape carries
the key inline never arms and is untouched.
Counts, PERRY_IC_DIAG over 20M reads, predicted before the arm was built and
committed in .lane/10863-prediction.md:
m64 armed 100.0% -> 0.758% (predicted <2%, point estimate 0.76%)
megamorphic 0.0% -> 96.97% (predicted >90%)
in_ways 0 -> 0, way_encoded_slot 0
m64inline unchanged: fresh 0.048% / armed 0.919% / megamorphic 99.03%
m64noarm unchanged: fresh 100% / armed 0 / megamorphic 0
Cost, perf stat instructions:u, min of 3, marginal per read fitted 500k->5M,
flat to 0.1 instr/read at a 2M midpoint:
m64 757.98 -> 737.69 -20.29 -2.68%
m64arm 772.92 -> 755.09 -17.83 -2.31%
m64noarm 759.97 -> 764.81 +4.84 +0.64% (see below)
m64inline 1352.85 -> 1352.83 -0.02 0.00%
own3/w4/inh/inh3 123.00/192.00/349.00/288.00 -> identical
#7753's "+37%" does not transfer and is not claimed: here every read already
misses into the full handler (~740 instr), so the way block is a ~20-instruction
slice of it.
The m64noarm row is a real regression and is attributed, not excused: a sabotage
arm with this fix compiled out measures +4.17 on the same fixture with behaviour
identical to base, so the residual is codegen layout in pic_prime_get /
get_field_ic_miss_impl. Six structural variants were measured; m64noarm landed
between +4.17 and +7.17 in all of them, including a minimal two-line variant
(+7.17). This is the best of the six and the only one that leaves m64inline
exactly unchanged.
Evidence that no encoded slot enters a way, which is the one thing that must
not regress:
* PERRY_IC_DIAG gains `way_encoded_slot`, scanning the four ways for a slot
carrying IC_SLOT_OVERFLOW_BIT on every prime. Reads 0 across ~57M primes on
four fixtures. The probe is #[cold] #[inline(never)] behind the existing
ic_on() arming, so an unarmed run pays what it paid before.
* assert_no_encoded_slot_in_a_way runs after EVERY individual prime in four
new unit tests, not once at the end.
Sabotaged deliberately and the two new latch tests fail with the issue's exact
signature (fresh=2 armed=25199 megamorphic=0) while the other ten pass;
restored afterwards.
(cherry picked from commit 65b2bc6)
String.prototype.replace (and match) read `rx.flags` on every call to learn two bits, global and unicode. That Get went through the generic property path -- js_reflect_get, an accessor-descriptor lookup, a shape lookup, a setjmp trap frame -- then the builtin getter performed eight more observable Gets, built a flags string, and the caller re-parsed it with from_utf8. On a 3-character subject that was most of the call (#10518). On a pristine RegExp none of that is observable, so the two bits are read straight off the header instead. "Pristine" has to cover all nine accessors the Get consults, not just `flags`: the builtin getter reads hasIndices, global, ignoreCase, multiline, dotAll, unicode, unicodeSets and sticky, and overriding any one of them changes the answer. So the predicate requires: * the instance's prototype is the canonical RegExp.prototype (a subclass or a reassigned prototype fails); * none of the nine names is an own property of the instance, as an accessor or as data; * RegExp.prototype's nine accessors are all still the builtins. The last check costs nine descriptor lookups, so its verdict is memoized against prop_plan_semantic_epoch -- the counter the property system already bumps for every event that can change what an inherited read answers, exposed for exactly this and already used by promise::then_probe. It is deliberately not keyed on the pointer-identity epoch, which the collector bumps at poll cadence. js_regexp_new stores `unicode` as contains('u') || contains('v'), which is what scan_flags derives, so the two paths agree by construction. "a+b".replace(/\+/g, " ") 104,802 -> 29,933 instructions, 98.1x -> 28.8x node The existing perex_match_reads_overridden_flags_and_global_getters test, which defeated an earlier version of this that checked only `flags`, passes. Answers match Node on overridden prototype getters, a lying getter, an own `flags` accessor, an own data `global`, the u/v flags and match; the one differing row is a subclass overriding `flags`, which diverges identically on main (#10700). (cherry picked from commit 52a4681)
(cherry picked from commit 7a0d63a)
…split Two per-call costs in `js_string_split_n`, both measured on #10519's own reproduction -- a 211-byte JWT split on "." -- after the engine path stopped answering plain-string splits. The source is scanned once per call to decide whether every part can take the all-ASCII metadata shortcut. That scan was `bytes.iter().all(|&b| b < 0x80)`, one byte per iteration; `perf annotate` put about 74% of this function's own time in its six-instruction loop, roughly 19% of the whole call. `is_ascii()` is the same predicate and std tests a word at a time. The parts are then found with `str::split(&str)`, which takes the two-way `StrSearcher` and pays its setup in full for a single-character needle -- the common `split(".")`, `split(",")`, `split(" ")` shape. A one-byte delimiter takes std's memchr-based searcher instead. The `byte < 0x80` guard on that second path is load-bearing rather than decorative: string storage can hold malformed bytes (the comment above the ASCII scan documents `[0x80, b'|', 0xF0]`), and `0x80 as char` is U+0080, which re-encodes as two bytes and would split on the wrong occurrences. JWT split on ".": 5,424 -> 4,268 instructions per call, -21.3% Against #10519's 345x and ~230k instructions, that row is now 9.2x Node 26.5.1. Split conformance is unchanged: the 27 cases where a byte scan and a UTF-16 unit scan can disagree, and the 12 non-string separator forms, all still match Node. (cherry picked from commit 437fe12)
…nger fold to the Array search helpers (#10894) TypeScript 5.7 made every typed array, `DataView` and (via @types/node) `Buffer` generic over the backing buffer, and `tsc` prints that argument, so `m: Uint8Array<ArrayBuffer>` is now a common annotation. HIR lowered it as `Type::Generic { base: "Uint8Array" }`, which no typed-array / buffer recognizer matches (they key on `Type::Named`), so the receiver read as "known not a string" and `m.indexOf(44, from)` folded to `Expr::ArrayIndexOf` over a `BufferHeader`: the byte payload was walked as NaN-boxed f64s and a present byte answered -1. `join` printed `5.09e-313-…`. `Float32Array<…>` survived only because it is a registry typed array the helpers already re-check. Two layers: * HIR: a type reference or `new X<…>()` whose base is a typed array, `Buffer` or `DataView` erases its arguments to `Type::Named(X)` (a user class of that name keeps `Generic`). The lowered body is now identical under both spellings for 41 members, asserted in `buffer_backed_generic_tests.rs`. * runtime: `js_array_indexOf_jsvalue` / `js_array_last_index_of_jsvalue` / `js_array_includes_jsvalue` / `js_array_join` ask `buffer_receiver_dispatch` above the array-only funnel (the #8137 pattern), so a Buffer that reaches them through a lying type (`Readonly<Uint8Array>`, `as unknown as number[]`) is answered by the shared uint8 dispatcher instead of by reinterpreting its bytes. Found by guest271314's Native Messaging host, whose reply splitter never split because `indexOf` returned -1; `test_gap_10894_native_messaging_split` is that loop verbatim at 1 MiB, 1 MiB + 5 B, 2 MiB and 5 MiB. (cherry picked from commit 73c3698)
(cherry picked from commit 0ddb0d0)
…alue (#10868) `Object.defineProperty` minted 3 ShapeIds per object for a data descriptor and 4 for an accessor, linearly and without bound, while the transition cache reported lookups and ZERO inserts. This makes the data case constant. Measured rather than read. Instrumenting every decline reason showed exactly one predicate refusing, 1000 times out of 1000, at `object_ops/keys_array.rs`: defineProperty key-add, edge outcomes: no publish: new_index >= inline_capacity 1000 eligible 1000 The receiver IS eligible and the lookup DOES run. Why it refuses every time: `INLINE_SLOT_FLOOR` is 2, and #7916 documents it as a pure FOOTPRINT dial -- "purely a growth-headroom dial for objects that gain properties by name after birth". A two-field literal therefore allocates exactly two slots and has zero growth headroom, so the first key added after birth lands at `new_index == inline_capacity == 2`, spills to overflow, and the edge is never published. Every receiver forks onto a private keys array, and from there the fork cascade does the rest: once the predecessor ShapeId is private, even #10287's DETERMINISTIC generation is deterministic in a value no other object shares, which is why an accessor costs 4 ids and not 1. A dial tuned for bytes silently decided whether transition edges get published at all; neither subsystem could see that from its own side. Raising the floor is not the fix: it trades footprint against a standing RSS budget and only moves the cliff to four-field literals. The correct behaviour already exists next door -- `field_set_by_name/tail.rs`'s overflow arm publishes the edge with no inline-capacity gate at all, which is why the `pool` control (the same objects grown by [[Set]]) already mints a constant 4,534 ids however much work it does. This is missing wiring, not a missing capability. Two changes, because publishing alone would have measured as a wash: the adopter gate refused overflow-located edges too, on the stated grounds that "a keys-only install (an accessor claiming its slot) writes no value, so the overflow entry such an edge implies would never be created". That reason is correct for accessors and over-broad for data. So publish the edge when the install will write a value, AND let the adopter take one in that case. `ensure_key_in_keys_array_for_value` is the opt-in and `define_property_force_store_value` is its only caller; every keys-only claim keeps today's behaviour by construction. `cached_target_fits` is untouched (it guards a different hazard), and the live-bound bump is now guarded in BOTH adopt arms -- unreachable in the first-key arm today, since its precondition is a null keys array, but an unguarded bump beside a gate that now admits overflow slots is a trap for whoever widens that precondition. Measured with #10885's census. Six controls, two arms built from their own trees, `cmp`-distinct binaries, output byte-identical to node on every row; mints via `PERRY_SHAPE_MINT_DIAG`, instructions fitted N=2,000 -> 20,000, min of 3: fixture mints @1k/@10k before -> after instr/iter before -> after same 1 / 1 -> 1 / 1 93.58 -> 93.58 pool 4,534 / 4,536 -> 4,534 / 4,536 15596.14 -> 15595.14 del 5,516 / 14,516 -> 5,516 / 14,516 21365.89 -> 21387.20 accd 3,002 / 30,002 -> 5 / 5 16188.99 -> 12539.77 (-22.5%) acc 4,002 / 40,002 -> 4,002 / 40,002 19244.19 -> 19315.15 diff 22,516/184,516 -> 22,516/184,516 220937.30 -> 220936.49 `accd` goes CONSTANT -- 5 ids whether it runs 1,000 iterations or 10,000 -- with 22.5% fewer instructions, so this is a speedup and not merely a smaller counter. `acc` being unchanged TO THE ID is not a missed opportunity; it is the result that makes the change sound. The accessor is a keys-only claim whose implied overflow entry is never created, so it must keep the private path. A change that improved both rows would have broken the keys-only-install argument rather than fixed the data one. tsc (`ts.transpileModule`, typescript 5.8.2 from source, output identical to node): MINTS 2,288,352 -> 2,287,800 and distinct key-NAME lists 10,867 -> 10,867. Flat, as predicted before measuring: tsc's mints are 97.4% `fresh_keys_known_list` + `key_count` from the [[Set]] CoW path, which is a different fork source. The requirement here was must-not-regress, and a flat tsc row confirms the attribution. The layout-count invariant held, so shape IDENTITY did not move -- only mint volume. `cargo test -p perry-runtime` on both arms back to back, same host state, skipping the one test that aborts the process on main (`bun_compat::plugin::tests::calls_setup_for_objects_and_functions_without_ running_hooks`, a non-unwinding panic, pre-existing): 4,204 passed / 0 failed on both. `cargo check` clean in both feature configurations. This is ONE fork source among several, not a fix for #10868. The exit criterion there is: mints ~= distinct layouts on tsc, no abort, and same layout + same facts => same ShapeId always. Reaching it needs canonical shape identity, because the authoritative dedup (`by_facts`) keys on the keys-array ADDRESS and that address is produced by a lossy 16,384-entry direct-mapped cache. (cherry picked from commit 89f157b)
2020 -> 1961. Moves cell_meta_slot and its cfg(test) companion; stops before the #[inline]/#[cfg_attr] pair that a comment-walk would swallow, and excludes cell_expando_ensure which holds the file's one raw-handle debt site.
This was referenced Sep 21, 2026
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (49)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Sep 21, 2026
proggeramlug
pushed a commit
that referenced
this pull request
Sep 22, 2026
The three gap-suite reds on this PR — `test_gap_http2_settings`,
`test_gap_3527_http_ctor_prototype`, `test_gap_gc_net_once_flags_rekey` — are
not compile failures. They are `PERRY_COMPILE_TIMEOUT` expiring. In the CI log
each one is exactly 300.1 s wide:
02:37:46.96 -> 02:42:47.08 test_gap_3527_http_ctor_prototype 300.12 s
02:45:24.62 -> 02:50:24.77 test_gap_gc_net_once_flags_rekey 300.15 s
02:47:57 (shard 2) test_gap_http2_settings 300.1 s
All three compile and pass by hand on this branch (perrymaster, cold
`target/perry-auto-*`): 3m43s, and byte-parity against the node 26.5.1 oracle.
WHY THE BUDGET IS TOO SMALL
#10757 sized one 300 s budget for every fixture, on the stated belief that
"the fast-mode/PERRY_SKIP_BUILD tiers don't pay [an auto-optimize rebuild] per
test". They do, and the reason is four hundred lines further down the same
file: the #7629 block unsets PERRY_NO_AUTO_OPTIMIZE for every fixture that
routes a module to a `perry-ext-*` wrapper, because no single prebuilt stdlib
can serve the mixed corpus. perry then runs `cargo build` for a
feature-stripped runtime + stdlib + wrapper INSIDE the per-test compile
budget, once per distinct feature set, into a fresh `target/perry-auto-<hash>`.
The gap-suite workflow comment already records that cost as ~200 s per
distinct feature set; with runner variance it measures 270-300 s. So the
rebuild sits ON the line:
#10930 test_gap_gc_net_once_flags_rekey 287.4 s PASS (12.6 s margin)
#10930 test_gap_http2_settings 300.13 s COMPILE_FAIL
#10930 test_gap_3527_http_ctor_prototype 300.13 s COMPILE_FAIL
That is merge train 254 — v0.5.1634, now on main. The same red, on a rotating
cast, is in every recent run: 11 fixtures on #10918, 5 on #10892, 4 on #10930.
All three merged. A gate whose red is overridden by hand every time is not a
gate, and this one cannot even tell a killed compile from a rejected one.
THE FIX
Split the budget by the property that predicts the cost — this compile may
rebuild the toolchain — and not by test name. `PERRY_EXT_COMPILE_TIMEOUT`
(default 900 s, 3x the observed cost) applies when auto-optimize is on for
that compile AND the fixture routes to an ext wrapper, reusing the existing
`test_routes_to_ext_wrapper` predicate. The ordinary 300 s budget is
untouched, so a genuine hang in a plain compile is still bounded at 300 s, and
the shard's 110-minute cap has room (shards run 17-46 min).
A killed compile now says so — `compile TIMEOUT after <N>s — killed, not
rejected` — in the console line and at the head of the persisted
`*.compile_error.log`. The old `(compile error)` with no message is what made
this take a night to find: the fixture compiled fine by hand, and the cause
was only visible by subtracting two timestamps out of a CI log.
WITNESS
Four arms on perrymaster, `test_gap_http2_settings`, PERRY_SKIP_BUILD=1 (the
CI path), `target/perry-auto-*` and the perry object cache wiped before each
cold arm:
A unpatched, cold, PERRY_COMPILE_TIMEOUT=120
-> FAIL (compile error) killed at 2:03
B patched, cold, PERRY_COMPILE_TIMEOUT=120
-> PASS 4:02
C patched, cold, PERRY_COMPILE_TIMEOUT=900 PERRY_EXT_COMPILE_TIMEOUT=60
-> FAIL (compile TIMEOUT after 60s — killed, not rejected)
D patched, test_gap_symbols (not ext-routed), PERRY_COMPILE_TIMEOUT=1
-> FAIL (compile TIMEOUT after 1s — killed, not rejected)
A vs B is the same fixture, same cold state, same ordinary budget: the patch
is the only difference. C shows the ext budget is the one governing an
ext-routed fixture. D shows the ordinary budget still bites, so this is not a
blanket raise.
The three fixtures then pass through the harness on this branch:
test_gap_http2_settings, test_gap_3527_http_ctor_prototype and
test_gap_gc_net_once_flags_rekey — 1/1 parity pass each, 0 compile fail.
proggeramlug
pushed a commit
that referenced
this pull request
Sep 22, 2026
The previous commit gave a compile that may rebuild the toolchain its own
budget, scoped to fixtures that route a module to a `perry-ext-*` wrapper.
CI then showed there is a second way in — and showed it in one line, which is
the whole point of the label that commit added:
FAIL test_gap_6558_webassembly_graceful_fail
(compile TIMEOUT after 300s — killed, not rejected)
No ext-routed import, auto-optimize off, so it kept the ordinary 300 s budget.
It still builds native artifacts inside the compile:
wasm-host: building perry-wasm-host from workspace source
wasm-host (no-auto): rebuilding runtime with wasm-host feature
Cold, that compile measures 395 s here — over the line by a third, and under
the old `(compile error)` wording it would have read as a broken fixture.
It is in #10918's list of the same red for the same reason.
So the predicate gets the second property, symmetric with the first and still
a property of the test rather than its name. Across the four runs sampled
(#10859, #10918, #10892, #10930) the two together cover the entire observed
population: 13 of 14 distinct fixtures route to an ext wrapper, and the 14th
is this one. `PERRY_EXT_COMPILE_TIMEOUT` is renamed
`PERRY_TOOLCHAIN_COMPILE_TIMEOUT` — it was never only about ext wrappers, and
it has not shipped under the old name.
WITNESS — three more arms, same harness, on `test_gap_6558_webassembly_graceful_fail`
unless stated:
E PERRY_COMPILE_TIMEOUT=1 PERRY_TOOLCHAIN_COMPILE_TIMEOUT=900
-> PASS the ordinary budget is not in force
F PERRY_COMPILE_TIMEOUT=900 PERRY_TOOLCHAIN_COMPILE_TIMEOUT=1
-> FAIL (compile TIMEOUT after 1s — killed, not rejected)
the toolchain budget is
G test_gap_symbols (neither ext-routed nor wasm), PERRY_COMPILE_TIMEOUT=1
-> FAIL (compile TIMEOUT after 1s)
the ordinary budget still bites
E and F are the same fixture and the same harness with the two budgets
swapped, so they isolate which one governs; G is the control that this is not
a blanket raise.
Run 35692315543 on the previous commit: gap shards 1, 2, 3, 4 and 5 green —
including 2 and 3, which carried all three of this PR's original reds — and 6
red on this one fixture alone.
proggeramlug
pushed a commit
that referenced
this pull request
Sep 22, 2026
The three gap-suite reds on this PR — `test_gap_http2_settings`,
`test_gap_3527_http_ctor_prototype`, `test_gap_gc_net_once_flags_rekey` — are
not compile failures. They are `PERRY_COMPILE_TIMEOUT` expiring. In the CI log
each one is exactly 300.1 s wide:
02:37:46.96 -> 02:42:47.08 test_gap_3527_http_ctor_prototype 300.12 s
02:45:24.62 -> 02:50:24.77 test_gap_gc_net_once_flags_rekey 300.15 s
02:47:57 (shard 2) test_gap_http2_settings 300.1 s
All three compile and pass by hand on this branch (perrymaster, cold
`target/perry-auto-*`): 3m43s, and byte-parity against the node 26.5.1 oracle.
WHY THE BUDGET IS TOO SMALL
#10757 sized one 300 s budget for every fixture, on the stated belief that
"the fast-mode/PERRY_SKIP_BUILD tiers don't pay [an auto-optimize rebuild] per
test". They do, and the reason is four hundred lines further down the same
file: the #7629 block unsets PERRY_NO_AUTO_OPTIMIZE for every fixture that
routes a module to a `perry-ext-*` wrapper, because no single prebuilt stdlib
can serve the mixed corpus. perry then runs `cargo build` for a
feature-stripped runtime + stdlib + wrapper INSIDE the per-test compile
budget, once per distinct feature set, into a fresh `target/perry-auto-<hash>`.
The gap-suite workflow comment already records that cost as ~200 s per
distinct feature set; with runner variance it measures 270-300 s. So the
rebuild sits ON the line:
#10930 test_gap_gc_net_once_flags_rekey 287.4 s PASS (12.6 s margin)
#10930 test_gap_http2_settings 300.13 s COMPILE_FAIL
#10930 test_gap_3527_http_ctor_prototype 300.13 s COMPILE_FAIL
That is merge train 254 — v0.5.1634, now on main. The same red, on a rotating
cast, is in every recent run: 11 fixtures on #10918, 5 on #10892, 4 on #10930.
All three merged. A gate whose red is overridden by hand every time is not a
gate, and this one cannot even tell a killed compile from a rejected one.
THE FIX
Split the budget by the property that predicts the cost — this compile may
rebuild the toolchain — and not by test name. `PERRY_EXT_COMPILE_TIMEOUT`
(default 900 s, 3x the observed cost) applies when auto-optimize is on for
that compile AND the fixture routes to an ext wrapper, reusing the existing
`test_routes_to_ext_wrapper` predicate. The ordinary 300 s budget is
untouched, so a genuine hang in a plain compile is still bounded at 300 s, and
the shard's 110-minute cap has room (shards run 17-46 min).
A killed compile now says so — `compile TIMEOUT after <N>s — killed, not
rejected` — in the console line and at the head of the persisted
`*.compile_error.log`. The old `(compile error)` with no message is what made
this take a night to find: the fixture compiled fine by hand, and the cause
was only visible by subtracting two timestamps out of a CI log.
WITNESS
Four arms on perrymaster, `test_gap_http2_settings`, PERRY_SKIP_BUILD=1 (the
CI path), `target/perry-auto-*` and the perry object cache wiped before each
cold arm:
A unpatched, cold, PERRY_COMPILE_TIMEOUT=120
-> FAIL (compile error) killed at 2:03
B patched, cold, PERRY_COMPILE_TIMEOUT=120
-> PASS 4:02
C patched, cold, PERRY_COMPILE_TIMEOUT=900 PERRY_EXT_COMPILE_TIMEOUT=60
-> FAIL (compile TIMEOUT after 60s — killed, not rejected)
D patched, test_gap_symbols (not ext-routed), PERRY_COMPILE_TIMEOUT=1
-> FAIL (compile TIMEOUT after 1s — killed, not rejected)
A vs B is the same fixture, same cold state, same ordinary budget: the patch
is the only difference. C shows the ext budget is the one governing an
ext-routed fixture. D shows the ordinary budget still bites, so this is not a
blanket raise.
The three fixtures then pass through the harness on this branch:
test_gap_http2_settings, test_gap_3527_http_ctor_prototype and
test_gap_gc_net_once_flags_rekey — 1/1 parity pass each, 0 compile fail.
proggeramlug
pushed a commit
that referenced
this pull request
Sep 22, 2026
The previous commit gave a compile that may rebuild the toolchain its own
budget, scoped to fixtures that route a module to a `perry-ext-*` wrapper.
CI then showed there is a second way in — and showed it in one line, which is
the whole point of the label that commit added:
FAIL test_gap_6558_webassembly_graceful_fail
(compile TIMEOUT after 300s — killed, not rejected)
No ext-routed import, auto-optimize off, so it kept the ordinary 300 s budget.
It still builds native artifacts inside the compile:
wasm-host: building perry-wasm-host from workspace source
wasm-host (no-auto): rebuilding runtime with wasm-host feature
Cold, that compile measures 395 s here — over the line by a third, and under
the old `(compile error)` wording it would have read as a broken fixture.
It is in #10918's list of the same red for the same reason.
So the predicate gets the second property, symmetric with the first and still
a property of the test rather than its name. Across the four runs sampled
(#10859, #10918, #10892, #10930) the two together cover the entire observed
population: 13 of 14 distinct fixtures route to an ext wrapper, and the 14th
is this one. `PERRY_EXT_COMPILE_TIMEOUT` is renamed
`PERRY_TOOLCHAIN_COMPILE_TIMEOUT` — it was never only about ext wrappers, and
it has not shipped under the old name.
WITNESS — three more arms, same harness, on `test_gap_6558_webassembly_graceful_fail`
unless stated:
E PERRY_COMPILE_TIMEOUT=1 PERRY_TOOLCHAIN_COMPILE_TIMEOUT=900
-> PASS the ordinary budget is not in force
F PERRY_COMPILE_TIMEOUT=900 PERRY_TOOLCHAIN_COMPILE_TIMEOUT=1
-> FAIL (compile TIMEOUT after 1s — killed, not rejected)
the toolchain budget is
G test_gap_symbols (neither ext-routed nor wasm), PERRY_COMPILE_TIMEOUT=1
-> FAIL (compile TIMEOUT after 1s)
the ordinary budget still bites
E and F are the same fixture and the same harness with the two budgets
swapped, so they isolate which one governs; G is the control that this is not
a blanket raise.
Run 35692315543 on the previous commit: gap shards 1, 2, 3, 4 and 5 green —
including 2 and 3, which carried all three of this PR's original reds — and 6
red on this one fixture alone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands ten PRs as v0.5.1633, expedited at the owner's request for #10885, #10896 and #10901.
perf(runtime): a default-off census that says WHY each ShapeId was mintedfix(runtime): preserve dynamic parent for spread superperf(#10863): a read site whose hot key is in the overflow regionfix(http): preserve upgrades over custom connectionsfix(hir): honor lexical fetch bindingsperf(runtime): publish a transition edge when a descriptor write …perf(regex): skip the flags Get on a pristine RegExpfix(hir): preserve computed field key scopeperf(string): scan for ASCII a word at a time, and memchr a one-byte needlefix(hir,runtime):Uint8Array<ArrayBuffer>-annotated receiversAssembly
Three were stacked, not conflicting. #10899 and #10900 build on #10886, and #10901 on #10885; their first commits were already in the train, so only their unique commits were taken.
One real conflict: #10900 and #10909 each add a test function at the same point in
perry-hir/src/lower/tests.rs. The hunk had both sides truncated mid-statement, so the complete function was extracted from its source commit and spliced in. Both tests run and pass.object/mod.rsreached 2020 lines — the ten PRs add 41 on top of the 1979 left by v0.5.1632. Split the cell-metadata edge (cell_meta_slotand its#[cfg(test)]companion) intoobject/cell_meta.rs, leaving 1961. The boundary deliberately excludescell_expando_ensure, which holds the file's single raw-handle debt site; moving it would land that site in a module with no ceiling.Evidence
perry-runtimefull suite: 4233 passed, 0 failed, 0 SIGABRT — completing, not truncated.perry-hirfull suite: 471 passed, 0 failed.cargo check --workspace --all-targetsunder-D warnings: clean.cargo fmt --all -- --check: clean.check_file_size,raw_handle_debt,addr_class_inventory,gc_store_site_inventory,shape_descriptor_census,gc_runtime_root_holders,check_test_registration,native_result_ledger: all rc=0.Not run, and stated rather than implied: the gap sweep and the compiler-output suites. Expedited on the owner's request.
A note on #10900
During assembly #10900 was briefly committed with its test but without its fix — the result of resolving the conflict by hand. The test failed, which is how it was caught, and the three missing source files were restored; the landed commit matches the original exactly. Worth recording because it is the property that made the error safe: a regression test that fails when its fix is absent.