Node Core  #2

Async node

Plan

  • Event loop
  • setImmediate(fn) vs setTimeout(fn, 0)
  • process.nextTick()
  • macrotasks vs microtasks
  • Worker threads and child processes

Event loop!

setTimeout(cb, 0)

when the cb will be executed?

Event-loop phases

Phases Overview

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

check: setImmediate() callbacks are invoked here.

close callbacks: some close callbacks, e.g. socket.on('close', ...).

Microtasks

setTimeout(() => console.log("timeout"));

Promise.resolve()
  .then(() => console.log("promise"));
  
console.log("main");

Explain this!

Only after tasks in microTasks are completed, event loop will next pick up tasks from macroTasks.

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();

queueMicrotask

const messageQueue = [];

let sendMessage = message => {
  messageQueue.push(message);

  if (messageQueue.length === 1) {
    queueMicrotask(() => {
      const json = JSON.stringify(messageQueue);
      messageQueue.length = 0;
      fetch("url-of-receiver", json);
    });
  }
};
console.log(1)
setTimeout(() => {
    queueMicrotask(() => {
       console.log(2)
     });
  console.log(3)
});
Promise.resolve().then(() => console.log(4))
queueMicrotask(() => {
    console.log(5)
    queueMicrotask(() => {
        console.log(6)
    });
});
console.log(7)

Just remember — sync tasks → micro tasks → macro tasks

What will be the output of this snippet?

When to use microtasks

Generally, it's about capturing or checking results, or performing cleanup, after the main body of a JavaScript execution context exits, but before any event handlers, timeouts and intervals, or other callbacks are processed.

setImmediate vs. nextTick

setImmediate()

setImmediate(callback[, ...args]) takes a callback and add it to the event queue( specifically the immediate queue).

console.log('Start')

setImmediate(() => console.log('Queued using setImmediate'))

console.log('End')
setTimeout(() => {
  console.log('timeout');
}, 0);

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

What will be the output of this snippet?

process.nextTick()

process.nextTick(callback[, ...args]) will also takes a callback and optional args parameters like setImmediate() function. But instead of "immediate queue", the callbacks are queued in the "next tick queue".

console.log('Start')

process.nextTick(() => console.log('Queued using process.nextTick'))

console.log('End')

It allows you to "starve" your I/O by making a recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.

Why use process.nextTick()?

There are two main reasons:

  1. Allow users to handle errors, cleanup 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

Promise.resolve().then(() => { console.log('1'); });

process.nextTick(() => {
  console.log('2.');
  process.nextTick(() => {
    console.log('3');
    process.nextTick(() => {
      console.log('3');
      process.nextTick(() => {
        console.log('5');
      })
    })
  })
})

Unlike process.nextTick(), recursive calls to setImmediate() won't block the event loop

let count = 0

const cb = () => {
    console.log(`Processing setImmediate cb ${++count}`)
    setImmediate(cb)
}

setImmediate(cb)
setTimeout(() => console.log('setTimeout executed'), 100)

console.log('Start')

We recommend developers use setImmediate() in all cases because it's easier to reason about (and it leads to code that's compatible with a wider variety of environments, like browser JS.)

Always use setImmediate.

Worker threads and child processes

If we are running this heavy CPU bound operation in the context of a web application,the single thread of node will be blocked and hence the webserver won't be able to respond to any request because it is busy calculating results.

const express = require("express")

const app = express()
app.get("/getfibonacci", (req, res) => {
  const startTime = new Date()
  const result = fibonacci(parseInt(req.query.number)) //parseInt is for converting string to number
  const endTime = new Date()
  res.json({
    number: parseInt(req.query.number),
    fibonacci: result,
    time: endTime.getTime() - startTime.getTime() + "ms",
  })
})
const fibonacci = n => {
  if (n <= 1) {
    return 1
  }
  return fibonacci(n - 1) + fibonacci(n - 2)
}
app.listen(3000, () => console.log("listening on port 3000"))

Wait, Cant Promises solve this problem?

Child process

const express = require("express")
const app = express()
const { fork } = require("child_process")

app.get("/isprime", (req, res) => {
  const childProcess = fork("./forkedchild.js") //the first argument to fork() is the name of the js file to be run by the child process
  childProcess.send({ number: parseInt(req.query.number) }) //send method is used to send message to child process through IPC
  const startTime = new Date()
  childProcess.on("message", message => {
    //on("message") method is used to listen for messages send by the child process
    const endTime = new Date()
    res.json({
      ...message,
      time: endTime.getTime() - startTime.getTime() + "ms",
    })
  })
})


// child.js
// 


process.on("message", message => {
  //child process is listening for messages by the parent process
  const result = isPrime(message.number)
  process.send(result)
  process.exit() // make sure to use exit() to prevent orphaned processes
})
function isPrime(number) {
  let isPrime = true
  for (let i = 3; i < number; i++) {
    if (number % i === 0) {
      isPrime = false
      break
    }
  }
  return {
    number: number,
    isPrime: isPrime,
  }
}

Thread worker

const {
  Worker, isMainThread, parentPort, workerData
} = require('worker_threads');

if (isMainThread) {
  module.exports = function parseJSAsync(script) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, {
        workerData: script
      });
      worker.on('message', resolve);
      worker.on('error', reject);
      worker.on('exit', (code) => {
        if (code !== 0)
          reject(new Error(`Worker stopped with exit code ${code}`));
      });
    });
  };
} else {
  const { parse } = require('some-js-parsing-library');
  const script = workerData;
  parentPort.postMessage(parse(script));
}
import { parentPort } from 'worker_threads';
parentPort.on('message', () => {
 const numberOfElements = 100;
 const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * numberOfElements);
 const arr = new Int32Array(sharedBuffer);
 for (let i = 0; i < numberOfElements; i += 1) {
   arr[i] = Math.round(Math.random() * 30);
 }
 parentPort.postMessage({ arr });
});

In order for the memory to be shared, an instance of ArrayBuffer or SharedArrayBuffer must be sent to the other thread as the data argument or inside the data argument.

SharedArrayBuffer

Cluster

const cluster = require("cluster")
const http = require("http")
const cpuCount = require("os").cpus().length //returns no of cores our cpu have
if (cluster.isMaster) {
  masterProcess()
} else {
  childProcess()
}
function masterProcess() {
  console.log(`Master process ${process.pid} is running`)
  //fork workers.
  for (let i = 0; i < cpuCount; i++) {
    console.log(`Forking process number ${i}...`)
    cluster.fork() //creates new node js processes
  }
  cluster.on("exit", (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`)
    cluster.fork() //forks a new process if any process dies
  })
}
function childProcess() {
  const express = require("express")
  const app = express()
  //workers can share TCP connection
  app.get("/", (req, res) => {
    res.send(`hello from server ${process.pid}`)
  })
  app.listen(5555, () =>
    console.log(`server ${process.pid} listening on port 5555`)
  )
}

Thank you!

Links

Node Core #2

By Vladimir Vyshko

Node Core #2

  • 453