I spent a little time this weekend learning how to build firefox plugins, and a little time spilled out into this week, but I think I'm at a point where I can share what I've done, what I'm seeing, and start asking for help to finalize this (if it is as meaningful as I hope it is).
First, the issue itself: It looks like we're actually leaking on both ends - that is, not just in the JVM, but in the browser itself. When Dev Mode transfers control back to the browser itself, it runs BrowserChannelServer.reactToMessages(). This listens at the open socket between the browser's plugin and itself, and waits for the next byte to float over the wire, meaning that the browser wants something. This waiting specifically happens in Message.readMessageType, where we block on stream.readByte(). Once a tab/window has been closed, the thread that is actively watching that connection stays stuck in readByte(), waiting for that next message to float over the wire. My first thought was "why can't we just ask if that socket is closed?" - well, the socket *isn't* closed, which means that the browser is leaking the socket itself, along with whatever supporting objects were set up to manage that socket. Note that this suggests a workaround to the leak - quit and relaunch firefox, and since the socket will be forcibly closed then, readByte() will throw a EOF exception, and reactToMessages will trip off a RemoteDeathError (not ideal, but at least it just logs it and moves on, finishing the leak off). Next I dug into how to make the plugin actually disconnect when the page was closed. I started this by finding exactly where the socket was opened (common/HostChannel.{h|cpp}), then what creates HostChannel objects in the firefox plugin. This turns out to be achieved in this line in xpcom/ExternalWrapper.cpp (plus instructive comments): // TODO(jat): leaks HostChannel -- need to save it in a session object and // return that so the host page can call a disconnect method on it at unload // time or when it gets GC'd. HostChannel* channel = new HostChannel(); Okay, so at the time it was written, it was known that this leaks the channel. This is where I started losing confidence, as it doesnt look like it could be this easy... The next block actually opens the connection, and passes it off to a FFSessionHandler instance, which is stored away in a field: Debug::log(Debug::Debugging) << "Connecting..." << Debug::flush; if (!channel->connectToHost(hostPart.c_str(), atoi(portPart.c_str()))) { *_retval = false; return NS_OK; } Debug::log(Debug::Debugging) << "...Connected" << Debug::flush; sessionHandler.reset(new FFSessionHandler(channel/*, ctx*/)); All this code is part of ExternalWrapper::Connect (note, defined twice for varying ff versions), and since Connect is never invoked internally, it was my assumption that I could build a ExternalWrapper::Disconnect that simply unwound this channel: NS_IMETHODIMP ExternalWrapper::Disconnect() { Debug::log(Debug::Info) << "Disconnecting..." << Debug::flush; sessionHandler.get()->disconnect(); Debug::log(Debug::Info) << "...Disconnected" << Debug::flush; return NS_OK; } That, combined with two other changes seems to expose this new function to be called from in the browser and actually triggers the QUIT case in reactToMessages - First, declare this new method in IOOPHM.idl, and second tweak ExternalWrapper's CanCallMethod check to allow invoking disconnect from within the browser. Actually building with the attached patch then modifies several other files (more on that later). Next, how to make disconnect() be invoked from within the browser - it is already declared and invoked from within hosted.html and devmode.js, but it seems to be in an invalid window callback: window.onUnload = function() { try { // wrap in try/catch since plugins are not required to supply this plugin.disconnect(); } catch (e) { } }; As far as I can tell, there is no such onUnload, but should be onunload instead. Indeed, onunload is defined later in each file, but since it just assigns an empty function, I suspect that one or the other is a typo: window.onunload = function() { }; To continue my exploration, I kept onUnload's name as is, and just called it from the onunload declaration - this avoids changing the name of any existing calls (in case there is actually a good reason for labelling it so) and should prevent any possible issues from clobbering com.google.gwt.user.client.impl.WindowImpl#initWindowCloseHandler by re-overriding onunload after initWindowCloseHandler has already replaced it (and kept a reference to the original): window.onunload = function() { window.onUnload && window.onUnload(); }; With that change to hosted.html (or a copy in my project's public/ dir) and the attached patch, the leak seems to be resolved. Questions I have: * How does one perform the complete plugin build process? How far back in time do we rebuild and test? * How many auto-generated files (mostly .h from the idl, but also the .dll/.dylib/etc) do we commit when such a change is made? * Apparently plugins don't need to provide a disconnect() method, but if any do, are we *sure* they are actually being called as expected? Or is onUnload a long-lived typo? Looking forward to discussing this here, in ##gwt on freenode, or on the call tomorrow, Colin -- http://groups.google.com/group/Google-Web-Toolkit-Contributors --- You received this message because you are subscribed to the Google Groups "GWT Contributors" group. To unsubscribe from this group and stop receiving emails from it, send an email to google-web-toolkit-contributors+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/groups/opt_out.
commit 6969765b5b60731f12ce529bbc6f730922b7e817 Author: Colin Alworth <niloc...@gmail.com> Date: Tue Sep 24 18:29:13 2013 -0500 Initial work on supporting a disconnect message from the browser to close off the connection to the code server diff --git a/xpcom/ExternalWrapper.cpp b/xpcom/ExternalWrapper.cpp index ab0423e..f4d61cb 100644 --- a/xpcom/ExternalWrapper.cpp +++ b/xpcom/ExternalWrapper.cpp @@ -399,6 +399,13 @@ NS_IMETHODIMP ExternalWrapper::Connect(const nsACString& suppliedUrl, return NS_OK; } +NS_IMETHODIMP ExternalWrapper::Disconnect() { + Debug::log(Debug::Info) << "Disconnecting..." << Debug::flush; + sessionHandler.get()->disconnect(); + Debug::log(Debug::Info) << "...Disconnected" << Debug::flush; + return NS_OK; +} + // nsISecurityCheckedComponent static char* cloneAllAccess() { static const char allAccess[] = "allAccess"; @@ -422,7 +429,7 @@ NS_IMETHODIMP ExternalWrapper::CanCreateWrapper(const nsIID * iid, NS_IMETHODIMP ExternalWrapper::CanCallMethod(const nsIID * iid, const PRUnichar *methodName, char **_retval) { Debug::log(Debug::Spam) << "ExternalWrapper::CanCallMethod" << Debug::flush; - if (strEquals(methodName, "connect") || strEquals(methodName, "init")) { + if (strEquals(methodName, "connect") || strEquals(methodName, "init") || strEquals(methodName, "disconnect")) { *_retval = cloneAllAccess(); } else { *_retval = nullptr; diff --git a/xpcom/IOOPHM.idl b/xpcom/IOOPHM.idl index 1aeea54..14e4d6f 100644 --- a/xpcom/IOOPHM.idl +++ b/xpcom/IOOPHM.idl @@ -8,4 +8,5 @@ interface IOOPHM : nsISupports boolean init(in nsIDOMWindow window); boolean connect(in ACString url, in ACString sessionKey, in ACString addr, in ACString moduleName, in ACString hostedHtmlVersion); + void disconnect(); };