5

I am confused by the following paragraph of the Node.js documentation.

setImmediate() vs setTimeout()

... The order in which the timers are executed will vary depending on the context in which they are called. If both are called from within the main module, then timing will be bound by the performance of the process (which can be impacted by other applications running on the machine).

For example, if we run the following script which is not within an I/O cycle (i.e. the main module), the order in which the two timers are executed is non-deterministic, as it is bound by the performance of the process:

It proceeds to show the following example

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

I don't understand what makes the result non-deterministic. Since the timers phase happens before the check phase, shouldn't the callbacks scheduled by setTimeout always execute before those scheduled by setImmediate? I don't think the order of the phases in the event loop would change due to a context switch or something.

The document also states that

However, if you move the two calls within an I/O cycle, the immediate callback is always executed first:

OK, but what makes so-called "I/O cycles" different from the main module?


I know there are a lot of related questions, but all answers merely state this fact by citing the documentation, without explaining where the non-determinism comes into play, so I don't think this is a duplicate.

nalzok
  • 14,965
  • 21
  • 72
  • 139
  • Have you checked the accepted answer on this question ( https://stackoverflow.com/questions/24117267/nodejs-settimeoutfn-0-vs-setimmediatefn ) ? "Also `(setTimeout,0)` will be slow because it will check the timer at least once before executing. At times it can be twice as slow. ". Also this article that you can find in one of the answers there: https://www.voidcanvas.com/setimmediate-vs-nexttick-vs-settimeout/ – Cristian-Florin Calina Jul 01 '22 at 20:22
  • I find the whole section of the doc quite confusing. In the 'phases', each description names what _the other phases do_ but barely describes what the phases themselves are for – Jonas Wilms Jul 01 '22 at 20:23
  • FWIW the timer scheduling and execution can be found in [libuv/timer.c](https://github.com/libuv/libuv/blob/47e0c5c575e92a25e0da10fc25b2732942c929f3/src/timer.c#L163). Actually it should be deterministic, i.e. every timer scheduled with timeout 0 should be directly picked up by uv__run_timers – Jonas Wilms Jul 01 '22 at 20:45
  • The actual "event loop" can be found [here](https://github.com/libuv/libuv/blob/1b8cc561949565190d54195042b8028f488ed068/src/win/core.c#L592) – Jonas Wilms Jul 01 '22 at 20:47
  • And the NodeJS part of it is [here](https://github.com/nodejs/node/blob/a36a5469c27a60bc2d9ff590fbf145d59529d9d6/src/env.cc#L868), though still nothing indeterministic. Really hope someone else knows the answer :) – Jonas Wilms Jul 01 '22 at 21:06
  • @JonasWilms https://github.com/nodejs/help/issues/392#issuecomment-305969168 here is the explaination while looking over the internal code --- "This call to `uv__hrtime` is platform dependent and is cpu-time-consuming work as it makes system call to `clock_gettime`. It's is impacted by other application running on the machine." – Cristian-Florin Calina Jul 01 '22 at 21:55
  • @Cristian-FlorinCalina yeah I don't quite follow that argument as the kernel could always preemptively interrupt the NodeJS process, thus the clock can always arbitrarily move forward. But the rest of this thread contains some pretty good explanations ... – Jonas Wilms Jul 01 '22 at 22:07

2 Answers2

4

The actual trick is in the Timeout constructor which is called by setTimeout and which increases times below 1 to 1. Thus setTimeout(fn, 0) is actually equivalent to setTimeout(fn, 1).

When libuv initializes, it starts with timers after updating its internal clock, and when one millisecond already passed till then it's gonna pick up the timer before proceeding to the poll phase (which is followed by the setImmediate phase).


Another interesting observation is that multiple timers might also run before and after setImmeadiate:

setTimeout(() => console.log('timer'), 1);
setTimeout(() => console.log('timer'), 1);
setImmediate(() => console.log('immediate'));
// can produce:
// timer
// immediate
// timer

That's because setTimeout calls getLibuvNow internally, which will call env->GetNow() and which not only gets the current time of libuv, but also updates it. Thus it might happen that the timer get put into the timer queue with different due times, and thus the timer phase will only pick up some of them.

OK, but what makes so-called "I/O cycles" different from the main module?

The main module gets run before libuv is initialized, whereas most other code will run in the poll phase of the libuv loop. Thus the main module initialization is followed by the timer phase, whereas the poll phase is followed by the 'check handles' phase, which among others runs the setImmediate callbacks. Thus usually in the main module timers would run before immediates (if they're due) and if scheduled in callbacks, immediates would run before timers.

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • Just to check: the code in `timeout_vs_immediate.js` will output `immediate` followed by `timeout` when I have a very fast processor which can initialize libuv in under 1 ms. Otherwise, the timer would fire when the timer phase is reached for the first time, and I would see `timeout` followed by `immediate` instead. Is that correct? – nalzok Jul 01 '22 at 23:24
  • 1
    Kind of. 'fast processor' is more likely 'the NodeJS process is not interrupted by the kernel in the meantime and performs initialization in < 1ms' – Jonas Wilms Jul 01 '22 at 23:42
1

I'll write an aswer as well for visibility, since it's a good question and node documentation indeed is misleading and I had to dig a bit to find some resources as well...

This question was answered here (accepted answer) very well with a performance benchmark testing..

But the actual explaination can be found in another answer that points to this article which explains the event loop better.

event loop explained

difference explained

Also, read this answer (this one is the best one, it goes through the internal code and explains which section is platform dependant and time consuming of the CPU and generates the non-determinism and also the fact that 0 is transformed into 1 internally for setTimeout) to an issue raised on nodejs asking the same question which seems to explain it even further.

One more important things to note is setTimeout when set to 0 is internally converted to 1.

This call to uv__hrtime is platform dependent and is cpu-time-consuming work as it makes system call to clock_gettime. It's is impacted by other application running on the machine.

If the preparation before the first loop took more than 1ms then the Timer Phase calls the callback associated with it. If it's is less than 1ms Event-loop continues to next phase and runs the setImmediate callback in check phase of the loop and setTimeout in the next tick of the loop.