“Unloading” frame scripts in restartless extensions

The big news is: e10s is coming to desktop Firefox after all, and it was even enabled in the nightly builds already. And while most of the times the add-ons continue working without any changes, this doesn’t always work correctly. Plus, using the compatibility shims faking a single-process environment might not be the most efficient approach. So reason enough for add-on authors to look into the dreaded and underdocumented message manager and start working with frame scripts again.

I tried porting a simple add-on to this API. The good news: the API hasn’t changed since Firefox 17, so the changes will be backwards-compatible. And the bad news? Well, there are several.

  • Bug 1051238 means that frame scripts are cached — so when a restartless add-on updates the old frame script code will still be used. You can work around that by randomizing the URL of your frame script (e.g. add "?" + Math.random() to it).
  • Bug 673569 means that all frame scripts run in the same shared scope prior to Firefox 29, so you should make sure there are no conflicting global variables. This can be worked around by wrapping your frame script in an anonymous function.
  • Duplicating the same script for each tab (originally there was only a single instance of that code) makes me wonder about the memory usage here. Sadly, I don’t see a way to figure that out. I assume that about:memory shows frame scripts under the outOfProcessTabChildGlobal entry. But due to the shared scope there is no way to see individual frame scripts there.
  • Finally, you cannot unload frame scripts if your restartless extension is uninstalled or disabled. messageManager.removeDelayedFrameScript() will merely make sure that the frame script won’t be injected into any new tabs. But what about tabs that are already open?

Interestingly, it seems that Mark Finkle was the only one to ask himself that question so far. The solution is: if you cannot unload the frame script, you should at least make sure it doesn’t have any effect. So when the extension unloads it should send a "myaddon@example.com:disable" message to the frame scripts and the frame scripts should stop doing anything.

So far so good. But isn’t there a race condition? Consider the following scenario:

  • An update is triggered for a restartless extension.
  • The old version is disabled and broadcasts “disable” message to the frame scripts.
  • The new version is installed and starts its frame scripts.
  • The “disable” message arrives and disabled all frame scripts (including the ones belonging to the new extension version).

The feedback I got from Dave Townsend says that this race condition doesn’t actually happen and that loadFrameScript and broadcastAsyncMessage are guaranteed to affect frame scripts in the order called. It would be nice to see this documented somewhere, until then it is an implementation detail that cannot be relied on. The work-around I found here: since the frame script URL is randomized anyway (due to bug 1051238), I can send it along with the “disable” message:

messageManager.broadcastAsyncMessage("myaddon@example.com:disable", frameScriptURL);

The frame script then processes the message only if the URL matches its own URL:

addMessageListener("myaddon@example.com:disable", function(message)
{
  if (message.data == Components.stack.filename)
  {
    ...
  }
});

Comments

There are currently no comments on this article.