Bubble Up your Node.js I/O
@matteocollina
These slides: https://mcollina.github.io/bubble-up-your-node-io/
How to debug asynchronous activity?
The Node.js Event Loop

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming    │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Source: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
    

  1. JS adds a function as a listener for an I/O event
  2. The I/O event happens
  3. The specified function is called
In Node.js, there is no parallelism of function execution.
What about nextTick, Promises, and setImmediate?
nextTicks are always executed before Promises and other I/O events.
Promises are exectued synchronously and resolved asynchronously, before any other I/O events.
setImmediate exercise the same flow of I/O events.
The hardest concept in Node.js is to know when a chunk of code is running relative to another.
How to debug multiple asynchronous activity?
The asynchronous context problem.

const http = require('http')
const { promisify } = require('util')
const sleep = promisify(setTimeout)

http.createServer((req, res) => {
  // A new logical asynchronous context is created
  // for each request. It comes in the form of closure data.

  sleep(20).then(() => {     // this is part of the same context
    res.end('hello world')
  }, (err) => {              // so is this
    res.statusCode = 500
    res.end(JSON.stringify(err))
  })
}).listen(3000)
    
There is no concept of asynchronous context in JavaScript
A lot of work has been done to formalize this in https://github.com/nodejs/diagnostics but it is still early days.
If there is no context, how can you track I/O events?
async_hooks

const asyncHooks = require('async_hooks')
exports.skipThis = false
const skipAsyncIds = new Set()
const hook = asyncHooks.createHook({
  init (asyncId, type, triggerAsyncId) {
    // Save the asyncId such nested async operations can be skiped later.
    if (exports.skipThis) return skipAsyncIds.add(asyncId)
    // This is a nested async operations, skip this and track futher nested
    // async operations.
    if (skipAsyncIds.has(triggerAsyncId)) return skipAsyncIds.add(asyncId)

    // Track async events that comes from this async operation
    exports.skipThis = true
    encoder.write({
      asyncId: asyncId,
      frames: stackTrace(2)
    })
    exports.skipThis = false
  },

  destroy (asyncId) {
    skipAsyncIds.delete(asyncId)
  }
})
hook.enable()
    
Yes, it's complex. It also generate a lot of data. We worked 6 months trying to make sense of that data.
How to visualize asynchronous activity?
Bubbles

const http = require('http')
const { promisify } = require('util')
const sleep = promisify(setTimeout)

http.createServer(function handle (req, res) => {
  sleep(20).then(() => {
    res.end('hello world')
  }, (err) => {
    res.statusCode = 500
    res.end(JSON.stringify(err))
  })
}).listen(3000)
    
demo

const http = require('http')
const { promisify } = require('util')
const sleep = promisify(setTimeout)

async function something (req, res) {
  await sleep(20)
  res.end('hello world')
}

http.createServer(function handle (req, res) {
  something(req, res).catch((err) => {
    res.statusCode = 500
    res.end(JSON.stringify(err))
  })
}).listen(3000)
    
demo

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

http.createServer(function f1 (req, res) {
  fs.readFile(__filename, function f2 (err, buf1) {
    if (err) throw err
    fs.readFile(__filename, function f3 (err, buf2) {
      if (err) throw err
      fs.readFile(__filename, function f4 (err, buf3) {
        if (err) throw err
        res.end(Buffer.concat([buf1, buf2, buf3]))
      })
    })
  })
}).listen(3000)
    
demo

const http = require('http')
const fs = require('fs')
const { promisify } = require('util')
const readFile = promisify(fs.readFile)

async function handle (req, res) {
  const a = await readFile(__filename)
  const b = await readFile(__filename)
  const c = await readFile(__filename)

  res.end(Buffer.concat([a, b, c]))
}

http.createServer(function (req, res) {
  handle(req, res).catch((err) => {
    res.statusCode = 500
    res.end('kaboom')
  })
}).listen(3000)
    
demo

const http = require('http')
const fs = require('fs')
const { promisify } = require('util')
const readFile = promisify(fs.readFile)

async function handle (req, res) {
  res.end(Buffer.concat(await Promise.all([
    readFile(__filename),
    readFile(__filename),
    readFile(__filename)
  ])))
}

http.createServer(function (req, res) {
  handle(req, res).catch((err) => {
    res.statusCode = 500
    res.end('kaboom')
  })
}).listen(3000)
    
demo
https://github.com/ipfs/js-ipfs/issues/1785
Performance Considerations
As a result of a "slow" I/O opreation, your application increase the amount of concurrent tasks.
A huge amount of concurrent tasks increase the memory consumption of your application.
An increase in memory consumption increase the amount of work the garbage collector (GC) needs to do on our CPU.
Under high load, the GC will steal CPU cycles from our JavaScript critical path.
Therefore, latency and throughput are connected.
https://clinicjs.org
If you need some help to improve the performance of your application, reach out!
These slides: https://mcollina.github.io/bubble-up-your-node-io/
Thanks
@matteocollina