Skip to content

Give async bodies and generators 512 KiB stacks - #807

Merged
ctate merged 3 commits into
mainfrom
ctate/give-async-bodies-2f4c4f
Oct 9, 2026
Merged

ctate merged 3 commits into
mainfrom
ctate/give-async-bodies-2f4c4f

Conversation

@ctate

@ctate ctate commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Async functions and generators run on fiber stacks of their own. The stack-overflow guard keeps a quarter of each stack in reserve for unwinding and error reporting. On 256 KiB fibers that left 192 KiB of frames, so recursion that Node runs fine inside an async body or generator threw RangeError: Maximum call stack size exceeded. On main this fails tests/corpus/native-worker-stack-overflow.ts on macOS arm64 in dev builds, where 3000 unoptimized frames of the worker's top-level code (an async module body) no longer fit. The same thing reproduces on Linux at greater depth: Node runs 8000 frames inside an async function, while the native build threw.

  • Fibers get 512 KiB (8 MiB under ASan, unchanged), so 384 KiB remains after the reserve: twice the current depth and 1.5× the depth before the guard. limitations.mdx is updated.
  • New corpus program stack-depth-async-bodies.ts pins recursion inside async functions and generators, alongside deeper recursion on the main stack.

Measured with 20,000 concurrent async functions each awaiting a timer (release, median of 9 alternating runs; stack pages are still committed lazily):

Fiber stack Time Max RSS
256 KiB 0.300 s 105.8 MB
512 KiB 0.310 s about 105.8 MB
1 MiB 0.340 s 106.0 MB

512 KiB keeps fiber creation and teardown cost within timer resolution. 1 MiB cost about 13% on this fiber-churn workload, likely from page-table overhead for sparser mappings, so 512 KiB is the better trade.

Validation: 380 async, await, generator, promise, worker, timer, island and stack programs pass in both differential lanes with a runtime pack built from this tree. Runtime tests and runtime-tree-shaking pass, apart from the string oracle, which is environment-only. TypeScript preflight/order baselines are complete and the new batch passes. Lint and format are clean.

Async functions and generators run on their own fiber stacks. The new
stack-overflow guard keeps a quarter of each stack as a reserve for
unwinding and error reporting, which cut the depth they could recurse to
from 256 KiB to 192 KiB of frames. Moderately deep recursion inside an
async body or generator then threw RangeError where Node runs it, for
example 3000 frames of an unoptimized build on arm64 macOS.

Fibers now get 512 KiB, which leaves 384 KiB after the reserve. Pages are
still committed lazily, so idle and shallow fibers cost address space, not
memory. The corpus pins recursion inside async functions and generators
next to deeper recursion on the main stack.
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 9, 2026 10:55pm UTC

@vercel vercel Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

The non-ASan island engine-stack budget (128 KiB) is now a quarter of the fiber stack size (raised to 512 KiB in this PR) instead of the documented half, breaking the stack-budget invariant stated in both scr_island.c and scr_async.c.

Fix on Vercel

The stack guard was a tentative definition, which some targets emit as a
common symbol. Localized library archives deliberately leave common
symbols shared, so on macOS the archive defined scr_stack_guard alongside
its declared exports. An explicit initializer makes it an ordinary
definition that localization hides like the rest of the runtime.
Embedded engine calls entered from a fiber get a stack budget documented
as half the fiber stack, leaving the other half as margin. The budget
stayed at 128 KiB when fibers grew to 512 KiB, so it is now 256 KiB,
which restores that ratio.
@ctate
ctate merged commit e455490 into main Oct 9, 2026
76 of 77 checks passed

This branch was successfully deployed

1 active deployment
Preview — 6d8eeb27 Deployed Oct 9, 2026 by vercel[bot]
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