Saturday, December 12, 2020

Types of Memory leaks in NodeJS

A Node application is a long-running process that is bootstrapped once until the process is killed or the server restarts. It handles all incoming requests and consumes resources until these are garbage collected by V8. Leaks are the kind of resources that keep their reference in memory and do not get garbage collected. 


The 4 Types of Memory Leaks

Global resources

Closures

Caching

Promises


Preparation

We will need the excellent Clinic.js and autocannon to debug these leaks. You can use any other load testing tool you want. Almost all of them will produce the same results. Clinic.js is an awesome tool developed by NearForm. This will help us do an initial diagnosis of performance issues like memory leaks and even loop delays. So, let’s install these tools first


npm i autocannon -g

npm i clinic -g


Global Resources

This is one of the most common causes of leaks in Node. Due to the nature of JavaScript as a language, it is very easy to add to global variables and resources. If these are not cleaned over time, they keep adding up and eventually crash the application. Let’s see a very simple example. Imagine this is the application’s server.js:


const http = require("http");


const requestLogs = [];

const server = http.createServer((req, res) => {

    requestLogs.push({ url: req.url, array: new Array(10000).join("*")

    res.end(JSON.stringify(requestLogs));

});


server.listen(3000);

console.log("Server listening to port 3000. Press Ctrl+C to stop it.");


Now, if we run:


clinic doctor --on-port 'autocannon -w 300 -c 100 -d 20 localhost:3000' -- node server.js



We are doing two things at once: load testing the server with autocannon and catching trace data to analyze with Clinic.


What we see is a steady increase of memory and delay in the event loop to serve requests. This is not only adding to the heap usage but also hampering the performance of the requests. Analyzing the code simply reveals that we are adding to the requestLogs global variable with each request and we never free it up. So it keeps growing and leaking.


We can trace the leak with Chrome’s Node Inspector by taking a heap dump when the application runs for the first time, taking another heap dump after 30 seconds of load testing, and then comparing the objects allocated between these two. 


It is the global variable requestLogs that’s causing the leak. Snapshot 2 is significantly higher in memory usage than Snapshot 1. Let’s fix that:


const http = require("http");


const server = http.createServer((req, res) => {

    const requestLogs = [];

    requestLogs.push({ url: req.url, array: new Array(10000).join("*")

    res.end(JSON.stringify(requestLogs));

});


server.listen(3000);

console.log("Server listening to port 3000. Press Ctrl+C to stop it.");


This is one solution. If you do need to persist this data, you could add external storage like databases to store the logs. And if we run the Clinic load testing again, we see everything is normal now:


Closures



Closures are common in JavaScript, and they can cause memory leaks that are elusive in nature. 


const http = require("http");


var theThing = null;

var replaceThing = function () {

    var originalThing = theThing;

    var unused = function () {

        if (originalThing) console.log("hi");

    };

    theThing = {

        longStr: new Array(10000).join("*"),

        someMethod: function () {

            console.log(someMessage);

        },

    };

};


const server = http.createServer((req, res) => {

    replaceThing();

    res.writeHead(200);

    res.end("Hello World");

});


server.listen(3000);

console.log("Server listening to port 3000. Press Ctrl+C to stop it.");



The memory is increasing fast, and if we wonder what is wrong with the script, it is clear that theThing is overwritten with every API call. Let’s take heap dumps and see the result:


When we compare two heap dumps, we see that someMethod is kept in memory for all its invocations and it is holding onto longStr, which is adding to the rapid increase in memory. In the code above, the someMethod closure is creating enclosing scope that is holding onto unused variable even though it is never invoked. This prevents the garbage collector from freeing originalThing. The solution is simply nullifying the originalThing at the end. We are freeing the object so that the closure scope is not retained anymore:



const http = require("http");


var theThing = null;

var replaceThing = function () {

    var originalThing = theThing;

    var unused = function () {

        if (originalThing) console.log("hi");

    };

    theThing = {

        longStr: new Array(10000).join("*"),

        someMethod: function () {

            console.log(someMessage);

        },

    };

    originalThing = null;

};


const server = http.createServer((req, res) => {

    replaceThing();

    res.writeHead(200);

    res.end("Hello World");

});


server.listen(3000);

console.log("Server listening to port 3000. Press Ctrl+C to stop it.");


The memory is increasing fast, and if we wonder what is wrong with the script, it is clear that theThing is overwritten with every API call. Let’s take heap dumps and see the result:


When we compare two heap dumps, we see that someMethod is kept in memory for all its invocations and it is holding onto longStr, which is adding to the rapid increase in memory. In the code above, the someMethod closure is creating enclosing scope that is holding onto unused variable even though it is never invoked. This prevents the garbage collector from freeing originalThing. The solution is simply nullifying the originalThing at the end. We are freeing the object so that the closure scope is not retained anymore:


Now if we run our load testing along with Clinic, we see:


No leaking of closures! Nice. We have to keep an eye on closures, as they create their own scope and retain references to outer scope variables.





References:

https://medium.com/better-programming/the-4-types-of-memory-leaks-in-node-js-and-how-to-avoid-them-with-the-help-of-clinic-js-part-1-3f0c0afda268


No comments:

Post a Comment