Memory leak mystery

In my ongoing quest for memory leaks I've come upon a mystery. A script that should leak memory according to the definitions and the experts, doesn't. Why not? I have no idea.

Leaks

Let's first look at a script that should leak memory and in fact does so, test script 4 of the 10,000 links test:

window.onload = init;

function init()
{
	createLinks();
	var x = document.getElementsByTagName('a');
	for (var i=0;i<x.length;i++)
	{
		createNewClick(x[i]);
	}
}

function createNewClick(obj)
{
	obj.onclick = function () {
		this.firstChild.nodeValue = ' Clicked! - ';
	}
}
  1. A link node contains a reference to the anonymous click function.
  2. The anonymous click function's scope includes the local variable obj from createNewClick.
  3. obj refers back to the link.

Therefore a circular reference is formed and memory starts to leak. This is all as it should be.

Doesn't leak

Now take a look at the next script; test script 5 of the 10,000 links test:

window.onload = init;

function init()
{
	createLinks();
	var x = document.getElementsByTagName('a');
	for (var i=0;i<x.length;i++)
	{
		x[i].onclick = function () {
			this.firstChild.nodeValue = ' Clicked! - ';
		}
	}
}
  1. A link node contains a reference to the anonymous click function.
  2. The anonymous click function's scope includes the local variables i and x. i is harmless.
  3. x, however, is an array with all link elements on the page. Therefore it contains references to all links.

Therefore, you'd say, a circular reference is formed and memory starts to leak. Unfortunately it doesn't, and I have no idea why not. Several commenters to earlier articles said that this script in fact leaks the memory for one link (x[x.length-1]), but I don't see why it should leak the memory of only one link when x refers to all

Please explain.

This is the blog of Peter-Paul Koch, web developer, consultant, and trainer. You can also follow him on Twitter or Mastodon.
Atom RSS

If you like this blog, why not donate a little bit of money to help me pay my bills?

Categories:

Comments

Comments are closed.

1 Posted by Laurens van den Oever on 25 October 2005 | Permalink

x is a DOM NodeList which refers to DOM Nodes which refer to the handler functions which have x in it's scope.

The reference from NodeList x to it's elements is not a JavaScript reference but a COM reference.

I don't know why, but this example proves that the internal COM reference breaks the circular reference. Which allows IE to free all memory.

2 Posted by Laurens van den Oever on 25 October 2005 | Permalink

Okey, I was wrong. This has nothing to do with the fact that the NodeList -> Node reference is a COM reference.

This doesn't leak because NodeLists are dynamic:

function init()
{
var x = document.getElementsByTagName("a");
x[0].onclick = function () {
this.firstChild.nodeValue = "Clicked!";
};
x[0].parentNode.removeChild(x[0]);
alert(x.length);
}

So when on unload all 'a' elements are removed from the DOM, x becomes empty and no longer refers to the nodes so there no longer is a circular reference. So there is no leak.

3 Posted by ppk on 25 October 2005 | Permalink

Okayyyyy....

Thanks, Laurens, this sounds promising.

Now let's see if anyone comes up with an alternative explanation.

4 Posted by Laurens van den Oever on 25 October 2005 | Permalink

> x, however, is an array with all link elements on the page. Therefore it contains references to all links.

x isn't an array, but if it would be, it would leak like the following modification shows:

var y = [];
for (var i=0;i<x.length;i++)
y[i] = x[i];

for (var i=0;i<y.length;i++)
{
y[i].onclick = function () {
this.firstChild.nodeValue = ' Clicked! - ';
}
}

5 Posted by Ismael Jurado on 25 October 2005 | Permalink

I can be wrong, but I guest that the answer is that on test4, x[i] is passed as an argument to createNewClick, therefore a copy of x[i] is created in the heap (javascript pass arguments by value, not by reference) and is this copy the reason for the leak: a COM object(the element) attached to a DOM node (the anonymous function).
On test 5, things change a little: for some reason (for loop scope?) an internal copy of x is created. This copy is shared in all the iterations of the loop, its reference being replaced by the correct [i], but in the end this copy is not freed, being attached to the last COM object (x[x.length-1]), as it happens on every call to createNewClick in test 4.
It's only my guest, I can't prove it and I can be wrong, but at least has some sense...

6 Posted by Analgesia on 25 October 2005 | Permalink

I think the problem lies, as Laurens says, in the fact that x is not an array but an object that implements the NodeList interface.

Perhaps IE uses some indexing system in stead of references to implement the NodeList interface.
If Nodelist doesn't store any references to the DOMNodes there's no circular reference.

This is somewhat confirmed by my testcase where I added the following line:

for (var i=0;i<x.length;i++){ x["test"+i]=x[i];//ADDED
x[i].onclick = function () {...}
}

With this code added it started leaking again.

It however stores the references using JS code whereas NodeList uses COM, so I not quite convinced.

7 Posted by Lon on 26 October 2005 | Permalink

Everyone is still speculating about this but Laurens said it already: x is a nodeList. NodeLists are dynamic in what they hold and this one is empty (holding no references) by the time the page is finishing its unload.

8 Posted by James Mc Parlane on 26 October 2005 | Permalink

Ok. Is x a nodeList or an array? After looking at this in an interactive debugger, x is not an array.

Can we assume that x is a DOM object? I'm pretty sure we can agree yes to this one.

In theory, as a DOM object it would be involved in the leak pattern because it would hold a reference to every single DOM element that was selected into it.

So it comes down to execution order. If the nodeList is dynamic, is it updated when an element that matches the original select is removed or added from the DOM, or is it updated when it is accessed? Either way a garbage collect pass should access it so its update should be triggered either way.

So I'm leaning towards Laurens's beautiful piece of thinking which he backs up with a very convincing experiment.

So nodeList is immune simply because it can only contain an element that is in the DOM and the tear-down of the DOM after an F5 empties it, this preventing the leak pattern from forming.

9 Posted by James Mc Parlane on 26 October 2005 | Permalink

I've done a little experiment and it does go the other way as well. If you add matching elements to the DOM, you don't need to do the select again.

function init()
{
createLinks();
var x = document.getElementsByTagName("a");
alert(x.length);
x[0].onclick = function () {
this.firstChild.nodeValue = "Clicked!";
};
x[0].parentNode.removeChild(x[0]);
alert(x.length);
createLinks();
alert(x.length);

}

Now as an aside, does this mean that in between refreshes that the nodeList, if itself in a leak pattern would automatically, but unintendedly pick up changes in the DOM when elements were added after an F5? :)

10 Posted by Maian on 27 October 2005 | Permalink

This is kinda OT, but I'll mention it anyway. Besides being dynamic, NodeLists are also much slower to access than arrays. In fact, the access may not be O(1) in some implementations. In my tests, Opera has O(n) access time for NodeLists. For example, accessing the 1000th child via childNodes takes far more time than accessing the 1st child.

11 Posted by liorean on 29 October 2005 | Permalink

It's already been said, but it's all because x is a NodeList. NodeLists need to be dynamic, which means they can do one of two things: Either they at access time rebuild the collection from the document just up to the element they are searching for (which is what some implementations that aren't O(1) does), or they can be made in such a way that they add or delete nodes from a real array at appendal (sp? whatever...) or removal time.

Anyway, this means that when the document is unloaded from the window and is being taken apart the nodes are detached from the document. Since the nodes are detached from the document they will no longer be in the NodeList, and thus there will not be a circular reference at that time.

Oh, Maian, I've seen this claim before, but for Mozilla. I made a test page for it at http://testsuite.liorean.net/dom/domcollection-access-speeds.html . I've run Opera 7.6 and 9 builds and they exhibit an O(1) progression*. Opera 7.52 on the other hand seems to take longer for the second access.


* Change the size of i as needed to see the progression, Opera 7.52 takes twenty times longer for the 5e3 iterations set on that page than Opera 9 does.

12 Posted by Maian on 31 October 2005 | Permalink

Actually, what I said isn't accurate, but Opera does have some weird issues with NodeLists. I've written a profiler script to run tests on, and here's a snippet:

var hiddendiv = document.createElement('div')
for (var i = 0; i = 0; i--)
var x = childNodes[i].nodeType;
}

Although they both iterate through all the nodes once, in Opera 8.50, the 2nd test is more than 3.5 times slower than the 1st test. In IE and Fx, they're the same speed.

13 Posted by Maian on 31 October 2005 | Permalink

Ack, stupid HTML stripper. Basically, what I was doing was iterating through a div with a 100 children via childNodes, first forwards (from 0 to 99), then backworks (from 99 to 0). Going backwards is much slower for Opera.

14 Posted by liorean on 1 November 2005 | Permalink

Well, since you access all elements in the collection an equal number of times, accessed the exact same way, there should be virtually no difference even if indeed some of the elements took more time to access than others. Which means there is some additional factor that must be active there. Can you put the test page online so we can check it, Maian?

15 Posted by Maian on 2 November 2005 | Permalink

Yeah: http://maian.50webs.com/tests/nodelists01.html

16 Posted by liorean on 15 November 2005 | Permalink

Maian: I asked about this on one of the Opera tester maininglists.

I got a nice answer to it (and permission to quote it back to you), but way too lengthy to convey in quirksmode comments. So, send me a mail, my nickname at gmail.com, and I'll send that back to you.

// lio

17 Posted by Ben on 21 November 2005 | Permalink

How about

obj.onclick = function (obj) {
// stuff here
};

Doesn't this explicitly pass obj into the scope of the anonymous function such that it will free the memory when that scope is expired?