Professional Software Development Outsourcing
Not all companies have the luxury of owning an in-house tech team. That is to say, the luxury of having a skilled team they can consult when looking to achieve tech-related business goal...
For the Node.js engineer, memory leaks in an application can be a nightmare from which awakening isn't always as straightforward as crying out in the night. In fact, the longer these leaks go undetected, the longer the bad dream can drag on.
This is to say that CPU, memory usage, load average and response time of an application with memory leak bugs will increase accordingly until the CPU reaches 100% of its capacity, after which the service will cease to respond. These are symptoms the developer may face when an application suffers a memory leak.
Should you be such a developer, read on and find out how to address these symptoms and how you can monitor your memory usage and memory leaks in Node.js applications.
In simple terms, a memory leak is an issue in which an application allocated a block of memory without freeing it up or where the Node.js Garbage Collector mechanism hadn't managed to sweep it.
Well, sort of...
For starters, we must bring Garbage Collector into the equation when on the topic of memory leak. In yet more simple terms, Garbage Collector is an automatic mechanism whose role is to reclaim memory occupied by unused objects.
In the first cycle (called "mark"), the Garbage Collector marks unused objects for deletion. In the second cycle, "sweep", memory is reclaimed.
For tutorial purposes, I have created a simple Express.js application in which each request to the application makes a leak array grow over its lifetime, thus slowing down the service. Nowhere is this leak array manually cleared, nor does garbage collection affect globally declared variables. Though the issue here may seem obvious, in real life such leaks can easily go undetected and lead to some rather unwelcome surprises further down the road.
Thankfully, we have some measures in-store to ensure this is not the case:
To use Node Inspector, we must run our app in inspect mode, and in CLI typescript:
node --inspect index.js
As an output you should get something similar to my one
➜ memory-leaks-training node --inspect index.js
Debugger listening on ws://127.0.0.1:9229/315b2c9a-daff-46fc-83fc-9cb822603970
For help, see: https://nodejs.org/en/docs/inspector
App listening on port 5000
Debugger attached.
In addition to your Chrome Devtools, you should begin with our script injected:
Should your Devtools fail to start, navigate to:
chrome://inspect
In your chrome browser, click the configure button:
Then add the IP address and port from the Node Inspector output -- in my case localhost:9229.
Click on done.
You should then get your script with the inspect option as listed in the Devtools/devices tab:
Click on inspect to have Chrome Devtools attached to the Node Script.
In our case, we are interested in the memory tab and heap snapshot option.
To test if our application has a memory leak we will take 3 heap snapshots. The first one will be just after starting the server; the second will follow a benchmark of 1000 requests to our server and the third after another benchmark of 1000 requests. This will show us whether the application takes more memory during its lifetime.
For benchmarks, I will use Apache Benchmark which is installed by default on my Mac machine and my CLI command for it is:
ab -n 1000 -c 50 127.0.0.1:5000/
Go ahead and generate some traffic with any preferred method. Now let’s take some heap snapshots:
Ok, the first one is just after the server starts -- 6 MB.
The second one is after the first benchmark of 1000 requests and suddenly we're given a warning sign, as our heap has now grown to 6.7 MB.
So let’s do the last snapshot after the second benchmark:
And again, a bigger value confirms our suspicions that the application has a memory leak. So... let's find the cause.
Select the third snapshot as on the screenshot below; click on the comparison option:
Compare it to the first snapshot:
We can now see that we've acquired several new objects and dates since starting the server. Here, our trial begins:
First, let's take a closer look at this object. Then, we should look for objects with the same allocation size. It seems we have many with an allocation size of 56. These should be our memory leaks.
Upon opening one, we notice that the object comes from our prepared memory leak.
leak.push({ baz: { foo: req.headers }, time: new Date() });
So now, let's have a look at the date object. Does it point to the time key in our leaky script?
And voilà -- leaks in Node.js fully detected!
From here, if you stick to well-known and tested libraries, you'll find fixing any Node.js memory leak a far easier task than identifying one in the first place. Crucial to this, of course, is having a full understanding of garbage collection in Devtools as well as any others you employ when building out your applications. With the various debuggers, leak cathers and usage graph generators at your disposal, your newly refined skills in isolating leaks where and when you find them will ensure your software performs faster and more efficiently.
At Startup Development House, we admit we're a bit expert when it comes to using Node.js and indeed solving any problems we encounter when doing so. If you're an aspiring developer and could use a bit of this expertise, then feel free to reach out to hello@start-up.house