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
Splitting the one unresolved item out of #8082 so the production App Route fixture can land on its own merits. The fixture's normal arms pass end-to-end; its forced-evacuation arms do not, and this is that bug.
Symptom
With the Next 16.3.0 production App Route fixture running under forced evacuation and a seeded GC schedule:
the server serves requests and then fails with TypeError: value is not a function, at roughly copying minor #238.
What it is not (each ruled out by measurement)
Not the statepoint lowering. The same application compiled with PERRY_RS4GC=0 (shadow-stack roots) fails identically — 80 TypeErrors in each arm, same seed, providers and workload. So it is neither fix(gc): index stack maps from loaded provider apps #8081's loaded-image stack maps nor compiled-frame root coverage.
Not the remembered set.skip_remembering never fires (336/336 cycles report remembering_skipped=false, and in-place promotion never happens), and remembered-set coverage is stable — zero cycles lose dirty pages between collections.
Not a holder in the GC heap.PERRY_GC_PROTECT_FROMSPACE_HOLDERS=1 (added in fix(next): pass production App Route dylib gate #8082) sweeps the whole live heap at fault time for any word still decoding to the faulting address and finds none.
Not a slot the rewrite pass enumerates.PERRY_GC_VERIFY_EVACUATION=1 never panics, so every registered scanner rewrites correctly.
Taken together: the stale copy is held by something unregistered and outside the GC heap — a runtime side table, an FFI structure, or a frame slot.
Where the fault lands
Under PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800, the stale value is a 48-byte closure consumed by js_value_typeof inside Next's compiled adapter:
reached through js_proxy_get (the adapter is a Proxy get trap over the wrapped route-module object). js_reflect_get itself is fully rooted, so the pre-move copy lives in whatever supplies that property read.
Suggested starting point
scripts/gc_runtime_root_holders.py audits crates/perry-runtime/src and crates/perry-stdlib/src only — pointed at the ext crates it matches 0 declarations, because its patterns do not parse them. Yet every ext crate that invokes closures (perry-ext-http, -net, -ws, -zlib, -fastify) keeps user closures in handle side tables and registers its scanners by hand, with nothing checking that a scanner reaches every table in its own crate. Extending that audit is the obvious next move.
Reproducing in seconds
Build the providers per tests/release/packages/next-app-route/fixture.sh steps 3–5 into a kept build dir (PERRY_NEXT_ROUTE_KEEP_BUILD=1), then run the provider host directly with the env above plus one node verify.mjs. PERRY_GC_FROMSPACE_SCAN=1 mines holders at the retiring minor — but read its counts carefully: it reports dead old-gen residue and raw payload bytes as offenders (impossible obj_types like 47/95/144), which cost this hunt a retracted diagnosis. The protect fault is the trustworthy signal.
The fixture ships this arm behind PERRY_NEXT_ROUTE_FORCED_GC=1, off by default, so it fails loudly for whoever picks this up rather than sitting green.
Splitting the one unresolved item out of #8082 so the production App Route fixture can land on its own merits. The fixture's normal arms pass end-to-end; its forced-evacuation arms do not, and this is that bug.
Symptom
With the Next 16.3.0 production App Route fixture running under forced evacuation and a seeded GC schedule:
the server serves requests and then fails with
TypeError: value is not a function, at roughly copying minor #238.What it is not (each ruled out by measurement)
PERRY_RS4GC=0(shadow-stack roots) fails identically — 80TypeErrors in each arm, same seed, providers and workload. So it is neither fix(gc): index stack maps from loaded provider apps #8081's loaded-image stack maps nor compiled-frame root coverage.skip_rememberingnever fires (336/336 cycles reportremembering_skipped=false, and in-place promotion never happens), and remembered-set coverage is stable — zero cycles lose dirty pages between collections.PERRY_GC_PROTECT_FROMSPACE_HOLDERS=1(added in fix(next): pass production App Route dylib gate #8082) sweeps the whole live heap at fault time for any word still decoding to the faulting address and finds none.PERRY_GC_VERIFY_EVACUATION=1never panics, so every registered scanner rewrites correctly.Taken together: the stale copy is held by something unregistered and outside the GC heap — a runtime side table, an FFI structure, or a frame slot.
Where the fault lands
Under
PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800, the stale value is a 48-byte closure consumed byjs_value_typeofinside Next's compiled adapter:reached through
js_proxy_get(the adapter is a Proxygettrap over the wrapped route-module object).js_reflect_getitself is fully rooted, so the pre-move copy lives in whatever supplies that property read.Suggested starting point
scripts/gc_runtime_root_holders.pyauditscrates/perry-runtime/srcandcrates/perry-stdlib/srconly — pointed at the ext crates it matches 0 declarations, because its patterns do not parse them. Yet every ext crate that invokes closures (perry-ext-http,-net,-ws,-zlib,-fastify) keeps user closures in handle side tables and registers its scanners by hand, with nothing checking that a scanner reaches every table in its own crate. Extending that audit is the obvious next move.Reproducing in seconds
Build the providers per
tests/release/packages/next-app-route/fixture.shsteps 3–5 into a kept build dir (PERRY_NEXT_ROUTE_KEEP_BUILD=1), then run the provider host directly with the env above plus onenode verify.mjs.PERRY_GC_FROMSPACE_SCAN=1mines holders at the retiring minor — but read its counts carefully: it reports dead old-gen residue and raw payload bytes as offenders (impossibleobj_types like 47/95/144), which cost this hunt a retracted diagnosis. The protect fault is the trustworthy signal.The fixture ships this arm behind
PERRY_NEXT_ROUTE_FORCED_GC=1, off by default, so it fails loudly for whoever picks this up rather than sitting green.