Post

Node.js Performance Hooks

Measure the performance of your app

Node.js Performance Hooks

1. Introduction

Performance measurement is a vital part of a successful application. To provide the best customer experience, we need to have a website that’s as fast as possible.

A failure to provide a well-performing application to our customers could result in losing a significant amount of revenue. For example, if every page on Amazon loaded just a second slower, the company would lose $1.6 billion per year 1. In addition, Google uses page speed as an essential attribute when ranking pages 2. Hence, to be able to optimize the performance of our application, we need to start monitoring it from day one.

In this article, we’re going to look into Node’s performance measurement API, which helps us monitor the performance in a convenient way.

2. The Classic Problems with Performance Monitoring

If we would like to see how long it takes to execute a piece of code, the first tool that might pop into our mind might be Date. For example, we could do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const someLongRunningOperation = () => {
    return new Promise(resolve => {
        setTimeout(resolve, 2000);
    });
};

const calculateSomething = async () => {
    const start = new Date();
    await someLongRunningOperation();
    const end = new Date();
    console.log(`It took ${end - start} ms to execute the function`);
};

calculateSomething();

This code takes the current timestamp before executing someLongRunningOperation and subtracts it from the timestamp taken after someLongRunningOperation finishes. At first sight, the solution works, but it introduces two problems.

First, Date is based on the system wall clock, which is not monotonic. The value of new Date() can jump backwards if the clock gets adjusted, for example by an NTP sync or a manual change, and a measurement that straddles such an adjustment is simply wrong.

Also, adding the measurement logic directly to our codebase should be avoided, if possible. The additional code clutters the codebase and makes it significantly harder to understand the primary purpose of the application.

3. Performance Hooks Basics

Node Performance Hooks provide functionality which helps us with the two issues mentioned in the previous section. First, performance hooks use a monotonic high-resolution clock, so the measurements are reliable. Secondly, performance hooks help us keep the measurement logic out of our core codebase.

Let’s take a look at the exact implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const { PerformanceObserver, performance } = require('perf_hooks');

const obs = new PerformanceObserver((items) => {
    console.log(items.getEntries()[0].duration);
    performance.clearMarks();
});

obs.observe({ entryTypes: ['measure'] });

const someLongRunningOperation = () => {
    return new Promise(resolve => {
        setTimeout(resolve, 2000);
    });
};

const calculateSomething = async () => {
    performance.mark('start');
    await someLongRunningOperation();
    performance.mark('end');
    performance.measure('Start to end', 'start', 'end');
};

calculateSomething();

As we can see, we need to require PerformanceObserver and performance. The first one is an observer, which will be responsible for providing notifications when performance entries are added to the performance timeline.

What is a performance timeline, after all? That’s where performance comes into play - it allows us to add specific events to the timeline using the methods mark and measure. For example, we add two entries to the timeline using mark - one called “start” and the other called “end”. Later we use measure to add a measurement entry to the timeline.

To listen for the entries on the performance timeline, we first need to create a new instance of PerformanceObserver by passing a callback which is invoked once matching entries are present in the timeline. After we have the observer, we need to make it listen to specific timeline entries using observe, which allows us to specify which types of entries we would like to be notified about. The types relevant to this article are mark, measure and function; Node also emits entries for things like garbage collection (gc).

Inside the callback, we can determine what we’d like to do with the timeline entry. For example, we could send the metric to Datadog, or publish it so Prometheus could read it.

4. Advanced Performance Hooks

Once we’ve grasped the basics of performance hooks, we can now look into the more advanced use cases.

If we’d like to keep the measurement code out of the function itself, we can do it using performance.timerify. timerify wraps a function with duration measurement logic. Once we invoke the wrapped function, a PerformanceObserver which is subscribed to the timeline entry type function gets notified. Let’s take a closer look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { PerformanceObserver, performance } = require('perf_hooks');

const obs = new PerformanceObserver((items) => {
    console.log(items.getEntries()[0].duration);
    performance.clearMarks();
});

obs.observe({ entryTypes: ['function'] });

const someLongRunningOperation = () => {
    for (let i = 0; i < 1_000_000_000; i++) {
        // busy loop
    }
};

const timedLongRunningOperation = performance.timerify(someLongRunningOperation);

const calculateSomething = () => {
    timedLongRunningOperation();
};

calculateSomething();

We create a function someLongRunningOperation which is wrapped by timerify. Once we execute the wrapped function called timedLongRunningOperation, the callback provided to the PerformanceObserver constructor gets executed.

5. Conclusion

In this article we briefly looked into Node’s Performance Hooks. They provide us with a convenient way to measure the performance of our application without writing any measurement-related code directly into our codebase.

© Kert Pjatkin. All rights reserved.