Bubble Up your Node.js
I/O
@matteocollina
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/
- JS adds a function as a listener for an I/O event
- The I/O event happens
- 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
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)
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)
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)
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)
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)
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.
If you need some help to improve the performance
of your application, reach out!