|
DomEventsAndMemoryLeaks
An explanation of DOM events, memory leaks, and how GWT handles both.
This article is a bit old (but still correct). I've added a more recent writeup at UnderstandingMemoryLeaks, with a bit more advice on dealing with these issues in practice. You may ask yourself, "Why do I have to use bitfields to sink DOM events?", and you may ask yourself, "Why can I not add event listeners directly to elements?". If you find yourself asking these questions, it's probably time to dig into the murky depths of DOM events and memory leaks. If you're creating a Widget from scratch (using DOM elements directly, as opposed to simply creating a Composite), the setup for event handling is a bit odd. Generally, it looks something like this: class MyWidget extends Widget {
public MyWidget() {
setElement(DOM.createDiv());
sinkEvents(Event.ONCLICK);
}
public void onBrowserEvent(Event evt) {
switch (DOM.eventGetType(evt)) {
case Event.ONCLICK:
// Do something insightful.
break;
}
}
}This may seem a bit obtuse, but there's a good reason for it. To understand this, you may first need to brush up on browser memory leaks. There are some good resources on the web:
The upshot of all this is that in some browsers, any reference cycle that involves a Javascript object and a DOM element (or other native object) has a nasty tendency to never get garbage-collected. The reason this is so insidious is that this is an extremely common pattern to create in Javascript UI libraries. Imagine the following (raw Javascript) example: function makeWidget() {
var widget = {};
widget.someVariable = "foo";
widget.elem = document.createElement ('div');
widget.elem.onclick = function() {
alert(widget.someVariable);
};
}Now, I'm not suggesting that you'd really build a Javascript library quite this way, but it serves to illustrate the point. The reference cycle created here is: widget -> elem(native) -> closure -> widget There are many different ways to run into the same problem, but they all tend to form a cycle that looks something like this. This cycle will never get broken unless you do it manually (often by clearing the onclick handler). There are a number of ways developers try to deal with this issue. One of the more common I've seen is to walk the DOM when window.onunload is fired, clearing out all event listeners. This is problematic for two reasons:
GWT's SolutionWhen designing GWT, we decided that leaks were simply unacceptable. You wouldn't tolerate egregious memory leaks in a desktop application, and a browser app should be no different. This raises some interesting problems, though. In order to avoid ever creating leaks, any Widget that might need to be get garbage collected must not be involved in a reference cycle with a native element. There's no way to find out "when a widget would have been collected had it not been involved in a reference cycle". So in GWT terms, a Widget must not be involved in a cycle when it is detached from the DOM. How do we enforce this? Each Widget has a single "root" element. Whenever the Widget becomes attached, we create exactly one "back reference" from the element to the Widget ( i.e. elem.__listener = widget, performed in DOM.setEventListener()). This is set whenever the Widget is attached, and cleared whenever it is detached. Which brings is back to that odd bitfield used in sinkEvents(). If you look at the implementation of DOM.sinkEvents(), you'll see that it does something like this: elem.onclick = (bits & 0x00001) ? $wnd.__dispatchEvent : null; Each element's events point back to a central dispatch function, which looks for the target element's __listener expando, in order to call onBrowserEvent(). The beauty of this is that it allows us to set and clear a single expando to clean up any potential event leaks. What this means in practice is that, as long as you don't set up any reference cycles on your own using JSNI, you can't write an app in GWT that will leak. We test carefully with every release to make sure we haven't done anything in the low-level code to introduce new leaks as well. The downside, of course, is that you can't hook event listeners directly to elements that are children of a Widget's element. Rather, you have to receive the event on the Widget itself, and figure out which child element it came from. But that's better than leaking gobs of memory on your users' machines, right? |
Thanks for the excellent explanation and thanks for keeping our apps memory leakless!
Thanks, very useful document!
You said that you test carefully with every release to make sure we haven't done anything in the low-level code to introduce new leaks as well.
How do you know you have a new memory leak in IE?
I typically use Drip (http://code.google.com/p/iedrip/), which I wrote some time ago. It's not perfect, but catches most leaks in practice.
The one downside is that 2 gwt modules on the same page are going to conflict with each other since the last one will likely overwrite the $wnd.dispatchEvent.
I am creating an application using GWT. It works fine in Mozilla while in IE (6 & 7 both), it slows down as I run my application. While I checked in "Windows Task Manager", I found that memory usage is increasingly continuously in IE while in Mozilla it remains constant. First I assumed that reason for this is memory leak in IE. But when I read this article, acc. to this, memory leak is properly handled in GWT and it can't be a problem (As I am not using JSNI anywhere in my application). But than why my application gets slow down in IE and also memory usage is increasing regularly (while it runs fine in mozilla)? PLEASE HELP ME as it has become a blocker issue for deployment of my application.
We have the same problem ganesh has. The memory is increasing continuously and is not freed even after refresh of the page. Using only GWT and Incubator Widgets.
Forms and IFrames can cause leaks in IE6/7
But I m using neither Forms not IFrames, still lot of memory leaks in IE6/7.
@Ganesh: Can you post a link to some code, and then we can take a look at it? Also, what version of GWT are you using?
Thanks!
I am creating a very big application and it's not feasible to post the code. Still I will try to make some sample code on weekend which causes memory leaks and post it asap. In the mean time, if u can suggest some points which should be taken care for avoiding memory leaks while creating GWT application, than it can be very helpful for me.
Thanks!
Hello... has the issues brought up by ganesh.bansal been resolved - memory leaks in IE6/7 with GWT ???
Hi, Same problem here, big memory leaks with GWT on IE6 but NOT on firefox/opera etc.
I saw memory increasing on IE6 when closing a tab and when I opened it again and again... each time I reopen a tab (a tab with many widgets inside) I see the memory increasing only in IE6 !!!
Any help would be great !!!
Cyril Lakech http://cyrillakech.blogspot.com
Cyril, Can you enter an issue report with some sample code here(http://code.google.com/p/google-web-toolkit/issues/entry)? Feel free to e-mail me the issue number directly after you are done, as any memory leak you can create with just a tab panel + widgets without iframes, etc. is one we'd definitely want to track down!
We have the same issues as described by ganesh.bansal abd cyril.lakech with GWT 1.5.2. It seems that memory in IE never released properly (I mean released less memory then was allocated during widgets creation). But if page is refreshed (just pressing refresh button) IE release allocated memory. Can you provide any suggestions or guides to program with GWT and avoid memory leaks?.
IE's garbage collector is extremely lazy. Are you certain that it's maybe not just IE not doing any GC? Try to run your application with drip as asked before and that should give you a better idea.
Is there a possibility to enforce the GC to happen in IE ? Just to be able to see if it is really a memory leak or the laziness of GC invocations ?
Just use the applicationCreator to create a starter application and test it using JS Leak Detector in IE7. It leaks memory! You can use JS Leak Detector against Windows Live Hotmail and it will show no leak!
@sachindatta: Thanks for pointing out the JS Leak Detector. It's much more up-to-date than my crufty old Drip app for finding leaks. But to determine whether an app actually leaks, read the documentation carefully -- in its default mode (IE7), it identifies patterns that might leak, by looking for circular reference patterns. If you switch to "Actual leaks" mode, you'll find that none of our samples leak. This happens because GWT generates circular reference patterns for every widget, but guarantees that they get cleaned up whenever a widget is detached, and forcibly detaches all widgets on window unload.
@Cyril, Alexey: Can you check your app against GWT 1.5 if you haven't already? There was a bug in GWT 1.4 ( issue 2014 ) that caused leaks under certain circumstances that we weren't adequately testing, but are now.
I want to know about wrapping elements in iframes with widgets and if this event model supports this. The full question is here: http://groups.google.com/group/Google-Web-Toolkit/browse_thread/thread/b53fc025f0dd3653
excellent explanation! thanks!
If I have classes in Java that reference each other, is this considered circular referencing and therefore will they never be GC'd? I'm having big-time memory leaks in FF and Safari without JSNI.
Very stripped down example:
class Controller{ Model m; View v; Controller(Model m, View v){ this.m = m; this.v = v; } //... } class View{ Model m; Controller c; View(Model m){ this.m = m; } setController(Controller c)[ this.c = c; } //... } class Run{ onModuleLoad(){ Model m = new Model(); View v = new View(m); Controller c = new Controller(m,v); v.setController(c); m = null; v = null; c = null //Do Controller and View still exist after this line, since they reference each other? } }Another question, is it dangerous to have a dialog box that gets added dynamically like this:
public void makeCoolButton(){ Button b = new Button(); b.addClickHandler(new ClickHandler(){ public void onClick(ClickEvent e){ new SpecialDialogBox(); } }); }In Java, I would expect the garbage collector to take care of everything, but this article has me scared about the JS garbage collector.
@micah: Both of the cases you describe are completely safe. To be precise, the only type of circular reference that will create a leak (on IE) is one that involves both Javascript? objects and native (COM) objects (usually Element, Window, and the like). Using the GWT libraries, there is generally no way to create such a leak without dropping to native Javascript code.
If you're seeing leaks in non-ancient versions of Gecko- and Webkit-based browsers, there are only two possibilities I'm aware of: 1. It's a traditional Java "pseudo-leak" (i.e., objects that accumulate in static collections and such), or 2. These browsers have created new memory leak patterns that I'm unware of.
I would investigate (1) first.
I'm sitting with a situation where either my code is horribly bad and I should not be in the development industry or there is something wrong.
The following short code sample will "leak". In IE 7/8 the odds of getting memory back are slim to none. FF2 gives some back and chrome(using the dev tools) releases most if not all. However the Chrome memory footprint using perfmon private bytes tracking also "leaks" (so it does not reflect what the chrome snapshots show).
Could this be a case of insufficient tools to truly track browser memory usage? I have after days of research found no answers. The code sample should be self explanatory :
public class ARGH implements EntryPoint { VerticalPanel dpFields = new VerticalPanel(); /** * This is the entry point method. */ public void onModuleLoad() { HorizontalPanel dp = new HorizontalPanel(); dp.setHeight("120"); RootPanel.get().add(dp); dp.add(new Button("Add", new ClickHandler() { public void onClick(ClickEvent evt) { TextBox t; for(int i = 0; i < 100; i++) { t = new TextBox(); t.setText("TextField " + i); t.setWidth("200"); dpFields.add(t); } } })); dp.add(new Button("Clear", new ClickHandler() { public void onClick(ClickEvent evt) { dpFields.clear(); } })); dp.add(new Button("Reset", new ClickHandler() { public void onClick(ClickEvent evt) { dpFields.clear(); RootPanel.get().remove(dpFields); dpFields = new VerticalPanel(); dpFields.setHeight("200"); RootPanel.get().add(dpFields); } })); dpFields.setHeight("200"); RootPanel.get().add(dpFields); } }I've run through this with IE7, IE8, Microsoft's IE JS Leak Detector, and Chrome, and I'm not seeing any evidence of it leaking, on IE at least. On my Vista box, memory grows steadily as I add more items, then mostly recovers as I clear or reset. There is a gentle upward trend in total usage as I do this, but it appears to peak out at around 30MB and no longer grows after that.
I'm seeing Chrome's "private memory" grow without obviously slowing down, but I have to admit to not being too clear on the details of what to expect from its generational GC. I can ping some people on the Chrome team about that, but my guess is that it's going to end up being that it never ran low enough on memory to be worth running a major GC cycle.
Thanks for the response,
I see now that only by running IE in "no addon" mode will stop the memory from growing, in fact it doesnt get near 30MB. Not that my normal IE installation has anything but the default addons but they seem to make the most trivial application over-use memory.
We also have quite a big GWT based application. In our application, we have also seen similar memory issues showing up in FireFox3?.6, IE and Chrome4.1 as well. Browsers eventually stopped responding (with CPU more than 70%) after memory reaching up to 1GB (happened with Firefox 3.6, this happened under a load test scenario though).
I tried with the above sample code. (Thanks for the code BTW, it's very useful to re-produce the problem quickly) And observed similar memory issue on all the browsers. What I have observed is, on Firefox 3.6 (Windows XP) memory does keep going up all the time but not every time it gets reclaimed/goes down. It goes down very intermittently. I guess that fact might very well be attributed to the way JavaScript? engine's garbage collection works or gets kicked-in.
I debugged the above code with certain modifications and did see that once cleared from the panel, the new elements do get unattached/removed from browser's DOM(so there may not be memory leaks), but that doesn't really make browser's memory to go down immediately. I am not entirely sure, but if it is due to the fact that perhaps the JavaScript? engine's GC is not getting kicked in frequently, is there any way we can hint/force Firefox to run the GC? Or is there any way GWT can provide so that we can force browser to run JavaScript? GC.
I'm not aware of any way on most browsers to force a GC. On IE, there's the global CollectGarbage?() function, but IE already runs GC too frequently most of the time anyway, so it's not much help. I'm less familiar with Firefox and Safari, but Chrome's GC is generational, so there are several different kinds of collection and compaction, but again I don't believe there's any way to force it.
As I noted above, I didn't see the example posted by GlacieredPyro? leaking in any obvious way. If you can post a (preferably small) example that causes the browser to run slowly and/or consume an enormous amount of memory, but that doesn't just have an enormous number of objects pinned (i.e., referenced globally or indirectly), I can forward it to the Chrome team so they can have a look.
Thanks for the response.
I agree with you the example posted here is not leaking in any obvious way. I did notice that the underlying elements are getting detached from the DOM (as those get cleared from the underlying container). But there is one very peculiar thing that I have noticed (and it's true for our big GWT application as well) which is, if you move away from the existing page (browse a different website, let's say, a very light page www.google.com), then browser's memory actually goes down substantially. Which makes me believe that the Javascript GC is definitely getting kicked-in when the DOM itself goes out of scope. But then question is why the heck Javascript GC is so unpredictable when the DOM is still in scope and there are elements to be re-claimed?
Let me see if I can forward you some small example code from our app.
I have a question regarding the com.google.gwt.event.shared.HandlerManager? and memory leaks. Let's say that Object A calls addHandler on Object B which contains an instance of HandlerManager?. If I destroy Object A, will Object B still have a reference to it? I could categorize that as a leak. Or, must I always call removeHandler?
@bob.whiton: I'm assuming you mean something like this:
This is just normal Java memory management -- nothing magic. So if B is still around (because it is, e.g., a widget still attached to the document), then A will still hang around until it's either removed from B's handler manager or B itself is removed and thus collected.