What exactly is a Node.js event loop tick?
Asked Answered
C

5

113

I've been getting more into the internals of the Node.js architecture, and a term I see coming up a lot is "tick" as in "next tick of the event loop" or the function nextTick().

What I haven't seen is a solid definition of what exactly a "tick" is. Based on various articles (such as this one), I've been able to piece a concept together in my head, but I'm not sure how accurate it is.

Can I get a precise and detailed description of a Node.js event loop tick?

Comeback answered 6/11, 2013 at 20:57 Comment(1)
since its "loop" , it means "the next time it loops", so a tick its a whole loop , it ends when no events are triggered and nodejs has looped all to check if any is triggered, "nextTick" it means the next loop after the current one.Apps
P
185

Remember that while JavaScript is single-threaded, all of node's I/O and calls to native APIs are either asynchronous (using platform-specific mechanisms), or run on a separate thread. (This is all handled through libuv.)

So when there's data available on a socket or a native API function has returned, we need a synchronized way to invoke the JavaScript function that is interested in the particular event that just happened.

It's not safe to just call the JS function from the thread where the native event happened for the same reasons that you'd encounter in a regular multi-threaded application – race conditions, non-atomic memory access, and so forth.

So what we do is place the event on a queue in a thread-safe manner. In oversimplified psuedocode, something like:

lock (queue) {
    queue.push(event);
}

Then, back on the main JavaScript thread (but on the C side of things), we do something like:

while (true) {
    // this is the beginning of a tick

    lock (queue) {
        var tickEvents = copy(queue); // copy the current queue items into thread-local memory
        queue.empty(); // ..and empty out the shared queue
    }

    for (var i = 0; i < tickEvents.length; i++) {
        InvokeJSFunction(tickEvents[i]);
    }

    // this the end of the tick
}

The while (true) (which doesn't actually exist in node's source code; this is purely illustrative) represents the event loop. The inner for invokes the JS function for each event that was on the queue.

This is a tick: the synchronous invocation of zero or more callback functions associated with any external events. Once the queue is emptied out and the last function returns, the tick is over. We go back to the beginning (the next tick) and check for events that were added to the queue from other threads while our JavaScript was running.

What can add things to the queue?

  • process.nextTick
  • setTimeout/setInterval
  • I/O (stuff from fs, net, and so forth)
  • crypto's processor-intensive functions like crypto streams, pbkdf2, and the PRNG (which are actually an example of...)
  • any native modules that use the libuv work queue to make synchronous C/C++ library calls look asynchronous
Pursy answered 6/11, 2013 at 21:52 Comment(21)
Yeah you nailed this. The copying of the queue and running through all the events on the copy was what I was specifically wondering about. Makes a lot of sense now though. Thanks.Comeback
Is this the famous "Asynchronous Iteration pattern" algo ?Moonlighting
@Stef: That is about writing regular JavaScript loops where a function call in the loop body contains an asynchronous callback. This post is about how node/V8 implements the event loop concept.Pursy
@Pursy When javascript main thread is infinitely in this while(true) block when is it doing its regular job beyond handling events?Albin
@sanjeev, what do you mean by "regular job"? The only thing an ongoing JavaScript application does is process events.Pursy
Thanks @josh3736. Coming from java world, this clarifies a lot. Honestly I have been reading event-driven nature of JS but it never occurred to me that event processing is all it does.Albin
This a great post but the best explanation I've read is here blog.carbonfive.com/2013/10/27/…Clow
I'd like to add that in 0.10.x setImmediate will also enqueue a function.Sungsungari
What is lock (queue) { ... }?Martyr
@Martyr it's thread synchronization construct I borrowed from C#.Pursy
Does tick mean event loop phase ?Fite
If there are no more events/messages on the event queue, then the lock procedure should block correct? And this is a good time for the OS to context switch out the Javascript thread, and run something completely different. The OS can wake up the javascript thread if something signals the OS to do so "interrupt-driven" (which can be a different thread or process), or if the lock procedure was polling-driven.Laveta
@josh3736, Does all the events accumulated in the event queue copied at once in the single turn of the event loop as shown in your code or only a single event is copied in the single turn ? From what i have saw in this video ( youtu.be/8aGhZQkoFbQ?t=20m2s ) only a single event is copied in a single turn.Koffman
@Pursy Here you sad, that tick is a phase. Documentation says, that there few phases, such as check, timers, etc. So, setInterval(), setImmediate(), setTimeout() triggering, poll phase (triggering callbacks for new http-connection, db-response, file reading completion), etc... - all of them are ticks? Have I understood you correctly: phases, listed in docs (timers, I/O callbacks, idle, poll, check, close callbacks), are ticks?Anomalistic
Phases are part of a tick. Put another way, a tick or turn is a single iteration of event loop; inside that iteration, many things happen (timers, I/O, etc), which are divided into phases.Pursy
It doesn't really runs in different thread, event loop runs in same threadOutburst
Hi, are they the same thing for event queue and callback queue?Astto
There's nothing says the length of a tick. I think a tick is to finish everything in that queue, no matter how long the queue is. After the queue is finished, it starts a new tick. From what i can understand, it provides a way to not run everything at the same time. Inside the same tick, everything is sync. But we refer async as adding things to next tick.Buttonwood
@windmaomao: Correct. The queue is emptied on every tick, and the next tick may or may not have additional things to process.Pursy
Node JS isn't single-threaded. It uses a thread pool.Merrimerriam
@Merrimerriam Well said! The Event Loop is single-threaded, but Node on its whole has a thread pool to delegate stuff to.Pigeonhole
R
14

A simpler answer for those new to JavaScript:

The first thing to understand is that JavaScript is a "single-threaded environment". This refers to JavaScript's behavior of executing your blocks of code one at a time from "the event loop" on a single thread. Below there's a rudimentary implemenation of the event loop taken from Kyle Simpson's book ydkJS and afterwards, an explanation:

// `eventLoop` is an array that acts as a queue (first-in, first-out)
var eventLoop = [ ];
var event;

// keep going "forever"
while (true) {
    // perform a "tick"
    if (eventLoop.length > 0) {
        // get the next event in the queue
        event = eventLoop.shift();

        // now, execute the next event
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}

The first while loop simulates the event loop. A tick is the dequeuing of an event from the "event loop queue" and the execution of said event.

Please see the response of 'Josh3796' for a more detailed explanation of what happens in the dequeuing and execution of an event.

Also, I recommend reading Kyle Simpson's book for those who are interested in getting a deep understanding of JavaScript. It's completely free and open-source and can be found at this link: https://github.com/getify/You-Dont-Know-JS

The specific section I referenced can be found here: https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/sync-async/ch1.md

Rubellite answered 18/2, 2018 at 1:33 Comment(1)
This kind of confuses me as this answer makes me feel there is just one Queue. And only in that queue dequing one event is considered one tick. Where from some other sources of Internet I see tick means processing all events in the queue of a single phase.Holyhead
S
7
                                                              
                          ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐             
                              THE EVENT LOOP                  
                          └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘             
                                                              
                     ┌───────────────────────────────┐        
                     │             poll              │        
                  ┌─▶│                               │──┐     
                  │  └───────────────┬───────────────┘  │     
                  │                  │                 tick   
                  │  ┌───────────────▼───────────────┐  │     
                  │  │             check             │  │     
                  │  │                               │◀─┘     
                  │  └───────────────┬───────────────┘        
                  │                  │                        
                  │  ┌───────────────▼───────────────┐        
                  │  │        close callbacks        │        
                  │  │                               │        
                loop └───────────────┬───────────────┘        
                  │                  │                        
                  │  ┌───────────────▼───────────────┐        
                  │  │            timers             │        
                  │  │                               │        
                  │  └───────────────┬───────────────┘        
                  │                  │                        
                  │  ┌───────────────▼───────────────┐        
                  │  │       pending callbacks       │        
                  │  │                               │        
                  │  └───────────────┬───────────────┘        
                  │                  │                        
                  │  ┌───────────────▼───────────────┐        
                  │  │         idle, prepare         │        
                  └──│                               │        
                     └───────────────────────────────┘        

The event loop (in Node.js) is an execution model where aspects of a script are executed in a cyclical manner according to a defined schedule.

It [event loop] is made up of a number of phases (as illustrated above). Each phase contains (1) a call stack, and (2) a callback queue. The call stack is where code is executed (on a LIFO basis), while the callback queue is where code is scheduled (on a FIFO basis) for later placement in the call stack for execution.

This callback queue can be sub-divided into 2 queues: a microTask queue and a macroTask queue. a micro-task (once scheduled) is a task that will be executed immediately after the current running script in the current phase, while a macro-task (once scheduled) is a task that will be executed in the next loop of said phase (after any micro-tasks in that phase).

The event loop runs in a cycle through all phases repeatedly until there is no more work to be done. Each cycle (through all the phases) can be referred to as a loop, while each complete invocation of scripts in a given queue can be referred to as a tick.

This tick will usually happen from one phase to another, but a tick can happen within a phase when both the microTask and macroTask queues are not empty e.g. when a Promise is resolved in the running script, its then method adds items to the microTask queue.

When you write code (say in a mycode.js file) and then invoke it (with node mycode.js), this code will be executed using the event loop according to how it is written.

Here's an example script:

process.nextTick(function() {
  console.log('next tick - 1 [scheduled from poll]');
});

console.log('poll phase - 1');

setImmediate(function() {
  console.log('check phase - 1');

  process.nextTick(function() {
    console.log('next tick - 2 [scheduled from check]');
  });

  Promise.resolve()
    .then(function() {
      console.log(`check phase - 1.1 [microTask]`);
    })
    .then(function() {
      console.log(`check phase - 1.2 [microTask]`);
    })
    .then(function() {
      setTimeout(function() {
        console.log('timers phase [scheduled from Promise in check]');
      });
      process.nextTick(function() {
        console.log('next tick - 3 [scheduled from Promise in check]');
      });
    });

  console.log('check phase - 2');
});

setTimeout(function() {
  console.log('timers phase - 1');

  setImmediate(function() {
    console.log('check phase [scheduled from timers]');
  });

  Promise.resolve()
    .then(function() {
      console.log('timers phase - 1.1 [microTask]');
    })
    .then(function() {
      console.log('timers phase - 1.2 [microTask]');
    })
    .then(function() {
      setTimeout(function() {
        console.log('timers phase [scheduled from Promise in timers]');
      });
    });
});

process.nextTick(function() {
  console.log('next tick - 4 [scheduled from poll]');
});

console.log('poll phase - 2');

Copy (or type) this into a .js file, and invoke it with node.

You should get the following output:

poll phase - 1
poll phase - 2
next tick - 1 [scheduled from poll]
next tick - 4 [scheduled from poll]
check phase - 1
check phase - 2
next tick - 2 [scheduled from check]
check phase - 1.1 [microTask]
check phase - 1.2 [microTask]
next tick - 3 [scheduled from Promise in check]
timers phase - 1
timers phase - 1.1 [microTask]
timers phase - 1.2 [microTask]
timers phase [scheduled from Promise in check]
check phase [scheduled from timers]
timers phase [scheduled from Promise in timers]

Note: Using Node.js version 16.15.0

Before the explanation, here are a few rules to remember:

  • setImmediate schedules scripts to run in the next check phase of the event loop (in the macroTask queue)
  • setTimeout schedules scripts to run in the next timers phase of the event loop (in the macroTask queue)
  • Process.nextTick schedules scripts to run before the next tick i.e. either (1) after the current script has run but before the microTask queue has run [if said queue is not empty], or (2) before the event loop traverses from one phase to the next [if microTask queue is empty]
  • Promise.prototype.then schedules scripts to run in the current microTask queue i.e. after the current script, but before scripts scheduled for the next phase
  • The microTask queue is run before the macroTask queue

Here's the explanation in the form of a timeline of events:

A. FROM POLL PHASE (LOOP 1)

  1. console.log('poll phase - 1') and console.log('poll phase - 2') are synchronous code and will run immediately in the current phase
  2. console.log('next tick - 1 [scheduled from poll]') and console.log('next tick - 4 [scheduled from poll]') are scheduled by process.nextTick to run before the next tick i.e. before the check phase (since there is nothing in microTask queue).
  3. The callback on setImmediate (Line 7) is scheduled to run in the check phase
  4. The callback on setTimeout (Line 33) is scheduled to run in the timers phase

B. BEFORE CHECK PHASE (LOOP 1) 5. console.log('next tick - 1 [scheduled from poll]') and console.log('next tick - 4 [scheduled from poll]') are executed

C. FROM CHECK PHASE (LOOP 1) 6. console.log('check phase - 1') and console.log('check phase - 2') [from callback previously scheduled by setImmediate (Line 7)] are executed immediately as they are synchronous 7. console.log('next tick - 2 [scheduled from check]') is scheduled by process.nextTick 8. The callbacks on Line 15, 18, and 21 are scheduled to run in the microTask queue. 9. console.log('next tick - 2 [scheduled from check]') is executed (because this is before the next tick i.e. after the current script but before microTask queue) 10. The callbacks on Line 15 and 18 are executed (because the microTask is executed immediately after the running script) 11. The callback on Line 21 is executed and schedules (1) console.log('timers phase [scheduled from Promise in check]') to run in the next timers phase, and (2) console.log('next tick - 3 [scheduled from Promise in check]') to run before the next tick i.e. before traversal from current phase (check) to the next active phase (timers)

D. BEFORE TIMERS PHASE (LOOP 1) 12. console.log('next tick - 3 [scheduled from Promise in check]') is executed

E. FROM TIMERS PHASE (LOOP 1) 13. console.log('timers phase - 1') is executed 14. setImmediate (Line 36) schedules its callback to be run in the next check phase 15. The Promise (Line 40) schedules three callbacks to be run in the microTask queue 16. console.log('timers phase - 1.1 [microTask]') and console.log('timers phase - 1.2 [microTask]') are executed as scheduled in 15. 17. console.log('timers phase [scheduled from Promise in check]') is executed. It was previously scheduled by setTimeout (Line 22). It's running now (after the code in 16. above) because it is a macroTask (so it runs after the microTask queue has been run)

E. FROM NEXT CHECK PHASE (LOOP 2) 18. console.log('check phase [scheduled from timers]') is executed. It was previously scheduled in the timers phase (of Loop 1) by setImmediate (Line 36)

F. FROM NEXT TIMERS PHASE (LOOP 2) 19. console.log('timers phase [scheduled from Promise in timers]') is executed. It was previously scheduled in the timers phase (of Loop 1) by setTimeout (Line 48)

References

Swart answered 15/6, 2022 at 14:23 Comment(4)
I Have a different execution result: poll phase - 1 poll phase - 2 next tick - 1 [scheduled from poll] next tick - 4 [scheduled from poll] timers phase - 1 timers phase - 1.1 [microTask] timers phase - 1.2 [microTask] check phase - 1 check phase - 2 next tick - 2 [scheduled from check] check phase - 1.1 [microTask] check phase - 1.2 [microTask] next tick - 3 [scheduled from Promise in check] check phase [scheduled from timers] timers phase [scheduled from Promise in timers] timers phase [scheduled from Promise in check]Defensive
@Defensive Pardon the delayed response. What version of Node.js are you on?Swart
Thanks for the detailed answer; aside from the changes in recent versions of Node (I got the same as @Defensive using v21.2, and the same as you using v16.15), I think there is a small mistake here which is that the operations you've labelled as being in the 'poll' phase are actually happening before the event loop starts - it's difficult to find good documentation out there but my impression so far is that Node executes the script synchronously, pushing any async work onto the various queues which the event loop then handles (e.g. blog.logrocket.com/complete-guide-node-js-event-loop).Paryavi
@DanParsonson Thanks for the feedback. I'll have to revisit and revise this answer for later versions of Node showing the differences in output and why.Swart
S
1

Very simple and short way of Event Loop tick is:

It is used by node internal mechanism where when set of requests on a queue is processed then tick is initiated which represents the completion of a task

Scranton answered 6/9, 2019 at 5:47 Comment(1)
can you provide some source for your answer please?Mingy
W
0

A "tick" refers to a complete pass through the event loop. Confusingly, setImmediate() takes a tick to run, whereas process.nextTick() is more immediate, so the two functions deserve a name swap.

Wommera answered 4/2, 2021 at 6:26 Comment(2)
"a complete pass through the event loop", shouldn't it called a loop?Defensive
I thought a tick was the movement from one phase to another phase within the loop, and not an iteration of the entire loop.Vacuva

© 2022 - 2024 — McMap. All rights reserved.