This article gives an overview of how Firefox extension developers can ensure that their code works with multiprocess Firefox.
In current versions of desktop Firefox, chrome code (including code inserted by extensions) and content run in the same operating system process. So extensions can access content directly:
gBrowser.selectedBrowser.contentDocument.body.innerHTML = "replaced by chrome code";
However, in multiprocess Firefox (also called Electrolysis or E10S), the extension's code will run in a different process from content, and this kind of direct access will no longer be possible.
Checking whether you're affected
You can use the Compatibility Lookup Tool to check if your add-on will be affected by these changes.
As a rule, you won't be affected if:
- you use the WebExtension API
- you only use the Add-on SDK's high-level APIs
- you don't access web content at all
- you load XUL content into tabs via chrome: URLs
You will be affected if:
- you access web content directly using an overlay extension, a bootstrapped extension, or low-level SDK APIs like window/utils or tabs/utils
For more details, see the article on Limitations of chrome scripts, which lists patterns that will no longer work in the chrome process.
Testing
In multiprocess Firefox, add-ons can run in two different modes: either with compatibility shims or without them. Add-ons are more likely to work with compatibility shims, but they will run much more slowly if they access web content often. Shims will be removed six months after multiprocess Firefox is released to users, so developers should avoid shims if possible.
Testing if your add-on works with shims: Make sure you're running Firefox Nightly, in which multiprocess support is enabled by default. To verify that multiple process are being used, visit about:support and confirm that the "Multiprocess Windows" row contains "default: true". (If you see a different value, open Firefox preferences and check "Enable multi-process Nightly" in the General pane.) Now install your add-on and test that all features work. If you're testing an unsigned version of your extension, make sure you turn signing enforcement off.
Testing if your add-on works without shims: As before, make sure you're running Firefox Nightly with multiple processes enabled. Next, add a new property to your extension's install.rdf named multiprocessCompatible
, with a value of true
. Now install your add-on and test that all features work.
The rest of this document explains how to make broken add-ons work in multiprocess Firefox without using shims.
Adapting to Multiple Processes
If your add-on is affected, there are several ways to adapt it. One option is to switch to an API that is works better with multiprocess Firefox.
- The WebExtension API is the preferred choice for future add-ons. However, its development is still in the early stages. A preview is available in stable Firefox, but the Nightly channel will be more up-to-date. Please use this survey to tell us which APIs you need, and see what else we're doing to mitigate the impact of this transition.
- The Add-on SDK's high-level APIs are the next best choice. They are more stable than WebExtensions, but they may not offer all the functionality needed by some add-ons.
- The last option is to use the message manager to communicate with the content process. The message manager can be used from the Add-on SDK or from overlay or bootstrapped add-ons. Use of the message manager from the SDK is covered in Multiprocess Firefox and the SDK. The rest of this guide discusses the use of the message manager from other types of add-ons.
There are people on hand to assist you every Tuesday in the #addons
channel at irc.mozilla.org. We're here to help!
Example Scenarios
Note to SDK developers: As of March 2016, SDK add-ons must be built with the new jpm
tool, which replaces cfx
, to continue being compatible with Firefox. The jpm
tool will be supported for as long as the SDK is supported.
1. SDK add-ons that use low-level APIs
If your SDK add-on uses low-level APIs like window/utils
or tabs/utils
to access web content, you will likely be affected. Test your add-on for compatibility, and once it works without shims, add the multiprocess
permission to your package.json
. If not, consider switching to high-level APIs or WebExtensions.
2. SDK add-ons that use legacy APIs
If your SDK add-on uses XPCOM or other legacy APIs, it will need to be updated both to support e10s and to eliminate the use of legacy APIs. You should use the SDK's high-level APIs or, if possible, switch to WebExtensions.
3. SDK add-ons that use only high-level APIs
If your add-on only uses high-level APIs, it should continue to work without problems. Please test its compatibility, and once it works without shims, add the multiprocess
permission to your package.json
.
4. Legacy XUL or XPCOM add-ons
We expect to remove XUL and XPCOM from Firefox by the end of 2017. We hope this will give us enough time to help you migrate and provide APIs that are suitable for replacing the features you might lose once we stop supporting these legacy technologies. To prepare for XUL/XPCOM deprecation, you must migrate your add-on to either a WebExtension or to the Add-on SDK. When using the SDK, you should stick to its high-level APIs for e10s compatibility.
Using the Message Manager
When using the message manager, the extension will need to refactor code that touches content into separate scripts that are called either process scripts or frame scripts. Both kinds of scripts run in the content process and can access content directly. They communicate with the rest of the extension using a message passing API.
Extension code running in the chrome process must use asynchronous messaging when communicating with its frame or process script running in the content process. This is to ensure that the Firefox UI process can't be blocked by the content process.
The content process is allowed to send either asynchronous or synchronous messages to the chrome process, but asynchronous communication is always preferred.
Process scripts run once per content process. They allow extensions to set up singletons like observers and content policies in the content process. Frame scripts run once per tab. The environment of a frame script allows it to access the tab's top-level window and docshell. Frame scripts are more useful for modifying the DOM. Add-on authors can use both frame scripts and process scripts based on their needs.
For more details on using the message manager and content scripts, refer to the message manager guide. The rest of this article explains provides an overview of the sorts of changes that are needed and walks through the process of porting some simple extension patterns so they work properly with multiprocess Firefox.
Updating your code
The general approach to updating your code is:
- factor the part of your extension that accesses web content into one or more separate frame/process scripts.
- register chrome:// URLs for your frame and process scripts
- use a message manager to load the scripts into
browser
objects - if you need to communicate between the main extension code and a frame/process script, use message manager APIs to do so
- if you load XUL in tabs, register these as about: URLs and load them with the about: URL.
There are more details on this in the message manager documentation.
Note that you can't do everything in a frame script that you could do in the chrome process. For some more details on this, see Limitations of frame scripts (which also apply to process scripts).
Backwards compatibility of the new APIs
With multiprocess support turned off, the e10s messaging APIs are still available and functional. They have been available in one form or another since Firefox 4; however, the original APIs are different from the current ones. Some known differences:
- Interface changes in Firefox 17
- Before Firefox 19, code like
new content.document.defaultView.XMLHttpRequest()
fails withNS_ERROR_FAILURE: Failure
You should test your changes not only in nightlies with multiprocess support turned on, but also in releases you intend to support with multiprocess support turned off.
Examples
This section walks through the process of porting a few different sorts of extension. The extensions are all extremely simple, and are intended to represent fundamental extension patterns that require different handling in multiprocess Firefox.
You can find all the source code for these examples in the e10s-example-addons GitHub repository.
Run a script in all pages
The first extension runs some code on every page load. The code doesn't need to interact with any other part of the extension: it just makes some predetermined modification to the page. In this case it adds a border to the document's body
.
It does this by attaching to a XUL overlay a version of the "On page load" code snippet:
var myExtension = { init: function() { // The event can be DOMContentLoaded, pageshow, pagehide, load or unload. if(gBrowser) gBrowser.addEventListener("DOMContentLoaded", this.onPageLoad, false); }, onPageLoad: function(aEvent) { var doc = aEvent.originalTarget; // doc is document that triggered the event if (doc.nodeName != "#document") return; // only documents // make whatever modifications you want to doc doc.body.style.border = "5px solid blue"; } } window.addEventListener("load", function load(event){ window.removeEventListener("load", load, false); //remove listener, no longer needed myExtension.init(); },false);
Because this code accesses web content directly, it won't work in multiprocess Firefox.
Porting to the message manager
To port this example using the message manager, we can put all the meat of the add-on in a frame script:
// frame-script.js // will run in the content process addEventListener("DOMContentLoaded", function(event) { var doc = event.originalTarget; if (doc.nodeName != "#document") return; // only documents doc.body.style.border = "5px solid red"; });
We'll register a chrome:// URL for the frame script:
// chrome.manifest content modify-all-pages chrome/content/
The main script, that we attach to the XUL overlay, is just a stub that uses the global message manager to load the frame script into each tab:
// chrome script // will run in the chrome process var globalMM = Cc["@mozilla.org/globalmessagemanager;1"] .getService(Ci.nsIMessageListenerManager); globalMM.loadFrameScript("chrome://modify-all-pages/content/frame-script.js", true);
Porting to the Add-on SDK
A good alternative for an extension like this is to port to the Add-on SDK. The Add-on SDK includes a module called page-mod which is designed to load scripts into web pages. The Add-on SDK calls these scripts content scripts.
In this case the main extension code creates a page-mod to load a content script into every page loaded by the user:
// main.js var pageMod = require("sdk/page-mod"); var self = require("sdk/self"); pageMod.PageMod({ include: "*", contentScriptFile: self.data.url("modify-all-pages.js") });
The content script can modify the page directly:
// modify-all-pages.js - content script document.body.style.border = "5px solid green";
Run a script in the active tab
The example demonstrates how an extension can:
- load a frame script into a specific XUL
<browser>
element - make a synchronous call from the frame script to the main extension
The example is a restartless extension that adds a button using the CustomizableUI module. When the user clicks the button, the extension runs some code that modifies the current tab. The basic infrastructure is taken from the Australis "Hello World" extension written by Jorge Villalobos.
What the code actually does is: find any <img>
elements and replace their src
with a link to a silly GIF randomly chosen from a list hardcoded into the extension. The silly gifs are taken from the list in the Whimsy extension.
The first version accesses the page directly, so it's not multiprocess compatible:
// bootstrap.js let Gifinate = { init : function() { let io = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); // the 'style' directive isn't supported in chrome.manifest for bootstrapped // extensions, so this is the manual way of doing the same. this._ss = Cc["@mozilla.org/content/style-sheet-service;1"]. getService(Ci.nsIStyleSheetService); this._uri = io.newURI("chrome://gifinate/skin/toolbar.css", null, null); this._ss.loadAndRegisterSheet(this._uri, this._ss.USER_SHEET); // create widget and add it to the main toolbar. CustomizableUI.createWidget( { id : "gifinate-button", defaultArea : CustomizableUI.AREA_NAVBAR, label : "Gifinate", tooltiptext : "Gifinate!", onCommand : function(aEvent) { Gifinate.replaceImages(aEvent.target.ownerDocument.defaultView.content.document); } }); }, replaceImages : function(contentDocument) { let images = contentDocument.getElementsByTagName("img"); for (var i = 0; i < images.length; ++i) { let gif = this.gifs[Math.floor(Math.random() * this.gifs.length)]; images[i].src = gif; } },
Porting to the message manager
To port this example to the message manager we'll make onCommand
load a frame script into the current <browser>
, then listen for "request-gifs" messages from the frame script. The "request-gifs" message is expected to contain the number of GIFs we need for this page: the message listener retrieves and returns that many GIFs.
// bootstrap.js // will run in the chrome process let Gifinate = { init : function() { let io = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); // the 'style' directive isn't supported in chrome.manifest for bootstrapped // extensions, so this is the manual way of doing the same. this._ss = Cc["@mozilla.org/content/style-sheet-service;1"]. getService(Ci.nsIStyleSheetService); this._uri = io.newURI("chrome://gifinate/skin/toolbar.css", null, null); this._ss.loadAndRegisterSheet(this._uri, this._ss.USER_SHEET); // create widget and add it to the main toolbar. CustomizableUI.createWidget( { id : "gifinate-button", defaultArea : CustomizableUI.AREA_NAVBAR, label : "Gifinate Button", tooltiptext : "Gifinate!", onCommand : function(aEvent) { Gifinate.replaceImages(aEvent.target.ownerDocument); } }); }, replaceImages : function(xulDocument) { var browserMM = xulDocument.defaultView.gBrowser.selectedBrowser.messageManager; browserMM.loadFrameScript("chrome://gifinate/content/frame-script.js", false); browserMM.addMessageListener("request-gifs", Gifinate.getGifs); }, getGifs : function(message) { var gifsToReturn = new Array(message.data); for (var i = 0; i < gifsToReturn.length; i++) { let gif = this.gifs[Math.floor(Math.random() * this.gifs.length)]; gifsToReturn[i] = gif; } return gifsToReturn; },
Again, we need to register a chrome:// URL for the frame script:
// chrome.manifest content gifinate frame-script.js
In the frame script, we get all the <img>
elements and send the "request-gifs" message to the main add-on code. Because this is a frame script we can make it a synchronous message, and update the src
attributes with the value it returns:
// frame-script.js // will run in the content process var images = content.document.getElementsByTagName("img"); var response = sendSyncMessage("request-gifs", images.length); var gifs = response[0]; for (var i = 0; i < images.length; ++i) { images[i].src = gifs[i]; }
The overall flow of the add-on now looks like this:
Known bugs
This is a list of open bugs likely to affect add-on developers migrating to multiprocess Firefox:
- Bug 1051238 - frame scripts are cached forever, so an add-on can't properly update without a browser restart
- Bug 1017320 - tracking bug for implementing compatibility shims