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 in Amazon would load just a second lower, the company will lose $1.6 billion per year 1. Besides, 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 1.
In this article, we’re going to look into the Node’s performance measurement API, which helps us monitor the performance in a convenient way.
2. The classical 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:
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 gets the current timestamp before executing someLongRunningOperation
and subtract the current timestamp after finishing with someLongRunningOperation
. On first sight, the solution works, but it introduces two problems.
First, the Date
API is not absolute. For example, the value of new Date()
could easily change to the past if we happen to switch to the summertime in Europe.
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 two issues mentioned in the previous paragraph. First, performance hooks provide us with a way to get guaranteed absolute measurements. Secondly, performance hooks help us get rid of the measurement in our critical codebase.
Let’s take a look at the exact implementation:
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 the place where performance
comes to play - it allows us to set specific events on the timeline using methods mark
and measure
. For example, we set 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 the correct entries will be present in the timeline. After we have the observer, we need to make it listen to the specific timeline entries using observe
, which allows us to specify which type of entries we would like to be notified on. The possible options in Node are mark
, measure
or function
.
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 get rid of any performance measurement code, we can do it using performance.timerify
. timerify
wraps a function with duration measurement logic. Once we invoke the function, then a PerformanceObserver
which is subscribed to timeline entry type function
gets notified. Let’s take a closer look:
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(i = 0; i < 1_000_000_000; i++) {
}
}
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
, then 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 a convenient way to measure the performance of our application without writing any measurment-related code directly to our codebase.