-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
setImmediate / setTimeout with delay 0 seems blocking the main thread #3333
Comments
For me this actually segfaults at the moment with
and
I am running bun |
UPDATED: Hmm that's interesting @simylein , I haven't had any issues with segfaults tho, with Anyway I tried again today with the latest commit in main ( In bun-debug, there are some differences in logs when running
|
|
Here is a simple code example to see it happen
run |
Troubleshooting the issue. I modified the test code to
I added debug statements in Output
In My understanding of the events: |
Ok after sleeping on it. I have the correct order of events.
So this introduces an infinite loop when a setTimeout with 0 interval adds another setTimeout with 0 interval. Any one have any thoughts on the appropriate flow for setTimeout 0 tasks? I'm thinking it should add the CallbackJob / task on one of the other queues so it doesn't get stuck in that loop. Updated test code function repeat(log, delay, count) {
if (count <= 0) return;
var i = 0;
while (i < 10000) {
i++;
}
console.log(log, i);
setTimeout(() => repeat(log, delay, count - 1), delay);
}
const timesToRun = 5;
repeat("11111", 0, timesToRun);
repeat("22222", 1, timesToRun); Bun Output /home/orange/bun/bun/packages/debug-bun-linux-x64/bun-debug test.js
[SYS] read(3, 4096) = 4096 (0.085ms)
[SYS] close(3)
[fs] openat(10, /home/orange/bun/bun/package.json) = 11
[SYS] close(11)
[fs] openat(10, /home/orange/bun/bun/tsconfig.json) = 11
[SYS] close(11)
[fs] openat(0, /home/orange/bun/bun/tsconfig.base.json) = 11
[SYS] close(11)
[SYS] close(6)
[SYS] close(7)
[SYS] close(8)
[SYS] close(9)
[SYS] close(10)
EventLoop.tick
tickWithCount
[fs] openat(0, /home/orange/bun/bun/test.js) = 12
[SYS] close(12)
11111 10000
setTimeout
set 0 id:1
22222 10000
setTimeout
set 1 id:2
[Loop] ref
tickWithCount
tickWithCount.while tasks.readItem
11111 10000
setTimeout
set 0 id:3
tickWithCount.while tasks.readItem
11111 10000
setTimeout
set 0 id:4
tickWithCount.while tasks.readItem
11111 10000
setTimeout
set 0 id:5
tickWithCount.while tasks.readItem
11111 10000
setTimeout
set 0 id:6
tickWithCount.while tasks.readItem
tickWithCount
tickWithCount
Loop.tick
[Loop] unref
[uws] Timer.deinit()
EventLoop.tick
tickWithCount
tickWithCount.while tasks.readItem
22222 10000
setTimeout
set 1 id:7
[Loop] ref
tickWithCount
tickWithCount
EventLoop.tick
tickWithCount
tickWithCount
Loop.tick
[Loop] unref
[uws] Timer.deinit()
EventLoop.tick
tickWithCount
tickWithCount.while tasks.readItem
22222 10000
setTimeout
set 1 id:8
[Loop] ref
tickWithCount
tickWithCount
Loop.tick
[Loop] unref
[uws] Timer.deinit()
EventLoop.tick
tickWithCount
tickWithCount.while tasks.readItem
22222 10000
setTimeout
set 1 id:9
[Loop] ref
tickWithCount
tickWithCount
Loop.tick
[Loop] unref
[uws] Timer.deinit()
EventLoop.tick
tickWithCount
tickWithCount.while tasks.readItem
22222 10000
setTimeout
set 1 id:10
[Loop] ref
tickWithCount
tickWithCount
Loop.tick
[Loop] unref
[uws] Timer.deinit()
EventLoop.tick
tickWithCount
tickWithCount.while tasks.readItem
tickWithCount
tickWithCount Node Output (exepected) 11111 10000
22222 10000
11111 10000
22222 10000
11111 10000
22222 10000
11111 10000
22222 10000
11111 10000
22222 10000 |
Expanding on @orangeswim 's comment. The core of the event_loop's while (this.tickWithCount() > 0) : (this.global.handleRejectedPromises()) {
this.tickConcurrent();
} where there's a loop that execute the tasks: // event_loop.zig > EventLoop > tickWithCount (L:581)
while (this.tasks.readItem()) |task| {...} Due to // bun.zig > Timer > set (L:3644)
vm.enqueueTask(JSC.Task.init(&job.task));
// event_loop.zig > EventLoop > enqueTask (L:902)
pub fn enqueueTask(this: *EventLoop, task: Task) void {
this.tasks.writeItem(task) catch unreachable;
} when a js functon is executed and calls This is the first problem, and can be solved by using an aux. queue as @orangeswim said, or some variation of this, for example: var executable_tasks: Queue = EventLoop.Queue.init(this.virtual_machine.allocator);
while (this.tasks.readItem()) |task| {
executable_tasks.writeItem(task) catch unreachable;
}
while (executable_tasks.readItem()) |task| {...} But the problem goes deeper, doing this unlocks the ticking of the event loop, but the loop still doesn't listen for uws events triggered by setTimeouts with delay greater than 0, since it gets stuck in this loop: // event_loop.zig > EventLoop > tick (L:841)
while (this.tickWithCount() > 0) : (this.global.handleRejectedPromises()) {
this.tickConcurrent();
} By doing something like this: while (this.tickWithCount() > 0) : (this.global.handleRejectedPromises()) {
this.tickConcurrent();
this.autoTick(); // <-- added
} the loop is able to insert tasks inbetween ticks, but this is probably not the right solution since in some tests that I did, it made the The test I'm using: const timesToRun = 10;
var runCount = 0
const obj = {}
function repeat(log, delay) {
if (runCount >= timesToRun) {
console.log(`${log}: ${obj[log]}`)
return
}
runCount += 1
obj[log] = (obj[log] || 0) + 1
// console.log(`${log}: ${obj[log]}`)
setTimeout(() => repeat(log, delay), delay);
}
repeat("1", 0)
repeat("2", 1) |
One interesting thing to keep in mind, running repeat("1", 0)
repeat("2", 1) in nodejs gives the exact same output as repeat("1", 1)
repeat("2", 1) regardless of the amount of repetitions, so it apparently treats When running repeat("1", 1)
repeat("2", 2) it gives the expected 2/1 ratio. |
I love the investigative work here I think making setTimeout have a minimum of 1 is a good option and leaving setImmediate to be the one with special behavior that appends to the task queue. Maybe in the future we will implement requestIdleCallback. |
That's probably a good temporary solution. Something that I just tried, was running if (ms == 0) {
timer_spec.it_value.tv_sec = 0;
timer_spec.it_value.tv_nsec = 1L;
} this'll only work on linux since it uses the epoll_kqueue event loop, but it seemed to work flawlessly test 1 (100_000 runs): repeat("1", 0) // (key, timeout[ms])
repeat("2", 1) result:
test 2 (100_000 runs): repeat("1", 0)
repeat("2", 2) result:
I don't know if it can be implemented like this for other OSs, nor the exact impact on compared performance when using timeout 0, but it seems like a interesting temporary solution as well. |
On macOS I believe you could use Grand Central Dispatch (GCD) via dispatch_after: dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(yourDelayInSeconds * NSEC_PER_SEC));
dispatch_after(delay, dispatch_get_main_queue(), ^{
// Your code to be executed after the specified delay
}); As for Windows you can use CreateTimerQueue, CreateTimerQueueTimer, and DeleteTimerQueue APIs: void CALLBACK TimerCallback(PVOID lpParameter, BOOLEAN TimerOrWaitFired) {
// Your code to be executed after the specified delay
printf("Timeout complete!\n");
}
void setTimeout(void (*callback)(), DWORD delayMs) {
HANDLE hTimerQueue = CreateTimerQueue();
if (hTimerQueue == NULL) {
// Handle error
return;
}
if (!CreateTimerQueueTimer(&hTimer, hTimerQueue, TimerCallback, NULL, delayMs, 0, 0)) {
// Handle error
}
// Clean up the timer queue when done (optional)
// DeleteTimerQueue(hTimerQueue);
} I suppose more platform-specific native code could be added to |
This will be fixed in the next version of Bun (once #6674 is merged) The script from @ThePrimeagen below: function test() {
console.log('11111');
setTimeout(test, 0);
}
function test2() {
console.log('22222');
setTimeout(test2, 1);
}
test();
test2(); Now logs the following infinitely repeating in bun and node: 11111
22222
11111
22222
11111
22222
11111
22222
11111
22222 |
What version of Bun is running?
0.6.9
What platform is your computer?
Linux 6.1.31-2-MANJARO x86_64 unknown
What steps can reproduce the bug?
So ive been playing with setImmediate and setTimeout in this repo, and try make a simple server with endpoints:
/ping
/blocking
/not-blocking
/blocking
and/not-blocking
has the same cpu intensive task, with difference that/blocking
is sync, and/not-blocking
is async.Steps:
/ping
every second with this command/not-blocking
In node, they are working as expected, when
/not-blocking
is still running, i can still receive response from/ping
endpoint. But in bun,/not-blocking
is blocking and/ping
response is returned after the task is finished.What is the expected behavior?
/not-blocking
first, then/ping
/ping
should return{ "data": "PONG" }
immediately without waiting for/not-blocking
to finishWhat do you see instead?
/not-blocking
first, then/ping
/ping
is waiting for/not-blocking
finish before returning{ "data": "PONG" }
Additional information
No response
The text was updated successfully, but these errors were encountered: