Commit Graph
169 Commits
Author SHA1 Message Date
Jimmy Miller d41b5dd3c3 Add generations to cycle detection (#30)
* runtime: pin the ScrCycHdr layout the LLVM backend hardcodes

scr_cyc_mark_live is a static inline, so there is no symbol for the LLVM
backend to call — it open-codes the retain hook as a raw `store i32 0` at
obj-16 and reaches the cycle header at obj-32. Three sites emit that
(llvm/shapes.ts, llvm/classes.ts, llvm/emitter.ts), which makes the C
struct's size and its `color` offset a real ABI between the runtime header
and hand-written IR. The stored VALUE is part of that ABI too: the emitted
store is the literal 0, not the enumerator, so SCR_CYC_BLACK's numeric value
is pinned as surely as the offsets are.

Nothing enforced any of it. llvm-runtime-abi.test.ts mechanically checks that
every `declare @scr_*` agrees with its C prototype, but that is the FUNCTION
ABI; a struct-offset skew is invisible to it, to the type system, and to the
C compiler. Narrowing `color` and putting another field in the freed bytes
compiles clean and silently zeroes that field on every retain; reordering the
color enum compiles clean and makes every retain write the wrong color.

Assert the four properties the emitted IR actually depends on, so the next
edit to this struct or that enum fails the build instead of miscompiling.

The three layout assertions bake in 64-bit pointers and size_t, which every
current target has. A 32-bit port would trip them — correctly: the backend's
hardcoded -16/-32 would need fixing too.

* runtime: collect cycles generationally

A trial-deletion pass costs O(objects reachable from its candidates), and a
candidate inside a large live structure reaches all of it. Collecting every
256 candidates therefore made total collection work quadratic in a growing
live heap: the V8 splay benchmark spent 749ms per iteration, ~99% of it in
the collector re-walking a tree that was entirely live and never garbage.

Make the collector generational, in the shape CPython uses for this same
algorithm. Each header carries a generation; a pass names the oldest one it
will walk and skips anything above it, exactly as it already skips NULL and
immortals; survivors are promoted so later nursery passes never re-walk
them. A pass then costs what was allocated since the last one rather than
the whole heap.

Restricting the walk stays sound because trial deletion is already
conservative in the safe direction: an edge from a skipped older object is
never trial-deleted, so its target keeps that count and reads as externally
referenced — the same answer the collector gives for a stack reference.
What it gives up is cycles that span generations, and the full pass (which
skips nothing, and so is exactly the old algorithm) is what finds those.

Three things that are load-bearing and were not obvious:

- A restricted pass may spare a candidate ONLY because the edge keeping it
  alive came from a generation it refused to walk. The candidate buffer is
  the collector's only root set and markRoots consumes the entry, so
  dropping such a survivor retires the one record that the object is a
  possible cycle root — and a dead cycle whose members were all spared that
  way is then invisible to every later pass, full ones included. So a
  restricted pass hands its survivors back as candidates. A full pass needs
  none of this: it walked every edge, so rc > 0 there is a genuine outside
  reference and Bacon-Rajan's own reasoning retires the candidate.

- Live data that is unlinked INTO a dead cycle grows no counter: those
  objects were tallied when they were allocated, and they are not in the
  nursery. A heap-growth trigger alone therefore never fires for a program
  that churns its long-lived structures, so the mature candidate buffer's
  own size is a second full-pass trigger.

- Restricting the walk leaves one edge accounted by NOBODY. markGray does
  not trial-delete an edge into a skipped generation, and the teardown
  contract has free_fn release exactly the UNTRACED children — because every
  traced edge was supposed to have been decremented by markGray. So freeing
  a white object that holds a traced edge into an older generation drops the
  edge while the target keeps the count, and that phantom reference reads as
  an external one to every later pass, full ones included: the target and
  everything it links to would never be reclaimable again.

  The white set pays those edges off explicitly, in two halves, because the
  walk cannot do it inline — a release there could free a survivor that
  scr_promote still points at, or cascade into the white set mid-teardown.
  Each target is PINNED with a retain while `gen` still holds the values the
  walk filtered on, and after the teardowns the pin is dropped and the edge
  given up for real. Two decrements per entry, because the pin is one of
  them; a single release would only undo the pin and leave the phantom edge
  where it was. The pin is also what makes the second decrement safe, since
  a teardown's releases of untraced children can reach a cycle-headered
  object — that is how they re-buffer survivors. One entry per skipped edge,
  so several edges sharing a target settle it once each. Nothing pinned can
  be in the white set: a skipped older object's edges were never
  trial-deleted, so anything it references was scan-blacked rather than
  whitened, by the same conservatism above.

The event loop's between-turns collection now runs one scheduled pass
instead of scr_collect_cycles(); a full sweep per turn would walk the whole
live heap every turn, which is the cost the generations exist to avoid.
scr_collect_cycles() stays the exit / session-reset sweep, where the RC
audit runs straight after and wants nothing reclaimable left.

757-cycle-cross-generation.ts covers the third point: a live list mutated
until its nodes promote, then dead nursery cycles holding traced edges into
it. Unaccounted, the sanitized lane reports 60 objects live at exit — the
whole list, held by phantom counts.

Measured, best-of-7 against the previous collector:

  splay          620.45ms -> 1.45ms   (428x)
  deltablue         1.25  -> 0.81     (1.55x)
  richards          1.196 -> 1.162    (1.03x)
  crypto/navier-stokes/raytrace/regexp   unchanged

  an LRU cache (200k entries, 200k ops)  32137ms -> 93ms   (345x)
  a large live cache + 500 event-loop turns  3095ms -> 776ms  (4.0x)

The trade is space: floating cyclic garbage is now a bounded fraction of the
live heap instead of a fixed candidate count, so peak RSS on
cyclic-garbage-heavy programs runs ~1.4x the old collector's. Bounded and
flat in run length, not a leak — verified by holding a 200k-object live heap
while churning 200k dead cycles per round for 30 rounds.

Test lanes show byte-identical failure sets against the previous collector:
Linux sanitized 164, Linux plain 528, macOS differential corpus 66 — all
pre-existing on those machines, none introduced or fixed here.

* runtime: bound scheduled mature cycle backlog

* runtime: rearm cycle trigger after candidate deaths
2026-08-08 11:32:45 -05:00
Chris Tate adae252881 Prepare v0.0.22 release (#87)
- Stamp all publishable packages at v0.0.22.

- Publish release notes for persistent builds and external coverage types.

- Refresh the compiler surface manifest for the release.
2026-08-03 01:31:43 -05:00
Chris Tate 9fb068ae27 Merge pull request #77 from vercel-labs/ctate/node24-fetch-inventory
feat(compat): inventory Node 24 fetch surface
2026-08-02 16:37:36 -05:00
Chris Tate 7de8be18c0 fix(runtime): reject inherited bodies for GET and HEAD 2026-08-02 16:09:08 -05:00
Chris Tate efb7e77281 fix: address fetch compatibility review findings 2026-08-02 15:31:00 -05:00
Chris Tate c356ff0ac0 Merge pull request #73 from vercel-labs/ctate/no-persist-compile
Persist native compilation cache
2026-08-02 14:58:35 -05:00
Chris Tate 3f2a78894e fix(fetch): address Node 24 compatibility gaps 2026-08-02 13:28:46 -05:00
Chris Tate 0b79e36973 fix: align fetch runtime with Node 24 semantics 2026-08-02 12:29:31 -05:00
Chris Tate f8377a7440 fix fetch computed dispatch semantics 2026-08-02 11:59:05 -05:00
Chris Tate 8c20c9ac73 fix: validate computed fetch dispatch 2026-08-02 11:29:23 -05:00
Chris Tate af685d05f9 fix: close fetch inventory review gaps 2026-08-02 10:41:37 -05:00
Chris Tate b90c0466b2 fix: enforce fetch surface compatibility 2026-08-02 01:09:40 -05:00
Chris Tate 53e07fc55c fix: harden native build caches 2026-08-01 15:39:44 -05:00
Chris Tate 89b4121ebe Harden persistent compile caches 2026-08-01 15:39:44 -05:00
Chris Tate 2d0bc2df0b Add generated Node 24 fetch conformance
- Pin the supported fetch surface to an explicit Node and Undici profile.
- Generate deterministic WebIDL, AbortSignal, and stream differential campaigns.
- Match Node stream pull scheduling while preserving typed source identity.
2026-08-01 15:33:37 -05:00
Chris Tate 0d3c006c0d Merge pull request #52 from vercel-labs/ctate/fix-fetch
feat: make fetch static-compatible
2026-08-01 14:49:41 -05:00
Chris Tate 6cf7cd9220 fix: align fetch APIs with Node coercion 2026-08-01 14:22:07 -05:00
Chris Tate bc00d3d65f fix(fetch): preserve WebIDL conversion semantics 2026-08-01 12:31:13 -05:00
Chris Tate 1544ccda88 fix(fetch): align static stream and body coercion 2026-08-01 09:10:35 -05:00
Chris Tate 5985d54a27 fix: preserve fetch coercion semantics 2026-08-01 08:28:01 -05:00
Chris Tate a6a68ea861 fix(fetch): preserve coercion and call semantics 2026-08-01 07:46:54 -05:00
Chris Tate 9db07aa05f fix fetch compatibility edge cases 2026-07-31 21:08:43 -05:00
Chris Tate 54ad052a98 fix(fetch): preserve live callback semantics 2026-07-31 19:52:47 -05:00
Chris Tate 9a6cc8f239 fix(fetch): preserve live Web API references 2026-07-31 18:53:28 -05:00
Chris Tate b7da02e53b fix(fetch): preserve typed reference semantics 2026-07-31 17:21:52 -05:00
Chris Tate c4ee48ea03 fix fetch stream parity edge cases 2026-07-31 15:58:29 -05:00
Chris Tate 07c131a55b fix: address remaining fetch review issues 2026-07-31 15:03:19 -05:00
Chris Tate 785d9ba3f6 fix: preserve static fetch response types 2026-07-31 13:59:23 -05:00
Chris Tate c25b5304b0 fix: address fetch runtime review issues 2026-07-31 13:18:55 -05:00
Chris Tate 78144ec8be fix(fetch): bind Web Stream source callbacks 2026-07-31 12:00:29 -05:00
Chris Tate f19df5a1ee fix(fetch): align listener and stream behavior 2026-07-31 11:35:28 -05:00
Chris Tate 5982347cf1 fix: preserve static stream value identity 2026-07-31 10:44:31 -05:00
Chris Tate 52c66e2286 fix(fetch): accept string request stream chunks 2026-07-31 09:03:40 -05:00
Chris Tate 47ec3b30f8 fix(runtime): align fetch and abort signal semantics 2026-07-31 08:30:23 -05:00
Chris Tate f21d489347 fix(fetch): support streamed bodies and redirect modes 2026-07-31 00:53:13 -05:00
Chris Tate ffaefc6ced fix: snapshot fetch environment at startup 2026-07-31 00:05:24 -05:00
Chris Tate fdb01b360c Prepare v0.0.21 release
- Stamp all publishable packages at v0.0.21.

- Publish release notes for the contract-sidecar fixes.

- Refresh the compiler surface manifest for the release.
2026-07-30 23:08:27 -05:00
Chris Tate 334febe43d fix(fetch): address remaining review findings 2026-07-30 22:48:36 -05:00
Chris Tate eec2cb2a3a fix(fetch): address static fetch review findings 2026-07-30 22:02:47 -05:00
Chris Tate baf050843d fix fetch proxy and event dispatch behavior 2026-07-30 21:19:57 -05:00
Chris Tate f318308079 fix(fetch): handle redirects and callback lifetimes 2026-07-30 20:14:04 -05:00
Chris Tate 6451bc80f6 fix(fetch): preserve proxy and network error semantics 2026-07-30 17:30:51 -05:00
Chris Tate fdf5010b28 Prepare v0.0.20 release
- Stamp all publishable packages at v0.0.20.

- Publish release notes for the contract-sidecar improvements.

- Refresh the compiler surface manifest for the release.
2026-07-30 16:26:39 -05:00
Chris Tate b14b594775 fix(fetch): preserve proxy and listener semantics 2026-07-30 16:26:34 -05:00
Chris Tate 861c0c68ae fix fetch abort and listener identity semantics 2026-07-30 15:07:52 -05:00
Chris Tate 6f0adcf7d9 fix(fetch): harden redirects and response handling 2026-07-30 14:27:35 -05:00
Chris Tate fb0b08a56f fix(fetch): settle abandoned responses and decode redirects 2026-07-30 13:51:18 -05:00
Chris Tate 1bd7e4af2e Prepare v0.0.19 release
- Stamp all published packages and the compiler surface manifest at 0.0.19.
- Promote the completed integer-slot fixes into the marked release notes.
2026-07-30 13:36:35 -05:00
Chris Tate 6955a5d267 fix fetch proxy and redirect edge cases 2026-07-30 13:15:10 -05:00
Chris Tate 8a204cc7e7 fix(fetch): address remaining review regressions 2026-07-30 12:33:26 -05:00