Archive for the ‘JavaScript’ Category
IE, Memory Management, and You
In a recent blog, commenters took me to task for a perceived IE 6 memory leak. It wasn’t actually there (they were wrong), but in attempting to prove myself right, I found a couple of memory leaks under IE in JSF’s Ajax support. Since I just spent a week learning how all this functioned, I thought I’d set it down so that others could learn from my efforts.
Now, none of the information that I’ll present here is new – it’s been discussed among Ajax programmers for at least the last 4 years. If you’re a web guru, it’s likely that you’re not going to learn anything new here (thought I’d welcome any additional information and corrections). But at least a couple of the points I’ll illustrate below are either poorly communicated or misunderstood. I’ll include a number of links at the end of this article. There are also very significant differences between IE 8 (which mostly works), IE 7 (which is bad), and IE 6 (which is just awful). I’ll try to point out the differences as they matter for each.
Tools
First – use the right tool for the job: In order to spot leaks, you’ll need to download a tool that can detect them. By all accounts, sIEve is the way to go. It uses IE itself, and introspects to get it’s data. The UI is pretty primitive, but I can’t recommend it enough – it’s truely invaluable. Since it uses IE for it’s work, you’ll need to run it on a machine that has IE6 installed – presumably in a VM. You’ll also want to have it running on a machine that has IE 7 and IE 8 as well, just to be sure. XP fits nicely on a VM that runs on my Mac, and this is how I use it.
Cyclic Leak
Now that that’s out of the way, it’s time to talk about the very worst of the memory leaks in IE – the dreaded cyclic reference, which the commenters thought that I’d committed. Under certain conditions, IE 6 will “leak” DOM nodes, retaining them, and the javascript objects that point to them, until the browser is either shut down, or crashes entirely due to lack of memory. Ugh! To understand how this happens, you really only need to know two things:
- IE 6 (and 7!) reportedly has very primitive garbage collection using reference counting
- There are two memory spaces in IE, one for JavaScript, and the other for the DOM, and they don’t communicate well.
What could go wrong? Well, lots. The commenters thought that the rule was: A leak will occur if any reference is made in JavaScript to an element that isn’t eventually set to null. That’s close, but not quite correct. The real rule is: A leak will occur if the JavaScript code contains any reference to the DOM that isn’t released in some way, either by going out of scope or being explicitly unset.
When IE 6 sees a JavaScript variable that is pointing to something in the DOM (typically, an element or node), it will record that reference, and not collect it – even when you surf over to a new page. And the DOM won’t be collected, since there’s a reference to it from JavaScript. These two objects, and all the stuff that references them, will stick around until shutdown. In IE 7, the geniuses at Microsoft saw the bug, and said “Hey, I know how to fix that, let’s garbage collect everything when we leave the page.”. Nice improvement, but it still doesn’t fix the bug, since if you’re developing a page that is designed to be used for a long period of time (like many page-as-application apps are now), it’ll still crash the browser. Apparently, they saw the error of their ways eventually, since this behavior is no longer present in IE8. (All this is confirmed by my testing with sIEve.)
So, in the example that had in my previous blog, there was no memory leak, because the variable that pointed to the element eventually went out of scope. So – how to you create variables that don’t go out of scope? The easiest way is to put them in an object – this was the leak that I eventually found in JSF. The fix there was to null out the object manually. But there’s another, more insidious way to create an object – create a closure. That creates a function object implicitly under the window object, and that will never go out of scope. But the key thing to remember is that you need to be aware of when things go out of scope when coding in IE, and act accordingly.
But wait! There’s more
If that was the only problem, life would have been fairly easy for me the last week. But that’s not the only bug that the Web Wizards of Redmond chose to deliver to their unsuspecting consumers. There’s another bug in IE (again, only in IE 6 and 7 – IE 8 appears to have fixed it per my testing), which also leaks DOM nodes that aren’t cleaned up until you leave the page. Apparently, when the IE DOM receives a call from the removeChild or replaceChild functions, it doesn’t actually, err, remove the nodes. It just leaves them there, hanging around the DOM like party guests that don’t have the sense to leave after the host has started handing out coats. While these nodes will eventually be cleaned up when the user leaves the page, this still causes problems for page-as-app programs, as in the cyclic leak for IE 7, above. While the removeChild call is fairly notorious for this, I had to find out about replaceChild with my own testing (though I did find a few obscure references once I went looking for it).
That means that instead of saying node.parentNode.replaceChild(newNode, node), you instead should say something like: node.parentNode.insertBefore(newNode, node); deleteNode(node); (with an appropriate if statement for isIE(), and a deleteNode function that doesn’t use removeChild). And instead of saying node.parentNode.removeChild(node); you instead are reduced to coding something like: node.outerHTML = ”; (again, with browser check). Except that when you combine that with IE’s horrible problems with manipulating tables, it may fail. So instead, you’re probably better off with something like this:
var temp = document.createElement('div');
try {
temp.appendChild(node.parentNode.removeChild(node));
temp.innerHTML = ""; // Prevent leak in IE
} catch (e) {
// at least we tried
}
deleteNode(temp);
Again, possibly with an isIE() check.
Hopefully you found this description of IE’s Memory “Management” useful. Here’s a few of the links that I used for research, that I found the most helpful.
- sIEve – the tool you should already have.
- Quirks blog link roundup of memory leak info.
- Microsoft MSDN article on the topic. Useful, even if it insultingly implies that it’s your fault the browser is leaking.
As always, I look forward to any comments. Especially about this topic – I’m far from expert in this area.
UPDATE: John Resig just posted about a very interesting looking tool. Haven’t checked it out yet, but if it’s got him excited…
(This article originally published on my java.net blog on November 13, 2009.)
Eval JavaScript in a global context
Even though it’s considered bad practice, it’s often handy to eval code in JavaScript. And in my case, it was simply necessary, since the JSF specification requires eval of scripts. And it’s also necessary to execute those evaluated scripts in the global scope. It’s not as easy as it first looks.
For our first naive implementation, we’d simply used eval(src) in our first pass at the implementation.
This is utterly wrong, and to understand why, you’ll need to understand scopes. JavaScript has what you can think of as two different scopes – function scope, where you’re executing something in the context of a function, and global scope, where you’re executing something in a global context – for instance, if I say var j = 1; within a function’s scope, then the variable j is set to 1 within that function. If I say the same expression, var j = 1 within the global scope, then j is set to 1 everywhere in the program – in every function, provided that that function doesn’t define a j variable in its local scope. In browsers, the global context is window – this is the default object that everything gets hung off of if you don’t specify any other object.
So, when we said eval(src), we were executing the src scripts within the local scope of the function where eval was called – that meant that I would be getting different results when variables were declared and set than would be expected – in fact, for some cases, it just seemed like the scripts weren’t being executed at all.
So, what to do? Well, as is usual for the browser JavaScript, there’s Internet Explorer, then there’s everyone else. As is usual, IE, the crazy cousin Larry of the browser world, has a convenient, well intentioned, and utterly nonstandard way to do this: window.execScript(src) It works great – and the other ways I’ll detail here break rather infamously, so use this non-standard function on IE.
For more standards-respecting browsers, the way to do this should be to use the call function, which is a standard function attached to every Function object. So, eval.call(window, src) should work. But to understand why, it’s important to know about context, in addition to scope. Every function call has it’s own context: this is the object that’s represented by the special value this. When we use the call function, the first parameter is the context object we’ll use for this. This is handy for all kinds of purposes, but for us, it’s just nice to use to set the context to the window object – which, you’ll recall, is the global.
Sadly, eval.call(window,src) breaks on Chrome – it complains about contexts not matching. Odd – and I was unable to Google up why this might be so. But a couple lucky guesses later, and I discovered that window.eval.call(window,src) works on all non-IE browsers. Now, when I say “var j = 1″, the window[j] is the variable that’s set… So, that’s good. Why do we have to add the extra window. on Chrome? Not sure – I could guess, but it’s too likely to be wrong.
At this point, I thought we’d licked the problem. No such luck. Sure, global variables are getting set, but it turns out that if you say: alert(this) – then you would correctly receive the global object back on Chrome and Safari, but not Firefox – there, you’d get back the object that was the enclosing object before the call function got called. Very odd, and likely a bug in their implementation.
With a little help from Werner Punz, we figured out that they best way to get around this issue is to wrap the calling function in an anonymous globally scoped function. Like the Chrome bug, I can guess why this might work, but it would only be a guess. Better not to clutter up the internets with more guesses – I’ll just stick to what I know works.
Here’s the code that I now use to do a global eval:
var globalEval = function globalEval(src) {
if (window.execScript) {
window.execScript(src);
return;
}
var fn = function() {
window.eval.call(window,src);
};
fn();
};
Hope this list of tricks is helpful to someone else who’s looking to do something similar.
(This article originally published on my java.net blog on September 8, 2009.)