NodeJS #4

Async

$ whoami

Inna Ivashchuk

Senior Software Engineer

JS developer, music fan, movie-dependent and Star Wars fan 🤓

May the Force be with you!

4+ years with GlobalLogic

6+ years in web-development

        GitHub page

Agenda

  • EventLoop

  • macrotasks vs microtasks

  • setImmediate(fn) vs setTimeout(fn, 0)

  • process.nextTick()

  • Streams

EventLoop

Node.js architecture

JavaScript is “single-threaded” because JavaScript works on “Event Loop”

NodeJS is NOT “single-threaded” as it has a libuv threadpool in its runtime.

The chefs are the libuv threadpool and OS Async Helpers.

The waiter is the V8 engine and the Event Loop

The JavaScript code you want to be executed is the food

Node.js restorant

What is the Event Loop?

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.

Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed. We'll explain this in further detail later in this topic.

Event Loop Explained

The following diagram shows a simplified overview of the event loop's order of operations.

setTimeout(cb, 0)

when the cb will be executed?

Event-loop phases

Phases Overview

Each phase has a FIFO queue of callbacks to execute and here is the list of them:

  • timers: this phase executes callbacks scheduled by setTimeout() and setInterval().
  • pending callbacks: executes I/O callbacks deferred to the next loop iteration.
  • idle, prepare: only used internally.
  • poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); the node will block here when appropriate.
  • check: setImmediate() callbacks are invoked here.
  • close callbacks: some close callbacks, e.g. socket.on('close', ...).

 

Phases in details

timers

   A timer specifies the threshold after which a provided callback may be executed rather than the exact time a person wants it to be executed. Timers callbacks will run as early as they can be scheduled after the specified amount of time has passed.

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }

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:

  1. Calculating how long it should block and poll for I/O, then
  2. 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.

check

This phase allows a person to execute callbacks immediately after the poll phase has been completed. If the poll phase becomes idle and scripts have been queued with setImmediate(), the event loop may continue to the check phase rather than waiting.

setImmediate() is actually a special timer that runs in a separate phase of the event loop. It uses a libuv API that schedules callbacks to execute after the poll phase has completed.

close callbacks

If a socket or handle is closed abruptly (e.g. socket.destroy()), the 'close' event will be emitted in this phase. Otherwise it will be emitted via process.nextTick().

macrotasks vs microtasks

macroTasks

setTimeout

setInterval

setImmediate

requestAnimationFrame

microTasks

I/O

UI rendering

process.nextTick

Promises

queueMicrotask

MutationObserver

microTasks and macroTasks

If microtasks continuously add more elements to microTasks queue, macroTasks will stall and won’t complete the event loop in a shorter time causing event loop delays.

function logA() { console.log('A') };
function logB() { console.log('B') };
function logC() { console.log('C') };
function logD() { console.log('D') };
function logE() { console.log('E') };
function logF() { console.log('F') };
function logG() { console.log('G') };
function logH() { console.log('H') };
function logI() { console.log('I') };
function logJ() { console.log('J') };

logA();
setTimeout(logG, 0);
Promise.resolve()
  .then(logC)
  .then(setTimeout(logH))
  .then(logD)
  .then(logE)
  .then(logF);
setTimeout(logI);
setTimeout(logJ);
logB();

When to use and what?

Basically, use microtasks when you need to do stuff asynchronously in a synchronous way (i.e. when you would say perform this (micro-)task in the most immediate future). Otherwise, stick to macrotasks.

  • Microtasks are processed when the current task ends and the microtask queue is cleared before the next macrotask cycle.
  • Microtasks can enqueue other microtasks. All are executed before the next task in line.
  • UI rendering is run after all microtasks execution (NA for nodejs).

setImmediate(fn) vs setTimeout(fn, 0)

setImmediate() and setTimeout() are similar, but behave in different ways depending on when they are called.

  • setImmediate() is designed to execute a script once the current poll phase completes.
  • setTimeout() schedules a script to be run after a minimum threshold in ms has elapsed.

setImmediate() vs setTimeout()

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

setImmediate(() => {
  console.log('immediate');
});

The main advantage - setImmediate() will always be executed before any timers if scheduled within an I/O cycle, independently of how many timers are present.

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

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

setImmediate() vs setTimeout()

process.nextTick()

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  process.nextTick(() => {
      //do something
  });

});

Every time the event loop takes a full trip, we call it a tick.

When we pass a function to process.nextTick(), we instruct the engine to invoke this function at the end of the current operation, before the next event loop tick starts:

Understanding process.nextTick()

process.nextTick() vs setImmediate()

We have two calls that are similar as far as users are concerned, but their names are confusing.

  • process.nextTick() fires immediately on the same phase
  • setImmediate() fires on the following iteration or 'tick' of the event loop

In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate(), but this is an artifact of the past which is unlikely to change.

Why use process.nextTick()?

  1. Allow users to handle errors, clean up any then unneeded resources, or perhaps try the request again before the event loop continues.

  2. At times it's necessary to allow a callback to run after the call stack has unwound but before the event loop continues.

One example is to match the user's expectations. Simple example:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(() => {
    this.emit('event');
  });
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});

Streams

What are streams

    Streams are one of the fundamental concepts that power Node.js applications.

     They are a way to handle reading/writing files, network communications, or any kind of end-to-end information exchange in an efficient way.

     The Node.js stream module provides the foundation upon which all streaming APIs are built. All streams are instances of EventEmitter

What are streams

Using Stream API from "fs" module

Why streams

Streams provide two major advantages over using other data handling methods:

  • Memory efficiency: you don't need to load large amounts of data in memory before you are able to process it
  • Time efficiency: it takes way less time to start processing data, since you can start processing as soon as you have it, rather than waiting till the whole data payload is available

 

An example of a stream

const http = require('http')
const fs = require('fs')

const server = http.createServer(function(req, res) {
  fs.readFile(__dirname + '/data.txt', (err, data) => {
    res.end(data)
  })
});

server.listen(3000)

Using the Node.js fs module:

An example of a stream

    If the file is big, the operation will take quite a bit of time. Here is the same thing written using streams:

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  const stream = fs.createReadStream(__dirname + '/data.txt')
  stream.pipe(res)
});

server.listen(3000);

pipe()

     The return value of the pipe() method is the destination stream, which is a very convenient thing that lets us chain multiple pipe() calls, like this:

src.pipe(dest1).pipe(dest2)
src.pipe(dest1)
dest1.pipe(dest2)

and it's equal to:

pipe()

Streams-powered Node.js APIs

  • process.stdin returns a stream connected to stdin
  • process.stdout returns a stream connected to stdout
  • process.stderr returns a stream connected to stderr
  • fs.createReadStream() creates a readable stream to a file
  • fs.createWriteStream() creates a writable stream to a file
  • net.connect() initiates a stream-based connection
  • http.request() returns an instance of the http.ClientRequest class, which is a writable stream
  • zlib.createGzip() compress data using gzip (a compression algorithm) into a stream
  • zlib.createGunzip() decompress a gzip stream.
  • zlib.createDeflate() compress data using deflate (a compression algorithm) into a stream
  • zlib.createInflate() decompress a deflate stream

Different types of streams

Writable

Readable

Duplex

Transform

streams to which data can be written (for example, fs.createWriteStream()).

streams from which data can be read (for example, fs.createReadStream())

streams that are both Readable and Writable (for example, net.Socket)

Duplex streams that can modify or transform the data as it is written and read (for example, zlib.createDeflate()).

How to create a readable stream

const Stream = require('stream')
const readableStream = new Stream.Readable()

// can be done like that or as an option
// readableStream._read = () => {}

const readableStream = new Stream.Readable({
  read() {}
})

// the stream is initialized, we can send data to it
readableStream.push('hi!')
readableStream.push('ho!')

How to get data from a readable stream

const Stream = require('stream')

const readableStream = new Stream.Readable({
  read() {}
})
const writableStream = new Stream.Writable()

writableStream._write = (chunk, encoding, next) => {
  console.log(chunk.toString())
  next()
}

readableStream.pipe(writableStream)

readableStream.push('hi!')
readableStream.push('ho!')

You can also consume a readable stream directly, using the readable event:

readableStream.on('readable', () => {
  console.log(readableStream.read())
})

How to create a writable stream

const Stream = require('stream')
const writableStream = new Stream.Writable()


// can be done like that or as an option
// const readableStream = new Stream.Write({
//   write() {}
// })

writableStream._write = (chunk, encoding, next) => {
  console.log(chunk.toString())
  next()
}


// the stream is initialized, we can send data to it
process.stdin.pipe(writableStream)

How to send data to a writable stream

writableStream.write('hey!\n')

Using the stream write() method:

const Stream = require('stream')

const readableStream = new Stream.Readable({
  read() {}
})
const writableStream = new Stream.Writable()

writableStream._write = (chunk, encoding, next) => {
  console.log(chunk.toString())
  next()
}

readableStream.pipe(writableStream)

readableStream.push('hi!')
readableStream.push('ho!')

writableStream.end()

Signaling a writable stream that you ended writing

How to create a transform stream

We get the Transform stream from the stream module, and we initialize it and implement the transform._transform() method.

First create a transform stream object:

const { Transform } = require('stream')
const TransformStream = new Transform();

// then implement _transform:
TransformStream._transform = (chunk, encoding, callback) => {
  console.log(chunk.toString().toUpperCase());
  callback();
}

// Pipe readable stream:
process.stdin.pipe(TransformStream);

Q & A

Homework

  1. Create a new directory tutorial4

  2. Create a functions microTasks and macroTasks and use the corresponding JS tasks inside them like setTimeout(), process.nextTick() an etc.

  3. Use a Promise to calculate the Fibonacci sequence

  4. Use fs.createReadStream to read a file

  5. Use fs.createWriteStream to write a file

  6. Use pipe() to read and compress files

Helpful souces

NodeJS Core #4

By Inna Ivashchuk

NodeJS Core #4

  • 468