Performance benchmarking in javascript
Performance benchmarking is generally done to measure how your code is performing in a particular aspect of execution e.g. time taken, memory consumption etc.
Measuring execution time
We will be using the performance.now() method for markting the timestamps before and after the executing the function. Then we can just take a difference between two to know time.
You might think why did we not use Date.now(), well for below given reasons -
In short, performance.now() is much accurate and stable for such kind of measurements.
async function measureExecutionTime(fn, iterations, ...args) {
// check if fn is valid function
if (typeof fn !== 'function') {
throw new Error("First argument must be a function");
}
if(!Number.isInteger(iterations) || iterations < 1){
throw new Error("Iterations has to be a positive number.");
}
let results = [];
let errors = [];
let totalTime = 0;
let successfulAttempts = 0;
let failedAttempts = 0;
let successfulTimes = [];
// warm-up runs
for(let j=0;j<5;j++){
try{
result = fn(...args);
if (result && typeof result?.then === 'function') {
result = await result;
}
}
catch(err){
}
}
// actual benchmarking runs
for (let i = 0; i < iterations; i++) {
const startTime = performance.now();
let result;
let error;
try {
result = fn(...args);
if (result && typeof result?.then === 'function') {
result = await result;
}
const endTime = performance.now();
const exTime = endTime - startTime;
results.push({
executionTime : exTime,
iteration : i+1,
result: result,
pass: true
});
totalTime += exTime;
successfulAttempts++;
successfulTimes.push(exTime);
}
catch (ex) {
const endTime = performance.now();
const exTime = endTime - startTime;
errors.push({
executionTime : exTime,
error : ex,
iteration: i+1,
pass: false
})
failedAttempts++;
}
}
return {
successfulAttempts,
failedAttempts,
averageExecutionTime : totalTime / successfulAttempts,
maxExecutionTime : Math.max(...successfulTimes),
minExecutionTime : Math.min(...successfulTimes),
}
}
function greet(name, country) {
console.log(`Welcome ${name} to ${country}`);
}
async function slowGreet(name, country) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Welcome ${name} to ${country}`);
resolve(200);
}, 3000)
})
}
(async () => {
const syncResult = await measureExecutionTime(greet, 10, 'Sjon', 'India');
console.log('Performance Report - ', syncResult);
const asyncResult = await measureExecutionTime(slowGreet, 10, 'Sjon', 'India');
console.log('Performance Report - ', asyncResult);
})();
async function measureExecutionTime(fn, iterations, ...args) {
// check if fn is valid function
if (typeof fn !== 'function') {
throw new Error("First argument must be a function");
}
if(!Number.isInteger(iterations) || iterations < 1){
throw new Error("Iterations has to be a positive number.");
}
let results = [];
let errors = [];
let totalTime = 0;
let successfulAttempts = 0;
let failedAttempts = 0;
let successfulTimes = [];
// warm-up runs
for(let j=0;j<5;j++){
try{
result = fn(...args);
if (result && typeof result?.then === 'function') {
result = await result;
}
}
catch(err){
}
}
// actual benchmarking runs
for (let i = 0; i < iterations; i++) {
const startTime = performance.now();
let result;
let error;
try {
result = fn(...args);
if (result && typeof result?.then === 'function') {
result = await result;
}
const endTime = performance.now();
const exTime = endTime - startTime;
results.push({
executionTime : exTime,
iteration : i+1,
result: result,
pass: true
});
totalTime += exTime;
successfulAttempts++;
successfulTimes.push(exTime);
}
catch (ex) {
const endTime = performance.now();
const exTime = endTime - startTime;
errors.push({
executionTime : exTime,
error : ex,
iteration: i+1,
pass: false
})
failedAttempts++;
}
}
return {
successfulAttempts,
failedAttempts,
averageExecutionTime : totalTime / successfulAttempts,
maxExecutionTime : Math.max(...successfulTimes),
minExecutionTime : Math.min(...successfulTimes),
}
}
function greet(name, country) {
console.log(`Welcome ${name} to ${country}`);
}
async function slowGreet(name, country) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Welcome ${name} to ${country}`);
resolve(200);
}, 3000)
})
}
(async () => {
const syncResult = await measureExecutionTime(greet, 10, 'Sjon', 'India');
console.log('Performance Report - ', syncResult);
const asyncResult = await measureExecutionTime(slowGreet, 10, 'Sjon', 'India');
console.log('Performance Report - ', asyncResult);
})();
You can capture various statistics related to execution time - e.g maximum execution time, average execution time, minimum execution time, no. of successful iterations, no. of failed iterations.
Note how we have warm-up runs in the above code. They are added to handle the cold-start problem.
If you run the code without the warm up runs, you will notice that the first run is always taking more time. This is because the JS engine hasnt optimized your code for running yet. Hence the delay.
You can safely ignore the intial runs to avoid this initial run time skewing the statistics.
Measuring execution memory usage
There are couple of options to measure the memory usage for a function.You can use the performance.memory.usedJSHeapSize property to get how much heap memory is currently being used.The drawback is that this is only supported in chrome browsers.
The second way to get memory usage is using the process object in node js. You can use the memoryUsage method to fetch the heap used before and after the functions run and then calculate the memory usage.
Here's a quick util function using the process object.
async function measureMemoryUsage(fn, iterations, ...args){
if(typeof fn !== 'function'){
throw new Error("First parameter should be function");
}
if(!Number.isInteger(iterations) || iterations < 0){
throw new Error('Iterations must be positive non-zero number');
}
// warm-up runs
for(let k = 0; k<3 ; k++){
try{
let result = fn(...args);
if(result && typeof result.then === 'function' ) {
result = await result;
}
}
catch(err){}
}
let avgMemoryUsed = 0;
// calculate actual memory consumption
for(let i=0; i<iterations; i++){
let memoryUsageBefore, memoryUsageAfter, fnMemoryUsage;
memoryUsageBefore = process?.memoryUsage();
try{
let result = fn(...args);
if(result && typeof result.then === 'function' ) {
result = await result;
}
memoryUsageAfter = process?.memoryUsage();
}
catch(err){
memoryUsageAfter = process?.memoryUsage();
}
fnMemoryUsage = memoryUsageAfter.heapUsed - memoryUsageBefore.heapUsed;
avgMemoryUsed += fnMemoryUsage;
}
console.log(`total memory used : ${ (avgMemoryUsed / iterations)/1024/1024} MB`);
}
async function measureMemoryUsage(fn, iterations, ...args){
if(typeof fn !== 'function'){
throw new Error("First parameter should be function");
}
if(!Number.isInteger(iterations) || iterations < 0){
throw new Error('Iterations must be positive non-zero number');
}
// warm-up runs
for(let k = 0; k<3 ; k++){
try{
let result = fn(...args);
if(result && typeof result.then === 'function' ) {
result = await result;
}
}
catch(err){}
}
let avgMemoryUsed = 0;
// calculate actual memory consumption
for(let i=0; i<iterations; i++){
let memoryUsageBefore, memoryUsageAfter, fnMemoryUsage;
memoryUsageBefore = process?.memoryUsage();
try{
let result = fn(...args);
if(result && typeof result.then === 'function' ) {
result = await result;
}
memoryUsageAfter = process?.memoryUsage();
}
catch(err){
memoryUsageAfter = process?.memoryUsage();
}
fnMemoryUsage = memoryUsageAfter.heapUsed - memoryUsageBefore.heapUsed;
avgMemoryUsed += fnMemoryUsage;
}
console.log(`total memory used : ${ (avgMemoryUsed / iterations)/1024/1024} MB`);
}
Measuring CPU usage
async function measureCPUUsage(fn, iterations, ...args) {
// Measures CPU-intensive operations by tracking event loop delays
const { performance, PerformanceObserver } = require('perf_hooks');
let cpuMetrics = [];
for (let i = 0; i < iterations; i++) {
const startCPU = process.cpuUsage();
await fn(...args);
const endCPU = process.cpuUsage(startCPU);
cpuMetrics.push({
user: endCPU.user / 1000, // microseconds to milliseconds
system: endCPU.system / 1000
});
}
return {
avgUserTime: cpuMetrics.reduce((sum, m) => sum + m.user, 0) / iterations,
avgSystemTime: cpuMetrics.reduce((sum, m) => sum + m.system, 0) / iterations,
totalCPUTime: cpuMetrics.reduce((sum, m) => sum + m.user + m.system, 0) / iterations
};
}
async function measureCPUUsage(fn, iterations, ...args) {
// Measures CPU-intensive operations by tracking event loop delays
const { performance, PerformanceObserver } = require('perf_hooks');
let cpuMetrics = [];
for (let i = 0; i < iterations; i++) {
const startCPU = process.cpuUsage();
await fn(...args);
const endCPU = process.cpuUsage(startCPU);
cpuMetrics.push({
user: endCPU.user / 1000, // microseconds to milliseconds
system: endCPU.system / 1000
});
}
return {
avgUserTime: cpuMetrics.reduce((sum, m) => sum + m.user, 0) / iterations,
avgSystemTime: cpuMetrics.reduce((sum, m) => sum + m.system, 0) / iterations,
totalCPUTime: cpuMetrics.reduce((sum, m) => sum + m.user + m.system, 0) / iterations
};
}