Monday, June 9th, 2008

Is “finally” the answer to all IE6 memory leak issues?

Category: IE, Microsoft, Testing, Tutorial

<>p>Hedger Wang has been scanning a lot of Chinese blogs lately for solutions to IE6 and memory leak issues. One of the things he stumbled upon is a pretty nifty way of nulling the objects to stop memory leaks by using the try ... finally construct. So instead of this solution which leaks memory:

javascript
< view plain text >
  1. function createButton() {
  2.       var obj = document.createElement("button");
  3.       obj.innerHTML = "click me";
  4.       obj.onclick = function() {
  5.         //handle onclick
  6.       }
  7.       obj.onmouseover = function() {
  8.         //handle onmouseover
  9.       }
  10.       return obj;//return a object which has memory leak problem in IE6
  11.     }
  12.     var dButton = document.getElementsById("d1").appendChild(createButton());
  13.     //skipped....

You can use the following which doesn’t:

javascript
< view plain text >
  1. function createButton() {
  2.       var obj = document.createElement("button");
  3.       obj.innerHTML = "click me";
  4.       obj.onclick = function() {
  5.         //handle onclick
  6.       }
  7.       obj.onmouseover = function() {
  8.         //handle onmouseover
  9.       }
  10.       //this helps to fix the memory leak issue
  11.       try {
  12.         return obj;
  13.  
  14.       } finally {
  15.         obj = null;
  16.       }
  17.     }
  18.     var dButton = document.getElementsById("d1").appendChild(createButton());
  19. }

More demos, proof of concept examples and the “finally” explanation is available on Hedger’s blog: Finally, the alternative fix for IE6′s memory leak is available

Posted by Chris Heilmann at 10:22 am
30 Comments

++++-
4.1 rating from 40 votes

30 Comments »

Comments feed TrackBack URI

Wow. If it works, that’s great. I have a heck of a time with IE6 (which I wish would go away, but it stubbornly persists).

Doesn’t really “solve” my problems unless the libraries I use pick it up as well, though. Hopefully all the library authors aree Ajaxian readers.

Comment by Nosredna — June 9, 2008

Aren’t the leaks typically caused by a chain of references from the event handler to the element and vice-versa. element.onblah = function() { … } references the function, and likewise, the function body eventually ends up referencing the element, so you have a cycle. I don’t see how nulling the local var is going to stop memory leaks.

Comment by cromwellian — June 9, 2008

@cromwellian – nulls are cleaned up by garbage collection.

Comment by MezZzeR — June 9, 2008

Why would this example code leak? I don’t think it would leak unless the onclick and onmouseover functions contained a reference to obj – or am I wrong?

Comment by lolli — June 9, 2008

One thing that really irritates me about IE6 is that the JScript leaks much worse than the VBScript.

Comment by Nosredna — June 9, 2008

The article…

http://www.hedgerwow.com/360/dhtml/ie6_memory_leak_fix/

…has more info and solutions to Crockford’s IE6 leak test case.

Comment by Nosredna — June 9, 2008

@Nosredna, yeah that is the last sentence of the blog post :)

Comment by Chris Heilmann — June 9, 2008

>>@Nosredna, yeah that is the last sentence of the blog post :)

I know, I was just emphasizing that it had Crockford’s leak case, a case that developers are likely to be familiar with.

Comment by Nosredna — June 9, 2008

“nulls are cleaned up by garbage collection.”

Well, that would be an amazing new definition of garbage collection for me. If an object is unreachable from all program roots, then yes, it should be collected. The problem has always been that IE uses a reference counting scheme for GC, and reference counting schemes are vulnerable to cycles. Creating an event handler which holds a reference to the element it is attached creates a cycle, such that even when no external references exists, the reference count is non-zero. IE tries to fix this, by disposing of all the attached DOM elements on unload, but this fails if you create or detach DOM elements and put event handlers on them.

I’m not saying the posted code doesn’t work (I’d like to see Drip or some other tool/test confirm it), but the question is *why* should it work. If I have a closure handling onclick which “captures” the element it is attached to, then there is a cycle, regardless of how many local variables are aliased to the element and/or nulled out.

Moreover, the example givens hows that the element is being returned from the function, so in no way is it safe to GC the created “button” element and closure, since in all likelyhood, that return value might be assigned somewhere else.

If this hack works, it’s an uber hack, because it’s leveraging some weird bugs/semantics of IE6′s GC.

Comment by cromwellian — June 9, 2008

Ok, the hedgerow link shows an example that perhaps won’t leak, but it’s doing something different than the proposed solution above. It creates an event handler that essentially ignores all of the DOM element’s information, except for an expando reference. It then nulls out the reference to the element. Cool and surprising that it works, but it doesn’t solve the usecase that the majority of us want.

Try changing the event handler to reference .innerHTML of the element, or call getAttribute() and see if it still doesn’t leak.

Comment by cromwellian — June 9, 2008

I played around with this finally approach tested it in sIEve and it does really seem to solve the circular reference memory leak. Very strange that it works. However it doesn’t directly solve the circular reference problem that the attachEvent method in IE introduces so I wrote a small proof of concept class that fakes this behavior.

I verified that it doesn’t leak in IE using sIEve and it works in all browsers:
http://blog.moxiecode.com/2008/06/09/leak-free-event-class-that-doesnt-use-the-unload-event/

Comment by Spocke — June 9, 2008

One way to avoid circular reference related DOM memory leaks in IE6 is to institute a CRIS (Circular Reference Isolation System). Your framework should provide methods for abstracting the assignment and removal of event handlers on DOM nodes. With such an abstraction, you can institute a CRIS to enforce a breakage in the circular reference between referenced DOM nodes and lambda closures that would implicitly be linked by reference to the DOM nodes.
 
Basically, the DOM node has a reference to the closure function (the event handler). The closure function has a reference to the state of its enclosing scope, which has a variable that is a reference to the DOM node. IE6 doesn’t identify and resolve circular references that span the JS/DOM divide.
 
A CRIS works by assigning an intermediary “joiner” function as the actual event handler of the DOM node, and ensures that this intermediary function does not have a direct reference to the handler function being assigned by the application. Instead, the real handler is looked up from a hash table, using a key that is stored through closure with intermediary. This effectively breaks the direct reference chain, so IE6 doesn’t hang on to DOM nodes. To the application developer, everything behaves the way they would like. They can assigned closure functions as event handlers without worrying about circular references.
 
PLUG (of course, you knew it was coming): The UIZE JavaScript Framework employs a CRIS in its Uize.Node package, so that the event handler methods ensure no circular references that would lead to DOM memory leaks in IE6.

Comment by uize — June 9, 2008

Why bother fixing IE6 issues? Yes, I know it is still being used heavily, but the issue remains. IE6 is garbage and we all know it – thats why we “fix” it all the time with CSS and JS hacks.

We need to make sure that the people USING IE6 know that it is garbage. From their point of view – “if it ain’t broke, dont fix it” so they may never upgrade.

While this is an interesting hack, the focus shouldn’t be on saving IE6 it should be on eliminating it and making sure that the end users know that they have better options.

Comment by Bryan — June 9, 2008

Here’s another solution that will completely eliminate the entire stack of problems;
http://ajaxwidgets.com/Blogs/thomas/the_entire_web_is__best_viewed.bb
;)

Comment by polterguy — June 9, 2008

@Bryan,
So, what you’re saying is that IE6 is in need of some garbage collection ;-)

Comment by uize — June 9, 2008

I’ve talked to people who have implemented CRIS style stuff before, it’s a fairly obvious workaround, but AFAIK, it doesn’t work perfectly either. The GC shouldn’t care whether references are direct or indirect with respect to cycles. If an element has a reference to another JS object, which references another, which eventually references the original element, you have a cycle. There’s nothing special about A->B->A vs A->B->C->A. It shouldn’t matter if C is a direct reference, or a hash table with a bunch of references, one of whom happens to be A.

AFAIK, no framework that doesn’t track unattached DOM elements and clean them up on page load (or unattachment) can avoid leaking.

Comment by cromwellian — June 9, 2008

>>We need to make sure that the people USING IE6 know that it is garbage.

They often do know it. Many of them work at places where IT has mandated IE6. These are often oppressive places to work, and telling the user that the browser sucks is just adding to the punishment they already endure.

You have to decide if it’s worth dealing with IE6 or not. A local newspaper’s site stopped working on IE6 recently. The programmers get the complain calls forwarded to them (something like 20% of the users had been on IE6 still), and they have to explain on the phone that IE6 is no longer supported. That seems like a great way to alienate your customers to me.

I’d LOVE it if IE6 went away. It uss up probably half my browser-specific time. It’ll be a big win when I can drop it.

Comment by Nosredna — June 9, 2008

Cromwellian’s comments (hi, Ray!) are pretty much right on the money. No amount of indirection through hash tables or any other mechanism will change the fact that you’ve got a circular reference. Think about it this way: If I’ve got a Javascript object that references a DOM element, and it’s possible to reach that object *in any way* from the DOM element (including its event handlers), then you’ve got an uncollectible circular reference in IE. This can only be fixed by breaking the circular reference at some point before the page unloads, and once you do so, whatever was depending on that reference (e.g. being able to handle events correctly) will no longer work.

The trick described in this article works for the sole reason that the only circular reference involved is { functionScope -> element -> eventHandler -> closure -> functionScope }. The finally block just clears out the { functionScope -> element } part of the reference after the return value is computed. This in no way solves the general problem described above. The only general solution is to break circular references when an element is “disposed” (i.e. whenever it is no longer needed by the app, and before the page unloads).

Comment by jgw — June 9, 2008

The trick is to break the *reference* chain. If the event handler that is directly assigned to the DOM node has a string variable (closure scoped) that contains a key which it can use to look up a true handler in a hash table, then the handler does *not* have a reference to the true handler function. Therefore, the DOM node does not have a reference to the true handler function. In all likelihood the true handler function hangs on to a reference to the DOM node, but this is OK because there is now no circular reference taking place.
 
It was a long time ago that I solved this problem and made a test to verify that it worked. I was using a little utlility called Drip at the time that indicated that with the CRIS in place, there were no leaked DOM nodes across page refreshes. Needless to say, it was a relief to solve this problem in an elegant way. Prior to this, UIZE had a workaround for IE6 where cleanup would happen on page unload (the traditional way). I remember that the key thing was to keep the direct handler function definition outside of a scope which held a closure reference to the hash table.

Comment by uize — June 9, 2008

The problem with that approach is that you now have the following reference chain: { handlerFunction -> hashTable -> trueHandlerFunction -> element }. If the hashTable is simply a global, then the handlerFunction still has an implicit reference to it (by virtue of the fact that everyone has an implicit reference to the global scope). This is generally not a big deal with a simple handler function, but if that handler has a reference to a more complex “peer” javascript object (very common in UI frameworks), then the leak can be much larger.

It is possible that Drip is showing that you have no leaks, because IE (7, at least) cleans up event handlers it can find in the DOM on unload. Drip can only check for leaks present *after* the page is fully done unloading (I wrote Drip a few years ago, but haven’t really maintained it much since).

Comment by jgw — June 9, 2008

Attaching “any” JS objects to a DOM element would introduce memory leak, and that’s why add expando to an element is a bad idea.

After M$ had released several fixes to fix the IE memory leaks bug, it turns out that IE does remove all JS objects attached to DOM elements before the unload event is triggered as long as those DOM elements remain valid in the document tree.

For element removed or replaced (eg.document.body.innerHTML = “”) from the document tree before the unload event, IE won’t be able to recycle the memory from the JS objects attached to those elements.

Comment by HedgerWang — June 9, 2008

It’s not just expandos tho. If you use .onXXX, or attachEvent, the same leak will occur. It’s any reachable reference from the DOM node into the JS which causes the issue for removed/replaced elements.

Comment by cromwellian — June 9, 2008

Isn’t a local factory just as good?
function createDOMThingy()
{
function createAndHookup(onClick)
{
var elem = document.createElement(‘div’);
elem.onclick = onClick;
return elem;
}
return createAndHookUp(function(){ alert(‘click!’); });
}

Comment by Fredrik — June 9, 2008

@jgw
Drip was used to test leaking in IE6 – not IE7. Also the hash table is not a global variable, but stored as a property of a global object. That particular pattern, at the time, resulted in Drip reporting no leaked DOM elements, and the memory usage of IE6 not creeping up uncontrollably on each subsequent page refresh.

Comment by uize — June 9, 2008

It’s a busy day at Zazzle (Monday’s are meeting days), but I will do some more digging and hopefully report something interesting.

Comment by uize — June 9, 2008

We use something like this in our company:

function create() {
var a = document.createElement("div");
a.onclick = function() {
//...
};
return (a = [a]).pop();
}

Comment by Jordan — June 10, 2008

huh, question :

function x()
{
var a = document.createElement(‘div’);
return a;
}
function y()
{
var b = x();
return b;
}
document.body.appendChild(y());

as I understand, i shall put “try{return a;} finally{a=null;}” in function x. but shall i put “try{return b;} finally{b=null;}” in function y ?

Comment by Geompse — June 10, 2008

@Geompse:
AFAIU your code example doesn’t leak, thus you don’t need any of the tricks above. It’s only when you created a closure that also “consumes” a DOM-element you need to do nulling.

Comment by Fredrik — June 11, 2008

@Jordan
That’s pretty cute.

Comment by uize — June 11, 2008

Thank for the info

Comment by Naruto — October 9, 2008

Leave a comment

You must be logged in to post a comment.