views:

150

answers:

3

I just cannot for the life of me figure out this memory leak in Internet Explorer.

insertTags simple takes string str and places each word within start and end tags for HTML (usually anchor tags). transliterate is for arabic numbers, and replaces normal numbers 0-9 with a &#..n; XML identity for their arabic counterparts.

fragment = document.createDocumentFragment();
for (i = 0, e = response.verses.length; i < e; i++)
{
    fragment.appendChild((function(){
        p = document.createElement('p');
        p.setAttribute('lang', (response.unicode) ? 'ar' : 'en');
        p.innerHTML = ((response.unicode) ? (response.surah + ':' + (i+1)).transliterate() : response.surah + ':' + (i+1)) + ' ' + insertTags(response.verses[i], '<a href="#" onclick="window.popup(this);return false;" class="match">', '</a>');
        try { return p } finally { p = null; }
    })());
}
params[0].appendChild( fragment );
fragment = null;

I would love some links other than MSDN and about.com, because neither of them have sufficiently explained to me why my script leaks memory. I am sure this is the problem, because without it everything runs fast (but nothing displays).

I've read that doing a lot of DOM manipulations can be dangerous, but the for loops a max of 286 times (# of verses in surah 2, the longest surah in the Qur'an).

* Memory leaks in IE7 and IE8, not sure about 6, but works perfectly fine in Safari 4, FF 3.6, Opera 10.5, Chrome 5... *

A: 

I can't really tell you why IE supposedly leaks memory, but this code is pretty darn complicated for what it does. And this line seems highly suspicious and superfluous: try { return p } finally { p = null; }.

How about simplifying it a bit and scoping the variables:

var fragment = document.createDocumentFragment();
var p, t;
for (var i = 0; i < response.verses.length; i++)
{
    p = document.createElement('p');
    if (response.unicode) {
        p.setAttribute('lang', 'ar');
        t = (response.surah + ':' + (i+1)).transliterate();
    } else {
        p.setAttribute('lang', 'en');
        t = response.surah + ':' + (i+1);
    }
    p.innerHTML = t + ' ' + insertTags(response.verses[i], '<a href="#" onclick="window.popup(this);return false;" class="match">', '</a>');
    fragment.appendChild(p);
}
params[0].appendChild(fragment);
fragment = p = t = null;  // likely unnecessary if they go out of scope anyway

That's still a lot of DOM operations though, which may take a while on slow Javascript engines.

deceze
http://geekswithblogs.net/FrostRed/archive/2008/11/29/127440.aspx is why I use try{}finally{}, it is a supposed way of preventing memory leaks, and it helps from my debugging in IE8, but not enough. The above code is also not equivalent.
Tom
@Tom You're right, I incorrectly disassembled your ternary operator. Fixed it. If you don't use a function as a way to construct the `p` element, you shouldn't get into a situation as described in that article to begin with.
deceze
@Tom: Very useful post, thank you. But it's first comment hints at what might be your issue. Take a look: **** You'll want to revise your code. IE has a bug where it won't execute your finally statement unless you supply a catch.see here for details.http://webbugtrack.blogspot.com/2007/11/bug-184-catch-to-try-catch-finally-in.htmlThis one had me pulling hair for days! man I hate debuggin in IE! ****
Fyodor Soikin
Variables in Javascript are scoped to functions, not if/else/for blocks. http://stackoverflow.com/questions/500431/javascript-variable-scope
Jeff Meatball Yang
@Jeff Yes, but that's not what I mean. If you don't use `var`, your variables will end up being in the global scope. Assuming this might be in a function, this is not what you want. You should always declare your variables with `var`.
deceze
@deceze: good point. ok.
Jeff Meatball Yang
A: 

Does it leak if you remove the onclick attribute from the link?

You could try removing the repeated onclick and replacing it with event delegation.

Also all of your variables appear to be in global scope -- that shouldn't get as bad as to cause the issues you're seeing, but you should fix that up regardless.

wombleton
The removal of the onclick helps, but it still has a significant leak otherwise. And no, nothing is in a global scope, it is all part of a function called to handle an Ajax response, and everything goes out of scope as soon as it is finished.
Tom
+4  A: 

Variables are scoped to functions, not if/else/for/while/etc. blocks. Every time you call

fragment.appendChild((function() { ...

you are creating a new function (new scope). This new function references the i and response variables. So now, i and response are scoped to BOTH the outer function and the new function.

That's not enough to leak memory. (i and response are normal variables that will go out-of-scope after the new function is done)

BUT, you create a p DOM element in the new function, and reference it in the outer function (you return it to the fragment.appendChild call as an argument). Now think about it: you have the outer scope fragment referencing a p DOM created from the inner scope, which needed to use the i and response variables from the outer scope to create the DOM element in the first place.

The fragment and p DOM objects each have a reference to each other. Despite your attempts to zero-out the reference count by nulling the variable pointers, p=null and fragment = null will not get rid of all references. The fragment still has a reference to the inner p, which still has a reference to the outer response variable. The two "scopes" will never be garbage collected because of this remaining circular dependency.

Anyone, please correct me if I've made any mistakes.


As for a solution, just don't use an inner function!

fragment = document.createDocumentFragment();
for (var i = 0, var e = response.verses.length; i < e; i++)
{
    var p = document.createElement('p');
    p.setAttribute('lang', (response.unicode) ? 'ar' : 'en');
    p.innerHTML = ((response.unicode) ? (response.surah + ':' + (i+1)).transliterate() : response.surah + ':' + (i+1)) + ' ' + insertTags(response.verses[i], '<a href="#" onclick="window.popup(this);return false;" class="match">', '</a>');
    fragment.appendChild(p);
}
params[0].appendChild( fragment );
Jeff Meatball Yang
Same conclusion, better explanation. I believe you *shouldn't* use `var p` inside the loop, since you shouldn't re-declare an existing variable.
deceze
The var inside the loop has no side effects. The declaration will be hoisted to the top by the parser. The advantage of declaring where used is just that, its clear where it is used.
Sean Kinsey
Thank you, but this leads to the same result. I use to use this code, but found the inner function actual lightened the load for IE a bit! I just don't understand.
Tom
There must be something else. Does `insertTags` retain a reference to `response` somehow?
Jeff Meatball Yang