fix(thread): worker gc-init, rooted rebuilt closures, cross-thread promises in malloc space, ws deferred resolution#6188
Conversation
…es, cross-thread promises in malloc space, ws deferred string resolution
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThis PR introduces a cross-thread promise allocation path that forces malloc-space allocation instead of nursery allocation, applies it to atomics wait and spawn operations, and adds GC initialization plus handle-scope rooting across parallelMap/parallelFilter/spawn worker threads. It also defers websocket message string construction to the main thread. ChangesCross-thread GC Safety and Promise Allocation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant TokioWorker
participant DeferredQueue
participant MainThread
participant Promise
TokioWorker->>DeferredQueue: queue_deferred_resolution(message)
DeferredQueue->>MainThread: dispatch closure
MainThread->>MainThread: construct StringHeader
MainThread->>Promise: resolve with JSValue::string_ptr(...).bits()
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the 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 |
Wave 1 of the 2026-07-09 GC audit (
fable-audit-perry-gc.md), threading dimension — the quick, mechanical subset. The structural cross-heap questions (owner-tagged queues, module globals in thread closures) are tracked in #6185; nothing here changes the threading model.1. Worker threads could collect with zero registered root scanners (P1)
MUTABLE_ROOT_SCANNERSis thread-local andensure_gc_initializedwas wired only throughjs_get_global_this. Aspawn/parallelMap/parallelFilterworker whose closure never reads a global but allocates past a trigger (e.g. deserializing large captures/elements) ran a collection with an empty scanner registry and an empty shadow stack — sweeping everything it had just built (singleton-closure caches, boxes, class side tables all invisible). Every worker entry now callsensure_gc_initialized()before its first allocation. It is idempotent and cheap.2. Rebuilt worker closures were unrooted across deserialization (P1)
The worker-side
local_closurewas a bare Rust local; everydeserialize_nanbox_on_current_threadbetweencall_fninvocations can allocate and trigger a worker GC, sweeping the closure and its captures mid-loop. The rebuilt closure is now rooted in aRuntimeHandleScopeand re-derived from the handle at each call (spawn body, parallelMap worker, parallelFilter worker).3. Cross-thread promises moved out of the copying nursery (P1)
spawn()/Atomics.waitAsyncpin a promise and hand its rawusizeto the globalPENDING_THREAD_RESULTSqueue, which no root scanner visits. The copied-minor from-space flip resets eden/survivor blocks wholesale — only root-reachable pins force the fallback — so a fire-and-forgetspawnpromise could be recycled while the worker still held its address;js_thread_process_pendingthen resolved through freed memory.New
js_promise_new_cross_thread()allocates these promises viagc_malloc: malloc space is non-moving, both sweep paths honorGC_FLAG_PINNED, and (since #6178) old→malloc edges are remembered. Used byspawn_implandjs_atomics_wait_async; ordinary promises are untouched.4.
ws.waitForMessageresolved with a tokio-arena string (P1)js_ws_wait_for_messageallocated the resultStringHeaderon the tokio worker's thread-local arena and queued the raw pointer for main-thread resolution — a cross-heap pointer freed under the main thread by the worker's GC — and tagged itPOINTER_TAG, so the awaited value was a string-like object (typeof === "object"). Converted to the #1292queue_deferred_resolutionpattern (bcrypt.rs): the owned bytes move into the converter closure, the JS string is built on the main thread, taggedSTRING_TAG. This was the onlyspawn_for_promisediscipline violation found in perry-stdlib.5.
single_thread_map/single_thread_filtercachedelements_ptracross user calls (P3)The input array's elements pointer was computed once and re-read after each user callback (which can trigger a moving minor). The input array is now rooted before the result allocation and the elements pointer re-derived from the handle per iteration, matching the array
iter_methods.rsdiscipline.Tests
Full
perry-runtimelib suite: 1164 passed / 0 failed; perry-stdlib compiles clean.cargo fmt --checkclean.Summary by CodeRabbit
Release Notes
Bug Fixes
Performance