-
-
Notifications
You must be signed in to change notification settings - Fork 159
fix(runtime): release closure-visible async boxes #8303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| ## Fix completed async frames retaining closure-visible box cells | ||
|
|
||
| Plain async functions now hand their complete boxed activation frame to the | ||
| release/reuse path. Closure capture counts follow GC moves, and authoritative | ||
| death pruning drops them. Full collections trace drained box payloads from | ||
| their live closures instead of rooting every pending box, so self-referential | ||
| box/closure cycles can die. After queued/running steps drain, uncaptured cells | ||
| publish immediately while each captured cell remains readable until its own | ||
| final closure dies, so one escaped closure cannot retain the rest of the frame. | ||
|
|
||
| This removes the closure-visible residue left by #8208 without weakening raw | ||
| box-pointer rejection or returning box-cell memory to the allocator. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -186,27 +186,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> { | |
| // inner arrow `(eid, arch, compId) => ... changeset ...` | ||
| // is created per-call but always with the same `this` (the | ||
| // World) and same captures (`this._changeset`). | ||
| // Boxed captures still allow the cache path: the closure | ||
| // stores the BOX POINTER (a stable per-allocation address), | ||
| // and the box's contents are read dynamically inside the | ||
| // body via `js_box_get`. Two closure-literal sites that | ||
| // capture the same boxed local store identical box-pointer | ||
| // bits, so the cache (keyed on bit-equality of capture | ||
| // slots) still hits. The cache backing is a small LRU per | ||
| // func_ptr, which tolerates the parallel-instance pattern | ||
| // (50 concurrent unitOfWork calls each capturing a | ||
| // different `__async_step` box) by holding multiple | ||
| // captures rather than overwriting one slot per call. | ||
| // | ||
| // We previously bailed out when any captured local was | ||
| // boxed (`mutable_captures` non-empty). That made the | ||
| // async-to-generator transform's per-`await` `cb_v` / | ||
| // `cb_e` closures (which capture the boxed `__async_step` | ||
| // self-reference) miss the cache 100% of the time — | ||
| // 2 fresh closure allocs per await ≈ 300 ns of `gc_malloc` | ||
| // work even though the box pointers are stable across | ||
| // call sites. The relaxed gate plus the multi-slot LRU | ||
| // backing reclaims that overhead. | ||
| // Boxed captures may use the bulk cache helper, but codegen still | ||
| // follows it with `js_closure_set_box_capture_ptr` for only those | ||
| // slots. The idempotent write declares exact lifetime edges | ||
| // without guessing from arbitrary pointer-shaped values. | ||
| // | ||
| // IDENTITY CAVEAT (#4831 follow-up — Stripe `protoExtend`): | ||
| // the singleton-sharing paths (`js_closure_alloc_singleton` / | ||
|
|
@@ -231,25 +214,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> { | |
| // symptom. | ||
| // | ||
| // To preserve the hot-path optimizations while restoring identity, | ||
| // a closure is singleton-eligible only when sharing one instance is | ||
| // observationally safe: | ||
| // - arrow functions: no own `.prototype`, not constructable, the | ||
| // `.map`/ECS callbacks the cache targets; OR | ||
| // - non-arrow closures all of whose captures are BOXED (mutable) | ||
| // locals: the compiler-synthesized async-step `cb_v`/`cb_e` | ||
| // per-await callbacks capture the boxed `__async_step` self-ref | ||
| // and are never used as constructors — keeping them cached | ||
| // avoids 2 `gc_malloc`s per `await`. | ||
| // A non-arrow closure capturing an UNBOXED value (Stripe's `Super`, | ||
| // or no captures at all) is treated as a potential constructor and | ||
| // always gets a fresh instance. | ||
| // arrow functions are singleton-eligible because they have no own | ||
| // `.prototype` and are not constructable. Compiler-synthesized | ||
| // non-arrow async callbacks whose captures are all boxes are also | ||
| // safe: they are never constructors and their cache key includes | ||
| // the box addresses. Other non-arrow closures are treated as | ||
| // potential constructors and always get a fresh instance. | ||
| let mut write_ids = std::collections::HashSet::new(); | ||
| crate::boxed_vars::collect_write_ids_in_stmts(body, &mut write_ids); | ||
| let writes_unboxed_capture = auto_captures | ||
| .iter() | ||
| .any(|cap_id| !ctx.boxed_vars.contains(cap_id) && write_ids.contains(cap_id)); | ||
| // All captures boxed (and at least one), with no reserved `this` / | ||
| // `new.target` slot: the compiler-synthesized async-callback shape. | ||
| let captures_all_boxed = !*captures_this | ||
| && !*captures_new_target | ||
| && !auto_captures.is_empty() | ||
|
|
@@ -346,15 +321,25 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> { | |
| .call_void("js_register_closure_async_function", &[(PTR, &func_ref)]); | ||
| } | ||
|
|
||
| // The captured-singleton helper writes captures internally | ||
| // (so the cached layout matches a fresh allocation). The | ||
| // other paths still need explicit per-slot writes. | ||
| if !captured_singleton { | ||
| let blk = ctx.block(); | ||
| for (idx, val_bits) in captured_value_bits.iter().enumerate() { | ||
| // The captured-singleton helper writes captures internally. Boxed | ||
| // slots still take the dedicated, idempotent setter afterward so | ||
| // their lifetime edges are declared; fresh closures need every | ||
| // slot initialized here. | ||
| let boxed_capture_slots = auto_captures | ||
| .iter() | ||
| .map(|cap_id| ctx.boxed_vars.contains(cap_id)) | ||
| .collect::<Vec<_>>(); | ||
| let blk = ctx.block(); | ||
| for (idx, val_bits) in captured_value_bits.iter().enumerate() { | ||
| if !captured_singleton || boxed_capture_slots[idx] { | ||
| let idx_str = idx.to_string(); | ||
| let setter = if boxed_capture_slots[idx] { | ||
| "js_closure_set_box_capture_ptr" | ||
|
Comment on lines
+328
to
+337
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Exclude module globals from boxed-capture classification. When This predicate checks only Use one predicate for actual boxed locals. Exclude Proposed predicate change- .map(|cap_id| ctx.boxed_vars.contains(cap_id))
+ .map(|cap_id| {
+ ctx.boxed_vars.contains(cap_id)
+ && !ctx.module_globals.contains_key(cap_id)
+ })🤖 Prompt for AI Agents |
||
| } else { | ||
| "js_closure_set_capture_bits" | ||
| }; | ||
| blk.call_void( | ||
| "js_closure_set_capture_bits", | ||
| setter, | ||
| &[(I64, &closure_handle), (I32, &idx_str), (I64, val_bits)], | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add affected paths and validation notes.
This fragment describes the behavior, but it does not identify the affected files or record the validation performed for
#8213. Add a short root-cause explanation, affected paths, and the runtime, codegen, regression, and audit checks that passed.Based on learnings, Perry defect-fix fragments include a long-form root-cause explanation, affected file paths, and validation notes.
🤖 Prompt for AI Agents
Source: Learnings