Skip to content

[Web] Keep large byte arguments and suspended calls off the reusable call stack - #20500

Open
akaashrp wants to merge 3 commits into
apache:mainfrom
akaashrp:memory/05-transient-ffi-bytes
Open

akaashrp wants to merge 3 commits into
apache:mainfrom
akaashrp:memory/05-transient-ffi-bytes

Conversation

@akaashrp

@akaashrp akaashrp commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Three lifetime problems in the packed-call path of the web runtime.

  1. Five helpers took a pooled call stack and returned it only on success, so a TVM error inside the call left the stack unrecycled and the next call allocated another one. They now return it in a finally block.
  2. Large byte arguments grow the reusable call stack, and one large call raises its capacity for the rest of the session. Byte payloads now stay out of the reusable stack once it reaches a 64 KiB soft limit and go into a per-call Wasm allocation that is released when the call finishes or throws, including across an Asyncify suspension. Offsets of Wasm-backed views are recorded before any allocation can grow linear memory and detach them.
  3. A packed function can be disposed through another wrapper while an Asyncify call is suspended, so the rewound call runs on a freed handle. The call frame now takes a reference when the call first unwinds and drops it when the frame is released. Synchronous calls do not take the reference.

In a Node microbenchmark a synchronous call with one int argument goes from 123 ns to about 165 ns, and 1 MiB byte arguments are 15 percent faster because they no longer grow the stack. Tests cover byte arguments across the soft limit, Wasm views that detach during a call, alias disposal during an Asyncify call, and frame cleanup that throws.

getGlobalFunc, typeKeyToIndex, registerFunc, withNewScope and the Wasm function factory took a pooled call stack and only returned it on the success path. A TVM error raised inside the call left the stack unrecycled, so the next call allocated a fresh one and the old buffer stayed alive with the instance. Return the stack in a finally block.
Large byte arguments currently grow the reusable PackedFunc call stack, so one large call permanently raises its capacity.

Keep byte payloads out of the reusable stack once it reaches a 64 KiB soft limit, and place larger payloads in per-call Wasm allocations. Keep those allocations with the prepared frame through Asyncify suspension and release them when the call finishes or fails.

Record offsets for Wasm-backed views before an allocation can grow linear memory and detach them.
Asyncify can suspend a prepared call after the JavaScript wrapper has been disposed through another alias. Take an extra reference on the PackedFunc when the call first unwinds and release it with the frame, so the rewound call still has a live callee. Synchronous calls do not take the reference.

Release the retained handle with the frame, including when cleanup fails.
@akaashrp
akaashrp force-pushed the memory/05-transient-ffi-bytes branch from f11d8b0 to 079fc38 Compare September 30, 2026 07:16

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant