Working with multiprocess Firefox

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.

Instead, the extension will need to factor code that touches content into separate scripts that are called frame scripts. Frame scripts run in the content process and get direct access to content. Frame scripts 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 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.

For more details on using the message manager and content scripts, refer to the message manager guide. The rest of this article explains how to work out if you're affected or not, provides an overview of the sorts of changes that are needed, then walks through the process of porting some simple extension patterns so they work properly with multiprocess Firefox.

If you're using the Add-on SDK, see this separate guide that covers some considerations that are specific to the SDK.

Checking whether you're affected

As a rule:

For more details, see the article on Limitations of chrome scripts, which lists patterns that will no longer work in the chrome process.

To know for sure, you need to test it:

  • First, make sure you're using Firefox Nightly, in which multiprocess support is enabled by default.
  • Next, declare that your extension is multiprocess compatible: to make migration to multiprocess Firefox easier, we've implemented shims that help extensions to work even when they're not compatible. To check whether your extension is really compatible you need to disable these shims. To do that, add a new property to your extension's install.rdf named multiprocessCompatible, with a value of true.

Now you'll be able to test your extension in multiprocess Firefox, with no compatibility shims.

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 scripts. In multiprocess Firefox these are called frame scripts.
  • register chrome:// URLs for your frame 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 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.

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:

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:

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

Document Tags and Contributors

Contributors to this page: bgirard_github, wbamberg, Sheppy, Jack_No1, Np, Minh Nguyen
Last updated by: bgirard_github,
Hide Sidebar