Skip to content

Commit 0b7c297

Browse files
committed
chore: US-028 route nextTick/queueMicrotask/timers through bridge
Route process.nextTick, queueMicrotask, and setTimeout(fn, 0) through the _scheduleTimer bridge handler instead of V8 microtasks. This prevents infinite microtask loops in V8's perform_microtask_checkpoint() caused by TUI render cycles (Pi's requestRender → nextTick(doRender) → doRender → requestRender pattern). Also increase session thread stack size to 32 MiB for V8 with large module graphs. Pi interactive tests remain blocked by V8 v134 SIGSEGV during TUI initialization — this is a V8 engine-level crash, not a bridge issue.
1 parent ca4e88d commit 0b7c297

2 files changed

Lines changed: 34 additions & 20 deletions

File tree

native/v8-runtime/src/session.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ impl SessionManager {
119119
};
120120
let join_handle = thread::Builder::new()
121121
.name(format!("session-{}", name_prefix))
122+
.stack_size(32 * 1024 * 1024) // 32 MiB — V8 with large module graphs needs extra stack
122123
.spawn(move || {
123124
session_thread(
124125
heap_limit_mb,

packages/nodejs/src/bridge/process.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,14 @@ const process: Record<string, unknown> & {
772772
},
773773

774774
nextTick(callback: (...args: unknown[]) => void, ...args: unknown[]): void {
775-
if (typeof queueMicrotask === "function") {
775+
// Route through bridge timer to avoid infinite microtask loops in V8's
776+
// perform_microtask_checkpoint() — TUI render cycles (Pi) use nextTick
777+
// in requestRender → doRender → requestRender loops
778+
if (typeof _scheduleTimer !== "undefined") {
779+
_scheduleTimer
780+
.apply(undefined, [0], { result: { promise: true } })
781+
.then(() => callback(...args));
782+
} else if (typeof queueMicrotask === "function") {
776783
queueMicrotask(() => callback(...args));
777784
} else {
778785
Promise.resolve().then(() => callback(...args));
@@ -1076,13 +1083,22 @@ function _checkTimerBudget(): void {
10761083
}
10771084
}
10781085

1079-
// queueMicrotask fallback
1086+
// queueMicrotask — route through bridge timer when available to prevent
1087+
// infinite microtask loops in V8's perform_microtask_checkpoint().
1088+
// TUI frameworks (Ink/React) schedule renders via queueMicrotask, which
1089+
// creates unbounded microtask chains that block the V8 event loop.
10801090
const _queueMicrotask =
1081-
typeof queueMicrotask === "function"
1082-
? queueMicrotask
1083-
: function (fn: () => void): void {
1084-
Promise.resolve().then(fn);
1085-
};
1091+
typeof _scheduleTimer !== "undefined"
1092+
? function (fn: () => void): void {
1093+
_scheduleTimer
1094+
.apply(undefined, [0], { result: { promise: true } })
1095+
.then(fn);
1096+
}
1097+
: typeof queueMicrotask === "function"
1098+
? queueMicrotask
1099+
: function (fn: () => void): void {
1100+
Promise.resolve().then(fn);
1101+
};
10861102

10871103
/**
10881104
* Timer handle that mimics Node.js Timeout (ref/unref/Symbol.toPrimitive).
@@ -1125,11 +1141,9 @@ export function setTimeout(
11251141

11261142
const actualDelay = delay ?? 0;
11271143

1128-
// Use host timer for actual delays if available and delay > 0
1129-
if (typeof _scheduleTimer !== "undefined" && actualDelay > 0) {
1130-
// _scheduleTimer.apply() returns a Promise that resolves after the delay
1131-
// Using { result: { promise: true } } tells the V8 runtime to wait for the
1132-
// host Promise to resolve before resolving the apply() Promise
1144+
// Route ALL timers through bridge when available (including delay=0) to
1145+
// avoid infinite microtask loops in V8's perform_microtask_checkpoint()
1146+
if (typeof _scheduleTimer !== "undefined") {
11331147
_scheduleTimer
11341148
.apply(undefined, [actualDelay], { result: { promise: true } })
11351149
.then(() => {
@@ -1143,7 +1157,7 @@ export function setTimeout(
11431157
}
11441158
});
11451159
} else {
1146-
// Use microtask for zero delay or when host timer is unavailable
1160+
// Use microtask only when host timer bridge is unavailable
11471161
_queueMicrotask(() => {
11481162
if (_timers.has(id)) {
11491163
_timers.delete(id);
@@ -1184,8 +1198,8 @@ export function setInterval(
11841198
const scheduleNext = () => {
11851199
if (!_intervals.has(id)) return; // Interval was cleared
11861200

1187-
if (typeof _scheduleTimer !== "undefined" && actualDelay > 0) {
1188-
// Use host timer for actual delays
1201+
if (typeof _scheduleTimer !== "undefined") {
1202+
// Route through bridge timer to avoid microtask loops
11891203
_scheduleTimer
11901204
.apply(undefined, [actualDelay], { result: { promise: true } })
11911205
.then(() => {
@@ -1200,7 +1214,7 @@ export function setInterval(
12001214
}
12011215
});
12021216
} else {
1203-
// Use microtask for zero delay or when host timer unavailable
1217+
// Use microtask only when host timer bridge is unavailable
12041218
_queueMicrotask(() => {
12051219
if (_intervals.has(id)) {
12061220
try {
@@ -1336,10 +1350,9 @@ export function setupGlobals(): void {
13361350
g.setImmediate = setImmediate;
13371351
g.clearImmediate = clearImmediate;
13381352

1339-
// queueMicrotask
1340-
if (typeof g.queueMicrotask === "undefined") {
1341-
g.queueMicrotask = _queueMicrotask;
1342-
}
1353+
// queueMicrotask — always override to route through bridge timer when
1354+
// available, preventing infinite microtask loops from TUI render cycles
1355+
g.queueMicrotask = _queueMicrotask;
13431356

13441357
// URL
13451358
if (typeof g.URL === "undefined") {

0 commit comments

Comments
 (0)