Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 07 Jun 2016 15:34:15 +0200
changeset 300904 0deb0ab8cf3ff074be1542471fd6eb0dbfadd98f
parent 300903 48b1777f5fe3b70d2c328da74c3b4986b8308e1e (current diff)
parent 300799 7f7c7d24700eb80ce328b05fd260ec58e9725ca4 (diff)
child 300905 56b9bf9665b141a04c9802fac4b9fd8f9d076a70
push id19599
push usercbook@mozilla.com
push dateWed, 08 Jun 2016 10:16:21 +0000
treeherderfx-team@81f4cc3f6f4c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone50.0a1
Merge mozilla-central to mozilla-inbound
dom/base/nsFrameLoader.cpp
--- a/.eslintignore
+++ b/.eslintignore
@@ -128,16 +128,17 @@ devtools/shared/locales/**
 devtools/shared/performance/**
 devtools/shared/qrcode/**
 devtools/shared/security/**
 devtools/shared/shims/**
 devtools/shared/tests/**
 !devtools/shared/tests/unit/test_csslexer.js
 devtools/shared/touch/**
 devtools/shared/transport/**
+!devtools/shared/transport/transport.js
 devtools/shared/webconsole/test/**
 devtools/shared/worker/**
 
 # Ignore devtools pre-processed files
 devtools/client/framework/toolbox-process-window.js
 devtools/client/performance/system.js
 devtools/client/webide/webide-prefs.js
 devtools/client/preferences/**
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -226,22 +226,22 @@ pref("dom.max_chrome_script_run_time", 0
 
 // plugins
 pref("plugin.disable", true);
 pref("dom.ipc.plugins.enabled", true);
 
 // product URLs
 // The breakpad report server to link to in about:crashes
 pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
-pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/b2g/%VERSION%/releasenotes/");
-pref("app.support.baseURL", "http://support.mozilla.com/b2g");
-pref("app.privacyURL", "http://www.mozilla.com/%LOCALE%/m/privacy.html");
-pref("app.creditsURL", "http://www.mozilla.org/credits/");
-pref("app.featuresURL", "http://www.mozilla.com/%LOCALE%/b2g/features/");
-pref("app.faqURL", "http://www.mozilla.com/%LOCALE%/b2g/faq/");
+pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/b2g/%VERSION%/releasenotes/");
+pref("app.support.baseURL", "https://support.mozilla.com/b2g");
+pref("app.privacyURL", "https://www.mozilla.com/%LOCALE%/m/privacy.html");
+pref("app.creditsURL", "https://www.mozilla.org/credits/");
+pref("app.featuresURL", "https://www.mozilla.com/%LOCALE%/b2g/features/");
+pref("app.faqURL", "https://www.mozilla.com/%LOCALE%/b2g/faq/");
 
 // Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
 pref("security.alternate_certificate_error_page", "certerror");
 
 pref("security.warn_viewing_mixed", false); // Warning is disabled.  See Bug 616712.
 
 // Block insecure active content on https pages
 pref("security.mixed_content.block_active_content", true);
--- a/browser/base/content/aboutDialog-appUpdater.js
+++ b/browser/base/content/aboutDialog-appUpdater.js
@@ -206,23 +206,26 @@ appUpdater.prototype =
    * Handles oncommand for the "Restart to Update" button
    * which is presented after the download has been downloaded.
    */
   buttonRestartAfterDownload: function() {
     if (!this.isPending && !this.isApplied) {
       return;
     }
 
+    gAppUpdater.selectPanel("restarting");
+
     // Notify all windows that an application quit has been requested.
     let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
                      createInstance(Components.interfaces.nsISupportsPRBool);
     Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
 
     // Something aborted the quit process.
     if (cancelQuit.data) {
+      gAppUpdater.selectPanel("apply");
       return;
     }
 
     let appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"].
                      getService(Components.interfaces.nsIAppStartup);
 
     // If already in safe mode restart in safe mode (bug 327119)
     if (Services.appinfo.inSafeMode) {
--- a/browser/base/content/aboutDialog.xul
+++ b/browser/base/content/aboutDialog.xul
@@ -107,16 +107,19 @@
                 <label>&update.otherInstanceHandlingUpdates;</label>
               </hbox>
               <hbox id="manualUpdate" align="center">
                 <label>&update.manual.start;</label><label id="manualLink" class="text-link"/><label>&update.manual.end;</label>
               </hbox>
               <hbox id="unsupportedSystem" align="center">
                 <label>&update.unsupported.start;</label><label id="unsupportedLink" class="text-link">&update.unsupported.linkText;</label><label>&update.unsupported.end;</label>
               </hbox>
+              <hbox id="restarting" align="center">
+                <label>&update.restarting;</label>
+              </hbox>
             </deck>
 #endif
           </vbox>
 
 #ifdef MOZ_UPDATER
           <description class="text-blurb" id="currentChannelText">
             &channel.description.start;<label id="currentChannel"/>&channel.description.end;
           </description>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3361,16 +3361,19 @@ var homeButtonObserver = {
     {
       // disallow setting home pages that inherit the principal
       let url = browserDragAndDrop.drop(aEvent, {}, true);
       setTimeout(openHomeDialog, 0, url);
     },
 
   onDragOver: function (aEvent)
     {
+      if (gPrefService.prefIsLocked("browser.startup.homepage")) {
+        return;
+      }
       browserDragAndDrop.dragOver(aEvent);
       aEvent.dropEffect = "link";
     },
   onDragExit: function (aEvent)
     {
     }
 }
 
--- a/browser/components/downloads/content/download.xml
+++ b/browser/components/downloads/content/download.xml
@@ -13,20 +13,22 @@
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="download"
            extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
     <content orient="horizontal"
              align="center"
              onclick="DownloadsView.onDownloadClick(event);">
-      <xul:image class="downloadTypeIcon"
-                 validate="always"
-                 xbl:inherits="src=image"/>
-      <xul:image class="downloadTypeIcon blockedIcon"/>
+      <xul:stack class="downloadStackIcon">
+        <xul:image class="downloadTypeIcon"
+                   validate="always"
+                   xbl:inherits="src=image"/>
+        <xul:image class="downloadTypeIcon blockedIcon" />
+      </xul:stack>
       <xul:vbox pack="center"
                 flex="1"
                 class="downloadContainer"
                 style="width: &downloadDetails.width;">
         <!-- We're letting localizers put a min-width in here primarily
              because of the downloads summary at the bottom of the list of
              download items. An element in the summary has the same min-width
              on a description, and we don't want the panel to change size if the
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -48,22 +48,16 @@ richlistitem.download[active] {
 richlistitem.download button {
   /* These buttons should never get focus, as that would "disable"
      the downloads view controller (it's only used when the richlistbox
      is focused). */
   -moz-user-focus: none;
 }
 
 /*** Visibility of controls inside download items ***/
-
-.download-state:-moz-any(     [state="6"], /* Blocked (parental) */
-                              [state="8"], /* Blocked (dirty)    */
-                              [state="9"]) /* Blocked (policy)   */
-                                           .downloadTypeIcon:not(.blockedIcon),
-
 .download-state:not(:-moz-any([state="6"], /* Blocked (parental) */
                               [state="8"], /* Blocked (dirty)    */
                               [state="9"]) /* Blocked (policy)   */)
                                            .downloadTypeIcon.blockedIcon,
 
 .download-state:not(:-moz-any([state="-1"],/* Starting (initial) */
                               [state="5"], /* Starting (queued)  */
                               [state="0"], /* Downloading        */
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -1,22 +1,28 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+                                  "resource://devtools/shared/event-emitter.js");
+
 XPCOMUtils.defineLazyGetter(this, "History", () => {
   Cu.import("resource://gre/modules/PlacesUtils.jsm");
   return PlacesUtils.history;
 });
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 const {
   normalizeTime,
+  SingletonEventManager,
 } = ExtensionUtils;
 
 let historySvc = Ci.nsINavHistoryService;
 const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
   ["link", historySvc.TRANSITION_LINK],
   ["typed", historySvc.TRANSITION_TYPED],
   ["auto_bookmark", historySvc.TRANSITION_BOOKMARK],
   ["auto_subframe", historySvc.TRANSITION_EMBED],
@@ -59,16 +65,44 @@ function convertNavHistoryContainerResul
   for (let i = 0; i < container.childCount; i++) {
     let node = container.getChild(i);
     results.push(convertNavHistoryResultNode(node));
   }
   container.containerOpen = false;
   return results;
 }
 
+var _observer;
+
+function getObserver() {
+  if (!_observer) {
+    _observer = {
+      onDeleteURI: function(uri, guid, reason) {
+        this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
+      },
+      onVisit: function() {},
+      onBeginUpdateBatch: function() {},
+      onEndUpdateBatch: function() {},
+      onTitleChanged: function() {},
+      onClearHistory: function() {
+        this.emit("visitRemoved", {allHistory: true});
+      },
+      onPageChanged: function() {},
+      onFrecencyChanged: function() {},
+      onManyFrecenciesChanged: function() {},
+      onDeleteVisits: function(uri, time, guid, reason) {
+        this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
+      },
+    };
+    EventEmitter.decorate(_observer);
+    PlacesUtils.history.addObserver(_observer, false);
+  }
+  return _observer;
+}
+
 extensions.registerSchemaAPI("history", "history", (extension, context) => {
   return {
     history: {
       addUrl: function(details) {
         let transition, date;
         try {
           transition = getTransitionType(details.transition);
         } catch (error) {
@@ -127,11 +161,22 @@ extensions.registerSchemaAPI("history", 
         let historyQuery = History.getNewQuery();
         historyQuery.searchTerms = query.text;
         historyQuery.beginTime = beginTime;
         historyQuery.endTime = endTime;
         let queryResult = History.executeQuery(historyQuery, options).root;
         let results = convertNavHistoryContainerResultNode(queryResult);
         return Promise.resolve(results);
       },
+
+      onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => {
+        let listener = (event, data) => {
+          context.runSafe(fire, data);
+        };
+
+        getObserver().on("visitRemoved", listener);
+        return () => {
+          getObserver().off("visitRemoved", listener);
+        };
+      }).api(),
     },
   };
 });
--- a/browser/components/extensions/schemas/history.json
+++ b/browser/components/extensions/schemas/history.json
@@ -299,17 +299,16 @@
           {
             "name": "result",
             "$ref": "HistoryItem"
           }
         ]
       },
       {
         "name": "onVisitRemoved",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when one or more URLs are removed from the history service.  When all visits have been removed the URL is purged from history.",
         "parameters": [
           {
             "name": "removed",
             "type": "object",
             "properties": {
               "allHistory": {
--- a/browser/components/extensions/test/browser/browser_ext_history.js
+++ b/browser/components/extensions/test/browser/browser_ext_history.js
@@ -6,31 +6,43 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://testing-common/PlacesTestUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
                                   "resource://gre/modules/ExtensionUtils.jsm");
 
 add_task(function* test_delete() {
   function background() {
+    let historyClearedCount = 0;
+    let removedUrls = [];
+
+    browser.history.onVisitRemoved.addListener(data => {
+      if (data.allHistory) {
+        historyClearedCount++;
+      } else {
+        browser.test.assertEq(1, data.urls.length, "onVisitRemoved received one URL");
+        removedUrls.push(data.urls[0]);
+      }
+    });
+
     browser.test.onMessage.addListener((msg, arg) => {
       if (msg === "delete-url") {
         browser.history.deleteUrl({url: arg}).then(result => {
           browser.test.assertEq(undefined, result, "browser.history.deleteUrl returns nothing");
           browser.test.sendMessage("url-deleted");
         });
       } else if (msg === "delete-range") {
         browser.history.deleteRange(arg).then(result => {
-          browser.test.assertEq(undefined, result, "browser.history.deleteUrl returns nothing");
-          browser.test.sendMessage("range-deleted");
+          browser.test.assertEq(undefined, result, "browser.history.deleteRange returns nothing");
+          browser.test.sendMessage("range-deleted", removedUrls);
         });
       } else if (msg === "delete-all") {
         browser.history.deleteAll().then(result => {
-          browser.test.assertEq(undefined, result, "browser.history.deleteUrl returns nothing");
-          browser.test.sendMessage("urls-deleted");
+          browser.test.assertEq(undefined, result, "browser.history.deleteAll returns nothing");
+          browser.test.sendMessage("history-cleared", [historyClearedCount, removedUrls]);
         });
       }
     });
 
     browser.test.sendMessage("ready");
   }
 
   const REFERENCE_DATE = new Date(1999, 9, 9, 9, 9);
@@ -38,19 +50,20 @@ add_task(function* test_delete() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["history"],
     },
     background: `(${background})()`,
   });
 
   yield extension.startup();
+  yield extension.awaitMessage("ready");
   yield PlacesTestUtils.clearHistory();
-  yield extension.awaitMessage("ready");
 
+  let historyClearedCount;
   let visits = [];
 
   // Add 5 visits for one uri and 3 visits for 3 others
   for (let i = 0; i < 8; ++i) {
     let baseUri = "http://mozilla.com/test_history/";
     let uri = (i > 4) ? `${baseUri}${i}/` : baseUri;
     let dbDate = PlacesUtils.toPRTime(Number(REFERENCE_DATE) + 3600 * 1000 * i);
 
@@ -58,33 +71,33 @@ add_task(function* test_delete() {
       uri,
       title: "visit " + i,
       visitDate: dbDate,
     };
     visits.push(visit);
   }
 
   yield PlacesTestUtils.addVisits(visits);
-
   is(yield PlacesTestUtils.visitsInDB(visits[0].uri), 5, "5 visits for uri found in history database");
 
   let testUrl = visits[6].uri.spec;
   ok(yield PlacesTestUtils.isPageInDB(testUrl), "expected url found in history database");
 
   extension.sendMessage("delete-url", testUrl);
   yield extension.awaitMessage("url-deleted");
   is(yield PlacesTestUtils.isPageInDB(testUrl), false, "expected url not found in history database");
 
   let filter = {
     startTime: PlacesUtils.toDate(visits[1].visitDate),
     endTime: PlacesUtils.toDate(visits[3].visitDate),
   };
 
   extension.sendMessage("delete-range", filter);
-  yield extension.awaitMessage("range-deleted");
+  let removedUrls = yield extension.awaitMessage("range-deleted");
+  ok(!removedUrls.includes(visits[0].uri.spec), `${visits[0].uri.spec} received by onVisitRemoved`);
 
   ok(yield PlacesTestUtils.isPageInDB(visits[0].uri), "expected uri found in history database");
   is(yield PlacesTestUtils.visitsInDB(visits[0].uri), 2, "2 visits for uri found in history database");
   ok(yield PlacesTestUtils.isPageInDB(visits[5].uri), "expected uri found in history database");
   is(yield PlacesTestUtils.visitsInDB(visits[5].uri), 1, "1 visit for uri found in history database");
 
   filter.startTime = PlacesUtils.toDate(visits[0].visitDate);
   filter.endTime = PlacesUtils.toDate(visits[5].visitDate);
@@ -95,19 +108,24 @@ add_task(function* test_delete() {
   is(yield PlacesTestUtils.isPageInDB(visits[0].uri), false, "expected uri not found in history database");
   is(yield PlacesTestUtils.visitsInDB(visits[0].uri), 0, "0 visits for uri found in history database");
   is(yield PlacesTestUtils.isPageInDB(visits[5].uri), false, "expected uri not found in history database");
   is(yield PlacesTestUtils.visitsInDB(visits[5].uri), 0, "0 visits for uri found in history database");
 
   ok(yield PlacesTestUtils.isPageInDB(visits[7].uri), "expected uri found in history database");
 
   extension.sendMessage("delete-all");
-  yield extension.awaitMessage("urls-deleted");
+  [historyClearedCount, removedUrls] = yield extension.awaitMessage("history-cleared");
   is(PlacesUtils.history.hasHistoryEntries, false, "history is empty");
-
+  is(historyClearedCount, 2, "onVisitRemoved called for each clearing of history");
+  is(removedUrls.length, 3, "onVisitRemoved called the expected number of times");
+  for (let index of [0, 5, 6]) {
+    let url = visits[index].uri.spec;
+    ok(removedUrls.includes(url), `${url} received by onVisitRemoved`);
+  }
   yield extension.unload();
 });
 
 add_task(function* test_search() {
   const SINGLE_VISIT_URL = "http://example.com/";
   const DOUBLE_VISIT_URL = "http://example.com/2/";
   const MOZILLA_VISIT_URL = "http://mozilla.com/";
 
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -705,16 +705,28 @@ nsDefaultCommandLineHandler.prototype = 
 
     return this;
   },
 
   _haveProfile: false,
 
   /* nsICommandLineHandler */
   handle : function dch_handle(cmdLine) {
+    // The -url flag is inserted by the operating system when the default
+    // application handler is used. We check for default browser to remove
+    // instances where users explicitly decide to "open with" the browser.
+    // Note that users who launch firefox manually with the -url flag will
+    // get erroneously counted.
+    if (cmdLine.findFlag("url", false) &&
+        ShellService.isDefaultBrowser(false, false)) {
+      try {
+        Services.telemetry.getHistogramById("FX_STARTUP_EXTERNAL_CONTENT_HANDLER").add();
+      } catch (e) {}
+    }
+
     var urilist = [];
 
     if (AppConstants.platform == "win") {
       // If we don't have a profile selected yet (e.g. the Profile Manager is
       // displayed) we will crash if we open an url and then select a profile. To
       // prevent this handle all url command line flags and set the command line's
       // preventDefault to true to prevent the display of the ui. The initial
       // command line will be retained when nsAppRunner calls LaunchChild though
--- a/browser/components/preferences/in-content/security.js
+++ b/browser/components/preferences/in-content/security.js
@@ -1,12 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
 Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 var gSecurityPane = {
   _pane: null,
 
   /**
    * Initializes master password UI.
    */
@@ -131,17 +134,17 @@ var gSecurityPane = {
   /**
    * Initializes master password UI: the "use master password" checkbox, selects
    * the master password button to show, and enables/disables it as necessary.
    * The master password is controlled by various bits of NSS functionality, so
    * the UI for it can't be controlled by the normal preference bindings.
    */
   _initMasterPasswordUI: function ()
   {
-    var noMP = !this._masterPasswordSet();
+    var noMP = !LoginHelper.isMasterPasswordSet();
 
     var button = document.getElementById("changeMasterPassword");
     button.disabled = noMP;
 
     var checkbox = document.getElementById("useMasterPassword");
     checkbox.checked = !noMP;
   },
 
@@ -214,35 +217,16 @@ var gSecurityPane = {
     if (!blockDownloadsPref.value) {
       blockUncommonUnwanted.setAttribute("disabled", "true");
     }
 
     blockUncommonUnwanted.checked = blockUnwantedPref.value && blockUncommonPref.value;
   },
 
   /**
-   * Returns true if the user has a master password set and false otherwise.
-   */
-  _masterPasswordSet: function ()
-  {
-    var secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].
-                   getService(Ci.nsIPKCS11ModuleDB);
-    var slot = secmodDB.findSlotByName("");
-    if (slot) {
-      var status = slot.status;
-      var hasMP = status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED &&
-                  status != Ci.nsIPKCS11Slot.SLOT_READY;
-      return hasMP;
-    } else {
-      // XXX I have no bloody idea what this means
-      return false;
-    }
-  },
-
-  /**
    * Enables/disables the master password button depending on the state of the
    * "use master password" checkbox, and prompts for master password removal if
    * one is set.
    */
   updateMasterPasswordButton: function ()
   {
     var checkbox = document.getElementById("useMasterPassword");
     var button = document.getElementById("changeMasterPassword");
--- a/browser/locales/en-US/chrome/browser/aboutDialog.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutDialog.dtd
@@ -60,16 +60,17 @@
 <!-- LOCALIZATION NOTE (update.checkingForUpdates): try to make the localized text short (see bug 596813 for screenshots). -->
 <!ENTITY update.checkingForUpdates  "Checking for updates…">
 <!-- LOCALIZATION NOTE (update.noUpdatesFound): try to make the localized text short (see bug 596813 for screenshots). -->
 <!ENTITY update.noUpdatesFound      "&brandShortName; is up to date">
 <!-- LOCALIZATION NOTE (update.adminDisabled): try to make the localized text short (see bug 596813 for screenshots). -->
 <!ENTITY update.adminDisabled       "Updates disabled by your system administrator">
 <!-- LOCALIZATION NOTE (update.otherInstanceHandlingUpdates): try to make the localized text short -->
 <!ENTITY update.otherInstanceHandlingUpdates "&brandShortName; is being updated by another instance">
+<!ENTITY update.restarting          "Restarting…">
 
 <!-- LOCALIZATION NOTE (update.failed.start,update.failed.linkText,update.failed.end):
      update.failed.start, update.failed.linkText, and update.failed.end all go into
      one line with linkText being wrapped in an anchor that links to a site to download
      the latest version of Firefox (e.g. http://www.firefox.com). As this is all in
      one line, try to make the localized text short (see bug 596813 for screenshots). -->
 <!ENTITY update.failed.start        "Update failed. ">
 <!ENTITY update.failed.linkText     "Download the latest version">
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1220,17 +1220,18 @@ html|span.ac-emphasize-text-url {
 
 /* Combined go/reload/stop button in location bar */
 
 #urlbar-go-button,
 #urlbar-reload-button,
 #urlbar-stop-button {
   -moz-appearance: none;
   list-style-image: url("chrome://browser/skin/reload-stop-go.png");
-  padding: 0 9px;
+  margin: -2px 0;
+  padding: 3px 9px;
   margin-inline-start: 5px;
   border-inline-start: 1px solid var(--urlbar-separator-color);
   border-image: linear-gradient(transparent 15%,
                                 var(--urlbar-separator-color) 15%,
                                 var(--urlbar-separator-color) 85%,
                                 transparent 85%);
   border-image-slice: 1;
 }
--- a/browser/themes/linux/downloads/allDownloadsViewOverlay.css
+++ b/browser/themes/linux/downloads/allDownloadsViewOverlay.css
@@ -5,28 +5,16 @@
 %include ../../shared/downloads/allDownloadsViewOverlay.inc.css
 
 /*** List items ***/
 
 :root {
   --downloads-item-height: 5em;
 }
 
-.blockedIcon {
-  list-style-image: url("moz-icon://stock/gtk-dialog-error?size=32");
-}
-
-@item@[verdict="PotentiallyUnwanted"] .blockedIcon {
-  list-style-image: url("moz-icon://stock/gtk-dialog-warning?size=32");
-}
-
-@item@[verdict="Uncommon"] .blockedIcon {
-  list-style-image: url("moz-icon://stock/gtk-dialog-info?size=32");
-}
-
 /*** Button icons ***/
 
 .downloadButton.downloadIconCancel {
   -moz-image-region: rect(0px, 16px, 16px, 0px);
 }
 @item@:hover .downloadButton.downloadIconCancel {
   -moz-image-region: rect(0px, 32px, 16px, 16px);
 }
--- a/browser/themes/linux/downloads/downloads.css
+++ b/browser/themes/linux/downloads/downloads.css
@@ -31,28 +31,16 @@
   --downloads-item-border-top-color: hsla(0,0%,100%,.2);
   --downloads-item-border-bottom-color: hsla(0,0%,0%,.15);
   --downloads-item-font-size-factor: 0.9;
   --downloads-item-target-margin-bottom: 7px;
   --downloads-item-details-margin-top: 1px;
   --downloads-item-details-opacity: 0.6;
 }
 
-.blockedIcon {
-  list-style-image: url("moz-icon://stock/gtk-dialog-error?size=32");
-}
-
-@item@[verdict="PotentiallyUnwanted"] .blockedIcon {
-  list-style-image: url("moz-icon://stock/gtk-dialog-warning?size=32");
-}
-
-@item@[verdict="Uncommon"] .blockedIcon {
-  list-style-image: url("moz-icon://stock/gtk-dialog-info?size=32");
-}
-
 .downloadButton:focus > .button-box {
   outline: 1px -moz-dialogtext dotted;
 }
 
 /*** Highlighted list items ***/
 
 @keyfocus@ @itemFocused@ {
   outline: 1px -moz-dialogtext dotted;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1829,19 +1829,19 @@ html|span.ac-emphasize-text-url {
 }
 
 
 /* ----- COMBINED GO/RELOAD/STOP BUTTON IN LOCATION BAR ----- */
 
 #urlbar-go-button,
 #urlbar-reload-button,
 #urlbar-stop-button {
-  margin: 0;
   list-style-image: url("chrome://browser/skin/reload-stop-go.png");
-  padding: 0 9px;
+  margin: -2px 0;
+  padding: 3px 9px;
   margin-inline-start: 5px;
   border-inline-start: 1px solid var(--urlbar-separator-color);
   border-image: linear-gradient(transparent 15%,
                                 var(--urlbar-separator-color) 15%,
                                 var(--urlbar-separator-color) 85%,
                                 transparent 85%);
   border-image-slice: 1;
 }
--- a/browser/themes/osx/downloads/allDownloadsViewOverlay.css
+++ b/browser/themes/osx/downloads/allDownloadsViewOverlay.css
@@ -5,24 +5,16 @@
 %include ../../shared/downloads/allDownloadsViewOverlay.inc.css
 
 /*** List items ***/
 
 :root {
   --downloads-item-height: 6em;
 }
 
-@item@[verdict="PotentiallyUnwanted"] .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/warning-32.png");
-}
-
-@item@[verdict="Uncommon"] .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/information-32.png");
-}
-
 /*** Button icons ***/
 
 .downloadButton.downloadIconCancel {
   -moz-image-region: rect(0px, 16px, 16px, 0px);
 }
 @item@:hover .downloadButton.downloadIconCancel {
   -moz-image-region: rect(0px, 32px, 16px, 16px);
 }
--- a/browser/themes/osx/downloads/downloads.css
+++ b/browser/themes/osx/downloads/downloads.css
@@ -42,24 +42,16 @@
   --downloads-item-details-opacity: 0.7;
 }
 
 .downloadButton:focus > .button-box {
   outline: 2px -moz-mac-focusring solid;
   outline-offset: -2px;
 }
 
-@item@[verdict="PotentiallyUnwanted"] .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/warning-32.png");
-}
-
-@item@[verdict="Uncommon"] .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/information-32.png");
-}
-
 /*** Highlighted list items ***/
 
 @keyfocus@ @itemFocused@,
 @notKeyfocus@ @itemFinished@[exists]:hover {
   border-radius: 3px;
   border-top: 1px solid hsla(0,0%,100%,.2);
   border-bottom: 1px solid hsla(0,0%,0%,.4);
   background-color: Highlight;
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/download-blocked.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+  <style>
+    circle {
+      fill: #D92215;
+    }
+
+    rect {
+      fill: #fff;
+    }
+  </style>
+
+  <circle cx="8" cy="8" r="8" />
+  <rect x="3" y="6" width="10" height="4" rx=".5" ry=".5" />
+</svg>
--- a/browser/themes/shared/downloads/allDownloadsViewOverlay.inc.css
+++ b/browser/themes/shared/downloads/allDownloadsViewOverlay.inc.css
@@ -26,32 +26,49 @@
 %endif
 #downloadsRichListBox > richlistitem.download {
   padding: 5px 8px;
 }
 %ifdef XP_WIN
 }
 %endif
 
+.downloadStackIcon {
+  --inline-offset: 8px;
+  --block-offset: 4px;
+  --icon-size: 32px;
+}
+
 .downloadTypeIcon {
   margin-inline-end: 8px;
-  width: 32px;
-  height: 32px;
+  width: calc(var(--icon-size) + var(--inline-offset));
+  height: calc(var(--icon-size) + var(--block-offset));
+  padding: var(--block-offset) var(--inline-offset) 0 0;
 }
 
 %ifdef XP_WIN
 @media not all and (-moz-os-version: windows-xp) {
   .downloadTypeIcon {
     margin-inline-start: 8px;
   }
 }
 %endif
 
 .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/Error.png");
+  --overlay-image-dimensions: top right / 16px no-repeat;
+  padding: 0;
+  background: url("chrome://browser/skin/download-blocked.svg") var(--overlay-image-dimensions);
+}
+
+@item@[verdict="PotentiallyUnwanted"] .blockedIcon {
+  background: url("chrome://browser/skin/warning.svg") var(--overlay-image-dimensions);
+}
+
+@item@[verdict="Uncommon"] .blockedIcon {
+  background: url("chrome://browser/skin/info.svg") var(--overlay-image-dimensions);
 }
 
 .downloadTarget {
   margin-bottom: 3px;
   cursor: inherit;
 }
 
 .downloadDetails {
--- a/browser/themes/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -87,25 +87,42 @@ richlistitem[type="download"] {
 richlistitem[type="download"]:first-child {
   border-top: 1px solid transparent;
 }
 
 richlistitem[type="download"]:last-child {
   border-bottom: 1px solid transparent;
 }
 
+.downloadStackIcon {
+  --inline-offset: 8px;
+  --block-offset: 4px;
+  --icon-size: 32px;
+}
+
 .downloadTypeIcon {
   margin-inline-end: 8px;
   /* Prevent flickering when changing states. */
-  height: 32px;
-  width: 32px;
+  width: calc(var(--icon-size) + var(--inline-offset));
+  height: calc(var(--icon-size) + var(--block-offset));
+  padding: var(--block-offset) var(--inline-offset) 0 0;
 }
 
 .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/Error.png");
+  --overlay-image-dimensions: top right / 16px no-repeat;
+  padding: 0;
+  background: url("chrome://browser/skin/download-blocked.svg") var(--overlay-image-dimensions);
+}
+
+@item@[verdict="PotentiallyUnwanted"] .blockedIcon {
+  background: url("chrome://browser/skin/warning.svg") var(--overlay-image-dimensions);
+}
+
+@item@[verdict="Uncommon"] .blockedIcon {
+  background: url("chrome://browser/skin/info.svg") var(--overlay-image-dimensions);
 }
 
 /* We hold .downloadTarget, .downloadProgress and .downloadDetails inside of
    a vbox with class .downloadContainer. We set the font-size of the entire
    container to --downloads-item-font-size-factor because:
 
    1) This is the size that we want .downloadDetails to be
    2) The container's width is set by localizers by &downloadDetails.width;,
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -46,16 +46,17 @@
   skin/classic/browser/customizableui/panelarrow-customizeTip.png  (../shared/customizableui/panelarrow-customizeTip.png)
   skin/classic/browser/customizableui/panelarrow-customizeTip@2x.png  (../shared/customizableui/panelarrow-customizeTip@2x.png)
   skin/classic/browser/customizableui/subView-arrow-back-inverted.png  (../shared/customizableui/subView-arrow-back-inverted.png)
   skin/classic/browser/customizableui/subView-arrow-back-inverted@2x.png  (../shared/customizableui/subView-arrow-back-inverted@2x.png)
   skin/classic/browser/customizableui/subView-arrow-back-inverted-rtl.png  (../shared/customizableui/subView-arrow-back-inverted-rtl.png)
   skin/classic/browser/customizableui/subView-arrow-back-inverted-rtl@2x.png  (../shared/customizableui/subView-arrow-back-inverted-rtl@2x.png)
   skin/classic/browser/customizableui/whimsy.png               (../shared/customizableui/whimsy.png)
   skin/classic/browser/customizableui/whimsy@2x.png            (../shared/customizableui/whimsy@2x.png)
+  skin/classic/browser/download-blocked.svg                    (../shared/download-blocked.svg)
   skin/classic/browser/downloads/contentAreaDownloadsView.css  (../shared/downloads/contentAreaDownloadsView.css)
   skin/classic/browser/drm-icon.svg                            (../shared/drm-icon.svg)
   skin/classic/browser/fullscreen/insecure.svg                 (../shared/fullscreen/insecure.svg)
   skin/classic/browser/fullscreen/secure.svg                   (../shared/fullscreen/secure.svg)
   skin/classic/browser/heartbeat-icon.svg                      (../shared/heartbeat-icon.svg)
   skin/classic/browser/heartbeat-star-lit.svg                  (../shared/heartbeat-star-lit.svg)
   skin/classic/browser/heartbeat-star-off.svg                  (../shared/heartbeat-star-off.svg)
   skin/classic/browser/identity-icon.svg                       (../shared/identity-block/identity-icon.svg)
--- a/browser/themes/shared/syncedtabs/sidebar.inc.css
+++ b/browser/themes/shared/syncedtabs/sidebar.inc.css
@@ -84,34 +84,32 @@ body {
 .client .item.tab > .item-title-container {
   padding-inline-start: 35px;
 }
 
 .item.tab > .item-title-container {
   padding-inline-start: 20px;
 }
 
-.item.client.device-image-desktop.selected > .item-title-container > .item-icon-container {
+.item.client.device-image-desktop > .item-title-container > .item-icon-container {
+  background-image: url("chrome://browser/skin/sync-desktopIcon.svg#icon");
+}
+
+.item.client.device-image-desktop.selected:focus > .item-title-container > .item-icon-container {
   background-image: url("chrome://browser/skin/sync-desktopIcon.svg#icon-inverted");
 }
 
-.item.client.device-image-desktop:not(.selected) > .item-title-container > .item-icon-container,
-.item.client.device-image-desktop.selected > .item-title-container > .item-icon-container:-moz-window-inactive {
-  background-image: url("chrome://browser/skin/sync-desktopIcon.svg#icon");
+.item.client.device-image-mobile > .item-title-container > .item-icon-container {
+  background-image: url("chrome://browser/skin/sync-mobileIcon.svg#icon");
 }
 
-.item.client.device-image-mobile.selected > .item-title-container > .item-icon-container {
+.item.client.device-image-mobile.selected:focus > .item-title-container > .item-icon-container {
   background-image: url("chrome://browser/skin/sync-mobileIcon.svg#icon-inverted");
 }
 
-.item.client.device-image-mobile:not(.selected) > .item-title-container > .item-icon-container,
-.item.client.device-image-mobile.selected > .item-title-container > .item-icon-container:-moz-window-inactive {
-  background-image: url("chrome://browser/skin/sync-mobileIcon.svg#icon");
-}
-
 .item.tab > .item-title-container > .item-icon-container {
   background-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
 }
 
 @media (min-resolution: 1.1dppx) {
 .item.tab > .item-title-container > .item-icon-container {
     background-image: url("chrome://mozapps/skin/places/defaultFavicon@2x.png");
   }
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1584,17 +1584,18 @@ html|span.ac-emphasize-text-url {
 /* combined go/reload/stop button in location bar */
 
 #urlbar-go-button,
 #urlbar-reload-button,
 #urlbar-stop-button {
   -moz-appearance: none;
   border-style: none;
   list-style-image: url("chrome://browser/skin/reload-stop-go.png");
-  padding: 0 9px;
+  margin: -1px 0;
+  padding: 3px 9px;
   margin-inline-start: 5px;
   border-inline-start: 1px solid var(--urlbar-separator-color);
   border-image: linear-gradient(transparent 15%,
                                 var(--urlbar-separator-color) 15%,
                                 var(--urlbar-separator-color) 85%,
                                 transparent 85%);
   border-image-slice: 1;
 }
--- a/browser/themes/windows/downloads/allDownloadsViewOverlay.css
+++ b/browser/themes/windows/downloads/allDownloadsViewOverlay.css
@@ -5,24 +5,16 @@
 %include ../../shared/downloads/allDownloadsViewOverlay.inc.css
 
 /*** List items ***/
 
 :root {
   --downloads-item-height: 6em;
 }
 
-@item@[verdict="PotentiallyUnwanted"] .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/Warning.png");
-}
-
-@item@[verdict="Uncommon"] .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/information-32.png");
-}
-
 /*** Highlighted list items ***/
 
 @media not all and (-moz-os-version: windows-xp) {
   @media (-moz-windows-default-theme) {
     /*
     -moz-appearance: menuitem is almost right, but the hover effect is not
     transparent and is lighter than desired.
 
--- a/browser/themes/windows/downloads/downloads.css
+++ b/browser/themes/windows/downloads/downloads.css
@@ -91,24 +91,16 @@
 .downloadButton > .button-box {
   border: 1px solid transparent;
 }
 
 @keyfocus@ .downloadButton:focus > .button-box {
   border: 1px dotted ThreeDDarkShadow;
 }
 
-@item@[verdict="PotentiallyUnwanted"] .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/Warning.png");
-}
-
-@item@[verdict="Uncommon"] .blockedIcon {
-  list-style-image: url("chrome://global/skin/icons/information-32.png");
-}
-
 /*** Highlighted list items ***/
 
 @keyfocus@ @itemFocused@ {
   outline: 1px -moz-dialogtext dotted;
   outline-offset: -1px;
 }
 
 @notKeyfocus@ @itemFinished@[exists]:hover {
--- a/devtools/client/commandline/test/browser_cmd_csscoverage_oneshot.js
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_oneshot.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the addon commands works as they should
 
-const csscoverage = require("devtools/server/actors/csscoverage");
+const csscoverage = require("devtools/shared/fronts/csscoverage");
 
 const PAGE_1 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page1.html";
 const PAGE_2 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page2.html";
 const PAGE_3 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page3.html";
 
 const SHEET_A = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetA.css";
 const SHEET_B = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetB.css";
 const SHEET_C = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetC.css";
@@ -45,17 +45,17 @@ function* navigate(usage, options) {
   ok(!usage.isRunning(), "csscoverage is still not running");
 }
 
 /**
  * Check the expected pages have been visited
  */
 function* checkPages(usage) {
   let expectedVisited = [ PAGE_3 ];
-  let actualVisited = yield usage._testOnly_visitedPages();
+  let actualVisited = yield usage._testOnlyVisitedPages();
   isEqualJson(actualVisited, expectedVisited, "Visited");
 }
 
 /**
  * Check that createEditorReport returns the expected JSON
  */
 function* checkEditorReport(usage) {
   // Page1
--- a/devtools/client/commandline/test/browser_cmd_csscoverage_startstop.js
+++ b/devtools/client/commandline/test/browser_cmd_csscoverage_startstop.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the addon commands works as they should
 
-const csscoverage = require("devtools/server/actors/csscoverage");
+const csscoverage = require("devtools/shared/fronts/csscoverage");
 
 const PAGE_1 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page1.html";
 const PAGE_2 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page2.html";
 const PAGE_3 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page3.html";
 
 const SHEET_A = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetA.css";
 const SHEET_B = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetB.css";
 const SHEET_C = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetC.css";
@@ -59,17 +59,17 @@ function* navigate(usage, options) {
 }
 
 /**
  * Check the expected pages have been visited
  */
 function* checkPages(usage) {
   // 'load' event order. '' is for the initial location
   let expectedVisited = [ "", PAGE_2, PAGE_1, PAGE_3 ];
-  let actualVisited = yield usage._testOnly_visitedPages();
+  let actualVisited = yield usage._testOnlyVisitedPages();
   isEqualJson(actualVisited, expectedVisited, "Visited");
 }
 
 /**
  * Check that createEditorReport returns the expected JSON
  */
 function* checkEditorReport(usage) {
   // Page1
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -472,21 +472,16 @@ CssRuleView.prototype = {
         let end = Math.max(target.selectionStart, target.selectionEnd);
         let count = end - start;
         text = target.value.substr(start, count);
       } else {
         text = this.styleWindow.getSelection().toString();
 
         // Remove any double newlines.
         text = text.replace(/(\r?\n)\r?\n/g, "$1");
-
-        // Remove "inline"
-        let inline = _strings.GetStringFromName("rule.sourceInline");
-        let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
-        text = text.replace(rx, "");
       }
 
       clipboardHelper.copyString(text);
     } catch (e) {
       console.error(e);
     }
   },
 
--- a/devtools/client/netmonitor/test/browser_net_statistics-01.js
+++ b/devtools/client/netmonitor/test/browser_net_statistics-01.js
@@ -1,72 +1,75 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 /**
  * Tests if the statistics view is populated correctly.
  */
 
-function test() {
-  initNetMonitor(STATISTICS_URL).then(([aTab, aDebuggee, aMonitor]) => {
-    info("Starting test... ");
+add_task(function* () {
+  let [,, monitor] = yield initNetMonitor(STATISTICS_URL);
+  info("Starting test... ");
 
-    let panel = aMonitor.panelWin;
-    let { document, $, $all, EVENTS, NetMonitorView } = panel;
-
-    is(NetMonitorView.currentFrontendMode, "network-inspector-view",
-      "The initial frontend mode is correct.");
+  let panel = monitor.panelWin;
+  let { $, $all, EVENTS, NetMonitorView } = panel;
+  is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+    "The initial frontend mode is correct.");
 
-    is($("#primed-cache-chart").childNodes.length, 0,
-      "There should be no primed cache chart created yet.");
-    is($("#empty-cache-chart").childNodes.length, 0,
-      "There should be no empty cache chart created yet.");
+  is($("#primed-cache-chart").childNodes.length, 0,
+    "There should be no primed cache chart created yet.");
+  is($("#empty-cache-chart").childNodes.length, 0,
+    "There should be no empty cache chart created yet.");
 
-    waitFor(panel, EVENTS.PLACEHOLDER_CHARTS_DISPLAYED).then(() => {
-      is($("#primed-cache-chart").childNodes.length, 1,
-        "There should be a placeholder primed cache chart created now.");
-      is($("#empty-cache-chart").childNodes.length, 1,
-        "There should be a placeholder empty cache chart created now.");
+  let onChartDisplayed = Promise.all([
+    waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+    waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+  ]);
+  let onPlaceholderDisplayed = waitFor(panel, EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
 
-      is($all(".pie-chart-container[placeholder=true]").length, 2,
-        "Two placeholder pie chart appear to be rendered correctly.");
-      is($all(".table-chart-container[placeholder=true]").length, 2,
-        "Two placeholder table chart appear to be rendered correctly.");
+  info("Displaying statistics view");
+  NetMonitorView.toggleFrontendMode();
+  is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+    "The current frontend mode is correct.");
 
-      promise.all([
-        waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
-        waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
-      ]).then(() => {
-        is($("#primed-cache-chart").childNodes.length, 1,
-          "There should be a real primed cache chart created now.");
-        is($("#empty-cache-chart").childNodes.length, 1,
-          "There should be a real empty cache chart created now.");
+  info("Waiting for placeholder to display");
+  yield onPlaceholderDisplayed;
+  is($("#primed-cache-chart").childNodes.length, 1,
+    "There should be a placeholder primed cache chart created now.");
+  is($("#empty-cache-chart").childNodes.length, 1,
+    "There should be a placeholder empty cache chart created now.");
 
-        Task.spawn(function* () {
-          yield until(() => $all(".pie-chart-container:not([placeholder=true])").length == 2);
-          ok(true, "Two real pie charts appear to be rendered correctly.");
+  is($all(".pie-chart-container[placeholder=true]").length, 2,
+    "Two placeholder pie chart appear to be rendered correctly.");
+  is($all(".table-chart-container[placeholder=true]").length, 2,
+    "Two placeholder table chart appear to be rendered correctly.");
 
-          yield until(() => $all(".table-chart-container:not([placeholder=true])").length == 2);
-          ok(true, "Two real table charts appear to be rendered correctly.");
+  info("Waiting for chart to display");
+  yield onChartDisplayed;
+  is($("#primed-cache-chart").childNodes.length, 1,
+    "There should be a real primed cache chart created now.");
+  is($("#empty-cache-chart").childNodes.length, 1,
+    "There should be a real empty cache chart created now.");
 
-          teardown(aMonitor).then(finish);
-        });
-      });
-    });
+  yield until(() => $all(".pie-chart-container:not([placeholder=true])").length == 2);
+  ok(true, "Two real pie charts appear to be rendered correctly.");
 
-    NetMonitorView.toggleFrontendMode();
+  yield until(() => $all(".table-chart-container:not([placeholder=true])").length == 2);
+  ok(true, "Two real table charts appear to be rendered correctly.");
 
-    is(NetMonitorView.currentFrontendMode, "network-statistics-view",
-      "The current frontend mode is correct.");
-  });
-}
+  yield teardown(monitor);
+});
 
 function waitForTick() {
   let deferred = promise.defer();
   executeSoon(deferred.resolve);
   return deferred.promise;
 }
 
 function until(predicate) {
   return Task.spawn(function* () {
-    while (!predicate()) yield waitForTick();
+    while (!predicate()) {
+      yield waitForTick();
+    }
   });
 }
--- a/devtools/client/netmonitor/test/browser_net_statistics-02.js
+++ b/devtools/client/netmonitor/test/browser_net_statistics-02.js
@@ -1,43 +1,42 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 /**
  * Tests if the network inspector view is shown when the target navigates
  * away while in the statistics view.
  */
 
-function test() {
-  initNetMonitor(STATISTICS_URL).then(([aTab, aDebuggee, aMonitor]) => {
-    info("Starting test... ");
+add_task(function* () {
+  let [, debuggee, monitor] = yield initNetMonitor(STATISTICS_URL);
+  info("Starting test... ");
 
-    let panel = aMonitor.panelWin;
-    let { document, EVENTS, NetMonitorView } = panel;
-
-    is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+  let panel = monitor.panelWin;
+  let { EVENTS, NetMonitorView } = panel;
+  is(NetMonitorView.currentFrontendMode, "network-inspector-view",
       "The initial frontend mode is correct.");
 
-    promise.all([
-      waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
-      waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
-    ]).then(() => {
-      is(NetMonitorView.currentFrontendMode, "network-statistics-view",
+  let onChartDisplayed = Promise.all([
+    waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
+    waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
+  ]);
+
+  info("Displaying statistics view");
+  NetMonitorView.toggleFrontendMode();
+  yield onChartDisplayed;
+  is(NetMonitorView.currentFrontendMode, "network-statistics-view",
         "The frontend mode is currently in the statistics view.");
 
-      waitFor(panel, EVENTS.TARGET_WILL_NAVIGATE).then(() => {
-        is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+  info("Reloading page");
+  let onWillNavigate = waitFor(panel, EVENTS.TARGET_WILL_NAVIGATE);
+  let onDidNavigate = waitFor(panel, EVENTS.TARGET_DID_NAVIGATE);
+  debuggee.location.reload();
+  yield onWillNavigate;
+  is(NetMonitorView.currentFrontendMode, "network-inspector-view",
           "The frontend mode switched back to the inspector view.");
-
-        waitFor(panel, EVENTS.TARGET_DID_NAVIGATE).then(() => {
-          is(NetMonitorView.currentFrontendMode, "network-inspector-view",
+  yield onDidNavigate;
+  is(NetMonitorView.currentFrontendMode, "network-inspector-view",
             "The frontend mode is still in the inspector view.");
-
-          teardown(aMonitor).then(finish);
-        });
-      });
-
-      aDebuggee.location.reload();
-    });
-
-    NetMonitorView.toggleFrontendMode();
-  });
-}
+  yield teardown(monitor);
+});
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -523,17 +523,21 @@ OutputParser.prototype = {
       // seemed simpler on the whole.
       let [, leader, , body, trailer] =
         /^(url\([ \t\r\n\f]*(["']?))(.*?)(\2[ \t\r\n\f]*\))$/i.exec(match);
 
       this._appendTextNode(leader);
 
       let href = url;
       if (options.baseURI) {
-        href = new URL(url, options.baseURI).href;
+        try {
+          href = new URL(url, options.baseURI).href;
+        } catch (e) {
+          // Ignore.
+        }
       }
 
       this._appendNode("a", {
         target: "_blank",
         class: options.urlClass,
         href: href
       }, body);
 
--- a/devtools/client/shared/test/browser_outputparser.js
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -1,15 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-var {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js",
-                         {});
 var {OutputParser} = require("devtools/client/shared/output-parser");
 
 add_task(function* () {
   yield addTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
@@ -211,33 +209,41 @@ function testParseURL(doc, parser) {
     },
     {
       desc: "bad url, missing paren",
       leader: "url(",
       trailer: "",
       expectedTrailer: ")"
     },
     {
+      desc: "bad url, missing paren, with baseURI",
+      baseURI: "data:text/html,<style></style>",
+      leader: "url(",
+      trailer: "",
+      expectedTrailer: ")"
+    },
+    {
       desc: "bad url, double quote, missing paren",
       leader: "url(\"",
       trailer: "\"",
       expectedTrailer: "\")",
     },
     {
       desc: "bad url, single quote, missing paren and quote",
       leader: "url('",
       trailer: "",
       expectedTrailer: "')"
     }
   ];
 
   for (let test of tests) {
     let url = test.leader + "something.jpg" + test.trailer;
     let frag = parser.parseCssProperty("background", url, {
-      urlClass: "test-urlclass"
+      urlClass: "test-urlclass",
+      baseURI: test.baseURI,
     });
 
     let target = doc.querySelector("div");
     target.appendChild(frag);
 
     let expectedTrailer = test.expectedTrailer || test.trailer;
 
     let expected = test.leader +
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -19,17 +19,17 @@ const EventEmitter = require("devtools/s
 const {gDevTools} = require("devtools/client/framework/devtools");
 /* import-globals-from StyleEditorUtil.jsm */
 Cu.import("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
 const {SplitView} = Cu.import("resource://devtools/client/shared/SplitView.jsm", {});
 const {StyleSheetEditor} = Cu.import("resource://devtools/client/styleeditor/StyleSheetEditor.jsm");
 loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
 const {PrefObserver, PREF_ORIG_SOURCES} =
       require("devtools/client/styleeditor/utils");
-const csscoverage = require("devtools/server/actors/csscoverage");
+const csscoverage = require("devtools/shared/fronts/csscoverage");
 const {console} = require("resource://gre/modules/Console.jsm");
 const promise = require("promise");
 const {ResponsiveUIManager} =
   Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
 
 const LOAD_ERROR = "error-load";
 const STYLE_EDITOR_TEMPLATE = "stylesheet";
 const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
--- a/devtools/server/actors/breakpoint.js
+++ b/devtools/server/actors/breakpoint.js
@@ -1,17 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { ActorClass, method } = require("devtools/shared/protocol");
+const { ActorClassWithSpec } = require("devtools/shared/protocol");
+const { breakpointSpec } = require("devtools/shared/specs/breakpoint");
 
 /**
  * Set breakpoints on all the given entry points with the given
  * BreakpointActor as the handler.
  *
  * @param BreakpointActor actor
  *        The actor handling the breakpoint hits.
  * @param Array entryPoints
@@ -28,19 +29,17 @@ function setBreakpointAtEntryPoints(acto
 
 exports.setBreakpointAtEntryPoints = setBreakpointAtEntryPoints;
 
 /**
  * BreakpointActors exist for the lifetime of their containing thread and are
  * responsible for deleting breakpoints, handling breakpoint hits and
  * associating breakpoints with scripts.
  */
-let BreakpointActor = ActorClass({
-  typeName: "breakpoint",
-
+let BreakpointActor = ActorClassWithSpec(breakpointSpec, {
   /**
    * Create a Breakpoint actor.
    *
    * @param ThreadActor threadActor
    *        The parent thread actor that contains this breakpoint.
    * @param OriginalLocation originalLocation
    *        The original location of the breakpoint.
    */
@@ -171,20 +170,20 @@ let BreakpointActor = ActorClass({
       }
     }
     return this.threadActor._pauseAndRespond(frame, reason);
   },
 
   /**
    * Handle a protocol request to remove this breakpoint.
    */
-  delete: method(function () {
+  delete: function () {
     // Remove from the breakpoint store.
     if (this.originalLocation) {
       this.threadActor.breakpointActorMap.deleteActor(this.originalLocation);
     }
     this.threadActor.threadLifetimePool.removeActor(this);
     // Remove the actual breakpoint from the associated scripts.
     this.removeScripts();
-  })
+  }
 });
 
 exports.BreakpointActor = BreakpointActor;
--- a/devtools/server/actors/csscoverage.js
+++ b/devtools/server/actors/csscoverage.js
@@ -6,24 +6,24 @@
 
 const { Cc, Ci, Cu } = require("chrome");
 
 const Services = require("Services");
 const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 
 const events = require("sdk/event/core");
 const protocol = require("devtools/shared/protocol");
-const { method, custom, RetVal, Arg } = protocol;
+const { custom } = protocol;
+const { cssUsageSpec } = require("devtools/shared/specs/csscoverage");
 
 loader.lazyGetter(this, "DOMUtils", () => {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 loader.lazyRequireGetter(this, "stylesheets", "devtools/server/actors/stylesheets");
 loader.lazyRequireGetter(this, "CssLogic", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
 
 const CSSRule = Ci.nsIDOMCSSRule;
 
 const MAX_UNUSED_RULES = 10000;
 
 /**
  * Allow: let foo = l10n.lookup("csscoverageFoo");
  */
@@ -63,26 +63,17 @@ const l10n = exports.l10n = {
  *         cssText: "p.quote { color: red; }",
  *         isUsed: true,
  *         presentOn: Set([ "http://eg.com/page1.html", ... ]),
  *         preLoadOn: Set([ "http://eg.com/page1.html" ]),
  *         isError: false,
  *       }, ...
  *     });
  */
-var CSSUsageActor = protocol.ActorClass({
-  typeName: "cssUsage",
-
-  events: {
-    "state-change" : {
-      type: "stateChange",
-      stateChange: Arg(0, "json")
-    }
-  },
-
+var CSSUsageActor = protocol.ActorClassWithSpec(cssUsageSpec, {
   initialize: function (conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
 
     this._tabActor = tabActor;
     this._running = false;
 
     this._onTabLoad = this._onTabLoad.bind(this);
     this._onChange = this._onChange.bind(this);
@@ -102,17 +93,17 @@ var CSSUsageActor = protocol.ActorClass(
 
   /**
    * Begin recording usage data
    * @param noreload It's best if we start by reloading the current page
    * because that starts the test at a known point, but there could be reasons
    * why we don't want to do that (e.g. the page contains state that will be
    * lost across a reload)
    */
-  start: method(function (noreload) {
+  start: function (noreload) {
     if (this._running) {
       throw new Error(l10n.lookup("csscoverageRunningError"));
     }
 
     this._isOneShot = false;
     this._visitedPages = new Set();
     this._knownRules = new Map();
     this._running = true;
@@ -147,58 +138,56 @@ var CSSUsageActor = protocol.ActorClass(
       // has just happened.
       this._onTabLoad(this._tabActor.window.document);
     }
     else {
       this._tabActor.window.location.reload();
     }
 
     events.emit(this, "state-change", { isRunning: true });
-  }, {
-    request: { url: Arg(0, "boolean") }
-  }),
+  },
 
   /**
    * Cease recording usage data
    */
-  stop: method(function () {
+  stop: function () {
     if (!this._running) {
       throw new Error(l10n.lookup("csscoverageNotRunningError"));
     }
 
     this._progress.removeProgressListener(this._progressListener, this._notifyOn);
     this._progress = undefined;
 
     this._running = false;
     events.emit(this, "state-change", { isRunning: false });
-  }),
+  },
 
   /**
    * Start/stop recording usage data depending on what we're currently doing.
    */
-  toggle: method(function () {
+  toggle: function () {
     return this._running ? this.stop() : this.start();
-  }),
+  },
 
   /**
    * Running start() quickly followed by stop() does a bunch of unnecessary
    * work, so this cuts all that out
    */
-  oneshot: method(function () {
+  oneshot: function () {
     if (this._running) {
       throw new Error(l10n.lookup("csscoverageRunningError"));
     }
 
     this._isOneShot = true;
     this._visitedPages = new Set();
     this._knownRules = new Map();
 
     this._populateKnownRules(this._tabActor.window.document);
     this._updateUsage(this._tabActor.window.document, false);
-  }),
+  },
 
   /**
    * Called by the ProgressListener to simulate a "load" event
    */
   _onTabLoad: function (document) {
     this._populateKnownRules(document);
     this._updateUsage(document, true);
 
@@ -321,17 +310,17 @@ var CSSUsageActor = protocol.ActorClass(
    *     {
    *       selectorText: "p#content",
    *       usage: "unused|used",
    *       start: { line: 3, column: 0 },
    *     },
    *     ...
    *   ]
    */
-  createEditorReport: method(function (url) {
+  createEditorReport: function (url) {
     if (this._knownRules == null) {
       return { reports: [] };
     }
 
     let reports = [];
     for (let [ruleId, ruleData] of this._knownRules) {
       let { url: ruleUrl, line, column } = deconstructRuleId(ruleId);
       if (ruleUrl !== url || ruleData.isUsed) {
@@ -346,20 +335,17 @@ var CSSUsageActor = protocol.ActorClass(
       if (ruleData.end) {
         ruleReport.end = ruleData.end;
       }
 
       reports.push(ruleReport);
     }
 
     return { reports: reports };
-  }, {
-    request: { url: Arg(0, "string") },
-    response: { reports: RetVal("array:json") }
-  }),
+  },
 
   /**
    * Returns a JSONable structure designed for the page report which shows
    * the recommended changes to a page.
    *
    * "preload" means that a rule is used before the load event happens, which
    * means that the page could by optimized by placing it in a <style> element
    * at the top of the page, moving the <link> elements to the bottom.
@@ -386,17 +372,17 @@ var CSSUsageActor = protocol.ActorClass(
    *       {
    *         url: "http://example.org/style1.css",
    *         shortUrl: "style1.css",
    *         rules: [ ... ]
    *       }
    *     ]
    *   }
    */
-  createPageReport: method(function () {
+  createPageReport: function () {
     if (this._running) {
       throw new Error(l10n.lookup("csscoverageRunningError"));
     }
 
     if (this._visitedPages == null) {
       throw new Error(l10n.lookup("csscoverageNotRunError"));
     }
 
@@ -470,28 +456,24 @@ var CSSUsageActor = protocol.ActorClass(
       }
     }
 
     return {
       summary: summary,
       preload: preload,
       unused: unused
     };
-  }, {
-    response: RetVal("json")
-  }),
+  },
 
   /**
    * For testing only. What pages did we visit.
    */
-  _testOnly_visitedPages: method(function () {
+  _testOnlyVisitedPages: function () {
     return [...this._visitedPages];
-  }, {
-    response: { value: RetVal("array:string") }
-  }),
+  },
 });
 
 exports.CSSUsageActor = CSSUsageActor;
 
 /**
  * Generator that filters the CSSRules out of _getAllRules so it only
  * iterates over the CSSStyleRules
  */
@@ -727,114 +709,8 @@ const sheetToUrl = exports.sheetToUrl = 
     let document = stylesheet.ownerNode.ownerDocument;
     let sheets = [...document.querySelectorAll("style")];
     let index = sheets.indexOf(stylesheet.ownerNode);
     return getURL(document) + " → <style> index " + index;
   }
 
   throw new Error("Unknown sheet source");
 };
-
-/**
- * Running more than one usage report at a time is probably bad for performance
- * and it isn't particularly useful, and it's confusing from a notification POV
- * so we only allow one.
- */
-var isRunning = false;
-var notification;
-var target;
-var chromeWindow;
-
-/**
- * Front for CSSUsageActor
- */
-const CSSUsageFront = protocol.FrontClass(CSSUsageActor, {
-  initialize: function (client, form) {
-    protocol.Front.prototype.initialize.call(this, client, form);
-    this.actorID = form.cssUsageActor;
-    this.manage(this);
-  },
-
-  _onStateChange: protocol.preEvent("state-change", function (ev) {
-    isRunning = ev.isRunning;
-    ev.target = target;
-
-    if (isRunning) {
-      let gnb = chromeWindow.document.getElementById("global-notificationbox");
-      notification = gnb.getNotificationWithValue("csscoverage-running");
-
-      if (notification == null) {
-        let notifyStop = reason => {
-          if (reason == "removed") {
-            this.stop();
-          }
-        };
-
-        let msg = l10n.lookup("csscoverageRunningReply");
-        notification = gnb.appendNotification(msg, "csscoverage-running",
-                                              "", // i.e. no image
-                                              gnb.PRIORITY_INFO_HIGH,
-                                              null, // i.e. no buttons
-                                              notifyStop);
-      }
-    }
-    else {
-      if (notification) {
-        notification.remove();
-        notification = undefined;
-      }
-
-      gDevTools.showToolbox(target, "styleeditor");
-      target = undefined;
-    }
-  }),
-
-  /**
-   * Server-side start is above. Client-side start adds a notification box
-   */
-  start: custom(function (newChromeWindow, newTarget, noreload = false) {
-    target = newTarget;
-    chromeWindow = newChromeWindow;
-
-    return this._start(noreload);
-  }, {
-    impl: "_start"
-  }),
-
-  /**
-   * Server-side start is above. Client-side start adds a notification box
-   */
-  toggle: custom(function (newChromeWindow, newTarget) {
-    target = newTarget;
-    chromeWindow = newChromeWindow;
-
-    return this._toggle();
-  }, {
-    impl: "_toggle"
-  }),
-
-  /**
-   * We count STARTING and STOPPING as 'running'
-   */
-  isRunning: function () {
-    return isRunning;
-  }
-});
-
-exports.CSSUsageFront = CSSUsageFront;
-
-const knownFronts = new WeakMap();
-
-/**
- * Create a CSSUsageFront only when needed (returns a promise)
- * For notes on target.makeRemote(), see
- * https://bugzilla.mozilla.org/show_bug.cgi?id=1016330#c7
- */
-const getUsage = exports.getUsage = function (target) {
-  return target.makeRemote().then(() => {
-    let front = knownFronts.get(target.client);
-    if (front == null && target.form.cssUsageActor != null) {
-      front = new CSSUsageFront(target.client, target.form);
-      knownFronts.set(target.client, front);
-    }
-    return front;
-  });
-};
--- a/devtools/server/actors/environment.js
+++ b/devtools/server/actors/environment.js
@@ -1,31 +1,30 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const { ActorClass, Arg, RetVal, method } = require("devtools/shared/protocol");
+const { ActorClassWithSpec } = require("devtools/shared/protocol");
 const { createValueGrip } = require("devtools/server/actors/object");
+const { environmentSpec } = require("devtools/shared/specs/environment");
 
 /**
  * Creates an EnvironmentActor. EnvironmentActors are responsible for listing
  * the bindings introduced by a lexical environment and assigning new values to
  * those identifier bindings.
  *
  * @param Debugger.Environment aEnvironment
  *        The lexical environment that will be used to create the actor.
  * @param ThreadActor aThreadActor
  *        The parent thread actor that contains this environment.
  */
-let EnvironmentActor = ActorClass({
-  typeName: "environment",
-
+let EnvironmentActor = ActorClassWithSpec(environmentSpec, {
   initialize: function (environment, threadActor) {
     this.obj = environment;
     this.threadActor = threadActor;
   },
 
   /**
    * Return an environment form for use in a protocol message.
    */
@@ -71,17 +70,17 @@ let EnvironmentActor = ActorClass({
    * Handle a protocol request to change the value of a variable bound in this
    * lexical environment.
    *
    * @param string name
    *        The name of the variable to be changed.
    * @param any value
    *        The value to be assigned.
    */
-  assign: method(function (name, value) {
+  assign: function (name, value) {
     // TODO: enable the commented-out part when getVariableDescriptor lands
     // (bug 725815).
     /* let desc = this.obj.getVariableDescriptor(name);
 
     if (!desc.writable) {
       return { error: "immutableBinding",
                message: "Changing the value of an immutable binding is not " +
                         "allowed" };
@@ -95,28 +94,23 @@ let EnvironmentActor = ActorClass({
           error: "threadWouldRun",
           message: "Assigning a value would cause the debuggee to run"
         };
       } else {
         throw e;
       }
     }
     return { from: this.actorID };
-  }, {
-    request: {
-      name: Arg(1),
-      value: Arg(2)
-    }
-  }),
+  },
 
   /**
    * Handle a protocol request to fully enumerate the bindings introduced by the
    * lexical environment.
    */
-  bindings: method(function () {
+  bindings: function () {
     let bindings = { arguments: [], variables: {} };
 
     // TODO: this part should be removed in favor of the commented-out part
     // below when getVariableDescriptor lands (bug 725815).
     if (typeof this.obj.getVariable != "function") {
     // if (typeof this.obj.getVariableDescriptor != "function") {
       return bindings;
     }
@@ -194,17 +188,12 @@ let EnvironmentActor = ActorClass({
           this.registeredPool, this.threadActor.objectGrip);
         descForm.set = createValueGrip(desc.set || undefined,
           this.registeredPool, this.threadActor.objectGrip);
       }
       bindings.variables[name] = descForm;
     }
 
     return bindings;
-  }, {
-    request: {},
-    response: {
-      bindings: RetVal("json")
-    }
-  })
+  }
 });
 
 exports.EnvironmentActor = EnvironmentActor;
--- a/devtools/server/actors/frame.js
+++ b/devtools/server/actors/frame.js
@@ -3,24 +3,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { ActorPool } = require("devtools/server/actors/common");
 const { createValueGrip } = require("devtools/server/actors/object");
-const { ActorClass } = require("devtools/shared/protocol");
+const { ActorClassWithSpec } = require("devtools/shared/protocol");
+const { frameSpec } = require("devtools/shared/specs/frame");
 
 /**
  * An actor for a specified stack frame.
  */
-let FrameActor = ActorClass({
-  typeName: "frame",
-
+let FrameActor = ActorClassWithSpec(frameSpec, {
   /**
    * Creates the Frame actor.
    *
    * @param frame Debugger.Frame
    *        The debuggee frame.
    * @param threadActor ThreadActor
    *        The parent thread actor for this frame.
    */
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -10,25 +10,26 @@ const Services = require("Services");
 const { Cc, Ci, Cu, Cr, components, ChromeWorker } = require("chrome");
 const { ActorPool, OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
 const { BreakpointActor, setBreakpointAtEntryPoints } = require("devtools/server/actors/breakpoint");
 const { EnvironmentActor } = require("devtools/server/actors/environment");
 const { FrameActor } = require("devtools/server/actors/frame");
 const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object");
 const { SourceActor, getSourceURL } = require("devtools/server/actors/source");
 const { DebuggerServer } = require("devtools/server/main");
-const { ActorClass } = require("devtools/shared/protocol");
+const { ActorClassWithSpec } = require("devtools/shared/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, dumpn, update, fetch } = DevToolsUtils;
 const promise = require("promise");
 const PromiseDebugging = require("PromiseDebugging");
 const xpcInspector = require("xpcInspector");
 const ScriptStore = require("./utils/ScriptStore");
 const { DevToolsWorker } = require("devtools/shared/worker/worker");
 const object = require("sdk/util/object");
+const { threadSpec } = require("devtools/shared/specs/script");
 
 const { defer, resolve, reject, all } = promise;
 
 loader.lazyGetter(this, "Debugger", () => {
   let Debugger = require("Debugger");
   hackDebugger(Debugger);
   return Debugger;
 });
@@ -403,19 +404,17 @@ EventLoop.prototype = {
  *          - preNest: Function called before entering a nested event loop.
  *          - postNest: Function called after exiting a nested event loop.
  *          - makeDebugger: A function that takes no arguments and instantiates
  *            a Debugger that manages its globals on its own.
  * @param aGlobal object [optional]
  *        An optional (for content debugging only) reference to the content
  *        window.
  */
-const ThreadActor = ActorClass({
-  typeName: "context",
-
+const ThreadActor = ActorClassWithSpec(threadSpec, {
   initialize: function (aParent, aGlobal) {
     this._state = "detached";
     this._frameActors = [];
     this._parent = aParent;
     this._dbg = null;
     this._gripDepth = 0;
     this._threadLifetimePool = null;
     this._tabClosed = false;
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -6,22 +6,23 @@
 
 "use strict";
 
 const { Cc, Ci } = require("chrome");
 const Services = require("Services");
 const { BreakpointActor, setBreakpointAtEntryPoints } = require("devtools/server/actors/breakpoint");
 const { OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
 const { createValueGrip } = require("devtools/server/actors/object");
-const { ActorClass, Arg, RetVal, method } = require("devtools/shared/protocol");
+const { ActorClassWithSpec, Arg, RetVal, method } = require("devtools/shared/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, fetch } = DevToolsUtils;
 const { joinURI } = require("devtools/shared/path");
 const promise = require("promise");
 const { defer, resolve, reject, all } = promise;
+const { sourceSpec } = require("devtools/shared/specs/source");
 
 loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
 loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);
 loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
 
 function isEvalSource(source) {
   let introType = source.introductionType;
   // These are all the sources that are essentially eval-ed (either
@@ -135,17 +136,17 @@ function resolveURIToLocalPath(aURI) {
  * @param Debugger.Source generatedSource
  *        Optional, passed in when aSourceMap is also passed in. The generated
  *        source object that introduced this source.
  * @param Boolean isInlineSource
  *        Optional. True if this is an inline source from a HTML or XUL page.
  * @param String contentType
  *        Optional. The content type of this source, if immediately available.
  */
-let SourceActor = ActorClass({
+let SourceActor = ActorClassWithSpec(sourceSpec, {
   typeName: "source",
 
   initialize: function ({ source, thread, originalUrl, generatedSource,
                           isInlineSource, contentType }) {
     this._threadActor = thread;
     this._originalUrl = originalUrl;
     this._source = source;
     this._generatedSource = generatedSource;
@@ -394,17 +395,17 @@ let SourceActor = ActorClass({
       }
     });
   },
 
   /**
    * Get all executable lines from the current source
    * @return Array - Executable lines of the current script
    **/
-  getExecutableLines: method(function () {
+  getExecutableLines: function () {
     function sortLines(lines) {
       // Converting the Set into an array
       lines = [...lines];
       lines.sort((a, b) => {
         return a - b;
       });
       return lines;
     }
@@ -427,17 +428,17 @@ let SourceActor = ActorClass({
         }
 
         return sortLines(lines);
       });
     }
 
     let lines = this.getExecutableOffsets(this.source, true);
     return sortLines(lines);
-  }, { response: { lines: RetVal("json") } }),
+  },
 
   /**
    * Extract all executable offsets from the given script
    * @param String url - extract offsets of the script with this url
    * @param Boolean onlyLine - will return only the line number
    * @return Set - Executable offsets/lines of the script
    **/
   getExecutableOffsets: function (source, onlyLine) {
@@ -449,60 +450,54 @@ let SourceActor = ActorClass({
     }
 
     return offsets;
   },
 
   /**
    * Handler for the "source" packet.
    */
-  onSource: method(function () {
+  onSource: function () {
     return resolve(this._init)
       .then(this._getSourceText)
       .then(({ content, contentType }) => {
         return {
           source: createValueGrip(content, this.threadActor.threadLifetimePool,
             this.threadActor.objectGrip),
           contentType: contentType
         };
       })
       .then(null, aError => {
         reportError(aError, "Got an exception during SA_onSource: ");
         throw new Error("Could not load the source for " + this.url + ".\n" +
                         DevToolsUtils.safeErrorString(aError));
       });
-  }, {
-    request: { type: "source" },
-    response: RetVal("json")
-  }),
+  },
 
   /**
    * Handler for the "prettyPrint" packet.
    */
-  prettyPrint: method(function (indent) {
+  prettyPrint: function (indent) {
     this.threadActor.sources.prettyPrint(this.url, indent);
     return this._getSourceText()
       .then(this._sendToPrettyPrintWorker(indent))
       .then(this._invertSourceMap)
       .then(this._encodeAndSetSourceMapURL)
       .then(() => {
         // We need to reset `_init` now because we have already done the work of
         // pretty printing, and don't want onSource to wait forever for
         // initialization to complete.
         this._init = null;
       })
       .then(this.onSource)
       .then(null, error => {
         this.disablePrettyPrint();
         throw new Error(DevToolsUtils.safeErrorString(error));
       });
-  }, {
-    request: { indent: Arg(0, "number") },
-    response: RetVal("json")
-  }),
+  },
 
   /**
    * Return a function that sends a request to the pretty print worker, waits on
    * the worker's response, and then returns the pretty printed code.
    *
    * @param Number aIndent
    *        The number of spaces to indent by the code by, when we send the
    *        request to the pretty print worker.
@@ -585,67 +580,65 @@ let SourceActor = ActorClass({
       sources.clearSourceMapCache(source.sourceMapURL);
       sources.setSourceMapHard(source, null, sm);
     });
   },
 
   /**
    * Handler for the "disablePrettyPrint" packet.
    */
-  disablePrettyPrint: method(function () {
+  disablePrettyPrint: function () {
     let source = this.generatedSource || this.source;
     let sources = this.threadActor.sources;
     let sm = sources.getSourceMap(source);
 
     sources.clearSourceMapCache(source.sourceMapURL, { hard: true });
 
     if (this._oldSourceMapping) {
       sources.setSourceMapHard(source,
                                this._oldSourceMapping.url,
                                this._oldSourceMapping.map);
       this._oldSourceMapping = null;
     }
 
     this.threadActor.sources.disablePrettyPrint(this.url);
     return this.onSource();
-  }, {
-    response: RetVal("json")
-  }),
+  },
 
   /**
    * Handler for the "blackbox" packet.
    */
-  blackbox: method(function () {
+  blackbox: function () {
     this.threadActor.sources.blackBox(this.url);
     if (this.threadActor.state == "paused"
         && this.threadActor.youngestFrame
         && this.threadActor.youngestFrame.script.url == this.url) {
       return true;
     }
     return false;
-  }, { response: { pausedInSource: RetVal("boolean") } }),
+  },
 
   /**
    * Handler for the "unblackbox" packet.
    */
-  unblackbox: method(function () {
+  unblackbox: function () {
     this.threadActor.sources.unblackBox(this.url);
-  }),
+  },
 
   /**
    * Handle a request to set a breakpoint.
    *
    * @param JSON request
    *        A JSON object representing the request.
    *
    * @returns Promise
    *          A promise that resolves to a JSON object representing the
    *          response.
    */
-  setBreakpoint: method(function (line, column, condition) {
+  setBreakpoint: function (line, column, condition) {
     if (this.threadActor.state !== "paused") {
       throw {
         error: "wrongState",
         message: "Cannot set breakpoint while debuggee is running."
       };
     }
 
     let location = new OriginalLocation(this, line, column);
@@ -660,26 +653,17 @@ let SourceActor = ActorClass({
 
       let actualLocation = actor.originalLocation;
       if (!actualLocation.equals(location)) {
         response.actualLocation = actualLocation.toJSON();
       }
 
       return response;
     });
-  }, {
-    request: {
-      location: {
-        line: Arg(0, "number"),
-        column: Arg(1, "nullable:number")
-      },
-      condition: Arg(2, "nullable:string")
-    },
-    response: RetVal("json")
-  }),
+  },
 
   /**
    * Get or create a BreakpointActor for the given location in the original
    * source, and ensure it is set as a breakpoint handler on all scripts that
    * match the given location.
    *
    * @param OriginalLocation originalLocation
    *        An OriginalLocation representing the location of the breakpoint in
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -1,15 +1,19 @@
 "use strict";
 
 var { Ci, Cu } = require("chrome");
 var { DebuggerServer } = require("devtools/server/main");
 var Services = require("Services");
 const protocol = require("devtools/shared/protocol");
 const { Arg, method, RetVal } = protocol;
+const {
+  workerSpec,
+  serviceWorkerRegistrationSpec,
+} = require("devtools/shared/specs/worker");
 
 loader.lazyRequireGetter(this, "ChromeUtils");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(
   this, "wdm",
   "@mozilla.org/dom/workers/workerdebuggermanager;1",
@@ -35,19 +39,17 @@ function matchWorkerDebugger(dbg, option
     if (window !== options.window) {
       return false;
     }
   }
 
   return true;
 }
 
-let WorkerActor = protocol.ActorClass({
-  typeName: "worker",
-
+let WorkerActor = protocol.ActorClassWithSpec(workerSpec, {
   initialize: function (conn, dbg) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this._dbg = dbg;
     this._attached = false;
     this._threadActor = null;
     this._transport = null;
     this.manage(this);
   },
@@ -64,17 +66,17 @@ let WorkerActor = protocol.ActorClass({
     };
     if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) {
       let registration = this._getServiceWorkerRegistrationInfo();
       form.scope = registration.scope;
     }
     return form;
   },
 
-  attach: method(function () {
+  attach: function () {
     if (this._dbg.isClosed) {
       return { error: "closed" };
     }
 
     if (!this._attached) {
       // Automatically disable their internal timeout that shut them down
       // Should be refactored by having actors specific to service workers
       if (this._dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE) {
@@ -86,35 +88,29 @@ let WorkerActor = protocol.ActorClass({
       this._dbg.addListener(this);
       this._attached = true;
     }
 
     return {
       type: "attached",
       url: this._dbg.url
     };
-  }, {
-    request: {},
-    response: RetVal("json")
-  }),
+  },
 
-  detach: method(function () {
+  detach: function () {
     if (!this._attached) {
       return { error: "wrongState" };
     }
 
     this._detach();
 
     return { type: "detached" };
-  }, {
-    request: {},
-    response: RetVal("json")
-  }),
+  },
 
-  connect: method(function (options) {
+  connect: function (options) {
     if (!this._attached) {
       return { error: "wrongState" };
     }
 
     if (this._threadActor !== null) {
       return {
         type: "connected",
         threadActor: this._threadActor
@@ -131,36 +127,28 @@ let WorkerActor = protocol.ActorClass({
       return {
         type: "connected",
         threadActor: this._threadActor,
         consoleActor: this._consoleActor
       };
     }, (error) => {
       return { error: error.toString() };
     });
-  }, {
-    request: {
-      options: Arg(0, "json"),
-    },
-    response: RetVal("json")
-  }),
+  },
 
-  push: method(function () {
+  push: function () {
     if (this._dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
       return { error: "wrongType" };
     }
     let registration = this._getServiceWorkerRegistrationInfo();
     let originAttributes = ChromeUtils.originAttributesToSuffix(
       this._dbg.principal.originAttributes);
     swm.sendPushEvent(originAttributes, registration.scope);
     return { type: "pushed" };
-  }, {
-    request: {},
-    response: RetVal("json")
-  }),
+  },
 
   onClose: function () {
     if (this._attached) {
       this._detach();
     }
 
     this.conn.sendActorEvent(this.actorID, "close");
   },
@@ -302,19 +290,18 @@ WorkerActorList.prototype = {
   }
 };
 
 exports.WorkerActorList = WorkerActorList;
 
 // Lazily load the service-worker-child.js process script only once.
 let _serviceWorkerProcessScriptLoaded = false;
 
-let ServiceWorkerRegistrationActor = protocol.ActorClass({
-  typeName: "serviceWorkerRegistration",
-
+let ServiceWorkerRegistrationActor =
+protocol.ActorClassWithSpec(serviceWorkerRegistrationSpec, {
   initialize: function (conn, registration) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this._registration = registration;
     this.manage(this);
   },
 
   form: function (detail) {
     if (detail === "actorid") {
@@ -322,48 +309,42 @@ let ServiceWorkerRegistrationActor = pro
     }
     return {
       actor: this.actorID,
       scope: this._registration.scope,
       url: this._registration.scriptSpec
     };
   },
 
-  start: method(function () {
+  start: function () {
     if (!_serviceWorkerProcessScriptLoaded) {
       Services.ppmm.loadProcessScript(
         "resource://devtools/server/service-worker-child.js", true);
       _serviceWorkerProcessScriptLoaded = true;
     }
     Services.ppmm.broadcastAsyncMessage("serviceWorkerRegistration:start", {
       scope: this._registration.scope
     });
     return { type: "started" };
-  }, {
-    request: {},
-    response: RetVal("json")
-  }),
+  },
 
-  unregister: method(function () {
+  unregister: function () {
     let { principal, scope } = this._registration;
     let unregisterCallback = {
       unregisterSucceeded: function () {},
       unregisterFailed: function () {
         console.error("Failed to unregister the service worker for " + scope);
       },
       QueryInterface: XPCOMUtils.generateQI(
         [Ci.nsIServiceWorkerUnregisterCallback])
     };
     swm.propagateUnregister(principal, unregisterCallback, scope);
 
     return { type: "unregistered" };
-  }, {
-    request: {},
-    response: RetVal("json")
-  }),
+  },
 });
 
 function ServiceWorkerRegistrationActorList(conn) {
   this._conn = conn;
   this._actors = new Map();
   this._onListChanged = null;
   this._mustNotify = false;
   this.onRegister = this.onRegister.bind(this);
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/csscoverage.js
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Services = require("Services");
+const {cssUsageSpec} = require("devtools/shared/specs/csscoverage");
+const protocol = require("devtools/shared/protocol");
+const {custom} = protocol;
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+
+/**
+ * Allow: let foo = l10n.lookup("csscoverageFoo");
+ */
+const l10n = exports.l10n = {
+  _URI: "chrome://devtools-shared/locale/csscoverage.properties",
+  lookup: function (msg) {
+    if (this._stringBundle == null) {
+      this._stringBundle = Services.strings.createBundle(this._URI);
+    }
+    return this._stringBundle.GetStringFromName(msg);
+  }
+};
+
+/**
+ * Running more than one usage report at a time is probably bad for performance
+ * and it isn't particularly useful, and it's confusing from a notification POV
+ * so we only allow one.
+ */
+var isRunning = false;
+var notification;
+var target;
+var chromeWindow;
+
+/**
+ * Front for CSSUsageActor
+ */
+const CSSUsageFront = protocol.FrontClassWithSpec(cssUsageSpec, {
+  initialize: function (client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+    this.actorID = form.cssUsageActor;
+    this.manage(this);
+  },
+
+  _onStateChange: protocol.preEvent("state-change", function (ev) {
+    isRunning = ev.isRunning;
+    ev.target = target;
+
+    if (isRunning) {
+      let gnb = chromeWindow.document.getElementById("global-notificationbox");
+      notification = gnb.getNotificationWithValue("csscoverage-running");
+
+      if (notification == null) {
+        let notifyStop = reason => {
+          if (reason == "removed") {
+            this.stop();
+          }
+        };
+
+        let msg = l10n.lookup("csscoverageRunningReply");
+        notification = gnb.appendNotification(msg, "csscoverage-running",
+                                              "",
+                                              gnb.PRIORITY_INFO_HIGH,
+                                              null,
+                                              notifyStop);
+      }
+    } else {
+      if (notification) {
+        notification.remove();
+        notification = undefined;
+      }
+
+      gDevTools.showToolbox(target, "styleeditor");
+      target = undefined;
+    }
+  }),
+
+  /**
+   * Server-side start is above. Client-side start adds a notification box
+   */
+  start: custom(function (newChromeWindow, newTarget, noreload = false) {
+    target = newTarget;
+    chromeWindow = newChromeWindow;
+
+    return this._start(noreload);
+  }, {
+    impl: "_start"
+  }),
+
+  /**
+   * Server-side start is above. Client-side start adds a notification box
+   */
+  toggle: custom(function (newChromeWindow, newTarget) {
+    target = newTarget;
+    chromeWindow = newChromeWindow;
+
+    return this._toggle();
+  }, {
+    impl: "_toggle"
+  }),
+
+  /**
+   * We count STARTING and STOPPING as 'running'
+   */
+  isRunning: function () {
+    return isRunning;
+  }
+});
+
+exports.CSSUsageFront = CSSUsageFront;
+
+const knownFronts = new WeakMap();
+
+/**
+ * Create a CSSUsageFront only when needed (returns a promise)
+ * For notes on target.makeRemote(), see
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1016330#c7
+ */
+exports.getUsage = function (trgt) {
+  return trgt.makeRemote().then(() => {
+    let front = knownFronts.get(trgt.client);
+    if (front == null && trgt.form.cssUsageActor != null) {
+      front = new CSSUsageFront(trgt.client, trgt.form);
+      knownFronts.set(trgt.client, front);
+    }
+    return front;
+  });
+};
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -6,16 +6,17 @@
 
 DevToolsModules(
     'actor-registry.js',
     'addons.js',
     'animation.js',
     'call-watcher.js',
     'canvas.js',
     'css-properties.js',
+    'csscoverage.js',
     'highlighters.js',
     'inspector.js',
     'preference.js',
     'settings.js',
     'storage.js',
     'styles.js',
     'stylesheets.js',
     'webaudio.js',
--- a/devtools/shared/gcli/commands/csscoverage.js
+++ b/devtools/shared/gcli/commands/csscoverage.js
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Cc, Ci } = require("chrome");
 
 const domtemplate = require("gcli/util/domtemplate");
-const csscoverage = require("devtools/server/actors/csscoverage");
+const csscoverage = require("devtools/shared/fronts/csscoverage");
 const l10n = csscoverage.l10n;
 
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
 
 loader.lazyImporter(this, "Chart", "resource://devtools/client/shared/widgets/Chart.jsm");
 
 /**
  * The commands/converters for GCLI
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/breakpoint.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {generateActorSpec} = require("devtools/shared/protocol");
+
+const breakpointSpec = generateActorSpec({
+  typeName: "breakpoint",
+
+  methods: {
+    delete: {}
+  },
+});
+
+exports.breakpointSpec = breakpointSpec;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/csscoverage.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {Arg, RetVal, generateActorSpec} = require("devtools/shared/protocol");
+
+const cssUsageSpec = generateActorSpec({
+  typeName: "cssUsage",
+
+  events: {
+    "state-change": {
+      type: "stateChange",
+      stateChange: Arg(0, "json")
+    }
+  },
+
+  methods: {
+    start: {
+      request: { url: Arg(0, "boolean") }
+    },
+    stop: {},
+    toggle: {},
+    oneshot: {},
+    createEditorReport: {
+      request: { url: Arg(0, "string") },
+      response: { reports: RetVal("array:json") }
+    },
+    createPageReport: {
+      response: RetVal("json")
+    },
+    _testOnlyVisitedPages: {
+      response: { value: RetVal("array:string") }
+    },
+  },
+});
+
+exports.cssUsageSpec = cssUsageSpec;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/environment.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {Arg, RetVal, generateActorSpec} = require("devtools/shared/protocol");
+
+const environmentSpec = generateActorSpec({
+  typeName: "environment",
+
+  methods: {
+    assign: {
+      request: {
+        name: Arg(1),
+        value: Arg(2)
+      }
+    },
+    bindings: {
+      request: {},
+      response: {
+        bindings: RetVal("json")
+      }
+    },
+  },
+});
+
+exports.environmentSpec = environmentSpec;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/frame.js
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {generateActorSpec} = require("devtools/shared/protocol");
+
+const frameSpec = generateActorSpec({
+  typeName: "frame",
+
+  methods: {},
+});
+
+exports.frameSpec = frameSpec;
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -3,24 +3,31 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'actor-registry.js',
     'addons.js',
     'animation.js',
+    'breakpoint.js',
     'call-watcher.js',
     'canvas.js',
     'css-properties.js',
+    'csscoverage.js',
+    'environment.js',
+    'frame.js',
     'heap-snapshot-file.js',
     'highlighters.js',
     'inspector.js',
     'node.js',
     'preference.js',
+    'script.js',
     'settings.js',
+    'source.js',
     'storage.js',
     'styleeditor.js',
     'styles.js',
     'stylesheets.js',
     'webaudio.js',
-    'webgl.js'
+    'webgl.js',
+    'worker.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/script.js
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {generateActorSpec} = require("devtools/shared/protocol");
+
+const threadSpec = generateActorSpec({
+  typeName: "context",
+
+  methods: {},
+});
+
+exports.threadSpec = threadSpec;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/source.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {Arg, RetVal, generateActorSpec} = require("devtools/shared/protocol");
+
+const sourceSpec = generateActorSpec({
+  typeName: "source",
+
+  methods: {
+    getExecutableLines: { response: { lines: RetVal("json") } },
+    onSource: {
+      request: { type: "source" },
+      response: RetVal("json")
+    },
+    prettyPrint: {
+      request: { indent: Arg(0, "number") },
+      response: RetVal("json")
+    },
+    disablePrettyPrint: {
+      response: RetVal("json")
+    },
+    blackbox: { response: { pausedInSource: RetVal("boolean") } },
+    unblackbox: {},
+    setBreakpoint: {
+      request: {
+        location: {
+          line: Arg(0, "number"),
+          column: Arg(1, "nullable:number")
+        },
+        condition: Arg(2, "nullable:string")
+      },
+      response: RetVal("json")
+    },
+  },
+});
+
+exports.sourceSpec = sourceSpec;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/worker.js
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {Arg, RetVal, generateActorSpec} = require("devtools/shared/protocol");
+
+const workerSpec = generateActorSpec({
+  typeName: "worker",
+
+  methods: {
+    attach: {
+      request: {},
+      response: RetVal("json")
+    },
+    detach: {
+      request: {},
+      response: RetVal("json")
+    },
+    connect: {
+      request: {
+        options: Arg(0, "json"),
+      },
+      response: RetVal("json")
+    },
+    push: {
+      request: {},
+      response: RetVal("json")
+    },
+  },
+});
+
+exports.workerSpec = workerSpec;
+
+const serviceWorkerRegistrationSpec = generateActorSpec({
+  typeName: "serviceWorkerRegistration",
+
+  methods: {
+    start: {
+      request: {},
+      response: RetVal("json")
+    },
+    unregister: {
+      request: {},
+      response: RetVal("json")
+    },
+  },
+});
+
+exports.serviceWorkerRegistrationSpec = serviceWorkerRegistrationSpec;
--- a/devtools/shared/transport/transport.js
+++ b/devtools/shared/transport/transport.js
@@ -1,34 +1,33 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+"use strict";
+
+/* global Pipe, ScriptableInputStream, uneval */
+
 // TODO: Get rid of this code once the marionette server loads transport.js as
 // an SDK module (see bug 1000814)
-(function (factory) { // Module boilerplate
-  if (this.module && module.id.indexOf("transport") >= 0) { // require
+(function (factory) {
+  if (this.module && module.id.indexOf("transport") >= 0) {
+    // require
     factory.call(this, require, exports);
-  } else { // loadSubScript
-    if (this.require) {
-      factory.call(this, require, this);
-    } else {
-      const Cu = Components.utils;
-      const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-      factory.call(this, require, this);
-    }
+  } else if (this.require) {
+    // loadSubScript
+    factory.call(this, require, this);
+  } else {
+    // Cu.import
+    const Cu = Components.utils;
+    const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+    factory.call(this, require, this);
   }
 }).call(this, function (require, exports) {
-
-  "use strict";
-
-  const { Cc, Ci, Cr, Cu, CC } = require("chrome");
-  const Services = require("Services");
+  const { Cc, Ci, Cr, CC } = require("chrome");
   const DevToolsUtils = require("devtools/shared/DevToolsUtils");
   const { dumpn, dumpv } = DevToolsUtils;
   const StreamUtils = require("devtools/shared/transport/stream-utils");
   const { Packet, JSONPacket, BulkPacket } =
   require("devtools/shared/transport/packets");
   const promise = require("promise");
   const EventEmitter = require("devtools/shared/event-emitter");
 
@@ -38,172 +37,172 @@
 
   DevToolsUtils.defineLazyGetter(this, "ScriptableInputStream", () => {
     return CC("@mozilla.org/scriptableinputstream;1",
             "nsIScriptableInputStream", "init");
   });
 
   const PACKET_HEADER_MAX = 200;
 
-/**
- * An adapter that handles data transfers between the debugger client and
- * server. It can work with both nsIPipe and nsIServerSocket transports so
- * long as the properly created input and output streams are specified.
- * (However, for intra-process connections, LocalDebuggerTransport, below,
- * is more efficient than using an nsIPipe pair with DebuggerTransport.)
- *
- * @param input nsIAsyncInputStream
- *        The input stream.
- * @param output nsIAsyncOutputStream
- *        The output stream.
- *
- * Given a DebuggerTransport instance dt:
- * 1) Set dt.hooks to a packet handler object (described below).
- * 2) Call dt.ready() to begin watching for input packets.
- * 3) Call dt.send() / dt.startBulkSend() to send packets.
- * 4) Call dt.close() to close the connection, and disengage from the event
- *    loop.
- *
- * A packet handler is an object with the following methods:
- *
- * - onPacket(packet) - called when we have received a complete packet.
- *   |packet| is the parsed form of the packet --- a JavaScript value, not
- *   a JSON-syntax string.
- *
- * - onBulkPacket(packet) - called when we have switched to bulk packet
- *   receiving mode. |packet| is an object containing:
- *   * actor:  Name of actor that will receive the packet
- *   * type:   Name of actor's method that should be called on receipt
- *   * length: Size of the data to be read
- *   * stream: This input stream should only be used directly if you can ensure
- *             that you will read exactly |length| bytes and will not close the
- *             stream when reading is complete
- *   * done:   If you use the stream directly (instead of |copyTo| below), you
- *             must signal completion by resolving / rejecting this deferred.
- *             If it's rejected, the transport will be closed.  If an Error is
- *             supplied as a rejection value, it will be logged via |dumpn|.
- *             If you do use |copyTo|, resolving is taken care of for you when
- *             copying completes.
- *   * copyTo: A helper function for getting your data out of the stream that
- *             meets the stream handling requirements above, and has the
- *             following signature:
- *     @param  output nsIAsyncOutputStream
- *             The stream to copy to.
- *     @return Promise
- *             The promise is resolved when copying completes or rejected if any
- *             (unexpected) errors occur.
- *             This object also emits "progress" events for each chunk that is
- *             copied.  See stream-utils.js.
- *
- * - onClosed(reason) - called when the connection is closed. |reason| is
- *   an optional nsresult or object, typically passed when the transport is
- *   closed due to some error in a underlying stream.
- *
- * See ./packets.js and the Remote Debugging Protocol specification for more
- * details on the format of these packets.
- */
+  /**
+   * An adapter that handles data transfers between the debugger client and
+   * server. It can work with both nsIPipe and nsIServerSocket transports so
+   * long as the properly created input and output streams are specified.
+   * (However, for intra-process connections, LocalDebuggerTransport, below,
+   * is more efficient than using an nsIPipe pair with DebuggerTransport.)
+   *
+   * @param input nsIAsyncInputStream
+   *        The input stream.
+   * @param output nsIAsyncOutputStream
+   *        The output stream.
+   *
+   * Given a DebuggerTransport instance dt:
+   * 1) Set dt.hooks to a packet handler object (described below).
+   * 2) Call dt.ready() to begin watching for input packets.
+   * 3) Call dt.send() / dt.startBulkSend() to send packets.
+   * 4) Call dt.close() to close the connection, and disengage from the event
+   *    loop.
+   *
+   * A packet handler is an object with the following methods:
+   *
+   * - onPacket(packet) - called when we have received a complete packet.
+   *   |packet| is the parsed form of the packet --- a JavaScript value, not
+   *   a JSON-syntax string.
+   *
+   * - onBulkPacket(packet) - called when we have switched to bulk packet
+   *   receiving mode. |packet| is an object containing:
+   *   * actor:  Name of actor that will receive the packet
+   *   * type:   Name of actor's method that should be called on receipt
+   *   * length: Size of the data to be read
+   *   * stream: This input stream should only be used directly if you can ensure
+   *             that you will read exactly |length| bytes and will not close the
+   *             stream when reading is complete
+   *   * done:   If you use the stream directly (instead of |copyTo| below), you
+   *             must signal completion by resolving / rejecting this deferred.
+   *             If it's rejected, the transport will be closed.  If an Error is
+   *             supplied as a rejection value, it will be logged via |dumpn|.
+   *             If you do use |copyTo|, resolving is taken care of for you when
+   *             copying completes.
+   *   * copyTo: A helper function for getting your data out of the stream that
+   *             meets the stream handling requirements above, and has the
+   *             following signature:
+   *     @param  output nsIAsyncOutputStream
+   *             The stream to copy to.
+   *     @return Promise
+   *             The promise is resolved when copying completes or rejected if any
+   *             (unexpected) errors occur.
+   *             This object also emits "progress" events for each chunk that is
+   *             copied.  See stream-utils.js.
+   *
+   * - onClosed(reason) - called when the connection is closed. |reason| is
+   *   an optional nsresult or object, typically passed when the transport is
+   *   closed due to some error in a underlying stream.
+   *
+   * See ./packets.js and the Remote Debugging Protocol specification for more
+   * details on the format of these packets.
+   */
   function DebuggerTransport(input, output) {
     EventEmitter.decorate(this);
 
     this._input = input;
     this._scriptableInput = new ScriptableInputStream(input);
     this._output = output;
 
-  // The current incoming (possibly partial) header, which will determine which
-  // type of Packet |_incoming| below will become.
+    // The current incoming (possibly partial) header, which will determine which
+    // type of Packet |_incoming| below will become.
     this._incomingHeader = "";
-  // The current incoming Packet object
+    // The current incoming Packet object
     this._incoming = null;
-  // A queue of outgoing Packet objects
+    // A queue of outgoing Packet objects
     this._outgoing = [];
 
     this.hooks = null;
     this.active = false;
 
     this._incomingEnabled = true;
     this._outgoingEnabled = true;
 
     this.close = this.close.bind(this);
   }
 
   DebuggerTransport.prototype = {
-  /**
-   * Transmit an object as a JSON packet.
-   *
-   * This method returns immediately, without waiting for the entire
-   * packet to be transmitted, registering event handlers as needed to
-   * transmit the entire packet. Packets are transmitted in the order
-   * they are passed to this method.
-   */
+    /**
+     * Transmit an object as a JSON packet.
+     *
+     * This method returns immediately, without waiting for the entire
+     * packet to be transmitted, registering event handlers as needed to
+     * transmit the entire packet. Packets are transmitted in the order
+     * they are passed to this method.
+     */
     send: function (object) {
       this.emit("send", object);
 
       let packet = new JSONPacket(this);
       packet.object = object;
       this._outgoing.push(packet);
       this._flushOutgoing();
     },
 
-  /**
-   * Transmit streaming data via a bulk packet.
-   *
-   * This method initiates the bulk send process by queuing up the header data.
-   * The caller receives eventual access to a stream for writing.
-   *
-   * N.B.: Do *not* attempt to close the stream handed to you, as it will
-   * continue to be used by this transport afterwards.  Most users should
-   * instead use the provided |copyFrom| function instead.
-   *
-   * @param header Object
-   *        This is modeled after the format of JSON packets above, but does not
-   *        actually contain the data, but is instead just a routing header:
-   *          * actor:  Name of actor that will receive the packet
-   *          * type:   Name of actor's method that should be called on receipt
-   *          * length: Size of the data to be sent
-   * @return Promise
-   *         The promise will be resolved when you are allowed to write to the
-   *         stream with an object containing:
-   *           * stream:   This output stream should only be used directly if
-   *                       you can ensure that you will write exactly |length|
-   *                       bytes and will not close the stream when writing is
-   *                       complete
-   *           * done:     If you use the stream directly (instead of |copyFrom|
-   *                       below), you must signal completion by resolving /
-   *                       rejecting this deferred.  If it's rejected, the
-   *                       transport will be closed.  If an Error is supplied as
-   *                       a rejection value, it will be logged via |dumpn|.  If
-   *                       you do use |copyFrom|, resolving is taken care of for
-   *                       you when copying completes.
-   *           * copyFrom: A helper function for getting your data onto the
-   *                       stream that meets the stream handling requirements
-   *                       above, and has the following signature:
-   *             @param  input nsIAsyncInputStream
-   *                     The stream to copy from.
-   *             @return Promise
-   *                     The promise is resolved when copying completes or
-   *                     rejected if any (unexpected) errors occur.
-   *                     This object also emits "progress" events for each chunk
-   *                     that is copied.  See stream-utils.js.
-   */
+    /**
+     * Transmit streaming data via a bulk packet.
+     *
+     * This method initiates the bulk send process by queuing up the header data.
+     * The caller receives eventual access to a stream for writing.
+     *
+     * N.B.: Do *not* attempt to close the stream handed to you, as it will
+     * continue to be used by this transport afterwards.  Most users should
+     * instead use the provided |copyFrom| function instead.
+     *
+     * @param header Object
+     *        This is modeled after the format of JSON packets above, but does not
+     *        actually contain the data, but is instead just a routing header:
+     *          * actor:  Name of actor that will receive the packet
+     *          * type:   Name of actor's method that should be called on receipt
+     *          * length: Size of the data to be sent
+     * @return Promise
+     *         The promise will be resolved when you are allowed to write to the
+     *         stream with an object containing:
+     *           * stream:   This output stream should only be used directly if
+     *                       you can ensure that you will write exactly |length|
+     *                       bytes and will not close the stream when writing is
+     *                       complete
+     *           * done:     If you use the stream directly (instead of |copyFrom|
+     *                       below), you must signal completion by resolving /
+     *                       rejecting this deferred.  If it's rejected, the
+     *                       transport will be closed.  If an Error is supplied as
+     *                       a rejection value, it will be logged via |dumpn|.  If
+     *                       you do use |copyFrom|, resolving is taken care of for
+     *                       you when copying completes.
+     *           * copyFrom: A helper function for getting your data onto the
+     *                       stream that meets the stream handling requirements
+     *                       above, and has the following signature:
+     *             @param  input nsIAsyncInputStream
+     *                     The stream to copy from.
+     *             @return Promise
+     *                     The promise is resolved when copying completes or
+     *                     rejected if any (unexpected) errors occur.
+     *                     This object also emits "progress" events for each chunk
+     *                     that is copied.  See stream-utils.js.
+     */
     startBulkSend: function (header) {
       this.emit("startBulkSend", header);
 
       let packet = new BulkPacket(this);
       packet.header = header;
       this._outgoing.push(packet);
       this._flushOutgoing();
       return packet.streamReadyForWriting;
     },
 
-  /**
-   * Close the transport.
-   * @param reason nsresult / object (optional)
-   *        The status code or error message that corresponds to the reason for
-   *        closing the transport (likely because a stream closed or failed).
-   */
+    /**
+     * Close the transport.
+     * @param reason nsresult / object (optional)
+     *        The status code or error message that corresponds to the reason for
+     *        closing the transport (likely because a stream closed or failed).
+     */
     close: function (reason) {
       this.emit("onClosed", reason);
 
       this.active = false;
       this._input.close();
       this._scriptableInput.close();
       this._output.close();
       this._destroyIncoming();
@@ -214,228 +213,231 @@
       }
       if (reason) {
         dumpn("Transport closed: " + DevToolsUtils.safeErrorString(reason));
       } else {
         dumpn("Transport closed.");
       }
     },
 
-  /**
-   * The currently outgoing packet (at the top of the queue).
-   */
-    get _currentOutgoing() { return this._outgoing[0]; },
+    /**
+     * The currently outgoing packet (at the top of the queue).
+     */
+    get _currentOutgoing() {
+      return this._outgoing[0];
+    },
 
-  /**
-   * Flush data to the outgoing stream.  Waits until the output stream notifies
-   * us that it is ready to be written to (via onOutputStreamReady).
-   */
+    /**
+     * Flush data to the outgoing stream.  Waits until the output stream notifies
+     * us that it is ready to be written to (via onOutputStreamReady).
+     */
     _flushOutgoing: function () {
       if (!this._outgoingEnabled || this._outgoing.length === 0) {
         return;
       }
 
-    // If the top of the packet queue has nothing more to send, remove it.
+      // If the top of the packet queue has nothing more to send, remove it.
       if (this._currentOutgoing.done) {
         this._finishCurrentOutgoing();
       }
 
       if (this._outgoing.length > 0) {
-        var threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+        let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
         this._output.asyncWait(this, 0, 0, threadManager.currentThread);
       }
     },
 
-  /**
-   * Pause this transport's attempts to write to the output stream.  This is
-   * used when we've temporarily handed off our output stream for writing bulk
-   * data.
-   */
+    /**
+     * Pause this transport's attempts to write to the output stream.  This is
+     * used when we've temporarily handed off our output stream for writing bulk
+     * data.
+     */
     pauseOutgoing: function () {
       this._outgoingEnabled = false;
     },
 
-  /**
-   * Resume this transport's attempts to write to the output stream.
-   */
+    /**
+     * Resume this transport's attempts to write to the output stream.
+     */
     resumeOutgoing: function () {
       this._outgoingEnabled = true;
       this._flushOutgoing();
     },
 
-  // nsIOutputStreamCallback
-  /**
-   * This is called when the output stream is ready for more data to be written.
-   * The current outgoing packet will attempt to write some amount of data, but
-   * may not complete.
-   */
+    // nsIOutputStreamCallback
+    /**
+     * This is called when the output stream is ready for more data to be written.
+     * The current outgoing packet will attempt to write some amount of data, but
+     * may not complete.
+     */
     onOutputStreamReady: DevToolsUtils.makeInfallible(function (stream) {
       if (!this._outgoingEnabled || this._outgoing.length === 0) {
         return;
       }
 
       try {
         this._currentOutgoing.write(stream);
       } catch (e) {
         if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
           this.close(e.result);
           return;
-        } else {
-          throw e;
         }
+        throw e;
       }
 
       this._flushOutgoing();
     }, "DebuggerTransport.prototype.onOutputStreamReady"),
 
-  /**
-   * Remove the current outgoing packet from the queue upon completion.
-   */
+    /**
+     * Remove the current outgoing packet from the queue upon completion.
+     */
     _finishCurrentOutgoing: function () {
       if (this._currentOutgoing) {
         this._currentOutgoing.destroy();
         this._outgoing.shift();
       }
     },
 
-  /**
-   * Clear the entire outgoing queue.
-   */
+    /**
+     * Clear the entire outgoing queue.
+     */
     _destroyAllOutgoing: function () {
       for (let packet of this._outgoing) {
         packet.destroy();
       }
       this._outgoing = [];
     },
 
-  /**
-   * Initialize the input stream for reading. Once this method has been called,
-   * we watch for packets on the input stream, and pass them to the appropriate
-   * handlers via this.hooks.
-   */
+    /**
+     * Initialize the input stream for reading. Once this method has been called,
+     * we watch for packets on the input stream, and pass them to the appropriate
+     * handlers via this.hooks.
+     */
     ready: function () {
       this.active = true;
       this._waitForIncoming();
     },
 
-  /**
-   * Asks the input stream to notify us (via onInputStreamReady) when it is
-   * ready for reading.
-   */
+    /**
+     * Asks the input stream to notify us (via onInputStreamReady) when it is
+     * ready for reading.
+     */
     _waitForIncoming: function () {
       if (this._incomingEnabled) {
         let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
         this._input.asyncWait(this, 0, 0, threadManager.currentThread);
       }
     },
 
-  /**
-   * Pause this transport's attempts to read from the input stream.  This is
-   * used when we've temporarily handed off our input stream for reading bulk
-   * data.
-   */
+    /**
+     * Pause this transport's attempts to read from the input stream.  This is
+     * used when we've temporarily handed off our input stream for reading bulk
+     * data.
+     */
     pauseIncoming: function () {
       this._incomingEnabled = false;
     },
 
-  /**
-   * Resume this transport's attempts to read from the input stream.
-   */
+    /**
+     * Resume this transport's attempts to read from the input stream.
+     */
     resumeIncoming: function () {
       this._incomingEnabled = true;
       this._flushIncoming();
       this._waitForIncoming();
     },
 
-  // nsIInputStreamCallback
-  /**
-   * Called when the stream is either readable or closed.
-   */
-    onInputStreamReady:
-  DevToolsUtils.makeInfallible(function (stream) {
-    try {
-      while (stream.available() && this._incomingEnabled &&
-            this._processIncoming(stream, stream.available())) {}
-      this._waitForIncoming();
-    } catch (e) {
-      if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
-        this.close(e.result);
-      } else {
-        throw e;
+    // nsIInputStreamCallback
+    /**
+     * Called when the stream is either readable or closed.
+     */
+    onInputStreamReady: DevToolsUtils.makeInfallible(function (stream) {
+      try {
+        while (stream.available() && this._incomingEnabled &&
+               this._processIncoming(stream, stream.available())) {
+           // Loop until there is nothing more to process
+        }
+        this._waitForIncoming();
+      } catch (e) {
+        if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+          this.close(e.result);
+        } else {
+          throw e;
+        }
       }
-    }
-  }, "DebuggerTransport.prototype.onInputStreamReady"),
+    }, "DebuggerTransport.prototype.onInputStreamReady"),
 
-  /**
-   * Process the incoming data.  Will create a new currently incoming Packet if
-   * needed.  Tells the incoming Packet to read as much data as it can, but
-   * reading may not complete.  The Packet signals that its data is ready for
-   * delivery by calling one of this transport's _on*Ready methods (see
-   * ./packets.js and the _on*Ready methods below).
-   * @return boolean
-   *         Whether incoming stream processing should continue for any
-   *         remaining data.
-   */
+    /**
+     * Process the incoming data.  Will create a new currently incoming Packet if
+     * needed.  Tells the incoming Packet to read as much data as it can, but
+     * reading may not complete.  The Packet signals that its data is ready for
+     * delivery by calling one of this transport's _on*Ready methods (see
+     * ./packets.js and the _on*Ready methods below).
+     * @return boolean
+     *         Whether incoming stream processing should continue for any
+     *         remaining data.
+     */
     _processIncoming: function (stream, count) {
       dumpv("Data available: " + count);
 
       if (!count) {
         dumpv("Nothing to read, skipping");
         return false;
       }
 
       try {
         if (!this._incoming) {
           dumpv("Creating a new packet from incoming");
 
           if (!this._readHeader(stream)) {
-            return false; // Not enough data to read packet type
+            // Not enough data to read packet type
+            return false;
           }
 
-        // Attempt to create a new Packet by trying to parse each possible
-        // header pattern.
+          // Attempt to create a new Packet by trying to parse each possible
+          // header pattern.
           this._incoming = Packet.fromHeader(this._incomingHeader, this);
           if (!this._incoming) {
             throw new Error("No packet types for header: " +
                           this._incomingHeader);
           }
         }
 
         if (!this._incoming.done) {
-        // We have an incomplete packet, keep reading it.
+          // We have an incomplete packet, keep reading it.
           dumpv("Existing packet incomplete, keep reading");
           this._incoming.read(stream, this._scriptableInput);
         }
       } catch (e) {
         let msg = "Error reading incoming packet: (" + e + " - " + e.stack + ")";
         dumpn(msg);
 
-      // Now in an invalid state, shut down the transport.
+        // Now in an invalid state, shut down the transport.
         this.close();
         return false;
       }
 
       if (!this._incoming.done) {
-      // Still not complete, we'll wait for more data.
+        // Still not complete, we'll wait for more data.
         dumpv("Packet not done, wait for more");
         return true;
       }
 
-    // Ready for next packet
+      // Ready for next packet
       this._flushIncoming();
       return true;
     },
 
-  /**
-   * Read as far as we can into the incoming data, attempting to build up a
-   * complete packet header (which terminates with ":").  We'll only read up to
-   * PACKET_HEADER_MAX characters.
-   * @return boolean
-   *         True if we now have a complete header.
-   */
+    /**
+     * Read as far as we can into the incoming data, attempting to build up a
+     * complete packet header (which terminates with ":").  We'll only read up to
+     * PACKET_HEADER_MAX characters.
+     * @return boolean
+     *         True if we now have a complete header.
+     */
     _readHeader: function () {
       let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length;
       this._incomingHeader +=
       StreamUtils.delimitedRead(this._scriptableInput, ":", amountToRead);
       if (dumpv.wantVerbose) {
         dumpv("Header read: " + this._incomingHeader);
       }
 
@@ -445,166 +447,164 @@
         }
         return true;
       }
 
       if (this._incomingHeader.length >= PACKET_HEADER_MAX) {
         throw new Error("Failed to parse packet header!");
       }
 
-    // Not enough data yet.
+      // Not enough data yet.
       return false;
     },
 
-  /**
-   * If the incoming packet is done, log it as needed and clear the buffer.
-   */
+    /**
+     * If the incoming packet is done, log it as needed and clear the buffer.
+     */
     _flushIncoming: function () {
       if (!this._incoming.done) {
         return;
       }
       if (dumpn.wantLogging) {
         dumpn("Got: " + this._incoming);
       }
       this._destroyIncoming();
     },
 
-  /**
-   * Handler triggered by an incoming JSONPacket completing it's |read| method.
-   * Delivers the packet to this.hooks.onPacket.
-   */
+    /**
+     * Handler triggered by an incoming JSONPacket completing it's |read| method.
+     * Delivers the packet to this.hooks.onPacket.
+     */
     _onJSONObjectReady: function (object) {
       DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => {
       // Ensure the transport is still alive by the time this runs.
         if (this.active) {
           this.emit("onPacket", object);
           this.hooks.onPacket(object);
         }
       }, "DebuggerTransport instance's this.hooks.onPacket"));
     },
 
-  /**
-   * Handler triggered by an incoming BulkPacket entering the |read| phase for
-   * the stream portion of the packet.  Delivers info about the incoming
-   * streaming data to this.hooks.onBulkPacket.  See the main comment on the
-   * transport at the top of this file for more details.
-   */
+    /**
+     * Handler triggered by an incoming BulkPacket entering the |read| phase for
+     * the stream portion of the packet.  Delivers info about the incoming
+     * streaming data to this.hooks.onBulkPacket.  See the main comment on the
+     * transport at the top of this file for more details.
+     */
     _onBulkReadReady: function (...args) {
       DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => {
       // Ensure the transport is still alive by the time this runs.
         if (this.active) {
           this.emit("onBulkPacket", ...args);
           this.hooks.onBulkPacket(...args);
         }
       }, "DebuggerTransport instance's this.hooks.onBulkPacket"));
     },
 
-  /**
-   * Remove all handlers and references related to the current incoming packet,
-   * either because it is now complete or because the transport is closing.
-   */
+    /**
+     * Remove all handlers and references related to the current incoming packet,
+     * either because it is now complete or because the transport is closing.
+     */
     _destroyIncoming: function () {
       if (this._incoming) {
         this._incoming.destroy();
       }
       this._incomingHeader = "";
       this._incoming = null;
     }
 
   };
 
   exports.DebuggerTransport = DebuggerTransport;
 
-/**
- * An adapter that handles data transfers between the debugger client and
- * server when they both run in the same process. It presents the same API as
- * DebuggerTransport, but instead of transmitting serialized messages across a
- * connection it merely calls the packet dispatcher of the other side.
- *
- * @param other LocalDebuggerTransport
- *        The other endpoint for this debugger connection.
- *
- * @see DebuggerTransport
- */
+  /**
+   * An adapter that handles data transfers between the debugger client and
+   * server when they both run in the same process. It presents the same API as
+   * DebuggerTransport, but instead of transmitting serialized messages across a
+   * connection it merely calls the packet dispatcher of the other side.
+   *
+   * @param other LocalDebuggerTransport
+   *        The other endpoint for this debugger connection.
+   *
+   * @see DebuggerTransport
+   */
   function LocalDebuggerTransport(other) {
     EventEmitter.decorate(this);
 
     this.other = other;
     this.hooks = null;
 
-  /*
-   * A packet number, shared between this and this.other. This isn't used
-   * by the protocol at all, but it makes the packet traces a lot easier to
-   * follow.
-   */
+    // A packet number, shared between this and this.other. This isn't used by the
+    // protocol at all, but it makes the packet traces a lot easier to follow.
     this._serial = this.other ? this.other._serial : { count: 0 };
     this.close = this.close.bind(this);
   }
 
   LocalDebuggerTransport.prototype = {
-  /**
-   * Transmit a message by directly calling the onPacket handler of the other
-   * endpoint.
-   */
+    /**
+     * Transmit a message by directly calling the onPacket handler of the other
+     * endpoint.
+     */
     send: function (packet) {
       this.emit("send", packet);
 
       let serial = this._serial.count++;
       if (dumpn.wantLogging) {
-      /* Check 'from' first, as 'echo' packets have both. */
+        // Check 'from' first, as 'echo' packets have both.
         if (packet.from) {
           dumpn("Packet " + serial + " sent from " + uneval(packet.from));
         } else if (packet.to) {
           dumpn("Packet " + serial + " sent to " + uneval(packet.to));
         }
       }
       this._deepFreeze(packet);
       let other = this.other;
       if (other) {
         DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => {
-        // Avoid the cost of JSON.stringify() when logging is disabled.
+          // Avoid the cost of JSON.stringify() when logging is disabled.
           if (dumpn.wantLogging) {
             dumpn("Received packet " + serial + ": " + JSON.stringify(packet, null, 2));
           }
           if (other.hooks) {
             other.emit("onPacket", packet);
             other.hooks.onPacket(packet);
           }
         }, "LocalDebuggerTransport instance's this.other.hooks.onPacket"));
       }
     },
 
-  /**
-   * Send a streaming bulk packet directly to the onBulkPacket handler of the
-   * other endpoint.
-   *
-   * This case is much simpler than the full DebuggerTransport, since there is
-   * no primary stream we have to worry about managing while we hand it off to
-   * others temporarily.  Instead, we can just make a single use pipe and be
-   * done with it.
-   */
+    /**
+     * Send a streaming bulk packet directly to the onBulkPacket handler of the
+     * other endpoint.
+     *
+     * This case is much simpler than the full DebuggerTransport, since there is
+     * no primary stream we have to worry about managing while we hand it off to
+     * others temporarily.  Instead, we can just make a single use pipe and be
+     * done with it.
+     */
     startBulkSend: function ({actor, type, length}) {
       this.emit("startBulkSend", {actor, type, length});
 
       let serial = this._serial.count++;
 
       dumpn("Sent bulk packet " + serial + " for actor " + actor);
       if (!this.other) {
-        return;
+        let error = new Error("startBulkSend: other side of transport missing");
+        return promise.reject(error);
       }
 
       let pipe = new Pipe(true, true, 0, 0, null);
 
       DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => {
         dumpn("Received bulk packet " + serial);
         if (!this.other.hooks) {
           return;
         }
 
-      // Receiver
+        // Receiver
         let deferred = promise.defer();
         let packet = {
           actor: actor,
           type: type,
           length: length,
           copyTo: (output) => {
             let copying =
             StreamUtils.copyStream(pipe.inputStream, output, length);
@@ -613,120 +613,120 @@
           },
           stream: pipe.inputStream,
           done: deferred
         };
 
         this.other.emit("onBulkPacket", packet);
         this.other.hooks.onBulkPacket(packet);
 
-      // Await the result of reading from the stream
+        // Await the result of reading from the stream
         deferred.promise.then(() => pipe.inputStream.close(), this.close);
       }, "LocalDebuggerTransport instance's this.other.hooks.onBulkPacket"));
 
-    // Sender
+      // Sender
       let sendDeferred = promise.defer();
 
-    // The remote transport is not capable of resolving immediately here, so we
-    // shouldn't be able to either.
+      // The remote transport is not capable of resolving immediately here, so we
+      // shouldn't be able to either.
       DevToolsUtils.executeSoon(() => {
         let copyDeferred = promise.defer();
 
         sendDeferred.resolve({
           copyFrom: (input) => {
             let copying =
             StreamUtils.copyStream(input, pipe.outputStream, length);
             copyDeferred.resolve(copying);
             return copying;
           },
           stream: pipe.outputStream,
           done: copyDeferred
         });
 
-      // Await the result of writing to the stream
+        // Await the result of writing to the stream
         copyDeferred.promise.then(() => pipe.outputStream.close(), this.close);
       });
 
       return sendDeferred.promise;
     },
 
-  /**
-   * Close the transport.
-   */
+    /**
+     * Close the transport.
+     */
     close: function () {
       this.emit("close");
 
       if (this.other) {
-      // Remove the reference to the other endpoint before calling close(), to
-      // avoid infinite recursion.
+        // Remove the reference to the other endpoint before calling close(), to
+        // avoid infinite recursion.
         let other = this.other;
         this.other = null;
         other.close();
       }
       if (this.hooks) {
         try {
           this.hooks.onClosed();
         } catch (ex) {
           console.error(ex);
         }
         this.hooks = null;
       }
     },
 
-  /**
-   * An empty method for emulating the DebuggerTransport API.
-   */
+    /**
+     * An empty method for emulating the DebuggerTransport API.
+     */
     ready: function () {},
 
-  /**
-   * Helper function that makes an object fully immutable.
-   */
+    /**
+     * Helper function that makes an object fully immutable.
+     */
     _deepFreeze: function (object) {
       Object.freeze(object);
       for (let prop in object) {
-      // Freeze the properties that are objects, not on the prototype, and not
-      // already frozen. Note that this might leave an unfrozen reference
-      // somewhere in the object if there is an already frozen object containing
-      // an unfrozen object.
+        // Freeze the properties that are objects, not on the prototype, and not
+        // already frozen. Note that this might leave an unfrozen reference
+        // somewhere in the object if there is an already frozen object containing
+        // an unfrozen object.
         if (object.hasOwnProperty(prop) && typeof object === "object" &&
-          !Object.isFrozen(object)) {
-          this._deepFreeze(o[prop]);
+            !Object.isFrozen(object)) {
+          this._deepFreeze(object[prop]);
         }
       }
     }
   };
 
   exports.LocalDebuggerTransport = LocalDebuggerTransport;
 
-/**
- * A transport for the debugging protocol that uses nsIMessageSenders to
- * exchange packets with servers running in child processes.
- *
- * In the parent process, |sender| should be the nsIMessageSender for the
- * child process. In a child process, |sender| should be the child process
- * message manager, which sends packets to the parent.
- *
- * |prefix| is a string included in the message names, to distinguish
- * multiple servers running in the same child process.
- *
- * This transport exchanges messages named 'debug:<prefix>:packet', where
- * <prefix> is |prefix|, whose data is the protocol packet.
- */
+  /**
+   * A transport for the debugging protocol that uses nsIMessageSenders to
+   * exchange packets with servers running in child processes.
+   *
+   * In the parent process, |sender| should be the nsIMessageSender for the
+   * child process. In a child process, |sender| should be the child process
+   * message manager, which sends packets to the parent.
+   *
+   * |prefix| is a string included in the message names, to distinguish
+   * multiple servers running in the same child process.
+   *
+   * This transport exchanges messages named 'debug:<prefix>:packet', where
+   * <prefix> is |prefix|, whose data is the protocol packet.
+   */
   function ChildDebuggerTransport(sender, prefix) {
     EventEmitter.decorate(this);
 
     this._sender = sender.QueryInterface(Ci.nsIMessageSender);
     this._messageName = "debug:" + prefix + ":packet";
   }
 
-/*
- * To avoid confusion, we use 'message' to mean something that
- * nsIMessageSender conveys, and 'packet' to mean a remote debugging
- * protocol packet.
- */
+  /*
+   * To avoid confusion, we use 'message' to mean something that
+   * nsIMessageSender conveys, and 'packet' to mean a remote debugging
+   * protocol packet.
+   */
   ChildDebuggerTransport.prototype = {
     constructor: ChildDebuggerTransport,
 
     hooks: null,
 
     ready: function () {
       this._sender.addMessageListener(this._messageName, this);
     },
@@ -749,32 +749,33 @@
 
     startBulkSend: function () {
       throw new Error("Can't send bulk data to child processes.");
     }
   };
 
   exports.ChildDebuggerTransport = ChildDebuggerTransport;
 
-// WorkerDebuggerTransport is defined differently depending on whether we are
-// on the main thread or a worker thread. In the former case, we are required
-// by the devtools loader, and isWorker will be false. Otherwise, we are
-// required by the worker loader, and isWorker will be true.
-//
-// Each worker debugger supports only a single connection to the main thread.
-// However, its theoretically possible for multiple servers to connect to the
-// same worker. Consequently, each transport has a connection id, to allow
-// messages from multiple connections to be multiplexed on a single channel.
+  // WorkerDebuggerTransport is defined differently depending on whether we are
+  // on the main thread or a worker thread. In the former case, we are required
+  // by the devtools loader, and isWorker will be false. Otherwise, we are
+  // required by the worker loader, and isWorker will be true.
+  //
+  // Each worker debugger supports only a single connection to the main thread.
+  // However, its theoretically possible for multiple servers to connect to the
+  // same worker. Consequently, each transport has a connection id, to allow
+  // messages from multiple connections to be multiplexed on a single channel.
 
   if (!this.isWorker) {
-    (function () { // Main thread
-    /**
-     * A transport that uses a WorkerDebugger to send packets from the main
-     * thread to a worker thread.
-     */
+    // Main thread
+    (function () {
+      /**
+       * A transport that uses a WorkerDebugger to send packets from the main
+       * thread to a worker thread.
+       */
       function WorkerDebuggerTransport(dbg, id) {
         this._dbg = dbg;
         this._id = id;
         this.onMessage = this._onMessage.bind(this);
       }
 
       WorkerDebuggerTransport.prototype = {
         constructor: WorkerDebuggerTransport,
@@ -812,21 +813,22 @@
             this.hooks.onPacket(packet.message);
           }
         }
       };
 
       exports.WorkerDebuggerTransport = WorkerDebuggerTransport;
     }).call(this);
   } else {
-    (function () { // Worker thread
-    /*
-     * A transport that uses a WorkerDebuggerGlobalScope to send packets from a
-     * worker thread to the main thread.
-     */
+    // Worker thread
+    (function () {
+      /**
+       * A transport that uses a WorkerDebuggerGlobalScope to send packets from a
+       * worker thread to the main thread.
+       */
       function WorkerDebuggerTransport(scope, id) {
         this._scope = scope;
         this._id = id;
         this._onMessage = this._onMessage.bind(this);
       }
 
       WorkerDebuggerTransport.prototype = {
         constructor: WorkerDebuggerTransport,
@@ -864,10 +866,9 @@
             this.hooks.onPacket(packet.message);
           }
         }
       };
 
       exports.WorkerDebuggerTransport = WorkerDebuggerTransport;
     }).call(this);
   }
-
 });
--- a/dom/base/nsFrameLoader.cpp
+++ b/dom/base/nsFrameLoader.cpp
@@ -2343,16 +2343,17 @@ nsFrameLoader::GetWindowDimensions(nsInt
 NS_IMETHODIMP
 nsFrameLoader::UpdatePositionAndSize(nsSubDocumentFrame *aIFrame)
 {
   if (IsRemoteFrame()) {
     if (mRemoteBrowser) {
       ScreenIntSize size = aIFrame->GetSubdocumentSize();
       nsIntRect dimensions;
       NS_ENSURE_SUCCESS(GetWindowDimensions(dimensions), NS_ERROR_FAILURE);
+      mLazySize = size;
       mRemoteBrowser->UpdateDimensions(dimensions, size);
     }
     return NS_OK;
   }
   UpdateBaseWindowPositionAndSize(aIFrame);
   return NS_OK;
 }
 
@@ -2373,23 +2374,50 @@ nsFrameLoader::UpdateBaseWindowPositionA
     baseWindow->GetPosition(&x, &y);
 
     if (!weakFrame.IsAlive()) {
       // GetPosition() killed us
       return;
     }
 
     ScreenIntSize size = aIFrame->GetSubdocumentSize();
+    mLazySize = size;
 
     baseWindow->SetPositionAndSize(x, y, size.width, size.height,
                                    nsIBaseWindow::eDelayResize);
   }
 }
 
 NS_IMETHODIMP
+nsFrameLoader::GetLazyWidth(uint32_t* aLazyWidth)
+{
+  *aLazyWidth = mLazySize.width;
+
+  nsIFrame* frame = GetPrimaryFrameOfOwningContent();
+  if (frame) {
+    *aLazyWidth = frame->PresContext()->DevPixelsToIntCSSPixels(*aLazyWidth);
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFrameLoader::GetLazyHeight(uint32_t* aLazyHeight)
+{
+  *aLazyHeight = mLazySize.height;
+
+  nsIFrame* frame = GetPrimaryFrameOfOwningContent();
+  if (frame) {
+    *aLazyHeight = frame->PresContext()->DevPixelsToIntCSSPixels(*aLazyHeight);
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsFrameLoader::GetEventMode(uint32_t* aEventMode)
 {
   *aEventMode = mEventMode;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsFrameLoader::SetEventMode(uint32_t aEventMode)
--- a/dom/base/nsFrameLoader.h
+++ b/dom/base/nsFrameLoader.h
@@ -370,16 +370,19 @@ private:
 
   TabParent* mRemoteBrowser;
   uint64_t mChildID;
 
   // See nsIFrameLoader.idl. EVENT_MODE_NORMAL_DISPATCH automatically
   // forwards some input events to out-of-process content.
   uint32_t mEventMode;
 
+  // Holds the last known size of the frame.
+  mozilla::ScreenIntSize mLazySize;
+
   bool mIsPrerendered : 1;
   bool mDepthTooGreat : 1;
   bool mIsTopLevelContent : 1;
   bool mDestroyCalled : 1;
   bool mNeedsAsyncDestroy : 1;
   bool mInSwap : 1;
   bool mInShow : 1;
   bool mHideCalled : 1;
--- a/dom/base/nsIFrameLoader.idl
+++ b/dom/base/nsIFrameLoader.idl
@@ -222,16 +222,31 @@ interface nsIFrameLoader : nsISupports
   readonly attribute boolean ownerIsMozBrowserOrAppFrame;
 
   /**
    * Find out whether the owner content really is a widget. If this attribute
    * returns true, |ownerIsMozBrowserOrAppFrame| must return true.
    */
   readonly attribute boolean ownerIsWidget;
 
+  /**
+   * The last known width of the frame. Reading this property will not trigger
+   * a reflow, and therefore may not reflect the current state of things. It
+   * should only be used in asynchronous APIs where values are not guaranteed
+   * to be up-to-date when received.
+   */
+  readonly attribute unsigned long lazyWidth;
+
+  /**
+   * The last known height of the frame. Reading this property will not trigger
+   * a reflow, and therefore may not reflect the current state of things. It
+   * should only be used in asynchronous APIs where values are not guaranteed
+   * to be up-to-date when received.
+   */
+  readonly attribute unsigned long lazyHeight;
 };
 
 %{C++
 class nsFrameLoader;
 %}
 
 native alreadyAddRefed_nsFrameLoader(already_AddRefed<nsFrameLoader>);
 
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -465,31 +465,31 @@ pref("dom.ipc.plugins.enabled", false);
 // the default so that we can migrate a user-set pref. See bug 885357.
 pref("plugins.click_to_play", true);
 // The default value for nsIPluginTag.enabledState (STATE_CLICKTOPLAY = 1)
 pref("plugin.default.state", 1);
 
 // product URLs
 // The breakpad report server to link to in about:crashes
 pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
-pref("app.support.baseURL", "http://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/");
+pref("app.support.baseURL", "https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/");
 
 // URL for feedback page
 // This should be kept in sync with the "feedback_link" string defined in strings.xml.in
 pref("app.feedbackURL", "https://input.mozilla.org/feedback/android/%VERSION%/%CHANNEL%/?utm_source=feedback-prompt");
 
 pref("app.privacyURL", "https://www.mozilla.org/privacy/firefox/");
-pref("app.creditsURL", "http://www.mozilla.org/credits/");
-pref("app.channelURL", "http://www.mozilla.org/%LOCALE%/firefox/channel/");
+pref("app.creditsURL", "https://www.mozilla.org/credits/");
+pref("app.channelURL", "https://www.mozilla.org/%LOCALE%/firefox/channel/");
 #if MOZ_UPDATE_CHANNEL == aurora
-pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/mobile/%VERSION%/auroranotes/");
+pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%/auroranotes/");
 #elif MOZ_UPDATE_CHANNEL == beta
-pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/mobile/%VERSION%beta/releasenotes/");
+pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%beta/releasenotes/");
 #else
-pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/mobile/%VERSION%/releasenotes/");
+pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%/releasenotes/");
 #endif
 
 pref("app.faqURL", "https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/faq");
 
 // Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
 pref("security.alternate_certificate_error_page", "certerror");
 
 pref("security.warn_viewing_mixed", false); // Warning is disabled.  See Bug 616712.
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -284,17 +284,17 @@ public class MediaControlService extends
         NotificationManagerCompat.from(this)
                 .notify(MEDIA_CONTROL_ID, notification);
     }
 
     private Notification.Action createNotificationAction(String action) {
         boolean isPlayAction = action.equals(ACTION_PLAY);
 
         int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause;
-        String title = getString(isPlayAction ? R.string.media_pause : R.string.media_pause);
+        String title = getString(isPlayAction ? R.string.media_play : R.string.media_pause);
 
         final Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
         intent.setAction(action);
         final PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
 
         //noinspection deprecation - The new constructor is only for API > 23
         return new Notification.Action.Builder(icon, title, pendingIntent).build();
     }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -109,16 +109,17 @@ OnPreferenceChangeListener,
 OnSharedPreferenceChangeListener
 {
     private static final String LOGTAG = "GeckoPreferences";
 
     // We have a white background, which makes transitions on
     // some devices look bad. Don't use transitions on those
     // devices.
     private static final boolean NO_TRANSITIONS = HardwareUtils.IS_KINDLE_DEVICE;
+    private static final int NO_SUCH_ID = 0;
 
     public static final String NON_PREF_PREFIX = "android.not_a_preference.";
     public static final String INTENT_EXTRA_RESOURCES = "resource";
     public static final String PREFS_TRACKING_PROTECTION_PROMPT_SHOWN = NON_PREF_PREFIX + "trackingProtectionPromptShown";
     public static String PREFS_HEALTHREPORT_UPLOAD_ENABLED = NON_PREF_PREFIX + "healthreport.uploadEnabled";
     public static final String PREFS_SYNC = NON_PREF_PREFIX + "sync";
 
     private static boolean sIsCharEncodingEnabled;
@@ -276,17 +277,22 @@ OnSharedPreferenceChangeListener
             // Because Android just rebuilt the activity itself with the
             // old language, we need to update the top title and other
             // wording again.
             if (onIsMultiPane()) {
                 updateActionBarTitle(R.string.settings_title);
             }
 
             // Update the title to for the preference pane that we're currently showing.
-            setTitle(R.string.pref_category_language);
+            final int titleId = getIntent().getExtras().getInt(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE);
+            if (titleId != NO_SUCH_ID) {
+                setTitle(titleId);
+            } else {
+                throw new IllegalStateException("Title id not found in intent bundle extras");
+            }
 
             // Don't finish the activity -- we just reloaded all of the
             // individual parts! -- but when it returns, make sure that the
             // caller knows the locale changed.
             setResult(RESULT_CODE_LOCALE_DID_CHANGE);
             return;
         }
 
@@ -423,16 +429,18 @@ OnSharedPreferenceChangeListener
             } else {
                 fragmentArgs.putString(INTENT_EXTRA_RESOURCES, "preferences_general_tablet");
             }
         }
 
         // Build fragment intent.
         intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, GeckoPreferenceFragment.class.getName());
         intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs);
+        // Used to get fragment title when locale changes (see onLocaleChanged method above)
+        intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE, R.string.settings_title);
     }
 
     @Override
     public boolean isValidFragment(String fragmentName) {
         return GeckoPreferenceFragment.class.getName().equals(fragmentName);
     }
 
     @TargetApi(11)
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java
@@ -2,21 +2,30 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.toolbar;
 
 import android.content.Context;
 import android.util.AttributeSet;
 
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.tabs.TabCurve;
 
 public class PhoneTabsButton extends ShapedButton {
     public PhoneTabsButton(Context context, AttributeSet attrs) {
         super(context, attrs);
+
+        if (!AppConstants.Versions.preLollipop) {
+            // The Android N preview has issues rendering our tabs button during animations
+            // unless we use hardware layers, see bug 1264783. For consistency we should
+            // try and set this across all devices, however on 4.X devices the background
+            // isn't drawn when we use hardware layers - hence we need to disable this there.
+            setLayerType(LAYER_TYPE_HARDWARE, null);
+        }
     }
 
     @Override
     protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
         super.onSizeChanged(width, height, oldWidth, oldHeight);
 
         mPath.reset();
 
--- a/mobile/android/chrome/content/about.xhtml
+++ b/mobile/android/chrome/content/about.xhtml
@@ -41,17 +41,17 @@
 #endif
 
     <div id="messages">
       <p id="distributionAbout" hidden="true"/>
       <p id="distributionID" hidden="true"/>
       <p id="telemetry" hidden="true">
         &aboutPage.warningVersion;
 #ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
-        &aboutPage.telemetryStart;<a href="http://www.mozilla.org/">&aboutPage.telemetryMozillaLink;</a>&aboutPage.telemetryEnd;
+        &aboutPage.telemetryStart;<a href="https://www.mozilla.org/">&aboutPage.telemetryMozillaLink;</a>&aboutPage.telemetryEnd;
 #endif
       </p>
     </div>
 
   </div>
 
     <ul id="aboutLinks">
       <div class="top-border"></div>
--- a/mobile/android/chrome/content/aboutRights.xhtml
+++ b/mobile/android/chrome/content/aboutRights.xhtml
@@ -21,21 +21,21 @@
 
 <body id="your-rights" dir="&rights.locale-direction;" class="aboutPageWideContainer">
 
 <h1>&rights.intro-header;</h1>
 
 <p>&rights.intro;</p>
 
 <ul>
-  <li>&rights.intro-point1a;<a href="http://www.mozilla.org/MPL/">&rights.intro-point1b;</a>&rights.intro-point1c;</li>
+  <li>&rights.intro-point1a;<a href="https://www.mozilla.org/MPL/">&rights.intro-point1b;</a>&rights.intro-point1c;</li>
   <!-- Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded.
        Point 3 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace)
        Point 4 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace) -->
-  <li>&rights.intro-point2-a;<a href="http://www.mozilla.org/foundation/trademarks/policy.html">&rights.intro-point2-b;</a>&rights.intro-point2-c;</li>
+  <li>&rights.intro-point2-a;<a href="https://www.mozilla.org/foundation/trademarks/policy.html">&rights.intro-point2-b;</a>&rights.intro-point2-c;</li>
   <li>&rights.intro-point2.5;</li>
   <li>&rights2.intro-point3a;<a href="https://www.mozilla.org/privacy/firefox/">&rights2.intro-point3b;</a>&rights.intro-point3c;</li>
   <li>&rights2.intro-point4a;<a href="about:rights#webservices" onclick="showServices();">&rights.intro-point4b;</a>&rights.intro-point4c;</li>
 </ul>
 
 <div id="webservices-container">
   <a name="webservices"/>
   <h3>&rights2.webservices-header;</h3>
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java
@@ -1,32 +1,25 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests;
 
 import org.mozilla.gecko.Actions;
 import org.mozilla.gecko.Assert;
-import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.Driver;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.tests.components.AboutHomeComponent;
 import org.mozilla.gecko.tests.components.AppMenuComponent;
 import org.mozilla.gecko.tests.components.BaseComponent;
 import org.mozilla.gecko.tests.components.GeckoViewComponent;
 import org.mozilla.gecko.tests.components.TabStripComponent;
 import org.mozilla.gecko.tests.components.ToolbarComponent;
 import org.mozilla.gecko.tests.helpers.HelperInitializer;
 
-import android.content.Intent;
-import android.content.res.Resources;
-import android.text.TextUtils;
-
 import com.robotium.solo.Solo;
 
 /**
  * A base test class for Robocop (UI-centric) tests. This and the related classes attempt to
  * provide a framework to improve upon the issues discovered with the previous BaseTest
  * implementation by providing simple test authorship and framework extension, consistency,
  * and reliability.
  *
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -149,35 +149,53 @@ var api = context => {
         let result = detectLanguage(text);
         return context.wrapPromise(result, callback);
       },
     },
   };
 };
 
 // Represents a content script.
-function Script(options, deferred = PromiseUtils.defer()) {
+function Script(extension, options, deferred = PromiseUtils.defer()) {
+  this.extension = extension;
   this.options = options;
   this.run_at = this.options.run_at;
   this.js = this.options.js || [];
   this.css = this.options.css || [];
   this.remove_css = this.options.remove_css;
 
   this.deferred = deferred;
 
   this.matches_ = new MatchPattern(this.options.matches);
   this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
   // TODO: MatchPattern should pre-mangle host-only patterns so that we
   // don't need to call a separate match function.
   this.matches_host_ = new MatchPattern(this.options.matchesHost || null);
   this.include_globs_ = new MatchGlobs(this.options.include_globs);
   this.exclude_globs_ = new MatchGlobs(this.options.exclude_globs);
+
+  this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
 }
 
 Script.prototype = {
+  get cssURLs() {
+    // We can handle CSS urls (css) and CSS code (cssCode).
+    let urls = [];
+    for (let url of this.css) {
+      urls.push(this.extension.baseURI.resolve(url));
+    }
+
+    if (this.options.cssCode) {
+      let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
+      urls.push(url);
+    }
+
+    return urls;
+  },
+
   matches(window) {
     let uri = window.document.documentURIObject;
     if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
       return false;
     }
 
     if (this.exclude_matches_.matches(uri)) {
       return false;
@@ -201,61 +219,60 @@ Script.prototype = {
       return false;
     }
 
     // TODO: match_about_blank.
 
     return true;
   },
 
-  tryInject(extension, window, sandbox, shouldRun) {
+  cleanup(window) {
+    if (!this.remove_css) {
+      let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindowUtils);
+
+      for (let url of this.cssURLs) {
+        runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
+      }
+    }
+  },
+
+  tryInject(window, sandbox, shouldRun) {
     if (!this.matches(window)) {
       this.deferred.reject({message: "No matching window"});
       return;
     }
 
     if (shouldRun("document_start")) {
       let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
 
-      // We can handle CSS urls (css) and CSS code (cssCode).
-      let cssUrls = [];
-      for (let cssUrl of this.css) {
-        cssUrl = extension.baseURI.resolve(cssUrl);
-        cssUrls.push(cssUrl);
-      }
+      let {cssURLs} = this;
+      if (cssURLs.length > 0) {
+        let method = this.remove_css ? winUtils.removeSheetUsingURIString : winUtils.loadSheetUsingURIString;
+        for (let url of cssURLs) {
+          runSafeSyncWithoutClone(method, url, winUtils.AUTHOR_SHEET);
+        }
 
-      if (this.options.cssCode) {
-        let cssUrl = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
-        cssUrls.push(cssUrl);
-      }
-
-      // We can insertCSS and removeCSS.
-      let method = this.remove_css ? winUtils.removeSheetUsingURIString : winUtils.loadSheetUsingURIString;
-      for (let cssUrl of cssUrls) {
-        runSafeSyncWithoutClone(method, cssUrl, winUtils.AUTHOR_SHEET);
-      }
-
-      if (cssUrls.length > 0) {
         this.deferred.resolve();
       }
     }
 
     let result;
     let scheduled = this.run_at || "document_idle";
     if (shouldRun(scheduled)) {
       for (let url of this.js) {
         // On gonk we need to load the resources asynchronously because the
         // app: channels only support asyncOpen. This is safe only in the
         // `document_idle` state.
         if (AppConstants.platform == "gonk" && scheduled != "document_idle") {
           Cu.reportError(`Script injection: ignoring ${url} at ${scheduled}`);
           continue;
         }
-        url = extension.baseURI.resolve(url);
+        url = this.extension.baseURI.resolve(url);
 
         let options = {
           target: sandbox,
           charset: "UTF-8",
           async: AppConstants.platform == "gonk",
         };
         try {
           result = Services.scriptloader.loadSubScriptWithOptions(url, options);
@@ -345,29 +362,30 @@ class ExtensionContext extends BaseConte
       // because it enables us to create the APIs object in this sandbox object and then copying it
       // into the iframe's window, see Bug 1214658 for rationale)
       this.sandbox = Cu.Sandbox(contentWindow, {
         sandboxPrototype: contentWindow,
         wantXrays: false,
         isWebExtensionContentScript: true,
       });
     } else {
-      // sandbox metadata is needed to be recognized and supported in
-      // the Developer Tools of the tab where the content script is running.
+      // This metadata is required by the Developer Tools, in order for
+      // the content script to be associated with both the extension and
+      // the tab holding the content page.
       let metadata = {
         "inner-window-id": getInnerWindowID(contentWindow),
         addonId: attrs.addonId,
       };
 
       this.sandbox = Cu.Sandbox(prin, {
         metadata,
         sandboxPrototype: contentWindow,
         wantXrays: true,
         isWebExtensionContentScript: true,
-        wantGlobalProperties: ["XMLHttpRequest"],
+        wantGlobalProperties: ["XMLHttpRequest", "fetch"],
       });
     }
 
     let delegate = {
       getSender(context, target, sender) {
         // Nothing to do here.
       },
     };
@@ -412,43 +430,49 @@ class ExtensionContext extends BaseConte
     }
   }
 
   get cloneScope() {
     return this.sandbox;
   }
 
   execute(script, shouldRun) {
-    script.tryInject(this.extension, this.contentWindow, this.sandbox, shouldRun);
+    script.tryInject(this.contentWindow, this.sandbox, shouldRun);
   }
 
   addScript(script) {
     let state = DocumentManager.getWindowState(this.contentWindow);
     this.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state));
 
     // Save the script in case it has pending operations in later load
-    // states, but only if we're before document_idle.
-    if (state != "document_idle") {
+    // states, but only if we're before document_idle, or require cleanup.
+    if (state != "document_idle" || script.requiresCleanup) {
       this.scripts.push(script);
     }
   }
 
   triggerScripts(documentState) {
     for (let script of this.scripts) {
       this.execute(script, scheduled => scheduled == documentState);
     }
     if (documentState == "document_idle") {
       // Don't bother saving scripts after document_idle.
-      this.scripts.length = 0;
+      this.scripts = this.scripts.filter(script => script.requiresCleanup);
     }
   }
 
   close() {
     super.unload();
 
+    for (let script of this.scripts) {
+      if (script.requiresCleanup) {
+        script.cleanup(this.contentWindow);
+      }
+    }
+
     this.childManager.close();
 
     // Overwrite the content script APIs with an empty object if the APIs objects are still
     // defined in the content window (See Bug 1214658 for rationale).
     if (this.isExtensionPage && !Cu.isDeadWrapper(this.contentWindow) &&
         Cu.waiveXrays(this.contentWindow).browser === this.chromeObj) {
       Cu.createObjectIn(this.contentWindow, {defineAs: "browser"});
       Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"});
@@ -560,19 +584,21 @@ DocumentManager = {
       this.trigger("document_end", window);
     } else if (event.type == "load") {
       this.trigger("document_idle", window);
     }
   },
 
   // Used to executeScript, insertCSS and removeCSS.
   executeScript(global, extensionId, options) {
+    let extension = ExtensionManager.get(extensionId);
+
     let executeInWin = (window) => {
       let deferred = PromiseUtils.defer();
-      let script = new Script(options, deferred);
+      let script = new Script(extension, options, deferred);
 
       if (script.matches(window)) {
         let context = this.getContentScriptContext(extensionId, window);
         context.addScript(script);
         return deferred.promise;
       }
       return null;
     };
@@ -710,17 +736,17 @@ DocumentManager = {
   },
 };
 
 // Represents a browser extension in the content process.
 function BrowserExtensionContent(data) {
   this.id = data.id;
   this.uuid = data.uuid;
   this.data = data;
-  this.scripts = data.content_scripts.map(scriptData => new Script(scriptData));
+  this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData));
   this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
   this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
 
   this.localeData = new LocaleData(data.localeData);
 
   this.manifest = data.manifest;
   this.baseURI = Services.io.newURI(data.baseURL, null, null);
 
--- a/toolkit/components/extensions/test/mochitest/file_permission_xhr.html
+++ b/toolkit/components/extensions/test/mochitest/file_permission_xhr.html
@@ -4,39 +4,48 @@
 <head>
 <meta charset="utf-8">
 </head>
 <body>
 
 <script>
 "use strict";
 
-/* globals privilegedXHR */
+/* globals privilegedFetch, privilegedXHR */
 /* eslint-disable mozilla/balanced-listeners */
 
 addEventListener("message", function rcv(event) {
   removeEventListener("message", rcv, false);
 
+  function assertTrue(condition, description) {
+    postMessage({msg: "assertTrue", condition, description}, "*");
+  }
+
   function passListener() {
-    postMessage(true, "*");
+    assertTrue(true, "Content XHR has no elevated privileges");
+    postMessage({"msg": "finish"}, "*");
   }
 
   function failListener() {
-    postMessage(false, "*");
+    assertTrue(false, "Content XHR has no elevated privileges");
+    postMessage({"msg": "finish"}, "*");
   }
 
-  let exc = null;
   try {
     new privilegedXHR();
+    assertTrue(false, "Content should not have access to privileged XHR constructor");
   } catch (e) {
-    exc = e;
+    assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged XHR constructor");
   }
-  if (!/Permission denied to access object/.exec(exc)) {
-    postMessage(false, "*");
-    return;
+
+  try {
+    new privilegedFetch();
+    assertTrue(false, "Content should not have access to privileged fetch() constructor");
+  } catch (e) {
+    assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged fetch() constructor");
   }
 
   let req = new XMLHttpRequest();
   req.addEventListener("load", failListener);
   req.addEventListener("error", passListener);
   req.open("GET", "http://example.org/example.txt");
   req.send();
 }, false);
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -42,16 +42,17 @@ skip-if = os == 'android' # Android does
 [test_ext_simple.html]
 [test_ext_geturl.html]
 [test_ext_content_security_policy.html]
 [test_ext_contentscript.html]
 skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
 [test_ext_contentscript_api_injection.html]
 [test_ext_contentscript_create_iframe.html]
 [test_ext_contentscript_devtools_metadata.html]
+[test_ext_contentscript_css.html]
 [test_ext_downloads.html]
 [test_ext_exclude_include_globs.html]
 [test_ext_i18n_css.html]
 [test_ext_generate.html]
 [test_ext_idle.html]
 [test_ext_localStorage.html]
 [test_ext_onmessage_removelistener.html]
 [test_ext_notifications.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for content script</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_content_script_css() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "content_scripts": [{
+        "matches": ["http://mochi.test/*/file_sample.html"],
+        "css": ["content.css"],
+      }],
+    },
+
+    files: {
+      "content.css": "body { max-width: 42px; }",
+    },
+  });
+
+  yield extension.startup();
+
+  let win = window.open("file_sample.html");
+  yield waitForLoad(win);
+
+  let style = win.getComputedStyle(win.document.body);
+  is(style.maxWidth, "42px", "Stylesheet correctly applied");
+
+  yield extension.unload();
+
+  style = win.getComputedStyle(win.document.body);
+  is(style.maxWidth, "none", "Stylesheet correctly removed");
+
+  win.close();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html
@@ -11,84 +11,88 @@
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 /* eslint-disable mozilla/balanced-listeners */
 
 add_task(function* test_simple() {
-  function runTests(cx, assertTrue, finish) {
-    function run(shouldFail, finish) {
+  function runTests(cx) {
+    function xhr(url) {
+      return new Promise((resolve, reject) => {
+        let req = new XMLHttpRequest();
+        req.open("GET", url);
+        req.addEventListener("load", resolve);
+        req.addEventListener("error", reject);
+        req.send();
+      });
+    }
+
+    function run(shouldFail, fetch) {
       function passListener() {
-        assertTrue(true, `${cx} pass listener`);
-        finish();
+        browser.test.succeed(`${cx}.${fetch.name} pass listener`);
       }
 
       function failListener() {
-        assertTrue(false, `${cx} fail listener`);
-        finish();
+        browser.test.fail(`${cx}.${fetch.name} fail listener`);
       }
 
-      let req = new XMLHttpRequest();
       if (shouldFail) {
-        req.addEventListener("load", failListener);
-        req.addEventListener("error", passListener);
-        req.open("GET", "http://example.org/example.txt");
+        return fetch("http://example.org/example.txt").then(failListener, passListener);
       } else {
-        req.addEventListener("load", passListener);
-        req.addEventListener("error", failListener);
-        req.open("GET", "http://example.com/example.txt");
+        return fetch("http://example.com/example.txt").then(passListener, failListener);
       }
-      req.send();
     }
 
-    run(true, function() {
-      run(false, finish);
-    });
+    return run(true, xhr)
+      .then(() => run(false, xhr))
+      .then(() => run(true, fetch))
+      .then(() => run(false, fetch))
+      .catch(err => {
+        browser.test.fail(`Error: ${err} :: ${err.stack}`);
+        browser.test.notifyFail("permission_xhr");
+      });
   }
 
   function background(runTests) {
-    browser.runtime.onMessage.addListener(([msg, ...args]) => {
-      if (msg == "assertTrue") {
-        browser.test.assertTrue(...args);
-      } else {
-        browser.test.sendMessage(msg, ...args);
-      }
-    });
-
-    runTests("bg", browser.test.assertTrue.bind(browser.test), () => {
+    runTests("bg").then(() => {
       browser.test.notifyPass("permission_xhr");
     });
   }
 
   let extensionData = {
     background: `(${background})(${runTests})`,
     manifest: {
       permissions: ["http://example.com/"],
       content_scripts: [{
         "matches": ["http://mochi.test/*/file_permission_xhr.html"],
         "js": ["content.js"],
       }],
     },
     files: {
       "content.js": "new " + function(runTests) {
-        function assertTrue(...args) {
-          browser.runtime.sendMessage(["assertTrue", ...args]);
-        }
-        runTests("content", assertTrue, () => {
+        runTests("content").then(() => {
+          window.wrappedJSObject.privilegedFetch = fetch;
           window.wrappedJSObject.privilegedXHR = XMLHttpRequest;
 
-          window.addEventListener("message", function rcv(event) {
-            if (event.data === "test") {
-              return;
+          window.addEventListener("message", function rcv({data}) {
+            switch (data.msg) {
+              case "test":
+                break;
+
+              case "assertTrue":
+                browser.test.assertTrue(data.condition, data.description);
+                break;
+
+              case "finish":
+                window.removeEventListener("message", rcv, false);
+                browser.test.sendMessage("content-script-finished");
+                break;
             }
-            window.removeEventListener("message", rcv, false);
-            browser.runtime.sendMessage(["assertTrue", event.data, "content page can't use XHR"]);
-            browser.runtime.sendMessage(["content-script-finished"]);
           }, false);
           window.postMessage("test", "*");
         });
       } + `(${runTests})`,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -1,10 +1,8 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  * Contains functions shared by different Login Manager components.
  *
  * This JavaScript module exists in order to share code between the different
@@ -64,18 +62,17 @@ this.LoginHelper = {
 
   /**
    * Due to the way the signons2.txt file is formatted, we need to make
    * sure certain field values or characters do not cause the file to
    * be parsed incorrectly.  Reject hostnames that we can't store correctly.
    *
    * @throws String with English message in case validation failed.
    */
-  checkHostnameValue: function (aHostname)
-  {
+  checkHostnameValue(aHostname) {
     // Nulls are invalid, as they don't round-trip well.  Newlines are also
     // invalid for any field stored as plaintext, and a hostname made of a
     // single dot cannot be stored in the legacy format.
     if (aHostname == "." ||
         aHostname.indexOf("\r") != -1 ||
         aHostname.indexOf("\n") != -1 ||
         aHostname.indexOf("\0") != -1) {
       throw new Error("Invalid hostname");
@@ -84,18 +81,17 @@ this.LoginHelper = {
 
   /**
    * Due to the way the signons2.txt file is formatted, we need to make
    * sure certain field values or characters do not cause the file to
    * be parsed incorrectly.  Reject logins that we can't store correctly.
    *
    * @throws String with English message in case validation failed.
    */
-  checkLoginValues: function (aLogin)
-  {
+  checkLoginValues(aLogin) {
     function badCharacterPresent(l, c)
     {
       return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
               (l.httpRealm     && l.httpRealm.indexOf(c)     != -1) ||
                                   l.hostname.indexOf(c)      != -1  ||
                                   l.usernameField.indexOf(c) != -1  ||
                                   l.passwordField.indexOf(c) != -1);
     }
@@ -216,20 +212,18 @@ this.LoginHelper = {
    *        Existing nsILoginInfo object to modify.
    * @param aNewLoginData
    *        The new login values, either as nsILoginInfo or nsIProperyBag.
    *
    * @return The newly created nsILoginInfo object.
    *
    * @throws String with English message in case validation failed.
    */
-  buildModifiedLogin: function (aOldStoredLogin, aNewLoginData)
-  {
-    function bagHasProperty(aPropName)
-    {
+  buildModifiedLogin(aOldStoredLogin, aNewLoginData) {
+    function bagHasProperty(aPropName) {
       try {
         aNewLoginData.getProperty(aPropName);
         return true;
       } catch (ex) { }
       return false;
     }
 
     aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
@@ -640,15 +634,30 @@ this.LoginHelper = {
         toDeletes.add(Path.join(profileDir, signonFile));
         Services.prefs.clearUserPref(pref);
       } catch (e) {}
     }
 
     for (let file of toDeletes) {
       File.remove(file);
     }
+  },
+
+  /**
+   * Returns true if the user has a master password set and false otherwise.
+   */
+  isMasterPasswordSet() {
+    let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].
+                   getService(Ci.nsIPKCS11ModuleDB);
+    let slot = secmodDB.findSlotByName("");
+    if (!slot) {
+      return false;
+    }
+    let hasMP = slot.status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED &&
+                slot.status != Ci.nsIPKCS11Slot.SLOT_READY;
+    return hasMP;
   }
 };
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let logger = LoginHelper.createLogger("LoginHelper");
   return logger;
 });
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -42,34 +42,34 @@ XPCOMUtils.defineLazyGetter(this, "log",
 var gEnabled, gAutofillForms, gStoreWhenAutocompleteOff;
 
 var observer = {
   QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
                                           Ci.nsIFormSubmitObserver,
                                           Ci.nsISupportsWeakReference]),
 
   // nsIFormSubmitObserver
-  notify : function (formElement, aWindow, actionURI) {
+  notify(formElement, aWindow, actionURI) {
     log("observer notified for form submission.");
 
     // We're invoked before the content's |onsubmit| handlers, so we
     // can grab form data before it might be modified (see bug 257781).
 
     try {
       let formLike = FormLikeFactory.createFromForm(formElement);
       LoginManagerContent._onFormSubmit(formLike);
     } catch (e) {
       log("Caught error in onFormSubmit(", e.lineNumber, "):", e.message);
       Cu.reportError(e);
     }
 
     return true; // Always return true, or form submit will be canceled.
   },
 
-  onPrefChange : function() {
+  onPrefChange() {
     gEnabled = Services.prefs.getBoolPref("signon.rememberSignons");
     gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms");
     gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff");
   },
 };
 
 Services.obs.addObserver(observer, "earlyformsubmit", false);
 var prefBranch = Services.prefs.getBranch("signon.");
@@ -78,32 +78,32 @@ prefBranch.addObserver("", observer.onPr
 observer.onPrefChange(); // read initial values
 
 
 function messageManagerFromWindow(win) {
   return win.QueryInterface(Ci.nsIInterfaceRequestor)
             .getInterface(Ci.nsIWebNavigation)
             .QueryInterface(Ci.nsIDocShell)
             .QueryInterface(Ci.nsIInterfaceRequestor)
-            .getInterface(Ci.nsIContentFrameMessageManager)
+            .getInterface(Ci.nsIContentFrameMessageManager);
 }
 
 // This object maps to the "child" process (even in the single-process case).
 var LoginManagerContent = {
 
   __formFillService : null, // FormFillController, for username autocompleting
   get _formFillService() {
     if (!this.__formFillService)
       this.__formFillService =
                       Cc["@mozilla.org/satchel/form-fill-controller;1"].
                       getService(Ci.nsIFormFillController);
     return this.__formFillService;
   },
 
-  _getRandomId: function() {
+  _getRandomId() {
     return Cc["@mozilla.org/uuid-generator;1"]
              .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   },
 
   _messages: [ "RemoteLogins:loginsFound",
                "RemoteLogins:loginsAutoCompleted" ],
 
   /**
@@ -131,17 +131,17 @@ var LoginManagerContent = {
   _deferredPasswordAddedTasksByRootElement: new WeakMap(),
 
   // Map from form login requests to information about that request.
   _requests: new Map(),
 
   // Number of outstanding requests to each manager.
   _managers: new Map(),
 
-  _takeRequest: function(msg) {
+  _takeRequest(msg) {
     let data = msg.data;
     let request = this._requests.get(data.requestId);
 
     this._requests.delete(data.requestId);
 
     let count = this._managers.get(msg.target);
     if (--count === 0) {
       this._managers.delete(msg.target);
@@ -150,17 +150,17 @@ var LoginManagerContent = {
         msg.target.removeMessageListener(message, this);
     } else {
       this._managers.set(msg.target, count);
     }
 
     return request;
   },
 
-  _sendRequest: function(messageManager, requestData,
+  _sendRequest(messageManager, requestData,
                          name, messageData) {
     let count;
     if (!(count = this._managers.get(messageManager))) {
       this._managers.set(messageManager, 1);
 
       for (let message of this._messages)
         messageManager.addMessageListener(message, this);
     } else {
@@ -173,17 +173,17 @@ var LoginManagerContent = {
     messageManager.sendAsyncMessage(name, messageData);
 
     let deferred = Promise.defer();
     requestData.promise = deferred;
     this._requests.set(requestId, requestData);
     return deferred.promise;
   },
 
-  receiveMessage: function (msg, window) {
+  receiveMessage(msg, window) {
     if (msg.name == "RemoteLogins:fillForm") {
       this.fillForm({
         topDocument: window.document,
         loginFormOrigin: msg.data.loginFormOrigin,
         loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
         recipes: msg.data.recipes,
         inputElement: msg.objects.inputElement,
       });
@@ -219,17 +219,17 @@ var LoginManagerContent = {
 
   /**
    * Get relevant logins and recipes from the parent
    *
    * @param {HTMLFormElement} form - form to get login data for
    * @param {Object} options
    * @param {boolean} options.showMasterPassword - whether to show a master password prompt
    */
-  _getLoginDataFromParent: function(form, options) {
+  _getLoginDataFromParent(form, options) {
     let doc = form.ownerDocument;
     let win = doc.defaultView;
 
     let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
     if (!formOrigin) {
       return Promise.reject("_getLoginDataFromParent: A form origin is required");
     }
     let actionOrigin = LoginUtils._getActionOrigin(form);
@@ -242,17 +242,17 @@ var LoginManagerContent = {
                         actionOrigin: actionOrigin,
                         options: options };
 
     return this._sendRequest(messageManager, requestData,
                              "RemoteLogins:findLogins",
                              messageData);
   },
 
-  _autoCompleteSearchAsync: function(aSearchString, aPreviousResult,
+  _autoCompleteSearchAsync(aSearchString, aPreviousResult,
                                      aElement, aRect) {
     let doc = aElement.ownerDocument;
     let form = FormLikeFactory.createFromField(aElement);
     let win = doc.defaultView;
 
     let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
     let actionOrigin = LoginUtils._getActionOrigin(form);
 
@@ -496,29 +496,29 @@ var LoginManagerContent = {
       form = FormLikeFactory.createFromField(inputElement);
       if (inputElement.type == "password") {
         clobberUsername = false;
       }
     }
     this._fillForm(form, true, clobberUsername, true, true, loginsFound, recipes, options);
   },
 
-  loginsFound: function({ form, loginsFound, recipes }) {
+  loginsFound({ form, loginsFound, recipes }) {
     let doc = form.ownerDocument;
     let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView);
 
     this._fillForm(form, autofillForm, false, false, false, loginsFound, recipes);
   },
 
   /*
    * onUsernameInput
    *
    * Listens for DOMAutoComplete and blur events on an input field.
    */
-  onUsernameInput : function(event) {
+  onUsernameInput(event) {
     if (!event.isTrusted)
       return;
 
     if (!gEnabled)
       return;
 
     var acInputField = event.target;
 
@@ -619,17 +619,17 @@ var LoginManagerContent = {
    * "theLoginField". If not null, the form is apparently a
    * change-password field, with oldPasswordField containing the password
    * that is being changed.
    *
    * Note that even though we can create a FormLike from a text field,
    * this method will only return a non-null usernameField if the
    * FormLike has a password field.
    */
-  _getFormFields : function (form, isSubmission, recipes) {
+  _getFormFields(form, isSubmission, recipes) {
     var usernameField = null;
     var pwFields = null;
     var fieldOverrideRecipe = LoginRecipesContent.getFieldOverrides(recipes, form);
     if (fieldOverrideRecipe) {
       var pwOverrideField = LoginRecipesContent.queryLoginField(
         form,
         fieldOverrideRecipe.passwordSelector
       );
@@ -849,17 +849,17 @@ var LoginManagerContent = {
    *                               overwritten
    * @param {bool} userTriggered is an indication of whether this filling was triggered by
    *                             the user
    * @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form
    * @param {Set} recipes that could be used to affect how the form is filled
    * @param {Object} [options = {}] is a list of options for this method.
             - [inputElement] is an optional target input element we want to fill
    */
-  _fillForm : function (form, autofillForm, clobberUsername, clobberPassword,
+  _fillForm(form, autofillForm, clobberUsername, clobberPassword,
                         userTriggered, foundLogins, recipes, {inputElement} = {}) {
     let ignoreAutocomplete = true;
     const AUTOFILL_RESULT = {
       FILLED: 0,
       NO_PASSWORD_FIELD: 1,
       PASSWORD_DISABLED_READONLY: 2,
       NO_LOGINS_FIT: 3,
       NO_SAVED_LOGINS: 4,
@@ -1226,44 +1226,44 @@ UserAutoCompleteResult.prototype = {
 
   // Interfaces from idl...
   searchString : null,
   searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
   defaultIndex : -1,
   errorDescription : "",
   matchCount : 0,
 
-  getValueAt : function (index) {
+  getValueAt(index) {
     if (index < 0 || index >= this.logins.length)
       throw new Error("Index out of range.");
 
     return this.logins[index].username;
   },
 
-  getLabelAt: function(index) {
+  getLabelAt(index) {
     return this.getValueAt(index);
   },
 
-  getCommentAt : function (index) {
+  getCommentAt(index) {
     return "";
   },
 
-  getStyleAt : function (index) {
+  getStyleAt(index) {
     return "";
   },
 
-  getImageAt : function (index) {
+  getImageAt(index) {
     return "";
   },
 
-  getFinalCompleteValueAt : function (index) {
+  getFinalCompleteValueAt(index) {
     return this.getValueAt(index);
   },
 
-  removeValueAt : function (index, removeFromDB) {
+  removeValueAt(index, removeFromDB) {
     if (index < 0 || index >= this.logins.length)
         throw new Error("Index out of range.");
 
     var [removedLogin] = this.logins.splice(index, 1);
 
     this.matchCount--;
     if (this.defaultIndex > this.logins.length)
       this.defaultIndex--;
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -962,25 +962,23 @@ LoginManagerPrompter.prototype = {
         eventCallback: function (topic) {
           switch (topic) {
             case "showing":
               currentNotification = this;
               chromeDoc.getElementById("password-notification-username")
                        .addEventListener("input", onInput);
               chromeDoc.getElementById("password-notification-password")
                        .addEventListener("input", onInput);
+              let toggleBtn = chromeDoc.getElementById("password-notification-visibilityToggle");
+
               if (Services.prefs.getBoolPref("signon.rememberSignons.visibilityToggle")) {
-                chromeDoc.getElementById("password-notification-visibilityToggle")
-                         .addEventListener("command", onVisibilityToggle);
-                chromeDoc.getElementById("password-notification-visibilityToggle")
-                         .setAttribute("label", togglePasswordLabel);
-                chromeDoc.getElementById("password-notification-visibilityToggle")
-                         .setAttribute("accesskey", togglePasswordAccessKey);
-                chromeDoc.getElementById("password-notification-visibilityToggle")
-                         .removeAttribute("hidden");
+                toggleBtn.addEventListener("command", onVisibilityToggle);
+                toggleBtn.setAttribute("label", togglePasswordLabel);
+                toggleBtn.setAttribute("accesskey", togglePasswordAccessKey);
+                toggleBtn.setAttribute("hidden", LoginHelper.isMasterPasswordSet());
               }
               break;
             case "shown":
               writeDataToUI();
               break;
             case "dismissed":
               readDataFromUI();
               // Fall through.
--- a/toolkit/components/passwordmgr/test/LoginTestUtils.jsm
+++ b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm
@@ -244,8 +244,52 @@ this.LoginTestUtils.recipes = {
     if (!LoginManagerParent.recipeParentPromise) {
       return null;
     }
     return LoginManagerParent.recipeParentPromise.then((recipeParent) => {
       return recipeParent;
     });
   },
 };
+
+this.LoginTestUtils.masterPassword = {
+  masterPassword: "omgsecret!",
+
+  _set(enable) {
+    let oldPW, newPW;
+    if (enable) {
+      oldPW = "";
+      newPW = this.masterPassword;
+    } else {
+      oldPW = this.masterPassword;
+      newPW = "";
+    }
+
+    let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"]
+                     .getService(Ci.nsIPKCS11ModuleDB);
+    let slot = secmodDB.findSlotByName("");
+    if (!slot) {
+      throw new Error("Can't find slot");
+    }
+
+    // Set master password. Note that this does not log you in, so the next
+    // invocation of pwmgr can trigger a MP prompt.
+    let pk11db = Cc["@mozilla.org/security/pk11tokendb;1"]
+                   .getService(Ci.nsIPK11TokenDB);
+    let token = pk11db.findTokenByName("");
+    if (slot.status == Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED) {
+      dump("MP initialized to " + newPW + "\n");
+      token.initPassword(newPW);
+    } else {
+      token.checkPassword(oldPW);
+      dump("MP change from " + oldPW + " to " + newPW + "\n");
+      token.changePassword(oldPW, newPW);
+    }
+  },
+
+  enable() {
+    this._set(true);
+  },
+
+  disable() {
+    this._set(false);
+  },
+};
--- a/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
@@ -46,17 +46,16 @@ add_task(function* test_empty_password()
                     "Can't add a login with a null or empty password.",
                     "Should fail for an empty password");
     });
 });
 
 /**
  * Test that the doorhanger password field shows plain or * text
  * when the checkbox is checked.
- *
  */
 add_task(function* test_toggle_password() {
   yield BrowserTestUtils.withNewTab({
       gBrowser,
       url: "https://example.com/browser/toolkit/components/" +
            "passwordmgr/test/browser/form_basic.html",
     }, function* (browser) {
       // Submit the form in the content page with the credentials from the test
@@ -80,8 +79,44 @@ add_task(function* test_toggle_password(
       Assert.ok(toggleCheckbox.checked);
       Assert.equal(passwordTextbox.type, "", "Password textbox changed to plain text");
 
       yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
       Assert.ok(!toggleCheckbox.checked);
       Assert.equal(passwordTextbox.type, "password", "Password textbox changed to * text");
     });
 });
+
+/**
+ * Test that the doorhanger password toggle checkbox is disabled
+ * when the master password is set.
+ */
+add_task(function* test_checkbox_disabled_if_has_master_password() {
+  yield BrowserTestUtils.withNewTab({
+      gBrowser,
+      url: "https://example.com/browser/toolkit/components/" +
+           "passwordmgr/test/browser/form_basic.html",
+    }, function* (browser) {
+      // Submit the form in the content page with the credentials from the test
+      // case. This will cause the doorhanger notification to be displayed.
+      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+                                                       "popupshown");
+
+      LoginTestUtils.masterPassword.enable();
+
+      yield ContentTask.spawn(browser, null, function* () {
+        let doc = content.document;
+        doc.getElementById("form-basic-username").value = "username";
+        doc.getElementById("form-basic-password").value = "p";
+        doc.getElementById("form-basic").submit();
+      });
+      yield promiseShown;
+
+      let notificationElement = PopupNotifications.panel.childNodes[0];
+      let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+      let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+      Assert.equal(passwordTextbox.type, "password", "Password textbox should show * text");
+      Assert.ok(toggleCheckbox.getAttribute("hidden"), "checkbox is hidden when master password is set");
+    });
+
+  LoginTestUtils.masterPassword.disable();
+});
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -4447,16 +4447,23 @@
     "bug_numbers": [1275114],
     "alert_emails": ["gijs@mozilla.com"],
     "expires_in_version": "53",
     "kind": "enumerated",
     "n_values": 15,
     "releaseChannelCollection": "opt-out",
     "description": "The browser that was the default on the initial profile migration. The values correspond to the internal browser ID (see MigrationUtils.jsm)"
   },
+  "FX_STARTUP_EXTERNAL_CONTENT_HANDLER": {
+    "bug_numbers": [1276027],
+    "alert_emails": ["jaws@mozilla.com"],
+    "expires_in_version": "53",
+    "kind": "count",
+    "description": "Count how often the browser is opened as an external app handler. This is generally used when the browser is set as the default browser."
+  },
   "INPUT_EVENT_RESPONSE_MS": {
     "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
     "bug_numbers": [1235908],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 50,
     "description": "Time (ms) from the Input event being created to the end of it being handled"
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -4702,27 +4702,25 @@ enum {
   // kE10sDisabledForMacGfx = 5, was removed in bug 1068674.
   kE10sDisabledForBidi = 6,
   kE10sDisabledForAddons = 7,
   kE10sForceDisabled = 8,
   kE10sDisabledForXPAcceleration = 9,
   kE10sDisabledForOperatingSystem = 10,
 };
 
-#if defined(XP_WIN) || defined(XP_MACOSX)
 const char* kAccessibilityLastRunDatePref = "accessibility.lastLoadDate";
 const char* kAccessibilityLoadedLastSessionPref = "accessibility.loadedInLastSession";
 
 static inline uint32_t
 PRTimeToSeconds(PRTime t_usec)
 {
   PRTime usec_per_sec = PR_USEC_PER_SEC;
   return uint32_t(t_usec /= usec_per_sec);
 }
-#endif // XP_WIN || XP_MACOSX
 
 const char* kForceEnableE10sPref = "browser.tabs.remote.force-enable";
 const char* kForceDisableE10sPref = "browser.tabs.remote.force-disable";
 
 
 uint32_t
 MultiprocessBlockPolicy() {
   if (gMultiprocessBlockPolicyInitialized) {
@@ -4743,17 +4741,17 @@ MultiprocessBlockPolicy() {
 #endif
 
   if (addonsCanDisable && disabledByAddons) {
     gMultiprocessBlockPolicy = kE10sDisabledForAddons;
     return gMultiprocessBlockPolicy;
   }
 
   bool disabledForA11y = false;
-#if defined(XP_WIN) || defined(XP_MACOSX)
+
   /**
    * Avoids enabling e10s if accessibility has recently loaded. Performs the
    * following checks:
    * 1) Checks a pref indicating if a11y loaded in the last session. This pref
    * is set in nsBrowserGlue.js. If a11y was loaded in the last session we
    * do not enable e10s in this session.
    * 2) Accessibility stores a last run date (PR_IntervalNow) when it is
    * initialized (see nsBaseWidget.cpp). We check if this pref exists and
@@ -4770,17 +4768,16 @@ MultiprocessBlockPolicy() {
     uint32_t now = PRTimeToSeconds(PR_Now());
     uint32_t difference = now - a11yRunDate;
     if (difference > ONE_WEEK_IN_SECONDS || !a11yRunDate) {
       Preferences::ClearUser(kAccessibilityLastRunDatePref);
     } else {
       disabledForA11y = true;
     }
   }
-#endif // XP_WIN || XP_MACOSX
 
   if (disabledForA11y) {
     gMultiprocessBlockPolicy = kE10sDisabledForAccessibility;
     return gMultiprocessBlockPolicy;
   }
 
   /**
    * Avoids enabling e10s for Windows XP users on the release channel.
--- a/widget/nsBaseWidget.cpp
+++ b/widget/nsBaseWidget.cpp
@@ -169,17 +169,17 @@ nsBaseWidget::nsBaseWidget()
 , mOriginalBounds(nullptr)
 , mClipRectCount(0)
 , mSizeMode(nsSizeMode_Normal)
 , mPopupLevel(ePopupLevelTop)
 , mPopupType(ePopupTypeAny)
 , mUpdateCursor(true)
 , mUseAttachedEvents(false)
 , mIMEHasFocus(false)
-#if defined(XP_WIN) || defined(XP_MACOSX)
+#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_GTK)
 , mAccessibilityInUseFlag(false)
 #endif
 {
 #ifdef NOISY_WIDGET_LEAKS
   gNumWidgets++;
   printf("WIDGETS+ = %d\n", gNumWidgets);
 #endif
 
@@ -237,17 +237,17 @@ WidgetShutdownObserver::Unregister()
 {
   if (mRegistered) {
     mWidget = nullptr;
     nsContentUtils::UnregisterShutdownObserver(this);
     mRegistered = false;
   }
 }
 
-#if defined(XP_WIN) || defined(XP_MACOSX)
+#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_GTK)
 // defined in nsAppRunner.cpp
 extern const char* kAccessibilityLastRunDatePref;
 
 static inline uint32_t
 PRTimeToSeconds(PRTime t_usec)
 {
   PRTime usec_per_sec = PR_USEC_PER_SEC;
   return uint32_t(t_usec /= usec_per_sec);
@@ -1887,17 +1887,17 @@ nsBaseWidget::GetRootAccessible()
   nsPresContext* presContext = presShell->GetPresContext();
   NS_ENSURE_TRUE(presContext->GetContainerWeak(), nullptr);
 
   // Accessible creation might be not safe so use IsSafeToRunScript to
   // make sure it's not created at unsafe times.
   nsCOMPtr<nsIAccessibilityService> accService =
     services::GetAccessibilityService();
   if (accService) {
-#if defined(XP_WIN) || defined(XP_MACOSX)
+#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_GTK)
     if (!mAccessibilityInUseFlag) {
       mAccessibilityInUseFlag = true;
       uint32_t now = PRTimeToSeconds(PR_Now());
       Preferences::SetInt(kAccessibilityLastRunDatePref, now);
     }
 #endif
     return accService->GetRootDocumentAccessible(presShell, nsContentUtils::IsSafeToRunScript());
   }
--- a/widget/nsBaseWidget.h
+++ b/widget/nsBaseWidget.h
@@ -583,17 +583,17 @@ protected:
   nsPopupType       mPopupType;
   SizeConstraints   mSizeConstraints;
 
   RefPtr<CompositorWidgetProxy> mCompositorWidgetProxy;
 
   bool              mUpdateCursor;
   bool              mUseAttachedEvents;
   bool              mIMEHasFocus;
-#if defined(XP_WIN) || defined(XP_MACOSX)
+#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_GTK)
   bool              mAccessibilityInUseFlag;
 #endif
   static nsIRollupListener* gRollupListener;
 
   // the last rolled up popup. Only set this when an nsAutoRollup is in scope,
   // so it can be cleared automatically.
   static nsIContent* mLastRollup;