Skip to content

Event Loop

Muhammad edited this page Sep 8, 2023 · 9 revisions

Yes, this is the source code for the event loop. A function of 49 lines of code. This image is from the repository of the event loop implementation, written in C, from the opensource cross-platform libuv project. The event loop is what allows Node.js to perform non-blocking I/O operations. Node.js is similar in design to, and influenced by, systems like Ruby's Event Machine and Python's Twisted. Node.js takes the event model a bit further. It presents an event loop as a runtime construct instead of as a library. The event loop of Node.js is single-threaded, however file I/O operations, cryptographic operations, and some network operations run in multiple threads through the thread pool.

Phases

The 6 phases of the event loop are as follows.

   ┌───────────────────────────┐  this phase executes callbacks scheduled by setTimeout()
┌─>│           timers          │  and setInterval().
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐  executes I/O callbacks, which were deferred to its next 
│  │     pending callbacks     │  loop's iteration.
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  only used internally
│  └─────────────┬─────────────┘    
│  ┌─────────────┴─────────────┐  retrieve new I/O events from the kernel; execute all I/O
│  │           poll            │  related callbacks
│  └─────────────┬─────────────┘  
│  ┌─────────────┴─────────────┐      
│  │           check           │  callbacks of setImmediate()
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │  callbacks for the event 'close'
   └───────────────────────────┘

Between each run of the event loop, Node.js checks if it is waiting for any asynchronous I/O or timers and shuts down cleanly if there are not any.

Each phase has a FIFO queue of callbacks to execute. While each phase is special in its own way, generally, when the event loop enters a given phase, it will perform any operations specific to that phase, then execute callbacks in that phase's queue until the queue has been exhausted or the maximum number of callbacks has executed. When the queue has been exhausted or the callback limit is reached, the event loop will move to the next phase, and so on.

Since any of these operations may schedule more operations and new events processed in the poll phase are queued by the kernel, poll events can be queued while polling events are being processed. As a result, long running callbacks can allow the poll phase to run much longer than a timer's threshold.

Phases in Detail
Timers

Operating system scheduling or the long running callbacks in the "poll" phase are what causes timers such as setTimeout() to delay beyond the specified time in milliseconds, resulting us to describe setTimeout(() => console.log('hello world'), 1000) as "the callback will execute after at least 1000 ms, but not guarranteed to be precisely 1000 ms, usually delays more than 1000 ms".

now = Date.now()
setTimeout(()=>console.log('timer is done', Date.now() - now), 100)
fs.readFile('small file', () => {})
// timer is done 102     (not exactly 100ms)
now = Date.now()
setTimeout(()=>console.log('timer is done', Date.now() - now), 100)
fs.readFile('huuuuuge file', () => {})
// timer is done 100     (always perfectly 100ms)
now = Date.now()
setTimeout(()=>console.log('timer is done', Date.now() - now), 100)
fs.readFile('small file', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback < 1000) { /* busy loop to block for 1000ms */ }
})
// timer is done 1012     (🤯 always delays more than 1000ms, 
//                         whereas it was supposed to delay for 100ms 🤯)

The second setTimeout() executes the callback in the correct delay of 100ms because in order to prevent starving the event loop, libuv has a hard maximum time before it stops polling for more events. Since the file read is taking beyond 100ms, event loop moves on to the next phases/iterations and runs the callback for the setTimeout(). This is evidence that, the event loop pauses/waits/sleeps in the "poll" phase to see whether anything happens; if nothing happens in 100ms (varies by platform) it needs to move on to the next phase.

The third piece of code is evidence that I/O callbacks are executed in the "poll" phase synchronously. This is also evidence that the "poll" phase is responsible for delaying timers, because it has to execute callbacks which can take any arbitrary amount of time.

Pending callbacks

This phase executes callbacks for some system operations such as types of TCP errors. For example if a TCP socket receives ECONNREFUSED when attempting to connect, some *nix systems want to wait to report the error. This will be queued to execute in the pending callbacks phase.

Poll

The poll phase has two main functions:

  • Calculating how long it should block and poll for I/O, then
  • Processing events in the poll queue.

When the event loop enters the poll phase and there are no timers scheduled, one of two things will happen:

  • If the poll queue is not empty, the event loop will iterate through its queue of callbacks executing them synchronously until either the queue has been exhausted, or the system-dependent hard limit is reached.
  • If the poll queue is empty, one of two more things will happen:
    • If scripts have been scheduled by setImmediate(), the event loop will end the poll phase and continue to the check phase to execute those scheduled scripts.
    • If scripts have not been scheduled by setImmediate(), the event loop will wait for callbacks to be added to the queue, then execute them immediately.

Once the poll queue is empty the event loop will check for timers whose time thresholds have been reached. If one or more timers are ready, the event loop will wrap back to the timers phase to execute those timers' callbacks.

Clone this wiki locally