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
A program that spawns background async work (e.g. a fire-and-forget fetch().then(...)) and returns to the event loop hangs forever on the second compile — the spawned task never runs. The first compile of the same source works.
importhttpfrom"node:http";// pulls the perry-ext-http wrapper into the linkconsole.log("http keys:",typeofhttp.STATUS_CODES);letdone=false;fetch("https://example.com/").then((r)=>{done=true;console.log("FETCH DONE",r.status);});letn=0;constiv=setInterval(()=>{n++;console.log("tick",n,"done=",done);if(done||n>=12){clearInterval(iv);console.log(done ? "PASS" : "FAIL: fetch starved");process.exit(done ? 0 : 1);}},1000);
compile linux compilation and README #2 (auto-optimize "archives fresh" fast path): binary ticks 12× with done= false → FAIL: fetch starved. Timers fire; only spawned tasks starve. sample shows the main thread parked in js_wait_for_event's condvar fallback (the branch taken when no wait-driver is registered).
Mechanism
The archives-fresh fast path returns prefer_well_known_before_stdlib: true (optimized_libs/driver.rs), so the well-known wrapper archives are linked beforelibperry_stdlib.a. The full-rebuild path returns false. That flips the link between compiles of identical source.
libperry_ext_http.a bundles its whole Rust dep graph — including a complete copy of the perry_runtime codegen unit. In the wrappers-first shape, that copy is the first definition of every extern runtime symbol, so the user object's calls (js_wait_for_event, js_promise_run_microtasks, …) bind to the wrapper's runtime copy. ld64 reports it:
ld: warning: duplicate symbol '_js_atomics_exchange' in:
.../_libperry_ext_http.a_trimmed.lib[12](perry_runtime-<h>.perry_runtime.<h>-cgu.0.rcgu.o)
.../libperry_stdlib.a[183](perry_stdlib-<h>.perry_runtime-<h>.perry_runtime.<h>-cgu.0.rcgu.o.rcgu.o)
(~3,900 such warnings in a large app's link.)
Meanwhile perry-stdlib's own code does not go through those extern symbols: its staticlib LTO internalized the runtime crate and promoted the internals it uses to hidden .llvm.-suffixed names, resolvable only by stdlib's own bundled runtime member:
$ nm -A libperry_stdlib.a | grep 'U .*llvm'
...perry_stdlib...cgu.o: U __ZN13perry_runtime10event_pump17WAIT_DRIVER_SLEEP17h...E.0.llvm.12589...
...perry_stdlib...cgu.o: U __ZN13perry_runtime10event_pump8NOTIFIED17h...E.llvm.12589...
...perry_stdlib...cgu.o: U __ZN13perry_runtime7promise10microtasks14run_microtasks17h...E.llvm.12589...
...perry_stdlib...cgu.o: U __ZN13perry_runtime9exception15EXCEPTION_STATE...llvm.12589...
So the process runs two disjoint copies of the runtime's mutable globals: two event-pump wait-driver slots, two NOTIFIED condvar states, two microtask queues, two exception states. The async bridge's spawn() → ensure_pump_registered() → js_register_wait_driver stores the driver in stdlib's copy; the main loop's js_wait_for_event — resolved from the wrapper's copy — reads a never-written slot, falls back to the condvar park, and never drives the current-thread runtime. Every spawned task starves. (block_on-style awaits still work, which is why simple awaited-fetch programs don't reproduce.)
Related: #5892 / #5911 fixed the cargo-test flavor of this family (test-only perry_ffi_* shims shipping in ext archives). This one is the perry-compile flavor — not shims, the entire bundled runtime cgu — and still fires with #5911's gate in place.
Fix direction
In the wrappers-before-stdlib link shape, drop the wrapper's bundled perry_runtime-* member(s) when it is safe, so stdlib's copy is the single provider (evidence-based per the v0.5.331 dedup standard):
the stdlib archive bundles the same codegen unit (stdlib's packaging renames members to perry_stdlib-<h>.<original-name>.rcgu.o, so match by name containment — same crate+cgu hash ⇒ same rlib input);
every symbol the member defines that a sibling member references is also defined by stdlib (a sibling depending on one of the copy's .llvm. internals keeps the member — fail-safe).
The standalone libperry_runtime.a linked after stdlib keeps filling DCE gaps as today. With this, the repro's second compile logs [strip-dedup] libperry_ext_http.a: dropped 1 bundled perry-runtime member(s), duplicate-symbol warnings drop ~4×, and the program passes in both link shapes.
Symptom
A program that spawns background async work (e.g. a fire-and-forget
fetch().then(...)) and returns to the event loop hangs forever on the second compile — the spawned task never runs. The first compile of the same source works.FETCH DONE→PASS.done= false→FAIL: fetch starved. Timers fire; only spawned tasks starve.sampleshows the main thread parked injs_wait_for_event's condvar fallback (the branch taken when no wait-driver is registered).Mechanism
The archives-fresh fast path returns
prefer_well_known_before_stdlib: true(optimized_libs/driver.rs), so the well-known wrapper archives are linked beforelibperry_stdlib.a. The full-rebuild path returnsfalse. That flips the link between compiles of identical source.libperry_ext_http.abundles its whole Rust dep graph — including a complete copy of theperry_runtimecodegen unit. In the wrappers-first shape, that copy is the first definition of every extern runtime symbol, so the user object's calls (js_wait_for_event,js_promise_run_microtasks, …) bind to the wrapper's runtime copy. ld64 reports it:(~3,900 such warnings in a large app's link.)
Meanwhile perry-stdlib's own code does not go through those extern symbols: its staticlib LTO internalized the runtime crate and promoted the internals it uses to hidden
.llvm.-suffixed names, resolvable only by stdlib's own bundled runtime member:So the process runs two disjoint copies of the runtime's mutable globals: two event-pump wait-driver slots, two
NOTIFIEDcondvar states, two microtask queues, two exception states. The async bridge'sspawn()→ensure_pump_registered()→js_register_wait_driverstores the driver in stdlib's copy; the main loop'sjs_wait_for_event— resolved from the wrapper's copy — reads a never-written slot, falls back to the condvar park, and never drives the current-thread runtime. Every spawned task starves. (block_on-style awaits still work, which is why simple awaited-fetch programs don't reproduce.)Related: #5892 / #5911 fixed the cargo-test flavor of this family (test-only
perry_ffi_*shims shipping in ext archives). This one is the perry-compile flavor — not shims, the entire bundled runtime cgu — and still fires with #5911's gate in place.Fix direction
In the wrappers-before-stdlib link shape, drop the wrapper's bundled
perry_runtime-*member(s) when it is safe, so stdlib's copy is the single provider (evidence-based per the v0.5.331 dedup standard):perry_stdlib-<h>.<original-name>.rcgu.o, so match by name containment — same crate+cgu hash ⇒ same rlib input);.llvm.internals keeps the member — fail-safe).The standalone
libperry_runtime.alinked after stdlib keeps filling DCE gaps as today. With this, the repro's second compile logs[strip-dedup] libperry_ext_http.a: dropped 1 bundled perry-runtime member(s), duplicate-symbol warnings drop ~4×, and the program passes in both link shapes.