Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
bd825a35b5 move tickImmediateTasks into us_loop_run_bun_tick
Run setImmediate callbacks from inside the C poll function
(us_loop_run_bun_tick) instead of from the Zig callers (autoTick /
autoTickActive). This ensures immediate tasks are processed closer to
the I/O polling layer and allows the callback to force a zero timeout
when more immediates are pending, preventing the poll syscall from
blocking unnecessarily.

On POSIX, the new Bun__tickImmediateTasks callback is invoked at the
top of us_loop_run_bun_tick — before the num_polls guard — so
immediates still run even when there are no registered file descriptors.
On Windows, tickImmediateTasks continues to be called directly from Zig
since Windows uses a different poll backend (libuv).

The wakeup() call after tickImmediateTasks is no longer needed on POSIX
because we now force a zero timeout when more immediates are pending.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-09 16:40:33 +00:00
3 changed files with 158 additions and 12 deletions

View File

@@ -247,8 +247,18 @@ void us_loop_run(struct us_loop_t *loop) {
}
extern void Bun__JSC_onBeforeWait(void * _Nonnull jsc_vm);
/* Returns true if there are more immediate tasks pending after running */
extern _Bool Bun__tickImmediateTasks(struct us_loop_t * _Nonnull loop);
void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout) {
/* Run setImmediate callbacks before polling.
* If there are more immediates pending after this, force a zero timeout
* so the poll syscall returns immediately instead of blocking. */
const struct timespec zero_timeout = {0, 0};
if (Bun__tickImmediateTasks(loop)) {
timeout = &zero_timeout;
}
if (loop->num_polls == 0)
return;
@@ -263,8 +273,7 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout
/* Emit pre callback */
us_internal_loop_pre(loop);
if (loop->data.jsc_vm)
if (loop->data.jsc_vm)
Bun__JSC_onBeforeWait(loop->data.jsc_vm);
/* Fetch ready polls */

View File

@@ -187,10 +187,26 @@ fn externRunCallback3(global: *jsc.JSGlobalObject, callback: jsc.JSValue, thisVa
loop.runCallback(callback, global, thisValue, &.{ arg0, arg1, arg2 });
}
/// Called from `us_loop_run_bun_tick` (C) before the poll syscall.
/// Runs setImmediate callbacks and returns whether there are more pending
/// immediate tasks (which tells the C side to use a zero timeout so it
/// doesn't block).
fn tickImmediateTasksFromUSockets(loop: *uws.Loop) callconv(.c) bool {
const parent = loop.internal_loop_data.getParent();
switch (parent) {
.js => |event_loop| {
event_loop.tickImmediateTasks(event_loop.virtual_machine);
return event_loop.immediate_tasks.items.len > 0;
},
.mini => return false,
}
}
comptime {
@export(&externRunCallback1, .{ .name = "Bun__EventLoop__runCallback1" });
@export(&externRunCallback2, .{ .name = "Bun__EventLoop__runCallback2" });
@export(&externRunCallback3, .{ .name = "Bun__EventLoop__runCallback3" });
@export(&tickImmediateTasksFromUSockets, .{ .name = "Bun__tickImmediateTasks" });
}
/// Prefer `runCallbackWithResult` unless you really need to make sure that microtasks are drained.
@@ -350,11 +366,8 @@ pub fn autoTick(this: *EventLoop) void {
const loop = this.usocketsLoop();
const ctx = this.virtual_machine;
this.tickImmediateTasks(ctx);
if (comptime Environment.isPosix) {
if (this.immediate_tasks.items.len > 0) {
this.wakeup();
}
if (comptime Environment.isWindows) {
this.tickImmediateTasks(ctx);
}
if (comptime Environment.isPosix) {
@@ -437,11 +450,8 @@ pub fn autoTickActive(this: *EventLoop) void {
var loop = this.usocketsLoop();
var ctx = this.virtual_machine;
this.tickImmediateTasks(ctx);
if (comptime Environment.isPosix) {
if (this.immediate_tasks.items.len > 0) {
this.wakeup();
}
if (comptime Environment.isWindows) {
this.tickImmediateTasks(ctx);
}
if (comptime Environment.isPosix) {

View File

@@ -0,0 +1,127 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("setImmediate runs before setTimeout(0)", async () => {
const order: string[] = [];
await new Promise<void>(resolve => {
setTimeout(() => {
order.push("timeout");
resolve();
}, 0);
setImmediate(() => {
order.push("immediate");
});
});
expect(order).toEqual(["immediate", "timeout"]);
});
test("nested setImmediate callbacks run on separate ticks", async () => {
const order: string[] = [];
await new Promise<void>(resolve => {
setImmediate(() => {
order.push("first");
setImmediate(() => {
order.push("third");
resolve();
});
});
setImmediate(() => {
order.push("second");
});
});
expect(order).toEqual(["first", "second", "third"]);
});
test("setImmediate microtasks drain between callbacks", async () => {
const order: string[] = [];
await new Promise<void>(resolve => {
setImmediate(() => {
order.push("immediate1");
Promise.resolve().then(() => order.push("microtask-from-immediate1"));
});
setImmediate(() => {
order.push("immediate2");
Promise.resolve().then(() => {
order.push("microtask-from-immediate2");
resolve();
});
});
});
// Microtask from immediate1 should drain before immediate2 runs
// (each runImmediateTask calls exitMaybeDrainMicrotasks)
expect(order).toEqual(["immediate1", "microtask-from-immediate1", "immediate2", "microtask-from-immediate2"]);
});
test("setImmediate works with active I/O", async () => {
const server = Bun.serve({
port: 0,
fetch() {
return new Promise<Response>(resolve => {
setImmediate(() => {
resolve(new Response("from-immediate"));
});
});
},
});
try {
const resp = await fetch(`http://localhost:${server.port}/`);
expect(await resp.text()).toBe("from-immediate");
} finally {
server.stop(true);
}
});
test("many setImmediate callbacks execute correctly", async () => {
const count = 1000;
let executed = 0;
await new Promise<void>(resolve => {
for (let i = 0; i < count; i++) {
setImmediate(() => {
executed++;
if (executed === count) {
resolve();
}
});
}
});
expect(executed).toBe(count);
});
test("setImmediate works when spawned as subprocess", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
let count = 0;
function tick() {
count++;
if (count < 5) {
setImmediate(tick);
} else {
console.log("done:" + count);
}
}
setImmediate(tick);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout.trim()).toBe("done:5");
expect(exitCode).toBe(0);
});