Insight into Node js internals-Event loop

Lashan Faliq
6 min readNov 30, 2020

What is Node js?

Before diving directly into what Node js is, we first need to discuss what V8 and libuv projects are.

V8 is the open source javascript engine developed by Google written in C++ which lets us run javascript code on our machine.

Libuv is a project also written in C++ which deals with operating system tasks like file systems, networking and which also deals with some aspects of concurrency.

How does node JS come in to the picture here?

Node js uses V8 and Libuv projects and creates some well refined modules like https, crypto, path and fs which lets us write plain javascript code without having to worry about the C++ code or any underlying implementations.

Node JS overview

Lets look at an hashing function pbkdf2.js implemented in the node crypto module to see how exactly this is happening.

This is the js implementation of pbkdf2 found in the lib directory of the node repository which calls the c++ function pbkdf2 using the below code snippet

const {
PBKDF2Job,
kCryptoJobAsync,
kCryptoJobSync,
} = internalBinding('crypto');

The function internalBinding basically grabs C++ functions and and makes them available to be used in javascript code. Previously process.binding() which was used for this exact same purpose now has been deprecated as seen from this github issue.

Now let’s look into the crypto_pbkdf2.cc file to see an example of how V8 and libuv have been used. As soon as we open this file we can plainly see where V8 has been used.

using v8::FunctionCallbackInfo;
using v8::Int32;
using v8::Just;
using v8::Maybe;
using v8::Nothing;
using v8::Value;

V8 is used to map or connect javascript functions or variables to the C++ code.

The use of libuv is not that straight forward, but if we look more closely at the c++ file which initializes the crypto module we can see that libuv has been used.

So what exactly happens when we call the crypto PBKDF2 hashing function from node is that javascript does some validations on the function’s arguments but the C++ code actually does the calculations to get the hash.

How does the Node js event loop work?

The eventloop can be imagined as a while loop which keeps running until there are no more pending operations to be executed. One iteration of this while loop is called a tick.

When the command node index.js is run, it first completely executes the script from top to bottom then the event loop keeps running until there are no more pending operations.

Here is some pseudocode to get the gist of what goes on in the event loop.

Let me explain the above in brief terms, initially all of the code in index.js is run from top to bottom which is shown by line 6.

Then the event loop will keep on running until the function shouldContinue keeps returning true. The function shouldContinue checks if there are anymore pending timers (setTimeout, setInterval) , pending OS operations (like listening on a specific port) and pendingOperations ( when a file has been read using the fs module function). So now as long as at least one of these are there the eventloop will continue to run. This also explains why a node program listening on a port doesn’t stop execution.

Now we will discuss what happens on each of the ticks of the event loop. First it checks if any pending timers are about to expire and calls the necessary callback functions if they are. Next it checks if any pending OS task or any other operation is done,if it is done call the relevant callback function. Then the execution is paused until either a timer is about to expire, a pending task is done or pending operation is done. Finally it checks and runs any cleanup code or close events that become available.

Is Node js actually single threaded?

Before answering this we first need to understand the libuv threadpool and how Node js uses it.

Libuv thread pool and how it works.

Node JS with libuv thread pool

Node js delegates heavy calculations(hash functions of crypto module) or file system related operations(fs module operations) to the libuv thread pool. The thread pool by default has 4 threads defined. Now we will see how the thread pool actually works on Node js.

libuv thread pool interaction with OS

The above diagram shows the high level interaction of the thread pool and the OS thread scheduler on my laptop which has 4 cores.

The below code can be used to get execution times of each of the function pbkdf2 calls, this pbkdf2 function is defined in the crypto module which is used to calculate hash values. Since this function has heavy calculations this is delegated to the libuv thread pool.

// threads.js
const crypto = require('crypto');
const start = Date.now();crypto.pbkdf2('a', 'b', 10000, 512, 'sha512', () => {
console.log('1', Date.now() - start);
});
crypto.pbkdf2('a', 'b', 10000, 512, 'sha512', () => {
console.log('2', Date.now() - start);
});
crypto.pbkdf2('a', 'b', 10000, 512, 'sha512', () => {
console.log('3', Date.now() - start);
});
crypto.pbkdf2('a', 'b', 10000, 512, 'sha512', () => {
console.log('4', Date.now() - start);
});

The execution results of the above code on my laptop yielded below results which averages to around 60 milliseconds per function call, these values may differ according to your machines performance. The reason these values are similar is because all four of the above calls are assigned to all four threads in the thread pool at the same time and since my laptop has four cores these threads are evenly divided and executed among them.

Now I will add two more pbkdf2 functions to the above code and we can see what happens to the execution results.

We can see a clear difference between the initial four execution times and the remaining two. This is because the default number of threads in the libuv thread pool is 4 so only the initial four function calls are taken up by the pool first, once two of those threads are freed the remaining two function calls are taken up which explains the reason they take similar to twice the average execution time.

The default thread pool size for libuv can be overwritten by setting the environment variable UV_THREADPOOL_SIZE to any value upto 1024. The below code needs to be added to top of threads.js to change the default value of the thread pool size to eight.

process.env.UV_THREADPOOL_SIZE = 8;

Now to show a clear difference of how the programme executes with different thread pool sizes, I will execute thread.js with 8 pbkdf2 function calls first with the thread pool count as 4 and then as 8.

The initial result shows a clear difference between the first 4 execution times and the remaining 4, the reason for this as you all should be able to guess by now is the default thread count being 4. The initial 4 function calls get taken up by the thread pool so the remaining 4 function calls need to stay until the thread pool is freed again which is why they have roughly twice the time.

Now for the second result, here I have overwritten the thread pool count to 8. Now all 8 function calls get taken up at the same time, but since my laptop has only 4 cores and since each core now has to do twice the amount work it roughly takes double the normal time for each of the function calls to complete.

Even though the threadpool does the calculation these function calls are still considered pending operations to the event loop.

Now to answer the question is node single threaded, it is not a simple yes or no. The node event loop is single threaded, but some of the node std modules (fs and some modules of crypto) uses multiple threads as I have explained above.

References

https://www.udemy.com/course/advanced-node-for-developers/

--

--