Finding memory leaks is a very difficult task, especially when it comes to modern JS code that makes use of many third party libraries. For example, I am currently facing down a memory leak in rollup, involving babel and a custom babel plugin. I am exploring several common strategies to hunting them down:
node --heap-prof node_modules/rollup/dist/bin/rollup -c
to analyze a rollup build). Then open it in Chrome Dev Tools, via Memory
-> Load
.This last question is also my question here: How can we analyze the object allocation tree in Node (or in V8 in general)? How can I find out where the objects that I identified in step (2) are kicking around?
Often, it is the answer to this question that tells us where to change our code to stop the leakage. (Of course, if your issue is memory churn, rather than memory leaks, then this question is probably not that important.)
In my example, I know that the memory is occupied by Babel AST nodes and path objects, but I don't know why they linger, that is I don't know where they are stored. If you just run Babel on its own, you can verify that it is not Babel leaking the memory. I am currently trying all kinds of tricks to find out where they are being stored, but still no luck.
Sadly, so far, I have not found any tools to help with question (3). Even relevant in-depth articles (like this and its slidedeck here) MANUALLY draw up heap allocation steps. Feels like there is no such tool, or am I wrong? If there is no tool, maybe is there a discussion about this somewhere?
Note that while you don't have to explicitely deallocate memory in JS, memory leaks can still arise. At the same time, Node memory profiling utilities are (almost criminally) underdocumented. Let's find out how to use them.
TLDR: Skip ahead to the hands-on section with examples below, titled "Finding Memory Leaks (with Examples)".
Since JS has a GC, memory leaks only have a few possibly causes:
You are hanging on to ("retaining") large objects, that are not used anymore, usually inside a variable in file or global scope. This is either accidental, or, part of a simplistic (indefinite) caching scheme:
let a;
function f() {
a = someLargeObject;
}
Sometimes objects are lingering in retained closures. E.g.:
let cb;
function f() {
const a = someLargeObject; // `a` is retained as long as `cb`
cb = function g() {
eval('console.log(a)');
};
}
You can easily fix such a memory leak by either never storing to, or by manually clearing those variables. The main difficulty is to find these lingering objects.
Firstly, Node.js and Chrome both use the same JS engine: v8. Because of that, it was feasible for the Chrome Dev Tools team to add Node debugging and profiling support. While there are other tools available, Chrome Dev Tools (CDT) are probably more mature (and probably much better funded), which is why we will (for now) focus on how to use Chrome Dev Tools for Node memory profiling and debugging.
There are two main ways of profiling Node memory using CDT:
--heap-prof
to generate a heap profile log file. Then load and analyze the log in CDT.--inspect
/--inspect-brk
flag in order to debug your Node application in CDT. Then just use CDT's Memory
tab (documentation here) to your liking.heap-prof
Run your app with --heap-prof
to generate a heap profile log file. Then load and analyze the log in CDT.
heap-prof
enabled. E.g.: node --heap-prof app.js
heap-prof
-related command line flags.Heap*.heapprofile
.Load
→ select Heap*.heapprofile
This step allows you to, first of all, verify a memory leak, and find out what kind of allocations or objects might be causing it.
Let's look at CDT's memory profiling tool. It has three modes:
Sadly, the log recorded by --heap-prof
only contains data for mode 1. However, this mode is insufficient to answer the OP's third question: How can you find out why/where allocated objects are still lingering (that is: "retained" after not being used anymore)?
As explained in the tab: Answering that question requires the second mode.
I don't know if there is a hidden way to change the profile mode for Node, but I have not found it. I tried a few things, including adding from this list of undocumented Node.js CLI flags.
That is why @jmrk proposed method (2) in his answer:
inspect
/inspect-brk
Run your app with --inspect
/--inspect-brk
flag in order to debug your Node application in CDT. Then just use CDT's Memory
tab (documentation here) to your liking.
node --inspect-brk app.js
chrome://inspect
in Chrome.--max-old-space-size=4096
(or bigger numbers) to double the default. Or, even better, simplify your test case to use less memory and speed up profiling, if possible.After following the steps of Method 2, you are now looking at all information you need to find your leak.
Let's look at some basic examples:
A simplistic memory leak is examplified in the code below: file-scoped a
stores data forever.
Complete Gist is here.
let a;
function test1() {
const b = [];
addPressure(N, b);
a = b;
gc(); // --expose-gc
}
test1();
debugger;
Notes:
gc
when profiling. This way we can make sure we get rid of all collectable references, and focus explicitly on the "lingering" objects.
expose-gc
flag for the gc()
call; e.g.: node --inspect-brk --expose-gc app.js
Once the breakpoint hits, I stop recording and I get this:
Constructor
view lists all lingering objects, grouped by constructor/type.
Shallow Size
or Retained Size
(both are explained here)string
is using up most memory. Let's open that up.
Constructor
, you find a list of all it's individual objects. The first (biggest) object(s) is/are often times the culprit. Select the first.Retainers
view now shows you where this object is still being retained.
Documentation on the Retainers
view is not quite complete. This is how I try to navigate it until it spits out the line of code that I'm looking for:
My findings are shown in this screenshot:
We see three functions playing a role in this object's lingering:
gc
- I'm not sure why this is. Probably related to GC internals. Might be because the gc
would cache references to some (if not all) lingering objects.addPressure
function allocated the object. This is also where the reference that retained it came from.test1
function is where we assigned the object to the file-scoped a
.
a
, or make sure, we clear a
after it's not being used anymore.I hope, this helps you get started on your exciting journey to finding and eradicating your memory leaks. Feel free to ask for more information below.
Chrome DevTools has a "Heap Snapshot" feature, which among other things lets you inspect "retaining paths" of objects (which is, in essence, your "question 3"). See https://developers.google.com/web/tools/chrome-devtools/memory-problems/heap-snapshots for details.
You can connect DevTools to Node when you start Node with --inspect
. See https://nodejs.org/en/docs/guides/debugging-getting-started/ for details.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With