From version 2.2.0 node-report has been enhanced to provide more details on the libuv handles on the event loop in the “Node.js libuv Handle Summary” section. This article explains more about this new section and how you can relate some of this information back to your Node.js code. The libuv event loop is the engine of Node.js responsible for scheduling all the different activities that happen within a Node.js application. For a guide to the event loop see: The Node.js Event Loop, Timers, and process.nextTick()

Here’s some minimal output created by running node -e "console.log(require('node-report').getReport());":

================================================================================
==== Node.js libuv Handle Summary ==============================================

(Flags: R=Ref, A=Active)
Flags  Type      Address             Details
[-A]   async     00000000x1012b33e8  
[-A]   async     00000000x101e0a350  
[--]   check     00000000x102036e10  
[R-]   idle      00000000x102036e88  
[--]   prepare   00000000x102036f78  
[--]   check     00000000x102036ff0  
[--]   idle      00000000x102036f00  
[R-]   timer     00000000x102037088  repeat: 0, timeout in: 2268121012661109602 ms
[R-]   tty       00000000x101e0b690  width: 132, height: 43, file descriptor: 10, write queue size: 0, writable
[-A]   signal    00000000x101e0bab8  signum: 28 (SIGWINCH)
[R-]   tty       00000000x101e0bc00  width: 132, height: 43, file descriptor: 12, write queue size: 0, writable
[-A]   async     00000000x101dd0c60

This is close to the smallest set of handles you can get on the event loop and simply shows the handles that Node.js creates by itself. The different handle types are explained more below.

The handle summary section contains a table listing all the handles with four columns, flags, type, address and details. The details vary by handle type and the address is simply the location of the handle structure in memory – it provides a unique identifier for each handle. The flag values are R (referenced) and A (active).

A referenced handle is one that will keep the Node.js process running. When there are no referenced handles the event loop the libuv event loop will exit. If you are used to Java then unreferenced handles serve a similar purpose to daemon threads in Java. They do work in the background but don’t block the process from shutting down if they are still running. The libuv event loop will shutdown when no referenced handles or active requests remain. If your Node.js application isn’t terminating when you expect it to then looking at the referenced handles in node-report may be a good starting place for debugging what is stopping your application from shutting down.

Depending on the handle type “active” may mean different things, this is covered in more detail in the libuv documentation. As a guide if the handle type T has a uv_T_start() function then it is active from when that is called until uv_T_stop() is called.

Below are the some examples of the handles node-report can provide extra detail on. Unfortunately two common handles aren’t included, async and check. As can be seen in the sample output above that’s because there are no extra details available for these. Both of these handles exist to allow a callback function to be triggered:

  • The async handle is used to trigger a callback from another thread.
  • The check handle is used to trigger a callback once per iteration of the event loop.

The callback they use will be a C function passed when the handle was created that may then call into Node.js to run JavaScript. Node.js uses these handles a lot but at present it’s not possible to see what the callbacks will invoke.

In the examples below I’ve trimmed the output to only include the extra information that the sample code created, I’ve omitted the default information from the simple example above.

Handle types.

fs_event and fs_poll

These two types of handle are for watching for file system events.

Test program:

const fs = require('fs')
const nodereport = require('node-report')

// Persistent means this will run 'till I hit Ctrl-C
fs.watch("/etc/passwd", {persistent: true}, function(eventType, filename) {
  console.log(eventType + " happened on " + filename)
});

fs.watch("/etc/group", {persistent: false}, function(eventType, filename) {
  console.log(eventType + " happened on " + filename)
});

// watchFile uses polling and is less efficient but we can see the
// difference in the node-report output
fs.watchFile("/etc/passwd", {persistent: true}, function(eventType, filename) {
  console.log(eventType + " happened on " + filename)
});

fs.watchFile("/etc/group", {persistent: false}, function(eventType, filename) {
  console.log(eventType + " happened on " + filename)
});

console.log(nodereport.getReport())

Output:

Flags  Type      Address             Details
...
[RA]   fs_event  00000000x104000eb8  filename: /etc/passwd
[-A]   fs_event  00000000x104001038  filename: /etc/group
[RA]   fs_poll   00000000x104000d50  filename: /etc/bashrc
[-A]   fs_poll   00000000x101f08090  filename: /etc/shells

By using different files for each call we can see which one relates to which call easily. You can see that fs.watch() uses the fs_event handles and the older (less efficient) fs.watchFile uses fs_poll. You can also see that when we set persistent: true the fs_event/fs_poll handles stay referenced. If you read the documentation for fs.watch it explains that setting persistent: true allows keeps the Node.js process alive as long as the file is being watched. Setting it to false means that your process will exit. This is implemented by unreferencing the non-persistent handles.

As discussed above you can use the referenced state of a handle to help debug why Node.js is or is not exiting. Using the filename from the fs_event/fs_poll handles allows you to relate this back to your Node.js code.

process

The process handle controls child processes that has been created through libuv. The functions in the Node.js child_process module are an easy way to demonstrate this. Node-report shows the pid for the process in the details field.

Test program:

const child_process = require('child_process')
const nodereport = require('node-report')

child_process.spawn(process.execPath, ['-e', "process.stdin.on('data', (x) => console.log(x.toString()));"]);

console.log(nodereport.getReport())

Output:

Flags  Type      Address             Details
...
[RA]   process   00000000x104100c48  pid: 45890
...

signal

The signal handle is used to trigger callbacks when the process receives a signal. You can create a listener in Node.js by using process.on() with a POSIX signal name. (See the Node.js documentation for Signal Events – signal behaviour can be unexpected and is very platform dependant.)

Test program:

const nodereport = require('node-report');

process.on('SIGINT', () => {
  console.log('Do something');
});

process.on('SIGINFO', () => {
  console.log('Do something else');
});

console.log(nodereport.getReport());

Output;

Flags  Type      Address             Details
...
[-A]   signal    00000000x103001318  signum: 2 (SIGINT)
[-A]   signal    00000000x1030013f8  signum: 29 (SIGINFO)
...

You can see that the signal handles are unreferenced. Adding a callback on a signal will not keep your Node.js process alive. The example above terminates immediately after writing the report.

tcp and udp

The tcp and udp handles are used for creating tcp and udp connections. The connection types are very different in behaviour but the information available through node-report is quite similar.

Test program:

const nodereport = require('node-report');
const dgram = require('dgram');
const http = require('http');

// tcp
const httpserver = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  // Browse to http://127.0.0.1:4321/ to see the
  // node-report output with handle information
  res.end(nodereport.getReport())
}).listen(4321, '127.0.0.1');

// udp
const udpserver = dgram.createSocket('udp4');

udpserver.on('error', (err) => {
  console.log(`udpserver error:\n${err.stack}`);
  udpserver.close();
});

udpserver.on('message', (msg, rinfo) => {
  console.log(`udpserver got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});

udpserver.on('listening', () => {
  var address = udpserver.address();
  console.log(`udpserver listening ${address.address}:${address.port}`);
});

udpserver.bind(41234, "127.0.0.1", function startSending() {
  console.log("Starting timer...");
  const client = dgram.createSocket('udp4');
    setInterval(() => { client.send("A Message", 41234, '127.0.0.1') }, 1000);
});

Output:

Flags  Type      Address             Details
...
[RA]   udp       00000000x103202e78  localhost:41234, send buffer size: 9216, recv buffer size: 196724, file descriptor: 12
[RA]   tcp       00000000x103200f90  localhost:4321 (not connected), send buffer size: 131072, recv buffer size: 131072, file descriptor: 11, write queue size: 0, readable, writable
[RA]   udp       00000000x103203998  0.0.0.0:55572, send buffer size: 9216, recv buffer size: 196724, file descriptor: 15
[RA]   tcp       00000000x101e0a230  localhost:4321 connected to localhost:57742, send buffer size: 146988, recv buffer size: 407886, file descriptor: 16, write queue size: 0, readable, writable
[RA]   tcp       00000000x101e0a4b0  localhost:4321 connected to localhost:57743, send buffer size: 146988, recv buffer size: 408300, file descriptor: 17, write queue size: 0, readable, writable
[RA]   tcp       00000000x101e0a6f0  localhost:4321 connected to localhost:57744, send buffer size: 146988, recv buffer size: 408300, file descriptor: 18, write queue size: 0, readable, writable
[RA]   tcp       00000000x101e0a9a0  localhost:4321 connected to localhost:57745, send buffer size: 146988, recv buffer size: 408300, file descriptor: 19, write queue size: 0, readable, writable
...

(The output above is trimmed to remove all other handles, including the timers the example setup as they are covered later.)

The report contains details about every tcp and udp handle: if they are connected, where they are connected from and to, the send and receive buffer sizes, the file descriptors they are using, the write queue size and whether they are readable or writeable. You can see from the port numbers in the example that the http server is creating the tcp handles to manage tcp connections as it uses port 4321. The udp server is listening on localhost:41234.

Any Node.js application that is serving http data is likely to have a number of tcp handles. You can use this output to see how many active connections to you have to your Node.js process. You can relate the output back to your Node.js program by looking at the port numbers, all the localhost:4321 connections are connections to the server that was listening on port 4321.

The file descriptor numbers match the values reported by lsof. If you compare the report output for a process against the output of lsof -p for the same process you can correlate the output.

timer

The timer handle is used to schedule callbacks that will happen in the future. As you might expect they are used by setInterval and setTimeout.

Test program:

const nodereport = require('node-report')

let timeout = setInterval(() => console.log("Tick... Tock...\n"), 1000);
timeout.unref();

setTimeout(() => console.log("Quitting now!\n"), 10000);

// Let the timers do something before we generate the report.
setTimeout( () => console.log(nodereport.getReport()), 3500);

Output:

Flags  Type      Address             Details
...
[-A]   timer     00000000x102805a38  repeat: 0, timeout in: 503 ms
[RA]   timer     00000000x102805b18  repeat: 0, timeout in: 6496 ms
[R-]   timer     00000000x102805bf8  repeat: 0, timeout expired: 4 ms ago
...

In the example above one timer is set to occur every second, one to occur in 10 seconds. The node-report is scheduled after 3.5 seconds just to make the example clearer by allowing some time to pass. You can see that node-report shows the time remaining on the timer handles. In the case of the handle running node-report the handle has already expired (and isn’t active) but it’s running the callback that is creating the report so it still exists. Node.js optimises it’s use of timer handles and may not create one per call to setInterval() or setTimeout() if it decides it can share them.

The program calls unref() on the timeout object returned by the setInterval call and you can see that the handle that belongs to that timer does not have an R in the Flags column. This means that this handle will not prevent the event loop from shutting down and the Node.js program from exiting.

The repeat field is set to 0 for all the timer handles, Node.js doesn’t appear to use it. The repeat field is not just included in the output for completeness but because other native code such as an npm module could put a timer handle on the event loop and use the repeat parameter.

tty

The tty handle is used to represent streams for input or output to the console. These generally aren’t created directly but they are created to enable Node.js to read from or write to the console.

Test program:

const nodereport = require('node-report')

process.stdin.on('data', (data) => {
  console.log(data)
});

console.log(nodereport.getReport())

Output:

Flags  Type      Address             Details
...
[R-]   tty       00000000x102904ea0  width: 132, height: 43, file descriptor: 10, write queue size: 0, readable
...

The test program causes Node.js to create a tty for stdin. (By default Node.js will create tty’s for stdout and stderr). The width and height values match the size of your terminal. You also get the size of the file descriptor number, the write queue size and whether the tty is readable or writable.

The example above creates two listeners on SIGINT and SIGINFO then immediately creates a report and exits. The output shows you the signal number and name that libuv was listening for which can be useful if your Node.js process isn’t responding to a signal as you expected. For example if a SIGINT handler is installed Node.js will no longer exit when your press Ctrl-C at a terminal – it will run the listener for SIGINT instead.

More Information

If you want more detailed information on libuv handles the best place to start is the libuv API. It’s also worth noting that although Node.js itself uses libuv heavily, npm modules with native code can also create libuv handles. They could also bypass libuv and create tcp connections or processes that aren’t managed by libuv and therefore aren’t visible in node-report. It’s even possible for a piece of native code to create another libuv event loop of it’s own that node-report would know nothing about. This isn’t that far fetched, when you run node --inspect it actually creates it’s own event loop. Node.js uses the default libuv event loop obtained by a call uv_default_loop() and that it is only the handles on included in the node-report output.

Finally, if you happen look at the source code for node-report you might notice the case statement used to look up handle types includes some that aren’t mentioned in this article and even some names you should never see, for example “unknown”. There’s two reasons for that, firstly the C compiler likes it if the case statement covers all the options and warns if you miss any. We could have a “default” case instead but then we wouldn’t get a warning if we missed one or libuv added a new handle type. Secondly node-report is a debugging tool, if it shows a handle without a type or one that’s unexpected or (theoretically) impossible then that’s something you really want to know about! At that point you might want to look at any npm modules with native code or even Node.js itself to see what’s going on.

Join The Discussion

Your email address will not be published. Required fields are marked *