mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Start nested Node entries below the frames a fiber switch left live
A host callback marks where it runs on Node's stack, and a call into Node.js from a fiber starts below that mark. Runtime code the callback runs may go deeper on Node's stack before it starts or resumes a fiber, and those frames stay live while the fiber runs: a call into Node.js from the fiber overwrote them. scr_switch now reports where it leaves Node's stack, and the mark moves down to it for the rest of the callback. 6823 runs a timer callback 200 frames deep that starts an async function calling into Node.js before its first await.
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6823-node-island-nested-stack/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "scr_runtime.h"
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
#include "scr_node_bridge.h"
|
||||
#include "scr_node_island.h"
|
||||
#endif
|
||||
|
||||
#include <errno.h>
|
||||
@@ -1659,6 +1660,10 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
|
||||
uintptr_t guard = scr_stack_guard;
|
||||
if (to_fiber) SCR_STACK_GUARD_SET(scr_stack_guard_for((uintptr_t)to_fiber->stack, SCR_FIBER_STACK));
|
||||
#endif
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* Leaving Node's stack keeps this frame and those above it live. */
|
||||
scr_node_island_leave_stack((uintptr_t)__builtin_frame_address(0));
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
/* SwitchToFiber snapshots the outgoing fiber's state inside its own
|
||||
* fiber object — `from` has nothing to record. */
|
||||
|
||||
@@ -865,8 +865,10 @@ static bool ni_stack_has(uintptr_t sp) {
|
||||
return ni_stack != NULL && sp > (uintptr_t)ni_stack && sp <= (uintptr_t)ni_stack + NI_STACK;
|
||||
}
|
||||
|
||||
/* The lowest address of Node's stack its frames use while runtime code
|
||||
* they called runs (a host callback below), 0 outside them. Runtime code
|
||||
/* The lowest address of Node's stack in use while runtime code Node's
|
||||
* frames called runs (a host callback below), 0 outside them: the
|
||||
* callback's frame, or lower, where that code switched to a fiber's stack
|
||||
* (scr_node_island_leave_stack) and left its own frames live. Runtime code
|
||||
* may resume an async fiber there, and the fiber may call into Node again:
|
||||
* that entry runs on Node's stack below this mark, so every frame of Node
|
||||
* stays on the one stack V8 scans (heap::base::Stack, bounded by
|
||||
@@ -883,6 +885,10 @@ static uintptr_t ni_mark = 0;
|
||||
} while (0)
|
||||
#define NI_MARK_END (ni_mark = ni_saved_mark)
|
||||
|
||||
void scr_node_island_leave_stack(uintptr_t sp) {
|
||||
if (ni_mark != 0 && ni_stack_has(sp) && sp < ni_mark) ni_mark = sp;
|
||||
}
|
||||
|
||||
/* A nested entry: job(arg) on Node's stack below the mark, from a fiber. */
|
||||
static void (*ni_nested_job)(void *) = NULL;
|
||||
static void *ni_nested_arg = NULL;
|
||||
|
||||
@@ -49,6 +49,11 @@ void scr_loop_hosted_io_state(struct ScrNodeIoState *state);
|
||||
void scr_loop_hosted_io(void);
|
||||
void scr_loop_hosted_microtasks_done(void);
|
||||
|
||||
/* Runtime code switches stacks (scr_switch) at `sp`: when that is Node's
|
||||
* stack, the frames above stay live until it switches back, so a call
|
||||
* into Node.js from the other stack runs below them. */
|
||||
void scr_node_island_leave_stack(uintptr_t sp);
|
||||
|
||||
/* Process events island code listens to (SCR_NODE_EVENT_* of the bridge
|
||||
* ABI). Their listeners sit in the runtime's registries (the
|
||||
* SCR_NODE_ISLANDS build of scr_events.c) at their registration position,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Returns its argument after using some of Node's stack.
|
||||
function down(n, depth) {
|
||||
return depth === 0 ? n : down(n, depth - 1);
|
||||
}
|
||||
|
||||
exports.echo = function (n) {
|
||||
return down(n, 500);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// @requires-libnode
|
||||
// A timer on Node's loop runs runtime code deep below the callback's own
|
||||
// frame, and an async function it starts calls into Node.js before its
|
||||
// first await: that call's frames go below the deepest live runtime frame
|
||||
// on Node's stack, so the frames above it return intact.
|
||||
import { callSync } from "@scriptc/node";
|
||||
|
||||
async function viaNode(n: number): Promise<number> {
|
||||
const echoed = callSync<number>("./echo.cjs", "echo", n);
|
||||
await null;
|
||||
return echoed;
|
||||
}
|
||||
|
||||
const started: Promise<number>[] = [];
|
||||
function deep(depth: number, n: number): number {
|
||||
const local = [depth, n, depth * n];
|
||||
if (depth > 0) return deep(depth - 1, n) + local[2]!;
|
||||
started.push(viaNode(n));
|
||||
return local[0]! + local[1]!;
|
||||
}
|
||||
|
||||
console.log("loaded", callSync<number>("./echo.cjs", "echo", 1));
|
||||
setTimeout(() => {
|
||||
console.log("deep", deep(200, 3));
|
||||
void Promise.all(started).then((values) => console.log("echoed", values.join(",")));
|
||||
}, 0);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 20000 }
|
||||
Reference in New Issue
Block a user