Avoiding leakage is one of the better marks of differenting excellent programmers from good ones. Or, if you have high standards (you might be Joel Spolsky, for instance, and only afford yourself the very cream of the crop). In a typical browser environment, you rarely notice a leak unless you really look for it, and even then, it has typically been a bit of a hassle, and lots of labour tracking them. Leak Monitor steps in, solving that problem.
Web page code that results in memory leaks in the browser is in a way not really a bug in the web page per se -- non-broken browsers should always catch and dispose of the garbage we generate from user code from the messy steamping pile of dung that is the web. The hallmark of a great browser is polishing up the chaotic and broken web, making it look polished and behave well, giving us a pleasant browsing experience. Which is difficult. Very difficult. A browser that fails to collect the garbage that we strew about is a buggy browser. It is a bit unfortunate that most browsers don't do a very good job about leaky web page code. Typically including our favourites; present day Firefox still has these issues, Internet Explorer too.
So it's worth the effort writing leak free code, registering matching unload handlers to unregister the event listeners you add to a page, for instance. Especially in code meant for others to use, such as libraries like Google Maps, or jQuery, Dojo and so on, where end users are both unlikely to notice them and even less likely to be able to fix or bypass them. It's no coincidence the Google Maps v2 API supplies a method meant to be called on page unload, to unregister all the event handlers the API sets up of its own accord. If you write v2 Maps applications, be sure to use it.
I'm hoping to eventually find some time to shape up my own blog, perhaps rewrite some parts of the calendar code and maybe repackaging a zip file of that with additional bugfixes (as the upstream maintainer seems to have treated their GPL:ed version of the code a non-maintained dead end). I also hope we will see leak fixed versions of the Google Toolbar, the Web Development Toolbar and Flashblock, now that it has become so easy spotting the culprits -- not only do you get to see which objects are leaked, you also get source filenames and line numbers, so it's not really even the matter of tracking down where the bad things happen.
I'd like to share a particularly useful piece of code I picked up on the Greasemonkey list a while ago, which has ended up mostly everywhere I handle events these days. It might need a bit of reshaping to fit an extension context, but the principle is the same. The original class was called EventManager, but I opted for something that would not be even more carpal tunnel syndrome typing than the original DOM API -- so below it's renamed EventMgr:
EventMgr = // avoid leaking event handlers
{
_registry:null,
initialize:function() {
if(this._registry == null) {
this._registry = [];
EventMgr.add(window, "_unload", this.cleanup);
}
},
add:function(o, t, fn, uc) {
this.initialize();
if(typeof o == "string")
o = document.getElementById(o);
if(o == null || fn == null)
return false;
if(t == "unload") {
// call later when cleanup is called. don't hook up
this._registry.push({obj:o, type:t, fn:fn, useCapture:uc});
return true;
}
var realType = t=="_unload"?"unload":t;
o.addEventListener(realType, fn, uc);
this._registry.push({obj:o, type:t, fn:fn, useCapture:uc});
return true;
},
cleanup:function() {
for(var i = 0; i < EventMgr._registry.length; i++)
with(EventMgr._registry[i])
if(type=="unload")
fn();
else {
if(type == "_unload") type = "unload";
obj.removeEventListener(type,fn,useCapture);
}
EventMgr._registry = null;
}
};
Usage is simple: where you previously wrote
node.addEventListener( "click", handler, false )
instead write EventMgr.add( node, "click", handler, false )
-- in other words, it's just moving the node to the head of the list of parameters and substituting addEventListener
for EventMgr.add
.The class assumes a DOM compliant browser (so it's not IE compatible, for instance), but I have had very good use for it in my user scripts, where the code gets to run on mostly any page I visit, across the entire web -- which can add up to lots of leakage in the long run, if you write sloppy code. But any event handler you add using the above method also gets removed as you leave the page without your lifting a finger.
Even before elder mozillan wizards squash the bugs deep down near the firefox core.
Small typo:
ReplyDeleteThe first if(type=="unload") in the add function should actually be if(t=="unload"), since no tpye variable is defined at this point.
Other option is, of course, to change the second argument of add from "t" to "type"
Thanks. I had to squeeze it together a bit to fit the page width, and seem to have done a bad job of it. Fixed now.
ReplyDeleteI have a cross-browser version of this script. It works all the way back to 4th generation browsers (also older netscapes by using obj.onevt)
ReplyDelete/*
EventMgr Class
By ecmanaut and Martin Szyszlican
*/
EventMgr = // avoid leaking event handlers, crossbrowser version
{
_registry:null,
initialize:function() {
if(this._registry == null) {
this._registry = [];
EventMgr.add(window, "_unload", this.cleanup);
}
},
add:function(o, t, fn, uc) {
this.initialize();
if(typeof o == "string")
o = document.getElementById(o);
if(o == null || fn == null)
return false;
if(t == "unload") {
// call later when cleanup is called. don't hook up
this._registry.push({obj:o, type:t, fn:fn, useCapture:uc});
return true;
}
var realType = t=="_unload"?"unload":t;
if (o.addEventListener)
o.addEventListener(realType,fn,uc);
else if (o.attachEvent)
o.attachEvent('on' + realType, fn);
else {
o["on"+realType] = fn;
if (o["on"+realType] != fn) {
alert("Error adding listener\nObject:"+o+" ID:"+o.id+" Name:"+o.name+" TagName:"+o.tagName+"\nEvent:"+realType);
}
}
this._registry[this._registry] = {obj:o, type:t, fn:fn, useCapture:uc};
return true;
},
cleanup:function() {
for(var i = 0; i < EventMgr._registry.length; i++)
with(EventMgr._registry[i])
if(type=="unload")
fn();
else {
if(type == "_unload") type = "unload";
removeListener(obj,type,fn,useCapture);
}
EventMgr._registry = null;
},
removeListener: function (o,type,fn,uc) {
if (o.removeEventListener)
o.removeEventListener(type,fn,uc);
else if (o.detachEvent)
o.detachEvent("on" + type,fn);
else
o["on"+type] = null;
}
};