Repository navigation
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Comment on lines
+265
to
+267
| const registered = process | ||
| .rawListeners(entry.event) | ||
| .find((raw) => (raw.listener ?? raw) === entry.listener); |
Contributor
Comment on lines
+4571
to
+4574
| case SCR_HOSTED_REFRESH: | ||
| if (j->firing) j->refreshed = true; | ||
| if (j->handle != 0) scr_loop_hosted->timer_refresh(j->handle); | ||
| return true; |
Contributor
Comment on lines
+247
to
+252
| process.on("removeListener", (event, listener) => { | ||
| const kind = eventKind(event); | ||
| if (kind === 0) return; | ||
| for (let i = islandListeners.length - 1; i >= 0; i--) { | ||
| const entry = islandListeners[i]; | ||
| if (entry.event === event && entry.listener === listener) { |
Contributor
Scope-B milestone NI-3. When a program loads Node in-process, its pending ticks, promise jobs, timers and immediates move onto Node's queues in order (timers keep their deadlines relative to the moment Node began loading; the work of a phase the switch interrupted finishes first) and every later one is scheduled there, so the jobs of both sides interleave as in Node. The runtime's I/O hooks are watched from Node's loop, and its liveness is re-read before each poll phase, after each check phase and after every microtask checkpoint. Node's loop runs on Node's stack, and V8's limit follows whichever stack an entry comes from. Island signal and exit listeners join the runtime's registries at their position, so the runtime keeps owning every signal disposition; Node's own 'exit' emission is left to the runtime's exit sequence, and the exit code island code leaves in process.exitCode becomes the program's. Bridge ABI 3. Ordering programs (tests/corpus-node-islands-order, harness node-island-order.test.ts), corpus programs 6804-6811 and the node-island-process harness pin the behavior against Node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cramforce
force-pushed
the
ni/3-hosted-loop
branch
from
October 21, 2026 20:32
f6f880c to
eaefbdf
Compare
…pcontext warning, fix a race in 6811 - ni_stack_limit classifies the stack by the frame address. Under ASan's use-after-return detection a local lives on a fake stack, so a call into Node from a hosted promise reaction got the wrong V8 stack limit and hit "Maximum call stack size exceeded" (6802, dev, sanitized). - The ordering harness drops ASan's once-per-process makecontext/swapcontext warning from stderr, as the other harnesses do. - 6811's island timers fire at 15 and 40 ms, after the program's immediate and its 5 and 10 ms timers, instead of racing the first loop turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Host the runtime's event loop on Node's libuv once Node loads (NI-3)
Scope-B milestone NI-3, stacked on #1685 (NI-1: in-process libnode loader, with
@scriptc/nodefrom #1686).Once a program loads Node in-process, there is one event loop, Node's, and one set of queues. The jobs of both sides interleave as they do in Node.
The switch (
scr_loop_host, theSCR_NODE_ISLANDSbuild ofscr_async.c)Right after Node starts, the runtime moves its pending work onto Node's queues, in order:
queueMicrotaskenvelopes become V8 microtasks;process.nextTickcallbacks;Every later scheduling call goes through the bridge:
scr_ready_push, the tick functions,scr_timer_pushand the timer ref, unref, refresh, hasRef and clear calls, and the immediate functions.A switch can happen in the middle of a loop pass. In that case:
UV_RUN_ONCE);Calls into JavaScript no longer drain queues themselves. The runtime's checkpoint (the bridge's
checkpoint) and Node's own dispatch do that. A throw that escapes a runtime job, or an unhandled runtime rejection decided after a V8 microtask checkpoint, ends the process the same way the runtime'smainwould.I/O
The runtime's
scr_loop_set_*slots stay where they are. On Node's side:io_statereports their fds (watched asuv_pollhandles, which are stopped rather than closed when an fd goes away and comes back);io_dispatchruns the dispatch stations with Node's checkpoint after each one. The bridge re-reads the state before each poll phase, after each check phase, and after every microtask checkpoint, so liveness is current wherever Node next decides it. Deferred fs callbacks are delivered on the turn after they are queued, as the runtime's own loop pass delivers them.Process state (the
SCR_NODE_ISLANDSbuild ofscr_events.c,scr_node_island_host.cjs)exitlisteners that island code adds join the runtime's registries at their registration position. The bootstrap removes Node'sstartListeningIfSignal/stopListeningIfSignalhooks, so libuv never owns a signal. Removing an island listener therefore no longer resets the signal toSIG_DFLunderneath the program's own handler.exitemission is left to the runtime's exit sequence, which runs the listeners of both sides in order.process.exitCodebecomes the program's when the loop ends.process.exitalready runs the runtime's exit sequence (NI-1).mainthat calls into Node got "Maximum call stack size exceeded".Bridge ABI
ScrNodeBridgeandScrNodeBridgeHostgain the hosted-loop entries listed inscr_node_bridge.h. NI-1's pump slot (backend_fd,backend_timeout,alive,pump) is gone. The bridge ABI goes from 2 to 3. NI-1's dedicated Node stack is kept: Node's loop runs on it once it is hosted, and V8's limit also follows an async fiber that a hosted job resumes and that calls into Node again (the newstack_limithost callback). The bridge smoke test drives the hosted entry points.Tests (Linux sandbox, community libnode 24.22,
node24.22 as the oracle)Ordering fidelity:
tests/harness/node-island-order.test.tsruns 36 programs intests/corpus-node-islands-order/:At 20 Node runs against 20 native runs, 664/720 native runs produced an order Node produced, and every queue-determined program matched 20/20. The 56 misses are timer-against-I/O races in p05, p08, p11, q04, s01 and s02, where the duration of the I/O differs between the sides (scriptc's fs callbacks complete in one turn, while Node's
readFiletakes several; the first module load inside Node is slower than in a Node process). These programs are marked// @order: race, and for them the harness requires the same lines in any order.Process state, corpus (byte-exact against Node):
process.exitinside a call;process.exitfrom an island timer;process.exitwith island listeners;uncaughtExceptionlistener after the switch.Process state, harness:
tests/harness/node-island-process.test.tscovers uncaught exceptions and unhandled rejections on either side. Exit status and stdout match Node, and stderr carries the error.Cost (sandbox, per callback, Node loaded / Node itself / scriptc without Node):
setImmediatechain: 3.6–4.2 / 2.2 / 0.11 µs;nextTick: 0.31 / 0.14 / 0.01 µs;await: 81 / 72 / 2 ns;setImmediate: 2.1 µs against Node's 1.7 µs.The budget is 12 µs per callback.
Zero cost
Every change to the shared units is behind
SCR_NODE_ISLANDS. In the island-free build,unifdefshows only macro aliases that expand to the old code, plus scoping braces. Zero-cost gate against the NI-1 base,--set=bench,self,corpus --corpus=all, 3546 programs: 3540 identical, 4 allowed (6800-6803, the programs that call into Node), and 2 that differed:2314-process-introspection:scr_active_resourceswas rewritten through a local variable. Fixed (the island-free path is now the original code), and a re-run over the bench, process, timer and event programs found 137 identical.fs-extra-package: the IR records the checkout path of the compiler's fs-extra shim (/vercel/sandbox/prevagainst/vercel/sandbox/cand). The two IR files are identical once that path is normalized, and two builds from the same checkout are identical. This is an artifact of the gate, not of this change.Not in this PR
cwdcache (Node cachesprocess.cwd()in JavaScript, so a nativeprocess.chdiris not visible to island code): a hook inscr_lib.cwould change island-free binaries, so it waits for a variant.beforeExitstays a documented divergence.process.listeners()does not see native signal listeners.🤖 Generated with Claude Code