How to not oversaturate libuv with tasks when using IO operations

Viewed 42

I am using Typescript and therefore libuv to perform any IO operations. In my specific scenario I am taking a fingerprint hash of a given file. For the sake of making a point here to my question, consider the input file being a file of 1TB. To get a fingerprint of the file, I could open the file through a file stream and update the hash:

return new Promise((resolve, reject) => {
    const hash = crypto.createHash('sha256');
    const fh = fse.createReadStream(filepath, {
        highWaterMark : 100000000
    });

    fh.on('data', (d) => { hash.update(d); });
    fh.on('end', () => {
        resolve(hash);
    });
    fh.on('error', reject);
});

The example above is fairly slow given it's sequential approach. So a faster approach I was thinking about is to split the calculation up into N blocks like this:

let promises = [];
for (let i = 0; i < N; ++i) {
    promises.push(calculateFilePart(file, from, to));
}
return Promise.all(all);

Given the example above, imagine N is 1000000, does that mean libuv starts 1000000 asynchronous I/O operations in the background at the same time? Or does libuv queue them automatically in batches to avoid an oversaturation of IO requests?

Any help in this topic is highly appreciated!

1 Answers

I will attempt to summarize some key concepts as briefly as possible. I will leave links for reference below so you can verify the facts.

Promise adds a task to something called a Microtask Queue. At each iteration of the Event Loop, when the call stack is empty, tasks from Microtask Queue is processed. This is known as a tick. So, every tick, some tasks from Microtask Queue will be processed.

For every process tick, there is a max depth (process.maxTickDepth). This specifies the number of tasks to be offloaded from Microtask Queue and pushed into the call stack.

Main part of your algorithm involves reading the contents that is an I/O operation. Such operations are pushed into a separate queue known as the Macrotask Queue. When a schedule Macrotask operation is completed and it has the specified chunk of the content, the event handler for the read operation is queued into Microtask Queue for processing at the next tick.

Given your snippet and constraints,if max depth is 1000, then for your algorithm to fully update the hash at least N / 1000 = 1000000 / 1000 = 1000 ticks need to be passed. This means that the Node.js process will only handle a specific number of tasks per tick.

I hope that provides you with the understanding you are looking for.

References:

Node.js Under The Hood #3 - Deep Dive Into the Event Loop

MDN Documentation on Promise.all

Related