Inspecting one of the most famous JavaScript bugs of all time.
One fine day evening, after writing a handful of beautiful JavaScript code, sipping your coffee, you smile — the code looks especially beautiful today, and you feel a sense or serenity filling your head.
Then you decide to fire up your debug console and take a look at the memory consumption, and the performance.
You take one peek at the console inspector and the sense of serenity is long gone, and something sends chills down your spine.
The trace shows a mounting slope that resembles the temperature graph of planet Earth. It’s bad. Quite bad.
You are leaking memory. And even forcing the GC to do a force sweep doesn’t do you any favors. Your once-so-beautiful code is now a tangled mess of leaky logic.
As developers, we all have gone through this scenario before. We have coded, inspected and spent days racking our brains over leaky code.
But the below sets a new standard for leaky code. And it required a new diagnosis to isolate and fix it.
Here, I am dissecting one of the most famous JavaScript bugs of all time, and yes, it was caused by a faulty implementation of closure.
Let’s jump right in.
Imagine the following code:
var theThing = null;var replaceThing = function (){var originalThing = theThing;theThing = { longStr: new Array(1000000).join(‘*’) };};setInterval(replaceThing, 1000);Every second, the replaceThing function will be executed. theThing variable will be replaced with a new object containing a gigantic string. Before that, the original value of theThing is saved in a local variable originalThing. After the function is returned, the old value of theThing can be garbage collected, which also happens to collect the long string inside it, because nothing refers to it anymore.
So the memory allocation is pretty straightforward: it allocates a big string at every iteration of the function, and at the same time, it deletes the previous big string. So there are no further references to originalThing and theThing, which leads to their collection.
Not a big deal huh?
But what if we had a certain specific set of needs that required us to create a closure inside replaceThing, thereby outlasting replaceThing?
Like in the following code:
var theThing = null;var replaceThing = function (){var originalThing = theThing;theThing = {longStr: new Array(1000000).join(‘*’),someMethod: function () {console.log(someMessage);}};};setInterval(replaceThing, 1000);Now there is a new function “someMethod” inside theThing object. This sets up a closure scope. This is another way of saying that someMethod needs access to originalThing and theThing even after the function replaceThing has exited. I mean, that’s the entire point of closures right? So this can lead to boundless memory growth, since each iteration of the replaceThing function, creates its own version of theThing and that version would point to the previous version through originalThing.
Fortunately, most JavaScript runtimes (that run behind Chrome, Node and React) are smart enough to understand that originalThing is not actually used in the closure “someMethod”. So it is not put into someMethod’s closure scope, and is eventually GC’d when the function replaceThing exits.
Lucky for us, there is no reference ever anywhere to originalThing.
Wow. So far so good.
So next level down, we bring in the big guns.
What if we had a reference somewhere to originalThing, that requires the closure to hold onto originalThing?
That would make things ugly.
var theThing = null;var replaceThing = function () { var originalThing = theThing;var unused = function () {if (originalThing) console.log(“hi”);};theThing = { longStr: new Array(1000000).join(‘*’), someMethod: function () { console.log(someMessage);}};setInterval(replaceThing, 1000);Fire up your console and take a look.
We’re using an extra megabyte every second! So it looks like we are leaking longStr.
But nothing seems to have changed. OriginalThing is referenced in only two places. One is in the main body of the function replaceThing, and the other is inside the function unused. Both of these get cleaned up once replaceThing ends. We don’t even run the function “unused” . The only stuff that escapes the destruction of replaceThing is the closure created around “someMethod”. Surprisingly, someMethod doesn’t event refer to originalString!
This behaviour has a lot to do with the way closures are implemented in JavaScript.
In JavaScript, as we already know, every function is an object. This object has access to another object that represents its scope. So, if a function has access to three variables a, b and c, the function will have access to dictionary style object with its own scope — containing a, b and c.
This dictionary styled object is the function’s lexical environment.
So in our case, both the functions “unused” and “someMethod” has access to the same lexical environment (or scope). This environment is their closure scope. It is imperative that this scope is shared between all the functions that has access to it.
That means if we have three functions, sample1, sample2 and sample3, all of them are inside a main function named “mainFun”. Imagine that there is a variable a, in the main body of mainFun.
function mainFun() {var a = 10; var sample1 = function () { console.log(a); };var sample2 = function () { console.log(a); };var sample3 = function () { console.log(a); };}The runtime needs to make sure that the three functions sample1, sample2 and sample3 need to have access to the same “a”.
This is the same thing that happens in our case with “unused” and “someMethod”. Both of them share the same lexical scope. Both of them have access to the same “originalThing”. But as we have already discussed, V8 is smart enough to keep originalThing out of lexical scope (to make them available for GC) if they are not referenced by any functions. This is why the first example doesn’t leak.
But as soon as the function “unused” references it, originalThing ends up in the lexical scope, which is not GC’d. This leads to boundless memory leak.
This bug was discovered in the Meteor framework.
How to solve this bug?
Simple!
Just add one single line:
originalThing = nullto the end of replaceThing. This way, after each function iteration, the name originalThing will exist in the lexical scope of someMethod, but there will not be a link to the long string.
var theThing = null;var replaceThing = function () { var originalThing = theThing;var unused = function () {if (originalThing) console.log(“hi”);};theThing = { longStr: new Array(1000000).join(‘*’), someMethod: function () {}};originalThing = null;};setInterval(replaceThing, 1000);There you go! So have a nice time until we meet again.
This is my learning blog. I write as I learn new things. This blog and codes are inspired from an Original bug discovered in the Meteor’s live HTML template rendering system. The Original blog post is here.
Inspecting one of the most famous JavaScript bugs of all time
Pages: 1 2