Merge mozilla-central to inbound
authorarthur.iakab <aiakab@mozilla.com>
Tue, 20 Nov 2018 18:49:12 +0200
changeset 503762 89d78de871e77d052cfa10523f0d459cba524080
parent 503761 584f730e1865b8dd3512ae68ac7350a9abdf07e2 (current diff)
parent 503654 8eff0a4f5d8f4442ce233d492185a90c460846ef (diff)
child 503763 da6b96f7f1210aa1e6d5aefe6a7b4db8fdd644fa
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to inbound
devtools/client/themes/images/devtools-components/checkbox.svg
modules/libpref/init/all.js
toolkit/components/places/mozIAsyncLivemarks.idl
toolkit/components/places/nsLivemarkService.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1314,60 +1314,29 @@ var gBrowserInit = {
     }
 
     // Call this after we set attributes that might change toolbars' computed
     // text color.
     ToolbarIconColor.init();
   },
 
   onDOMContentLoaded() {
-    gBrowser = window._gBrowser;
-    delete window._gBrowser;
-    gBrowser.init();
-
+    // This needs setting up before we create the first remote browser.
     window.docShell.treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIXULWindow)
           .XULBrowserWindow = window.XULBrowserWindow;
     window.browserDOMWindow = new nsBrowserAccess();
+
+    gBrowser = window._gBrowser;
+    delete window._gBrowser;
+    gBrowser.init();
+
     BrowserWindowTracker.track(window);
 
-    let initBrowser = gBrowser.initialBrowser;
-
-    // remoteType and sameProcessAsFrameLoader are passed through to
-    // updateBrowserRemoteness as part of an options object, which itself defaults
-    // to an empty object. So defaulting them to undefined here will cause the
-    // default behavior in updateBrowserRemoteness if they don't get set.
-    let isRemote = gMultiProcessBrowser;
-    let remoteType;
-    let sameProcessAsFrameLoader;
-
-    let tabArgument = this.getTabToAdopt();
-    if (tabArgument) {
-      // The window's first argument is a tab if and only if we are swapping tabs.
-      // We must set the browser's usercontextid before updateBrowserRemoteness(),
-      // so that the newly created remote tab child has the correct usercontextid.
-      if (tabArgument.hasAttribute("usercontextid")) {
-        initBrowser.setAttribute("usercontextid",
-                                 tabArgument.getAttribute("usercontextid"));
-      }
-
-      let linkedBrowser = tabArgument.linkedBrowser;
-      if (linkedBrowser) {
-        remoteType = linkedBrowser.remoteType;
-        isRemote = remoteType != E10SUtils.NOT_REMOTE;
-        sameProcessAsFrameLoader = linkedBrowser.frameLoader;
-      }
-      initBrowser.removeAttribute("blank");
-    }
-
-    gBrowser.updateBrowserRemoteness(initBrowser, isRemote, {
-      remoteType, sameProcessAsFrameLoader,
-    });
-
     gNavToolbox.palette = document.getElementById("BrowserToolbarPalette");
     gNavToolbox.palette.remove();
     let areas = CustomizableUI.areas;
     areas.splice(areas.indexOf(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL), 1);
     for (let area of areas) {
       let node = document.getElementById(area);
       CustomizableUI.registerToolbarNode(node);
     }
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -275,20 +275,58 @@ window._gBrowser = {
   },
 
   get selectedBrowser() {
     return this._selectedBrowser;
   },
 
   _setupInitialBrowserAndTab() {
     // See browser.js for the meaning of window.arguments.
+    // Bug 1485961 covers making this more sane.
     let userContextId = window.arguments && window.arguments[6];
-    let browser = this._createBrowser({uriIsAboutBlank: false, userContextId});
+
+    // We default to a remote content browser, except if:
+    // - e10s is disabled.
+    // - there's a parent process opener (e.g. parent process about: page) for
+    //   the content tab.
+    let remoteType;
+    if (gMultiProcessBrowser && !window.hasOpenerForInitialContentBrowser) {
+      remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
+    } else {
+      remoteType = E10SUtils.NOT_REMOTE;
+    }
+
+    // We only need sameProcessAsFrameLoader in the case where we're passed a tab
+    let sameProcessAsFrameLoader;
+    let tabArgument = gBrowserInit.getTabToAdopt();
+    if (tabArgument) {
+      // The window's first argument is a tab if and only if we are swapping tabs.
+      // We must set the browser's usercontextid so that the newly created remote
+      // tab child has the correct usercontextid.
+      if (tabArgument.hasAttribute("usercontextid")) {
+        userContextId = parseInt(tabArgument.getAttribute("usercontextid"), 10);
+      }
+
+      let linkedBrowser = tabArgument.linkedBrowser;
+      if (linkedBrowser) {
+        remoteType = linkedBrowser.remoteType;
+        sameProcessAsFrameLoader = linkedBrowser.frameLoader;
+      }
+    }
+    let createOptions = {
+      uriIsAboutBlank: false,
+      userContextId,
+      sameProcessAsFrameLoader,
+      remoteType,
+    };
+    let browser = this._createBrowser(createOptions);
     browser.setAttribute("primary", "true");
-    browser.setAttribute("blank", "true");
+    if (!tabArgument) {
+      browser.setAttribute("blank", "true");
+    }
     if (gBrowserAllowScriptsToCloseInitialTabs) {
       browser.setAttribute("allowscriptstoclose", "true");
     }
     browser.droppedLinkHandler = handleDroppedLink;
     browser.loadURI = _loadURI.bind(null, browser);
 
     let uniqueId = this._generateUniquePanelID();
     let panel = this.getPanel(browser);
@@ -308,16 +346,18 @@ window._gBrowser = {
       tab.setAttribute("usercontextid", userContextId);
       ContextualIdentityService.setTabStyle(tab);
     }
 
     this._tabForBrowser.set(browser, tab);
 
     this._appendStatusPanel();
 
+    // Only necessary because of pageloader talos tests which access this.
+    // Bug 1508171 covers removing this.
     this.initialBrowser = browser;
 
     let autoScrollPopup = browser._createAutoScrollPopup();
     autoScrollPopup.id = "autoscroller";
     document.getElementById("mainPopupSet").appendChild(autoScrollPopup);
     browser.setAttribute("autoscrollpopup", autoScrollPopup.id);
     this._autoScrollPopup = autoScrollPopup;
 
--- a/browser/components/places/content/bookmarkProperties.js
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -91,18 +91,16 @@ var BookmarkPropertiesPanel = {
   _action: null,
   _itemType: null,
   _uri: null,
   _title: "",
   _URIs: [],
   _keyword: "",
   _postData: null,
   _charSet: "",
-  _feedURI: null,
-  _siteURI: null,
 
   _defaultInsertionPoint: null,
   _hiddenRows: [],
 
   /**
    * This method returns the correct label for the dialog's "accept"
    * button based on the variant of the dialog.
    */
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -922,25 +922,25 @@ PlacesController.prototype = {
   setDataTransfer: function PC_setDataTransfer(aEvent) {
     let dt = aEvent.dataTransfer;
 
     let result = this._view.result;
     let didSuppressNotifications = result.suppressNotifications;
     if (!didSuppressNotifications)
       result.suppressNotifications = true;
 
-    function addData(type, index, feedURI) {
-      let wrapNode = PlacesUtils.wrapNode(node, type, feedURI);
+    function addData(type, index) {
+      let wrapNode = PlacesUtils.wrapNode(node, type);
       dt.mozSetDataAt(type, wrapNode, index);
     }
 
-    function addURIData(index, feedURI) {
-      addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI);
-      addData(PlacesUtils.TYPE_UNICODE, index, feedURI);
-      addData(PlacesUtils.TYPE_HTML, index, feedURI);
+    function addURIData(index) {
+      addData(PlacesUtils.TYPE_X_MOZ_URL, index);
+      addData(PlacesUtils.TYPE_UNICODE, index);
+      addData(PlacesUtils.TYPE_HTML, index);
     }
 
     try {
       let nodes = this._view.draggableSelection;
       for (let i = 0; i < nodes.length; ++i) {
         var node = nodes[i];
 
         // This order is _important_! It controls how this and other
--- a/browser/components/resistfingerprinting/test/mochitest/test_pointer_event.html
+++ b/browser/components/resistfingerprinting/test/mochitest/test_pointer_event.html
@@ -19,16 +19,18 @@ https://bugzilla.mozilla.org/show_bug.cg
   SimpleTest.waitForExplicitFinish();
 
   var target0 = window.document.getElementById("target0");
   var target1 = window.document.getElementById("target1");
   var utils = SpecialPowers.Ci.nsIDOMWindowUtils;
 
   // A helper function to check that whether the pointer is spoofed correctly.
   function checkPointerEvent(aEvent) {
+    is(aEvent.pointerId, utils.DEFAULT_MOUSE_POINTER_ID,
+       "The spoofed pointer event should always have the mouse pointer id.");
     is(aEvent.width, 1, "The spoofed pointer event should always have width as 1.");
     is(aEvent.height, 1, "The spoofed pointer event should always have width as 1.");
     if (aEvent.buttons === 0) {
       is(aEvent.pressure, 0.0,
          "The spoofed pointer event should have pressure as 0.0 if it is not in a active buttons state.");
     } else {
       is(aEvent.pressure, 0.5,
          "The spoofed pointer event should have pressure as 0.5 if it is in a active buttons state.");
@@ -106,41 +108,91 @@ https://bugzilla.mozilla.org/show_bug.cg
     synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: MouseEvent.MOZ_SOURCE_PEN });
     synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource: MouseEvent.MOZ_SOURCE_PEN });
     synthesizeMouse(target1, 5, 5, { type: "mousemove", inputSource: MouseEvent.MOZ_SOURCE_PEN });
 
     await Promise.all(eventPromises);
   }
 
   // A test for gotpointercapture and lostpointercapture events.
+  // We would also test releasePointerCapture for only accepting spoofed pointer
+  // Id here.
   async function doTestForPointerCapture() {
+    // We test for both mouse and touch to see whether the capture events are
+    // filed properly. We don't check pen here since it won't file capture
+    // events.
+    let inputSources = [ MouseEvent.MOZ_SOURCE_MOUSE,
+                         MouseEvent.MOZ_SOURCE_TOUCH ];
+
+    for (let inputSource of inputSources) {
+      function eventHandler(event) {
+        checkPointerEvent(event);
+        if (event.type === "pointerdown") {
+          target0.setPointerCapture(event.pointerId);
+        } else if (event.type === "pointermove") {
+          if (inputSource === MouseEvent.MOZ_SOURCE_TOUCH) {
+            try {
+              target0.releasePointerCapture(utils.DEFAULT_TOUCH_POINTER_ID);
+              ok(false, "The releasePointerCapture should fail here, but it is not.");
+            } catch (e) {
+              ok(true, "The releasePointerCapture fails properly.");
+            }
+          }
+          target0.releasePointerCapture(event.pointerId);
+        }
+      }
+
+      let eventPromises = [
+        promiseForEvent("pointerover", eventHandler),
+        promiseForEvent("pointerenter", eventHandler),
+        promiseForEvent("pointerdown", eventHandler),
+        promiseForEvent("gotpointercapture", eventHandler),
+        promiseForEvent("pointermove", eventHandler),
+        promiseForEvent("lostpointercapture", eventHandler),
+        promiseForEvent("pointerup", eventHandler),
+        promiseForEvent("pointerout", eventHandler),
+        promiseForEvent("pointerleave", eventHandler),
+      ];
+
+      synthesizeMouse(target0, 5, 5, { type: "mousedown", inputSource });
+      synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource });
+      synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource });
+      synthesizeMouse(target1, 5, 5, { type: "mousemove", inputSource });
+
+      await Promise.all(eventPromises);
+    }
+  }
+
+  // A test for setPointerCapture() for only accepting spoofed pointer id.
+  async function doTestForSetPointerCapture() {
     function eventHandler(event) {
       checkPointerEvent(event);
       if (event.type === "pointerdown") {
-        target0.setPointerCapture(event.pointerId);
-      } else if (event.type === "pointermove") {
-        target0.releasePointerCapture(event.pointerId);
+        try {
+          target0.setPointerCapture(utils.DEFAULT_TOUCH_POINTER_ID);
+          ok(false, "The setPointerCapture should fail here, but it is not.");
+        } catch (e) {
+          ok(true, "The setPointerCapture fails properly.");
+        }
       }
     }
 
     let eventPromises = [
       promiseForEvent("pointerover", eventHandler),
       promiseForEvent("pointerenter", eventHandler),
       promiseForEvent("pointerdown", eventHandler),
-      promiseForEvent("gotpointercapture", eventHandler),
       promiseForEvent("pointermove", eventHandler),
-      promiseForEvent("lostpointercapture", eventHandler),
       promiseForEvent("pointerup", eventHandler),
       promiseForEvent("pointerout", eventHandler),
       promiseForEvent("pointerleave", eventHandler),
     ];
 
-    synthesizeMouse(target0, 5, 5, { type: "mousedown", inputSource: MouseEvent.MOZ_SOURCE_TOUCH, pressure: 0.75 });
-    synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: MouseEvent.MOZ_SOURCE_TOUCH, pressure: 0.75 });
-    synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource: MouseEvent.MOZ_SOURCE_TOUCH, pressure: 0.75 });
+    synthesizeMouse(target0, 5, 5, { type: "mousedown", inputSource: MouseEvent.MOZ_SOURCE_TOUCH });
+    synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: MouseEvent.MOZ_SOURCE_TOUCH });
+    synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource: MouseEvent.MOZ_SOURCE_TOUCH });
 
     await Promise.all(eventPromises);
   }
 
   // A test for assuring that script generated events won't be spoofed.
   function doTestNoSpoofingForScriptGeneratedEvent() {
 
     return new Promise(resolve => {
@@ -170,16 +222,17 @@ https://bugzilla.mozilla.org/show_bug.cg
     });
   }
 
   async function doTests() {
     await doTestForTouchPointerEvent();
     await doTestForTouchPointerCancelEvent();
     await doTestForPenPointerEvent();
     await doTestForPointerCapture();
+    await doTestForSetPointerCapture();
     await doTestNoSpoofingForScriptGeneratedEvent();
 
     SimpleTest.finish();
   }
 
   SimpleTest.waitForFocus(() => {
     SpecialPowers.pushPrefEnv({"set": [["dom.w3c_pointer_events.enabled", true],
                                       ["privacy.resistFingerprinting", true]]},
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -236,17 +236,16 @@
 @RESPATH@/components/simpleServices.js
 @RESPATH@/components/pluginGlue.manifest
 @RESPATH@/components/ProcessSingleton.manifest
 @RESPATH@/components/MainProcessSingleton.js
 @RESPATH@/components/ContentProcessSingleton.js
 @RESPATH@/components/nsURLFormatter.manifest
 @RESPATH@/components/nsURLFormatter.js
 @RESPATH@/components/toolkitplaces.manifest
-@RESPATH@/components/nsLivemarkService.js
 @RESPATH@/components/nsTaggingService.js
 @RESPATH@/components/UnifiedComplete.js
 @RESPATH@/components/nsPlacesExpiration.js
 @RESPATH@/components/PageIconProtocolHandler.js
 @RESPATH@/components/PlacesCategoriesStarter.js
 @RESPATH@/components/ColorAnalyzer.js
 @RESPATH@/components/PageThumbsStorageService.js
 @RESPATH@/components/mozProtocolHandler.js
--- a/build/pgo/server-locations.txt
+++ b/build/pgo/server-locations.txt
@@ -113,20 +113,16 @@ https://untrusted.example.com:443      p
 https://expired.example.com:443        privileged,cert=expired
 https://requestclientcert.example.com:443         privileged,clientauth=request
 https://requireclientcert.example.com:443         privileged,clientauth=require
 https://mismatch.expired.example.com:443	privileged,cert=expired
 https://mismatch.untrusted.example.com:443	privileged,cert=untrusted
 https://untrusted-expired.example.com:443	privileged,cert=untrustedandexpired
 https://mismatch.untrusted-expired.example.com:443	privileged,cert=untrustedandexpired
 
-# This is here so that we don't load the default live bookmark over
-# the network in every test suite.
-http://fxfeeds.mozilla.com:80
-
 # Prevent safebrowsing tests from hitting the network for its-a-trap.html and
 # its-an-attack.html.
 http://www.itisatrap.org:80
 https://www.itisatrap.org:443
 
 #
 # These are subdomains of <ält.example.org>.
 #
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -178,23 +178,23 @@ button {
 }
 
 .service-worker-multi-process .update-button {
   margin: 5px 0;
 }
 
 .warning {
   display: inline-block;
-  width: 16px;
-  height: 16px;
-  vertical-align: -2px;
-  margin-inline-end: 6px;
+  width: 12px;
+  height: 12px;
+  vertical-align: 0;
+  margin-inline-end: 8px;
   background-image: url(chrome://devtools/skin/images/alert.svg);
   background-repeat: no-repeat;
-  background-size: cover;
+  background-size: contain;
   -moz-context-properties: fill;
   fill: #d7b600;
 }
 
 .addons-install-error .warning,
 .service-worker-multi-process .warning {
   /* The warning icon can be hard to see on red / yellow backgrounds, this turns the icon
   to a black icon. */
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -87,21 +87,24 @@ devtools.jar:
     skin/dark-theme.css (themes/dark-theme.css)
     skin/light-theme.css (themes/light-theme.css)
     skin/toolbars.css (themes/toolbars.css)
     skin/toolbox.css (themes/toolbox.css)
     skin/tooltips.css (themes/tooltips.css)
     skin/images/accessibility.svg (themes/images/accessibility.svg)
     skin/images/add.svg (themes/images/add.svg)
     skin/images/alert.svg (themes/images/alert.svg)
+    skin/images/arrow.svg (themes/images/arrow.svg)
+    skin/images/arrow-big.svg (themes/images/arrow-big.svg)
     skin/images/arrowhead-left.svg (themes/images/arrowhead-left.svg)
     skin/images/arrowhead-right.svg (themes/images/arrowhead-right.svg)
     skin/images/arrowhead-down.svg (themes/images/arrowhead-down.svg)
     skin/images/arrowhead-up.svg (themes/images/arrowhead-up.svg)
     skin/images/breadcrumbs-divider.svg (themes/images/breadcrumbs-divider.svg)
+    skin/images/checkbox.svg (themes/images/checkbox.svg)
     skin/images/filters.svg (themes/images/filters.svg)
     skin/images/filter-swatch.svg (themes/images/filter-swatch.svg)
     skin/images/aboutdebugging-collapse-icon.svg (themes/images/aboutdebugging-collapse-icon.svg)
     skin/images/aboutdebugging-connect-icon.svg (themes/images/aboutdebugging-connect-icon.svg)
     skin/images/aboutdebugging-firefox-aurora.svg (themes/images/aboutdebugging-firefox-aurora.svg)
     skin/images/aboutdebugging-firefox-beta.svg (themes/images/aboutdebugging-firefox-beta.svg)
     skin/images/aboutdebugging-firefox-logo.svg (themes/images/aboutdebugging-firefox-logo.svg)
     skin/images/aboutdebugging-firefox-nightly.svg (themes/images/aboutdebugging-firefox-nightly.svg)
@@ -134,16 +137,17 @@ devtools.jar:
     skin/images/command-rulers.svg (themes/images/command-rulers.svg)
     skin/images/command-measure.svg (themes/images/command-measure.svg)
     skin/images/command-noautohide.svg (themes/images/command-noautohide.svg)
     skin/images/command-chevron.svg (themes/images/command-chevron.svg)
     skin/markup.css (themes/markup.css)
     skin/images/editor-error.png (themes/images/editor-error.png)
     skin/images/breakpoint.svg (themes/images/breakpoint.svg)
     skin/webconsole.css (themes/webconsole.css)
+    skin/images/webconsole/error.svg (themes/images/webconsole/error.svg)
     skin/images/webconsole/info.svg (themes/images/webconsole/info.svg)
     skin/images/webconsole/input.svg (themes/images/webconsole/input.svg)
     skin/images/webconsole/return.svg (themes/images/webconsole/return.svg)
     skin/images/webconsole/jump.svg (themes/images/webconsole/jump.svg)
     skin/images/breadcrumbs-scrollbutton.svg (themes/images/breadcrumbs-scrollbutton.svg)
     skin/animation.css (themes/animation.css)
     skin/canvasdebugger.css (themes/canvasdebugger.css)
     skin/perf.css (themes/perf.css)
@@ -262,12 +266,8 @@ devtools.jar:
     content/netmonitor/src/assets/icons/play.svg (netmonitor/src/assets/icons/play.svg)
     content/netmonitor/src/assets/icons/shield.svg (netmonitor/src/assets/icons/shield.svg)
     content/netmonitor/index.html (netmonitor/index.html)
     content/netmonitor/src/assets/styles/StatusCode.css (netmonitor/src/assets/styles/StatusCode.css)
 
     # Application panel
     content/application/index.html (application/index.html)
 
-    # Devtools-components
-    skin/images/arrow.svg (themes/images/arrow.svg)
-    skin/images/arrow-big.svg (themes/images/arrow-big.svg)
-    skin/images/devtools-components/checkbox.svg (themes/images/devtools-components/checkbox.svg)
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -702,17 +702,17 @@ checkbox:-moz-focusring {
 
 .theme-checkbox {
   display: inline-block;
   width: 14px;
   height: 14px;
   border: 0;
   padding: 0;
   outline: none;
-  background-image: url("chrome://devtools/skin/images/devtools-components/checkbox.svg");
+  background-image: url("chrome://devtools/skin/images/checkbox.svg");
   background-position: center;
   background-repeat: no-repeat;
   background-size: 14px 14px;
   /* Using fill to paint the border, and stroke for the tick */
   -moz-context-properties: fill, stroke;
   fill: rgba(0,0,0,.3);
   stroke: transparent;
 }
--- a/devtools/client/themes/images/alert.svg
+++ b/devtools/client/themes/images/alert.svg
@@ -1,6 +1,6 @@
 <!-- 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" viewBox="0 0 12 12" fill="context-fill #0b0b0b">
-  <path d="M1.12 9.4L5 1.6c.41-.81 1.58-.81 2 0l3.88 7.78a1.11 1.11 0 0 1-1 1.61H2.11a1.11 1.11 0 0 1-1-1.6zM6 7.8a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm0-4.4a1 1 0 0 0-1 1V6a1 1 0 1 0 2 0V4.4a1 1 0 0 0-1-1z" />
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
+  <path fill="context-fill #0b0b0b" fill-rule="evenodd" d="M6 0a1 1 0 0 1 .89.54l5 9.6A1 1 0 0 1 11 11.6H1a1 1 0 0 1-.89-1.46l5-9.6A1 1 0 0 1 6 0zm-.25 8a.75.75 0 0 0-.75.75v.5c0 .41.34.75.75.75h.5c.41 0 .75-.34.75-.75v-.5A.75.75 0 0 0 6.25 8h-.5zM7 3.7a1 1 0 1 0-2 0v2.6a1 1 0 1 0 2 0V3.7z" />
 </svg>
rename from devtools/client/themes/images/devtools-components/checkbox.svg
rename to devtools/client/themes/images/checkbox.svg
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/webconsole/error.svg
@@ -0,0 +1,6 @@
+<!-- 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" viewBox="0 0 12 12" fill="context-fill #0b0b0b">
+  <path fill-rule="evenodd" d="M12 6A6 6 0 1 1 0 6a6 6 0 0 1 12 0zM5.75 8a.75.75 0 0 0-.75.75v.5c0 .41.34.75.75.75h.5c.41 0 .75-.34.75-.75v-.5A.75.75 0 0 0 6.25 8h-.5zM5 6c0 .54.46 1 1 1s1-.46 1-1V3.15c0-.54-.46-1-1-1s-1 .46-1 1V6z"/>
+</svg>
--- a/devtools/client/themes/images/webconsole/info.svg
+++ b/devtools/client/themes/images/webconsole/info.svg
@@ -1,6 +1,7 @@
 <!-- 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" viewBox="0 0 12 12" fill="context-fill #0b0b0b">
-  <path d="M6 1a5 5 0 1 1 0 10A5 5 0 0 1 6 1zm0 1.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm0 2.8a1 1 0 0 0-1 1V8a1 1 0 1 0 2 0V6.4a1 1 0 0 0-1-1z"/>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
+  <circle cx="6" cy="6" r="5.5" fill="none" opacity="0.8" stroke="context-fill"/>
+  <path d="M7 8a1 1 0 0 1-2 0V6.25a1 1 0 0 1 2 0V8zM7 3.5c0 .45-.45.8-1 .8s-1-.35-1-.8c0-.45.45-.8 1-.8s1 .35 1 .8z" fill="context-fill #0b0b0b"/>
 </svg>
--- a/devtools/client/themes/images/webconsole/input.svg
+++ b/devtools/client/themes/images/webconsole/input.svg
@@ -1,7 +1,6 @@
 <!-- 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" viewBox="0 0 12 12" fill="context-fill #0b0b0b">
-  <path d="M11.04 5.46L7.29 1.71a.75.75 0 0 0-1.06 1.06L9.45 6 6.23 9.21a.75.75 0 1 0 1.06 1.06l3.75-3.75c.3-.3.3-.77 0-1.06z"/>
-  <path d="M6.04 5.46L2.29 1.71a.75.75 0 0 0-1.06 1.06L4.45 6 1.23 9.21a.75.75 0 1 0 1.06 1.06l3.75-3.75c.3-.3.3-.77 0-1.06z"/>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
+  <path fill="none" stroke="context-fill" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M1.7 1.85L5.85 6L1.7 10.15 M6.7 1.85L10.85 6L6.7 10.15"/>
 </svg>
--- a/devtools/client/themes/images/webconsole/jump.svg
+++ b/devtools/client/themes/images/webconsole/jump.svg
@@ -1,15 +1,7 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
-    <title>Group 7</title>
-    <desc>Created with Sketch.</desc>
-    <defs></defs>
-    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="Group-7" transform="translate(1.000000, 1.000000)">
-            <g id="Group-6">
-                <circle id="Oval-2" stroke="#0274E8" cx="8" cy="8" r="8"></circle>
-                <polygon id="Triangle" fill="#0274E8" transform="translate(9.000000, 8.000000) rotate(90.000000) translate(-9.000000, -8.000000) " points="9 5 13 11 5 11"></polygon>
-            </g>
-        </g>
-    </g>
-</svg>
\ No newline at end of file
+<!-- 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 viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
+  <circle fill="none" stroke="#0274E8" cx="9" cy="9" r="8"/>
+  <path fill="#0274E8" d="M13 9l-6 4V5z"/>
+</svg>
--- a/devtools/client/themes/images/webconsole/return.svg
+++ b/devtools/client/themes/images/webconsole/return.svg
@@ -1,6 +1,6 @@
 <!-- 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" viewBox="0 0 12 12" fill="context-fill #0b0b0b">
-  <path d="M3.87 5.25h5.88a.75.75 0 1 1 0 1.5H3.89l2.46 2.46a.75.75 0 1 1-1.06 1.06L1.54 6.52a.75.75 0 0 1 0-1.06l3.75-3.75a.75.75 0 0 1 1.06 1.06L3.87 5.25z"/>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
+  <path fill="none" stroke="context-fill" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.15 10.15L2 6L6.15 1.85 M3 6H10"/>
 </svg>
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -190,23 +190,23 @@ a {
 }
 
 .message.result > .icon {
   color: var(--console-output-icon-medium-color);
   background-image: url(chrome://devtools/skin/images/webconsole/return.svg);
 }
 
 .message.info > .icon {
-  color: var(--console-output-icon-medium-color);
+  color: var(--console-output-icon-strong-color);
   background-image: url(chrome://devtools/skin/images/webconsole/info.svg);
 }
 
 .message.error > .icon {
   color: var(--console-output-icon-error-color);
-  background-image: url(chrome://devtools/skin/images/alert.svg);
+  background-image: url(chrome://devtools/skin/images/webconsole/error.svg);
 }
 
 .message.warn > .icon {
   color: var(--console-output-icon-warning-color);
   background-image: url(chrome://devtools/skin/images/alert.svg);
 }
 
 .message.navigationMarker > .icon {
--- a/devtools/shared/webconsole/js-property-provider.js
+++ b/devtools/shared/webconsole/js-property-provider.js
@@ -285,54 +285,65 @@ function JSPropertyProvider({
   }
 
   const completionPart = lastStatement;
   const lastDotIndex = completionPart.lastIndexOf(".");
   const lastOpeningBracketIndex = isElementAccess ? completionPart.lastIndexOf("[") : -1;
   const lastCompletionCharIndex = Math.max(lastDotIndex, lastOpeningBracketIndex);
   const startQuoteRegex = /^('|"|`)/;
 
+  // AST representation of the expression before the last access char (`.` or `[`).
+  let astExpression;
+  let matchProp = completionPart.slice(lastCompletionCharIndex + 1).trimLeft();
+
   // Catch literals like [1,2,3] or "foo" and return the matches from
   // their prototypes.
   // Don't run this is a worker, migrating to acorn should allow this
   // to run in a worker - Bug 1217198.
   if (!isWorker && lastCompletionCharIndex > 0) {
     const parser = new Parser();
     parser.logExceptions = false;
     const parsedExpression = completionPart.slice(0, lastCompletionCharIndex);
     const syntaxTree = parser.get(parsedExpression);
     const lastTree = syntaxTree.getLastSyntaxTree();
     const lastBody = lastTree && lastTree.AST.body[lastTree.AST.body.length - 1];
 
     // Finding the last expression since we've sliced up until the dot.
     // If there were parse errors this won't exist.
     if (lastBody) {
-      const expression = lastBody.expression;
+      astExpression = lastBody.expression;
       let matchingObject;
 
-      if (expression.type === "ArrayExpression") {
+      if (astExpression.type === "ArrayExpression") {
         matchingObject = Array.prototype;
-      } else if (expression.type === "Literal" && typeof expression.value === "string") {
+      } else if (
+        astExpression.type === "Literal" &&
+        typeof astExpression.value === "string"
+      ) {
         matchingObject = String.prototype;
-      } else if (expression.type === "Literal" && Number.isFinite(expression.value)) {
+      } else if (
+        astExpression.type === "Literal" &&
+        Number.isFinite(astExpression.value)
+      ) {
         // The parser rightfuly indicates that we have a number in some cases (e.g. `1.`),
         // but we don't want to return Number proto properties in that case since
         // the result would be invalid (i.e. `1.toFixed()` throws).
         // So if the expression value is an integer, it should not end with `{Number}.`
         // (but the following are fine: `1..`, `(1.).`).
         if (
-          !Number.isInteger(expression.value) ||
+          !Number.isInteger(astExpression.value) ||
           /\d[^\.]{0}\.$/.test(completionPart) === false
         ) {
           matchingObject = Number.prototype;
+        } else {
+          return null;
         }
       }
 
       if (matchingObject) {
-        const matchProp = completionPart.slice(lastCompletionCharIndex + 1).trimLeft();
         let search = matchProp;
 
         let elementAccessQuote;
         if (isElementAccess && startQuoteRegex.test(matchProp)) {
           elementAccessQuote = matchProp[0];
           search = matchProp.replace(startQuoteRegex, "");
         }
 
@@ -346,25 +357,36 @@ function JSPropertyProvider({
           matchProp,
           matches: props,
         };
       }
     }
   }
 
   // We are completing a variable / a property lookup.
-  const properties = completionPart.split(".");
-  let matchProp;
-  if (isElementAccess) {
-    const lastPart = properties[properties.length - 1];
-    const openBracketIndex = lastPart.lastIndexOf("[");
-    matchProp = lastPart.substr(openBracketIndex + 1);
-    properties[properties.length - 1] = lastPart.substring(0, openBracketIndex);
+  let properties = [];
+
+  if (astExpression) {
+    if (lastCompletionCharIndex > -1) {
+      properties = getPropertiesFromAstExpression(astExpression);
+
+      if (properties === null) {
+        return null;
+      }
+    }
   } else {
-    matchProp = properties.pop().trimLeft();
+    properties = completionPart.split(".");
+    if (isElementAccess) {
+      const lastPart = properties[properties.length - 1];
+      const openBracketIndex = lastPart.lastIndexOf("[");
+      matchProp = lastPart.substr(openBracketIndex + 1);
+      properties[properties.length - 1] = lastPart.substring(0, openBracketIndex);
+    } else {
+      matchProp = properties.pop().trimLeft();
+    }
   }
 
   let search = matchProp;
   let elementAccessQuote;
   if (isElementAccess && startQuoteRegex.test(search)) {
     elementAccessQuote = search[0];
     search = search.replace(startQuoteRegex, "");
   }
@@ -411,18 +433,21 @@ function JSPropertyProvider({
 
   if (!isObjectUsable(obj)) {
     return null;
   }
 
   // We get the rest of the properties recursively starting from the
   // Debugger.Object that wraps the first property
   for (let [index, prop] of properties.entries()) {
-    prop = prop.trim();
-    if (!prop) {
+    if (typeof prop === "string") {
+      prop = prop.trim();
+    }
+
+    if (prop === undefined || prop === null || prop === "") {
       return null;
     }
 
     if (!invokeUnsafeGetter && DevToolsUtils.isUnsafeGetter(obj, prop)) {
       // If the unsafe getter is not the last property access of the input, bail out as
       // things might get complex.
       if (index !== properties.length - 1) {
         return null;
@@ -444,32 +469,63 @@ function JSPropertyProvider({
       obj = DevToolsUtils.getProperty(obj, prop, invokeUnsafeGetter);
     }
 
     if (!isObjectUsable(obj)) {
       return null;
     }
   }
 
+  const prepareReturnedObject = matches => {
+    if (isElementAccess) {
+      // If it's an element access, we need to wrap properties in quotes (either the one
+      // the user already typed, or `"`).
+      matches = wrapMatchesInQuotes(matches, elementAccessQuote);
+    }
+    return {isElementAccess, matchProp, matches};
+  };
+
   // If the final property is a primitive
   if (typeof obj != "object") {
-    return {
-      isElementAccess,
-      matchProp,
-      matches: getMatchedProps(obj, search),
-    };
+    return prepareReturnedObject(getMatchedProps(obj, search));
   }
 
-  let matches = getMatchedPropsInDbgObject(obj, search);
-  if (isElementAccess) {
-    // If it's an element access, we need to wrap properties in quotes (either the one
-    // the user already typed, or `"`).
-    matches = wrapMatchesInQuotes(matches, elementAccessQuote);
+  return prepareReturnedObject(getMatchedPropsInDbgObject(obj, search));
+}
+
+/**
+ * @param {Object} ast: An AST representing a property access (e.g. `foo.bar["baz"].x`)
+ * @returns {Array|null} An array representing the property access
+ *                       (e.g. ["foo", "bar", "baz", "x"]).
+ */
+function getPropertiesFromAstExpression(ast) {
+  let result = [];
+  if (!ast) {
+    return result;
   }
-  return {isElementAccess, matchProp, matches};
+  const {type, property, object, name} = ast;
+  if (type === "ThisExpression") {
+    result.unshift("this");
+  } else if (type === "Identifier" && name) {
+    result.unshift(name);
+  } else if (type === "MemberExpression") {
+    if (property) {
+      if (property.type === "Identifier" && property.name) {
+        result.unshift(property.name);
+      } else if (property.type === "Literal") {
+        result.unshift(property.value);
+      }
+    }
+    if (object) {
+      result = (getPropertiesFromAstExpression(object) || []).concat(result);
+    }
+  } else {
+    return null;
+  }
+  return result;
 }
 
 function wrapMatchesInQuotes(matches, quote = `"`) {
   return new Set([...matches].map(p =>
     `${quote}${p.replace(new RegExp(`${quote}`, "g"), `\\${quote}`)}${quote}`));
 }
 
 /**
--- a/devtools/shared/webconsole/test/unit/test_js_property_provider.js
+++ b/devtools/shared/webconsole/test/unit/test_js_property_provider.js
@@ -93,16 +93,33 @@ function runChecks(dbgObject, environmen
   let results = propertyProvider("t");
   test_has_result(results, "this");
 
   if (dbgObject != null) {
     info("Test that suggestions are given for 'this.'");
     results = propertyProvider("this.");
     test_has_result(results, "testObject");
 
+    info("Test that suggestions are given for '(this).'");
+    results = propertyProvider("(this).");
+    test_has_result(results, "testObject");
+
+    info("Test that suggestions are given for deep 'this' properties access");
+    results = propertyProvider("(this).testObject.propA.");
+    test_has_result(results, "shift");
+
+    results = propertyProvider("(this).testObject.propA[");
+    test_has_result(results, `"shift"`);
+
+    results = propertyProvider("(this)['testObject']['propA'][");
+    test_has_result(results, `"shift"`);
+
+    results = propertyProvider("(this).testObject['propA'].");
+    test_has_result(results, "shift");
+
     info("Test that no suggestions are given for 'this.this'");
     results = propertyProvider("this.this");
     test_has_no_results(results);
   }
 
   info("Testing lexical scope issues (Bug 1207868)");
   results = propertyProvider("foobar");
   test_has_result(results, "foobar");
@@ -173,41 +190,53 @@ function runChecks(dbgObject, environmen
   test_has_no_results(results);
   results = propertyProvider("`foo`");
   test_has_no_results(results);
   results = propertyProvider("[1,2,3]");
   test_has_no_results(results);
   results = propertyProvider("[1,2,3].\n'foo'");
   test_has_no_results(results);
 
-  info("Test that suggestions are not given for numeric literals.");
-  results = propertyProvider("1.");
-  Assert.equal(null, results);
-
   info("Test that suggestions are not given for index that's out of bounds.");
   results = propertyProvider("testArray[10].");
   Assert.equal(null, results);
 
-  info("Test that no suggestions are given if an index is not numerical "
-       + "somewhere in the chain.");
-  results = propertyProvider("testArray[0]['propC'][0].");
-  Assert.equal(null, results);
-
-  results = propertyProvider("testObject['propA'][0].");
-  Assert.equal(null, results);
-
-  results = propertyProvider("testArray[0]['propC'].");
-  Assert.equal(null, results);
-
+  info("Test that invalid element access syntax does not return anything");
   results = propertyProvider("testArray[][1].");
   Assert.equal(null, results);
 
-  info("Test that suggestions are not given if there is an hyphen in the chain.");
+  info("Test that deep element access works.");
+  results = propertyProvider("testObject['propA'][0].");
+  test_has_result(results, "propB");
+
+  results = propertyProvider("testArray[1]['propC'].");
+  test_has_result(results, "shift");
+
+  results = propertyProvider("testArray[1].propC[0][");
+  test_has_result(results, `"trim"`);
+
+  results = propertyProvider("testArray[1].propC[0].");
+  test_has_result(results, "trim");
+
+  info("Test that suggestions are displayed when variable is wrapped in parens");
+  results = propertyProvider("(testObject)['propA'][0].");
+  test_has_result(results, "propB");
+
+  results = propertyProvider("(testArray)[1]['propC'].");
+  test_has_result(results, "shift");
+
+  results = propertyProvider("(testArray)[1].propC[0][");
+  test_has_result(results, `"trim"`);
+
+  results = propertyProvider("(testArray)[1].propC[0].");
+  test_has_result(results, "trim");
+
+  info("Test that suggestions are given if there is an hyphen in the chain.");
   results = propertyProvider("testHyphenated['prop-A'].");
-  Assert.equal(null, results);
+  test_has_result(results, "trim");
 
   info("Test that we have suggestions for generators.");
   const gen1Result = Cu.evalInSandbox("gen1.next().value", sandbox);
   results = propertyProvider("gen1.");
   test_has_result(results, "next");
   info("Test that the generator next() was not executed");
   const gen1NextResult = Cu.evalInSandbox("gen1.next().value", sandbox);
   Assert.equal(gen1Result + 1, gen1NextResult);
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -93,17 +93,16 @@
 #include "nsBindingManager.h"
 #include "nsXBLBinding.h"
 #include "nsPIDOMWindow.h"
 #include "nsPIBoxObject.h"
 #include "mozilla/dom/DOMRect.h"
 #include "nsSVGUtils.h"
 #include "nsLayoutUtils.h"
 #include "nsGkAtoms.h"
-#include "nsContentUtils.h"
 #include "ChildIterator.h"
 
 #include "nsIDOMEventListener.h"
 #include "nsIWebNavigation.h"
 #include "nsIBaseWindow.h"
 #include "nsIWidget.h"
 
 #include "nsNodeInfoManager.h"
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -38,16 +38,17 @@
 #include "mozilla/dom/DOMTokenListSupportedTokens.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/Nullable.h"
 #include "mozilla/dom/PointerEventHandler.h"
 #include "mozilla/UniquePtr.h"
 #include "Units.h"
 #include "DOMIntersectionObserver.h"
+#include "nsContentUtils.h"
 
 class mozAutoDocUpdate;
 class nsIFrame;
 class nsIMozBrowserFrame;
 class nsIURI;
 class nsIScrollableFrame;
 class nsAttrValueOrString;
 class nsContentList;
@@ -1180,16 +1181,21 @@ public:
 
   void InsertAdjacentText(const nsAString& aWhere,
                           const nsAString& aData,
                           ErrorResult& aError);
 
   void SetPointerCapture(int32_t aPointerId, ErrorResult& aError)
   {
     bool activeState = false;
+    if (nsContentUtils::ShouldResistFingerprinting(GetComposedDoc()) &&
+        aPointerId != PointerEventHandler::GetSpoofedPointerIdForRFP()) {
+      aError.Throw(NS_ERROR_DOM_INVALID_POINTER_ERR);
+      return;
+    }
     if (!PointerEventHandler::GetPointerInfo(aPointerId, activeState)) {
       aError.Throw(NS_ERROR_DOM_INVALID_POINTER_ERR);
       return;
     }
     if (!IsInUncomposedDoc()) {
       aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
       return;
     }
@@ -1202,16 +1208,21 @@ public:
     if (!activeState) {
       return;
     }
     PointerEventHandler::SetPointerCaptureById(aPointerId, this);
   }
   void ReleasePointerCapture(int32_t aPointerId, ErrorResult& aError)
   {
     bool activeState = false;
+    if (nsContentUtils::ShouldResistFingerprinting(GetComposedDoc()) &&
+        aPointerId != PointerEventHandler::GetSpoofedPointerIdForRFP()) {
+      aError.Throw(NS_ERROR_DOM_INVALID_POINTER_ERR);
+      return;
+    }
     if (!PointerEventHandler::GetPointerInfo(aPointerId, activeState)) {
       aError.Throw(NS_ERROR_DOM_INVALID_POINTER_ERR);
       return;
     }
     if (HasPointerCapture(aPointerId)) {
       PointerEventHandler::ReleasePointerCaptureById(aPointerId);
     }
   }
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -2425,16 +2425,22 @@ nsGlobalWindowInner::GetInstallTrigger()
 }
 
 nsIDOMWindowUtils*
 nsGlobalWindowInner::GetWindowUtils(ErrorResult& aRv)
 {
   FORWARD_TO_OUTER_OR_THROW(WindowUtils, (), aRv, nullptr);
 }
 
+bool
+nsGlobalWindowInner::HasOpenerForInitialContentBrowser()
+{
+  FORWARD_TO_OUTER(HasOpenerForInitialContentBrowser, (), false);
+}
+
 nsGlobalWindowInner::CallState
 nsGlobalWindowInner::ShouldReportForServiceWorkerScopeInternal(const nsACString& aScope,
                                                                bool* aResultOut)
 {
   MOZ_DIAGNOSTIC_ASSERT(aResultOut);
 
   // First check to see if this window is controlled.  If so, then we have
   // found a match and are done.
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -993,16 +993,18 @@ public:
   bool ShouldReportForServiceWorkerScope(const nsAString& aScope);
 
   void PropagateClearSiteDataReload(const nsACString& aOrigin);
 
   already_AddRefed<mozilla::dom::InstallTriggerImpl> GetInstallTrigger();
 
   nsIDOMWindowUtils* GetWindowUtils(mozilla::ErrorResult& aRv);
 
+  bool HasOpenerForInitialContentBrowser();
+
   void UpdateTopInnerWindow();
 
   virtual bool IsInSyncOperation() override
   {
     return GetExtantDoc() && GetExtantDoc()->IsInSyncOperation();
   }
 
 protected:
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -2261,21 +2261,21 @@ nsGlobalWindowOuter::SetOpenerWindow(nsP
                "aOriginalOpener is true, but not first call to "
                "SetOpenerWindow!");
   NS_ASSERTION(aOpener || !aOriginalOpener,
                "Shouldn't set mHadOriginalOpener if aOpener is null");
 
   mOpener = opener.forget();
   NS_ASSERTION(mOpener || !aOpener, "Opener must support weak references!");
 
-  if (mDocShell && aOpener) {
+  if (mDocShell) {
     // TODO(farre): Here we really wish to only consider the case
     // where 'aOriginalOpener' is false, and we also really want to
     // move opener entirely to BrowsingContext. See bug 1502330.
-    GetBrowsingContext()->SetOpener(aOpener->GetBrowsingContext());
+    GetBrowsingContext()->SetOpener(aOpener ? aOpener->GetBrowsingContext() : nullptr);
   }
 
   // Check that the js visible opener matches! We currently don't depend on this
   // being true outside of nightly, so we disable the assertion in optimized
   // release / beta builds.
   nsPIDOMWindowOuter* contentOpener = GetSanitizedOpener(aOpener);
 
   // contentOpener is not used when the DIAGNOSTIC_ASSERT is compiled out.
--- a/dom/base/nsGlobalWindowOuter.h
+++ b/dom/base/nsGlobalWindowOuter.h
@@ -724,16 +724,20 @@ public:
                            mozilla::ErrorResult& aError);
   void SetReturnValue(JSContext* aCx, JS::Handle<JS::Value> aReturnValue,
                       nsIPrincipal& aSubjectPrincipal,
                       mozilla::ErrorResult& aError);
 
   already_AddRefed<nsWindowRoot> GetWindowRootOuter();
 
   nsIDOMWindowUtils* WindowUtils();
+  bool HasOpenerForInitialContentBrowser()
+  {
+    return !!mOpenerForInitialContentBrowser;
+  }
 
   virtual bool IsInSyncOperation() override
   {
     return GetExtantDoc() && GetExtantDoc()->IsInSyncOperation();
   }
 
   void ParentWindowChanged()
   {
--- a/dom/bindings/BindingUtils.cpp
+++ b/dom/bindings/BindingUtils.cpp
@@ -1538,17 +1538,17 @@ struct IdToIndexComparator
   }
 };
 
 static const PropertyInfo*
 XrayFindOwnPropertyInfo(JSContext* cx, JS::Handle<jsid> id,
                         const NativeProperties* nativeProperties)
 {
   if (MOZ_UNLIKELY(nativeProperties->iteratorAliasMethodIndex >= 0) &&
-      id == SYMBOL_TO_JSID(JS::GetWellKnownSymbol(cx, JS::SymbolCode::iterator))) {
+      id.get() == SYMBOL_TO_JSID(JS::GetWellKnownSymbol(cx, JS::SymbolCode::iterator))) {
     return nativeProperties->MethodPropertyInfos() +
            nativeProperties->iteratorAliasMethodIndex;
   }
 
   size_t idx;
   const uint16_t* sortedPropertyIndices = nativeProperties->sortedPropertyIndices;
   const PropertyInfo* propertyInfos = nativeProperties->PropertyInfos();
 
@@ -1887,25 +1887,25 @@ XrayResolveOwnProperty(JSContext* cx, JS
     }
 
     // For non-global instance Xrays there are no other properties, so return
     // here for them.
     if (type != eGlobalInstance) {
       return true;
     }
   } else if (type == eInterface) {
-    if (id == GetJSIDByIndex(cx, XPCJSContext::IDX_PROTOTYPE)) {
+    if (id.get() == GetJSIDByIndex(cx, XPCJSContext::IDX_PROTOTYPE)) {
       return nativePropertyHooks->mPrototypeID == prototypes::id::_ID_Count ||
              ResolvePrototypeOrConstructor(cx, wrapper, obj,
                                            nativePropertyHooks->mPrototypeID,
                                            JSPROP_PERMANENT | JSPROP_READONLY,
                                            desc, cacheOnHolder);
     }
 
-    if (id == GetJSIDByIndex(cx, XPCJSContext::IDX_ISINSTANCE) &&
+    if (id.get() == GetJSIDByIndex(cx, XPCJSContext::IDX_ISINSTANCE) &&
         DOMIfaceAndProtoJSClass::FromJSClass(js::GetObjectClass(obj))->
           wantsInterfaceHasInstance) {
       cacheOnHolder = true;
       JSNativeWrapper interfaceIsInstanceWrapper = { InterfaceIsInstance,
                                                       nullptr };
       JSObject* funObj = XrayCreateFunction(cx, wrapper,
                                             interfaceIsInstanceWrapper, 1, id);
       if (!funObj) {
@@ -1915,17 +1915,17 @@ XrayResolveOwnProperty(JSContext* cx, JS
       desc.value().setObject(*funObj);
       desc.setAttributes(0);
       desc.object().set(wrapper);
       desc.setSetter(nullptr);
       desc.setGetter(nullptr);
       return true;
     }
 
-    if (id == SYMBOL_TO_JSID(JS::GetWellKnownSymbol(cx, JS::SymbolCode::hasInstance)) &&
+    if (id.get() == SYMBOL_TO_JSID(JS::GetWellKnownSymbol(cx, JS::SymbolCode::hasInstance)) &&
         DOMIfaceAndProtoJSClass::FromJSClass(js::GetObjectClass(obj))->
           wantsInterfaceHasInstance) {
       cacheOnHolder = true;
       JSNativeWrapper interfaceHasInstanceWrapper = { InterfaceHasInstance,
                                                       nullptr };
       JSObject* funObj = XrayCreateFunction(cx, wrapper,
                                             interfaceHasInstanceWrapper, 1, id);
       if (!funObj) {
@@ -1937,17 +1937,17 @@ XrayResolveOwnProperty(JSContext* cx, JS
       desc.object().set(wrapper);
       desc.setSetter(nullptr);
       desc.setGetter(nullptr);
       return true;
     }
   } else {
     MOZ_ASSERT(IsInterfacePrototype(type));
 
-    if (id == GetJSIDByIndex(cx, XPCJSContext::IDX_CONSTRUCTOR)) {
+    if (id.get() == GetJSIDByIndex(cx, XPCJSContext::IDX_CONSTRUCTOR)) {
       return nativePropertyHooks->mConstructorID == constructors::id::_ID_Count ||
              ResolvePrototypeOrConstructor(cx, wrapper, obj,
                                            nativePropertyHooks->mConstructorID,
                                            0, desc, cacheOnHolder);
     }
 
     // The properties for globals live on the instance, so return here as there
     // are no properties on their interface prototype object.
--- a/dom/bindings/DOMJSProxyHandler.h
+++ b/dom/bindings/DOMJSProxyHandler.h
@@ -172,20 +172,21 @@ extern jsid s_length_id;
 // A return value of UINT32_MAX indicates "not an array index".  Note, in
 // particular, that UINT32_MAX itself is not a valid array index in general.
 inline uint32_t
 GetArrayIndexFromId(JSContext* cx, JS::Handle<jsid> id)
 {
   // Much like js::IdIsIndex, except with a fast path for "length" and another
   // fast path for starting with a lowercase ascii char.  Is that second one
   // really needed?  I guess it is because StringIsArrayIndex is out of line...
+  // as of now, use id.get() instead of id otherwise operands mismatch error occurs.
   if (MOZ_LIKELY(JSID_IS_INT(id))) {
     return JSID_TO_INT(id);
   }
-  if (MOZ_LIKELY(id == s_length_id)) {
+  if (MOZ_LIKELY(id.get() == s_length_id)) {
     return UINT32_MAX;
   }
   if (MOZ_UNLIKELY(!JSID_IS_ATOM(id))) {
     return UINT32_MAX;
   }
 
   JSLinearString* str = js::AtomToLinearString(JSID_TO_ATOM(id));
   char16_t s;
--- a/dom/events/PointerEvent.cpp
+++ b/dom/events/PointerEvent.cpp
@@ -153,19 +153,21 @@ PointerEvent::GetPointerType(nsAString& 
     aPointerType.AssignLiteral("mouse");
     return;
   }
 
   ConvertPointerTypeToString(mEvent->AsPointerEvent()->inputSource, aPointerType);
 }
 
 int32_t
-PointerEvent::PointerId()
+PointerEvent::PointerId(CallerType aCallerType)
 {
-  return mEvent->AsPointerEvent()->pointerId;
+  return ShouldResistFingerprinting(aCallerType) ?
+           PointerEventHandler::GetSpoofedPointerIdForRFP() :
+           mEvent->AsPointerEvent()->pointerId;
 }
 
 int32_t
 PointerEvent::Width(CallerType aCallerType)
 {
   return ShouldResistFingerprinting(aCallerType) ?
            1 : mEvent->AsPointerEvent()->mWidth;
 }
--- a/dom/events/PointerEvent.h
+++ b/dom/events/PointerEvent.h
@@ -39,17 +39,17 @@ public:
               const PointerEventInit& aParam,
               ErrorResult& aRv);
 
   static already_AddRefed<PointerEvent>
   Constructor(EventTarget* aOwner,
               const nsAString& aType,
               const PointerEventInit& aParam);
 
-  int32_t PointerId();
+  int32_t PointerId(CallerType aCallerType);
   int32_t Width(CallerType aCallerType);
   int32_t Height(CallerType aCallerType);
   float Pressure(CallerType aCallerType);
   float TangentialPressure(CallerType aCallerType);
   int32_t TiltX(CallerType aCallerType);
   int32_t TiltY(CallerType aCallerType);
   int32_t Twist(CallerType aCallerType);
   bool IsPrimary();
--- a/dom/events/PointerEventHandler.cpp
+++ b/dom/events/PointerEventHandler.cpp
@@ -12,16 +12,18 @@
 
 namespace mozilla {
 
 using namespace dom;
 
 static bool sPointerEventEnabled = true;
 static bool sPointerEventImplicitCapture = false;
 
+Maybe<int32_t> PointerEventHandler::sSpoofedPointerId;
+
 class PointerInfo final
 {
 public:
   uint16_t mPointerType;
   bool mActiveState;
   bool mPrimaryState;
   bool mPreventMouseEventByContent;
   explicit PointerInfo(bool aActiveState, uint16_t aPointerType,
@@ -95,23 +97,26 @@ PointerEventHandler::UpdateActivePointer
   if (!IsPointerEventEnabled() || !aEvent) {
     return;
   }
   switch (aEvent->mMessage) {
   case eMouseEnterIntoWidget:
     // In this case we have to know information about available mouse pointers
     sActivePointersIds->Put(aEvent->pointerId,
                             new PointerInfo(false, aEvent->inputSource, true));
+
+    MaybeCacheSpoofedPointerID(aEvent->inputSource, aEvent->pointerId);
     break;
   case ePointerDown:
     // In this case we switch pointer to active state
     if (WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent()) {
       sActivePointersIds->Put(pointerEvent->pointerId,
                               new PointerInfo(true, pointerEvent->inputSource,
                                               pointerEvent->mIsPrimary));
+      MaybeCacheSpoofedPointerID(pointerEvent->inputSource, pointerEvent->pointerId);
     }
     break;
   case ePointerCancel:
     // pointercancel means a pointer is unlikely to continue to produce pointer
     // events. In that case, we should turn off active state or remove the
     // pointer from active pointers.
   case ePointerUp:
     // In this case we remove information about pointer or turn off active state
@@ -256,16 +261,40 @@ PointerEventHandler::CheckPointerCapture
   if (!aEvent) {
     return;
   }
   MOZ_ASSERT(IsPointerEventEnabled());
   MOZ_ASSERT(aEvent->mClass == ePointerEventClass);
 
   PointerCaptureInfo* captureInfo = GetPointerCaptureInfo(aEvent->pointerId);
 
+  // When fingerprinting resistance is enabled, we need to map other pointer
+  // ids into the spoofed one. We don't have to do the mapping if the capture
+  // info exists for the non-spoofed pointer id because of we won't allow
+  // content to set pointer capture other than the spoofed one. Thus, it must be
+  // from chrome if the capture info exists in this case. And we don't have to
+  // do anything if the pointer id is the same as the spoofed one.
+  if (nsContentUtils::ShouldResistFingerprinting() &&
+      aEvent->pointerId != (uint32_t)GetSpoofedPointerIdForRFP() &&
+      !captureInfo) {
+    PointerCaptureInfo* spoofedCaptureInfo =
+      GetPointerCaptureInfo(GetSpoofedPointerIdForRFP());
+
+    // We need to check the target element is content or chrome. If it is chrome
+    // we don't need to send a capture event since the capture info of the
+    // original pointer id doesn't exist in the case.
+    if (!spoofedCaptureInfo ||
+        (spoofedCaptureInfo->mPendingContent &&
+        spoofedCaptureInfo->mPendingContent->IsInChromeDocument())) {
+      return;
+    }
+
+    captureInfo = spoofedCaptureInfo;
+  }
+
   if (!captureInfo ||
       captureInfo->mPendingContent == captureInfo->mOverrideContent) {
     return;
   }
   // cache captureInfo->mPendingContent since it may be changed in the pointer
   // event listener
   nsIContent* pendingContent = captureInfo->mPendingContent.get();
   if (captureInfo->mOverrideContent) {
@@ -659,9 +688,21 @@ PointerEventHandler::DispatchGotOrLostPo
                                     &localEvent,
                                     aCaptureTarget->GetPrimaryFrame(),
                                     aCaptureTarget, &status);
 
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                        "DispatchGotOrLostPointerCaptureEvent failed");
 }
 
+/* static */ void
+PointerEventHandler::MaybeCacheSpoofedPointerID(uint16_t aInputSource,
+                                                uint32_t aPointerId)
+{
+  if (sSpoofedPointerId.isSome() ||
+      aInputSource != SPOOFED_POINTER_INTERFACE) {
+    return;
+  }
+
+  sSpoofedPointerId.emplace(aPointerId);
+}
+
 } // namespace mozilla
--- a/dom/events/PointerEventHandler.h
+++ b/dom/events/PointerEventHandler.h
@@ -164,27 +164,43 @@ public:
 
   static bool ShouldGeneratePointerEventFromTouch(WidgetGUIEvent* aEvent)
   {
     return aEvent->mMessage == eTouchStart || aEvent->mMessage == eTouchMove ||
            aEvent->mMessage == eTouchEnd || aEvent->mMessage == eTouchCancel ||
            aEvent->mMessage == eTouchPointerCancel;
   }
 
+  static MOZ_ALWAYS_INLINE int32_t GetSpoofedPointerIdForRFP()
+  {
+    return sSpoofedPointerId.valueOr(0);
+  }
+
 private:
   // GetPointerType returns pointer type like mouse, pen or touch for pointer
   // event with pointerId. The return value must be one of
   // MouseEvent_Binding::MOZ_SOURCE_*
   static uint16_t GetPointerType(uint32_t aPointerId);
 
   // GetPointerPrimaryState returns state of attribute isPrimary for pointer
   // event with pointerId
   static bool GetPointerPrimaryState(uint32_t aPointerId);
 
   static void DispatchGotOrLostPointerCaptureEvent(
                 bool aIsGotCapture,
                 const WidgetPointerEvent* aPointerEvent,
                 nsIContent* aCaptureTarget);
+
+  // The cached spoofed pointer ID for fingerprinting resistance. We will use a
+  // mouse pointer id for desktop. For mobile, we should use the touch pointer
+  // id as the spoofed one, and this work will be addressed in Bug 1492775.
+  static Maybe<int32_t> sSpoofedPointerId;
+
+  // A helper function to cache the pointer id of the spoofed interface, we
+  // would only cache the pointer id once. After that, we would always stick to
+  // that pointer id for fingerprinting resistance.
+  static void MaybeCacheSpoofedPointerID(uint16_t aInputSource,
+                                         uint32_t aPointerId);
 };
 
 } // namespace mozilla
 
 #endif // mozilla_PointerEventHandler_h
--- a/dom/webidl/PointerEvent.webidl
+++ b/dom/webidl/PointerEvent.webidl
@@ -8,16 +8,17 @@
  * Portions Copyright 2013 Microsoft Open Technologies, Inc. */
 
 interface WindowProxy;
 
 [Pref="dom.w3c_pointer_events.enabled",
  Constructor(DOMString type, optional PointerEventInit eventInitDict)]
 interface PointerEvent : MouseEvent
 {
+  [NeedsCallerType]
   readonly attribute long pointerId;
 
   [NeedsCallerType]
   readonly attribute long width;
   [NeedsCallerType]
   readonly attribute long height;
   [NeedsCallerType]
   readonly attribute float pressure;
--- a/dom/webidl/Window.webidl
+++ b/dom/webidl/Window.webidl
@@ -362,16 +362,19 @@ partial interface Window {
   [Replaceable]
   readonly attribute InstallTriggerImpl? InstallTrigger;
 
   /**
    * Get the nsIDOMWindowUtils for this window.
    */
   [Constant, Throws, ChromeOnly]
   readonly attribute nsIDOMWindowUtils windowUtils;
+
+  [ChromeOnly]
+  readonly attribute boolean hasOpenerForInitialContentBrowser;
 };
 
 Window implements TouchEventHandlers;
 
 Window implements OnErrorEventHandlerForWindow;
 
 #if defined(MOZ_WIDGET_ANDROID)
 // https://compat.spec.whatwg.org/#windoworientation-interface
--- a/gfx/webrender/src/device/gl.rs
+++ b/gfx/webrender/src/device/gl.rs
@@ -1887,23 +1887,21 @@ impl Device {
         }
 
         // Set up the init state that will be used in link_program.
         let init_state = Some(ProgramInitState {
             base_filename: base_filename.to_owned(),
             sources,
         });
 
-        let u_transform = self.gl.get_uniform_location(pid, "uTransform");
-        let u_mode = self.gl.get_uniform_location(pid, "uMode");
-
+        // Use 0 for the uniforms as they are initialized by link_program.
         let program = Program {
             id: pid,
-            u_transform,
-            u_mode,
+            u_transform: 0,
+            u_mode: 0,
             init_state,
         };
 
         Ok(program)
     }
 
     pub fn bind_shader_samplers<S>(&mut self, program: &Program, bindings: &[(&'static str, S)])
     where
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-262edcb7d48deb72509141e14e1c7d914c1970fb
+85336e717d99c81e06b261a3958fe9ef90212e0c
--- a/js/public/Id.h
+++ b/js/public/Id.h
@@ -33,31 +33,38 @@
 
 // Use 0 for JSID_TYPE_STRING to avoid a bitwise op for atom <-> id conversions.
 #define JSID_TYPE_STRING                 0x0
 #define JSID_TYPE_VOID                   0x2
 #define JSID_TYPE_SYMBOL                 0x4
 #define JSID_TYPE_EMPTY                  0x6
 #define JSID_TYPE_MASK                   0x7
 
-struct jsid
+namespace JS {
+    
+struct PropertyKey
 {
     size_t asBits;
 
-    constexpr jsid() : asBits(JSID_TYPE_VOID) {}
+    constexpr PropertyKey() : asBits(JSID_TYPE_VOID) {}
 
-    static constexpr MOZ_ALWAYS_INLINE jsid fromRawBits(size_t bits) {
-        jsid id;
+    static constexpr MOZ_ALWAYS_INLINE PropertyKey fromRawBits(size_t bits) {
+        PropertyKey id;
         id.asBits = bits;
         return id;
     }
 
-    bool operator==(const jsid& rhs) const { return asBits == rhs.asBits; }
-    bool operator!=(const jsid& rhs) const { return asBits != rhs.asBits; }
+    bool operator==(const PropertyKey& rhs) const { return asBits == rhs.asBits; }
+    bool operator!=(const PropertyKey& rhs) const { return asBits != rhs.asBits; }
 } JS_HAZ_GC_POINTER;
+
+}   // namespace JS
+
+using jsid = JS::PropertyKey;
+
 #define JSID_BITS(id) (id.asBits)
 
 // Avoid using canonical 'id' for jsid parameters since this is a magic word in
 // Objective-C++ which, apparently, wants to be able to #include jsapi.h.
 #define id iden
 
 static MOZ_ALWAYS_INLINE bool
 JSID_IS_STRING(jsid id)
--- a/js/public/TypeDecls.h
+++ b/js/public/TypeDecls.h
@@ -28,19 +28,19 @@ class JSAtom;
 struct JSContext;
 class JSFunction;
 class JSObject;
 struct JSRuntime;
 class JSScript;
 class JSString;
 struct JSFreeOp;
 
-struct jsid;
+namespace JS {
 
-namespace JS {
+struct PropertyKey;
 
 typedef unsigned char Latin1Char;
 
 class Symbol;
 #ifdef ENABLE_BIGINT
 class BigInt;
 #endif
 union Value;
@@ -51,28 +51,28 @@ struct Runtime;
 class Zone;
 
 template <typename T> class Handle;
 template <typename T> class MutableHandle;
 template <typename T> class Rooted;
 template <typename T> class PersistentRooted;
 
 typedef Handle<JSFunction*> HandleFunction;
-typedef Handle<jsid>        HandleId;
+typedef Handle<PropertyKey> HandleId;
 typedef Handle<JSObject*>   HandleObject;
 typedef Handle<JSScript*>   HandleScript;
 typedef Handle<JSString*>   HandleString;
 typedef Handle<JS::Symbol*> HandleSymbol;
 #ifdef ENABLE_BIGINT
 typedef Handle<JS::BigInt*> HandleBigInt;
 #endif
-typedef Handle<Value>       HandleValue;
+typedef Handle<Value> HandleValue;
 
 typedef MutableHandle<JSFunction*> MutableHandleFunction;
-typedef MutableHandle<jsid>        MutableHandleId;
+typedef MutableHandle<PropertyKey> MutableHandleId;
 typedef MutableHandle<JSObject*>   MutableHandleObject;
 typedef MutableHandle<JSScript*>   MutableHandleScript;
 typedef MutableHandle<JSString*>   MutableHandleString;
 typedef MutableHandle<JS::Symbol*> MutableHandleSymbol;
 #ifdef ENABLE_BIGINT
 typedef MutableHandle<JS::BigInt*> MutableHandleBigInt;
 #endif
 typedef MutableHandle<Value>       MutableHandleValue;
@@ -80,31 +80,33 @@ typedef MutableHandle<Value>       Mutab
 typedef Rooted<JSObject*>       RootedObject;
 typedef Rooted<JSFunction*>     RootedFunction;
 typedef Rooted<JSScript*>       RootedScript;
 typedef Rooted<JSString*>       RootedString;
 typedef Rooted<JS::Symbol*>     RootedSymbol;
 #ifdef ENABLE_BIGINT
 typedef Rooted<JS::BigInt*>     RootedBigInt;
 #endif
-typedef Rooted<jsid>            RootedId;
+typedef Rooted<PropertyKey>     RootedId;
 typedef Rooted<JS::Value>       RootedValue;
 
 typedef PersistentRooted<JSFunction*> PersistentRootedFunction;
-typedef PersistentRooted<jsid>        PersistentRootedId;
+typedef PersistentRooted<PropertyKey> PersistentRootedId;
 typedef PersistentRooted<JSObject*>   PersistentRootedObject;
 typedef PersistentRooted<JSScript*>   PersistentRootedScript;
 typedef PersistentRooted<JSString*>   PersistentRootedString;
 typedef PersistentRooted<JS::Symbol*> PersistentRootedSymbol;
 #ifdef ENABLE_BIGINT
 typedef PersistentRooted<JS::BigInt*> PersistentRootedBigInt;
 #endif
 typedef PersistentRooted<Value>       PersistentRootedValue;
 
 } // namespace JS
 
+using jsid = JS::PropertyKey;
+
 #ifdef ENABLE_BIGINT
 #define IF_BIGINT(x, y) x
 #else
 #define IF_BIGINT(x, y) y
 #endif
 
 #endif /* js_TypeDecls_h */
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -342,44 +342,49 @@ class QueueEntry : public NativeObject
     }
 };
 
 const Class QueueEntry::class_ = {
     "QueueEntry",
     JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
 };
 
+/**
+ * TeeState objects implement the local variables in Streams spec 3.3.9
+ * ReadableStreamTee, which are accessed by several algorithms.
+ */
 class TeeState : public NativeObject
 {
   public:
     /**
      * Memory layout for TeeState instances.
      *
      * The Reason1 and Reason2 slots store opaque values, which might be
      * wrapped objects from other compartments. Since we don't treat them as
      * objects in Streams-specific code, we don't have to worry about that
      * apart from ensuring that the values are properly wrapped before storing
      * them.
      *
-     * Promise is always created in TeeState::create below, so is guaranteed
-     * to be in the same compartment as the TeeState instance itself.
+     * CancelPromise is always created in TeeState::create below, so is
+     * guaranteed to be in the same compartment as the TeeState instance
+     * itself.
      *
      * Stream can be from another compartment. It is automatically wrapped
      * before storing it and unwrapped upon retrieval. That means that
      * TeeState consumers need to be able to deal with unwrapped
      * ReadableStream instances from non-current compartments.
      *
      * Branch1 and Branch2 are always created in the same compartment as the
      * TeeState instance, so cannot be from another compartment.
      */
     enum Slots {
         Slot_Flags = 0,
         Slot_Reason1,
         Slot_Reason2,
-        Slot_Promise,
+        Slot_CancelPromise,
         Slot_Stream,
         Slot_Branch1,
         Slot_Branch2,
         SlotCount
     };
 
   private:
     enum Flags {
@@ -421,18 +426,18 @@ class TeeState : public NativeObject
         return getFixedSlot(Slot_Reason1);
     }
 
     Value reason2() const {
         MOZ_ASSERT(canceled2());
         return getFixedSlot(Slot_Reason2);
     }
 
-    PromiseObject* promise() {
-        return &getFixedSlot(Slot_Promise).toObject().as<PromiseObject>();
+    PromiseObject* cancelPromise() {
+        return &getFixedSlot(Slot_CancelPromise).toObject().as<PromiseObject>();
     }
 
     ReadableStreamDefaultController* branch1() {
         ReadableStreamDefaultController* controller =
             &getFixedSlot(Slot_Branch1).toObject()
             .as<ReadableStreamDefaultController>();
         MOZ_ASSERT(controller->flags() & ReadableStreamController::Flag_TeeBranch);
         MOZ_ASSERT(controller->isTeeBranch1());
@@ -459,23 +464,23 @@ class TeeState : public NativeObject
     }
 
     static TeeState* create(JSContext* cx, Handle<ReadableStream*> unwrappedStream) {
         Rooted<TeeState*> state(cx, NewBuiltinClassInstance<TeeState>(cx));
         if (!state) {
             return nullptr;
         }
 
-        Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
-        if (!promise) {
+        Rooted<PromiseObject*> cancelPromise(cx, PromiseObject::createSkippingExecutor(cx));
+        if (!cancelPromise) {
             return nullptr;
         }
 
         state->setFixedSlot(Slot_Flags, Int32Value(0));
-        state->setFixedSlot(Slot_Promise, ObjectValue(*promise));
+        state->setFixedSlot(Slot_CancelPromise, ObjectValue(*cancelPromise));
         RootedObject wrappedStream(cx, unwrappedStream);
         if (!cx->compartment()->wrap(cx, &wrappedStream)) {
             return nullptr;
         }
         state->setFixedSlot(Slot_Stream, ObjectValue(*wrappedStream));
 
         return state;
     }
@@ -867,17 +872,17 @@ CLASS_SPEC(ReadableStream, 0, SlotCount,
 
 
 /*** 3.3. General readable stream abstract operations ********************************************/
 
 // Streams spec, 3.3.1. AcquireReadableStreamBYOBReader ( stream )
 // Always inlined.
 
 // Streams spec, 3.3.2. AcquireReadableStreamDefaultReader ( stream )
-// Always inlined.
+// Always inlined. See CreateReadableStreamDefaultReader.
 
 // Streams spec, 3.3.3. CreateReadableStream ( startAlgorithm, pullAlgorithm, cancelAlgorithm [, highWaterMark [, sizeAlgorithm ] ] )
 // Not implemented.
 
 // Streams spec, 3.3.4. CreateReadableByteStream ( startAlgorithm, pullAlgorithm, cancelAlgorithm [, highWaterMark [, autoAllocateChunkSize ] ] )
 // Not implemented.
 
 // Streams spec, 3.3.5. InitializeReadableStream ( stream )
@@ -913,135 +918,146 @@ static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerClose(JSContext* cx,
                                      Handle<ReadableStreamDefaultController*> unwrappedController);
 
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerEnqueue(JSContext* cx,
                                        Handle<ReadableStreamDefaultController*> unwrappedController,
                                        HandleValue chunk);
 
+/**
+ * Streams spec, 3.3.9. ReadableStreamTee steps 12.a.i-ix.
+ */
 static bool
 TeeReaderReadHandler(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     Rooted<TeeState*> unwrappedTeeState(cx, UnwrapCalleeSlot<TeeState>(cx, args, 0));
     HandleValue resultVal = args.get(0);
 
-    // Step a: Assert: Type(result) is Object.
+    // Step i: Assert: Type(result) is Object.
     RootedObject result(cx, &resultVal.toObject());
 
-    // Step b: Let value be ? Get(result, "value").
+    // Step ii: Let value be ? Get(result, "value").
     RootedValue value(cx);
     if (!GetPropertyPure(cx, result, NameToId(cx->names().value), value.address())) {
         return false;
     }
 
-    // Step c: Let done be ? Get(result, "done").
+    // Step iii: Let done be ? Get(result, "done").
     RootedValue doneVal(cx);
     if (!GetPropertyPure(cx, result, NameToId(cx->names().done), doneVal.address())) {
         return false;
     }
 
-    // Step d: Assert: Type(done) is Boolean.
+    // Step iv: Assert: Type(done) is Boolean.
     bool done = doneVal.toBoolean();
 
-    // Step e: If done is true and teeState.[[closedOrErrored]] is false,
+    // Step v: If done is true and closedOrErrored is false,
     if (done && !unwrappedTeeState->closedOrErrored()) {
-        // Step i: If teeState.[[canceled1]] is false,
+        // Step v.1: If canceled1 is false,
         if (!unwrappedTeeState->canceled1()) {
-            // Step 1: Perform ! ReadableStreamDefaultControllerClose(branch1).
-            Rooted<ReadableStreamDefaultController*> branch1(cx, unwrappedTeeState->branch1());
-            if (!ReadableStreamDefaultControllerClose(cx, branch1)) {
+            // Step v.1.a: Perform ! ReadableStreamDefaultControllerClose(
+            //             branch1.[[readableStreamController]]).
+            Rooted<ReadableStreamDefaultController*> unwrappedBranch1(cx,
+                unwrappedTeeState->branch1());
+            if (!ReadableStreamDefaultControllerClose(cx, unwrappedBranch1)) {
                 return false;
             }
         }
 
-        // Step ii: If teeState.[[canceled2]] is false,
+        // Step v.2: If teeState.[[canceled2]] is false,
         if (!unwrappedTeeState->canceled2()) {
-            // Step 1: Perform ! ReadableStreamDefaultControllerClose(branch1).
-            Rooted<ReadableStreamDefaultController*> branch2(cx, unwrappedTeeState->branch2());
-            if (!ReadableStreamDefaultControllerClose(cx, branch2)) {
+            // Step v.2.a: Perform ! ReadableStreamDefaultControllerClose(
+            //             branch2.[[readableStreamController]]).
+            Rooted<ReadableStreamDefaultController*> unwrappedBranch2(cx,
+                unwrappedTeeState->branch2());
+            if (!ReadableStreamDefaultControllerClose(cx, unwrappedBranch2)) {
                 return false;
             }
         }
 
-        // Step iii: Set teeState.[[closedOrErrored]] to true.
+        // Step v.3: Set closedOrErrored to true.
         unwrappedTeeState->setClosedOrErrored();
     }
 
-    // Step f: If teeState.[[closedOrErrored]] is true, return.
+    // Step vi: If closedOrErrored is true, return.
     if (unwrappedTeeState->closedOrErrored()) {
         return true;
     }
 
-    // Step g: Let value1 and value2 be value.
+    // Step vii: Let value1 and value2 be value.
     RootedValue value1(cx, value);
     RootedValue value2(cx, value);
 
-    // Step h: If teeState.[[canceled2]] is false and cloneForBranch2 is
-    //         true, set value2 to
-    //         ? StructuredDeserialize(StructuredSerialize(value2),
-    //                                 the current Realm Record).
+    // Step viii: If canceled2 is false and cloneForBranch2 is true,
+    //            set value2 to
+    //            ? StructuredDeserialize(? StructuredSerialize(value2),
+    //                                    the current Realm Record).
     // We don't yet support any specifications that use cloneForBranch2, and
     // the Streams spec doesn't offer any way for author code to enable it,
     // so it's always false here.
     MOZ_ASSERT(!unwrappedTeeState->cloneForBranch2());
 
-    // Step i: If teeState.[[canceled1]] is false, perform
-    //         ? ReadableStreamDefaultControllerEnqueue(branch1, value1).
+    // Step ix: If canceled1 is false, perform
+    //          ? ReadableStreamDefaultControllerEnqueue(
+    //                branch1.[[readableStreamController]], value1).
     Rooted<ReadableStreamDefaultController*> unwrappedController(cx);
     if (!unwrappedTeeState->canceled1()) {
         unwrappedController = unwrappedTeeState->branch1();
         if (!ReadableStreamDefaultControllerEnqueue(cx, unwrappedController, value1)) {
             return false;
         }
     }
 
-    // Step j: If teeState.[[canceled2]] is false,
-    //         perform ? ReadableStreamDefaultControllerEnqueue(branch2, value2).
+    // Step x: If canceled2 is false, perform
+    //         ? ReadableStreamDefaultControllerEnqueue(
+    //               branch2.[[readableStreamController]], value2).
     if (!unwrappedTeeState->canceled2()) {
         unwrappedController = unwrappedTeeState->branch2();
         if (!ReadableStreamDefaultControllerEnqueue(cx, unwrappedController, value2)) {
             return false;
         }
     }
 
     args.rval().setUndefined();
     return true;
 }
 
 static MOZ_MUST_USE JSObject*
 ReadableStreamDefaultReaderRead(JSContext* cx,
                                 Handle<ReadableStreamDefaultReader*> unwrappedReader);
 
+/**
+ * Streams spec, 3.3.9. ReadableStreamTee step 12, "Let pullAlgorithm be the
+ * following steps:"
+ */
 static MOZ_MUST_USE JSObject*
 ReadableStreamTee_Pull(JSContext* cx, Handle<TeeState*> unwrappedTeeState)
 {
-    // Step 1: Let reader be F.[[reader]], branch1 be F.[[branch1]],
-    //         branch2 be F.[[branch2]], teeState be F.[[teeState]], and
-    //         cloneForBranch2 be F.[[cloneForBranch2]].
-
-    // Step 2: Return the result of transforming
-    //         ! ReadableStreamDefaultReaderRead(reader) by a fulfillment
-    //         handler which takes the argument result and performs the
-    //         following steps:
+    // Implicit in the spec: Unpack the closed-over variables `stream` and
+    // `reader` from the TeeState.
     Rooted<ReadableStream*> unwrappedStream(cx,
         UnwrapInternalSlot<ReadableStream>(cx, unwrappedTeeState, TeeState::Slot_Stream));
     if (!unwrappedStream) {
         return nullptr;
     }
     Rooted<ReadableStreamReader*> unwrappedReaderObj(cx,
         UnwrapReaderFromStream(cx, unwrappedStream));
     if (!unwrappedReaderObj) {
         return nullptr;
     }
-
     Rooted<ReadableStreamDefaultReader*> unwrappedReader(cx,
         &unwrappedReaderObj->as<ReadableStreamDefaultReader>());
 
+    // Step 12.a: Return the result of transforming
+    // ! ReadableStreamDefaultReaderRead(reader) with a fulfillment handler
+    // which takes the argument result and performs the following steps:
+    //
+    // The steps under 12.a are implemented in TeeReaderReadHandler.
     RootedObject readPromise(cx, ::ReadableStreamDefaultReaderRead(cx, unwrappedReader));
     if (!readPromise) {
         return nullptr;
     }
 
     RootedObject teeState(cx, unwrappedTeeState);
     if (!cx->compartment()->wrap(cx, &teeState)) {
         return nullptr;
@@ -1051,34 +1067,37 @@ ReadableStreamTee_Pull(JSContext* cx, Ha
         return nullptr;
     }
 
     return JS::CallOriginalPromiseThen(cx, readPromise, onFulfilled, nullptr);
 }
 
 /**
  * Cancel one branch of a tee'd stream with the given |reason_|.
+ *
+ * Streams spec, 3.3.9. ReadableStreamTee steps 13 and 14: "Let
+ * cancel1Algorithm/cancel2Algorithm be the following steps, taking a reason
+ * argument:"
  */
 static MOZ_MUST_USE JSObject*
 ReadableStreamTee_Cancel(JSContext* cx,
                          Handle<TeeState*> unwrappedTeeState,
                          Handle<ReadableStreamDefaultController*> unwrappedBranch,
                          HandleValue reason)
 {
-    // Step 1: Let stream be F.[[stream]] and teeState be F.[[teeState]].
     Rooted<ReadableStream*> unwrappedStream(cx,
         UnwrapInternalSlot<ReadableStream>(cx, unwrappedTeeState, TeeState::Slot_Stream));
     if (!unwrappedStream) {
         return nullptr;
     }
 
     bool bothBranchesCanceled = false;
 
-    // Step 2: Set teeState.[[canceled1]] to true.
-    // Step 3: Set teeState.[[reason1]] to reason.
+    // Step 13/14.a: Set canceled1/canceled2 to true.
+    // Step 13/14.b: Set reason1/reason2 to reason.
     {
         RootedValue unwrappedReason(cx, reason);
         {
             AutoRealm ar(cx, unwrappedTeeState);
             if (!cx->compartment()->wrap(cx, &unwrappedReason)) {
                 return nullptr;
             }
         }
@@ -1087,67 +1106,70 @@ ReadableStreamTee_Cancel(JSContext* cx,
             bothBranchesCanceled = unwrappedTeeState->canceled2();
         } else {
             MOZ_ASSERT(unwrappedBranch->isTeeBranch2());
             unwrappedTeeState->setCanceled2(unwrappedReason);
             bothBranchesCanceled = unwrappedTeeState->canceled1();
         }
     }
 
-    // Step 4: If teeState.[[canceled1]] is true,
-    // Step 4: If teeState.[[canceled2]] is true,
+    // Step 13/14.c: If canceled2/canceled1 is true,
     if (bothBranchesCanceled) {
-        // Step a: Let compositeReason be
-        //         ! CreateArrayFromList(« teeState.[[reason1]], teeState.[[reason2]] »).
+        // Step 13/14.c.i: Let compositeReason be
+        //                 ! CreateArrayFromList(« reason1, reason2 »).
         RootedNativeObject compositeReason(cx, NewDenseFullyAllocatedArray(cx, 2));
         if (!compositeReason) {
             return nullptr;
         }
 
         compositeReason->setDenseInitializedLength(2);
 
         RootedValue reason1(cx, unwrappedTeeState->reason1());
         RootedValue reason2(cx, unwrappedTeeState->reason2());
         if (!cx->compartment()->wrap(cx, &reason1) || !cx->compartment()->wrap(cx, &reason2)) {
             return nullptr;
         }
         compositeReason->initDenseElement(0, reason1);
         compositeReason->initDenseElement(1, reason2);
         RootedValue compositeReasonVal(cx, ObjectValue(*compositeReason));
 
-        // Step b: Let cancelResult be ! ReadableStreamCancel(stream, compositeReason).
+        // Step 13/14.c.ii: Let cancelResult be
+        //                  ! ReadableStreamCancel(stream, compositeReason).
+        // In our implementation, this can fail with OOM. The best course then
+        // is to reject cancelPromise with an OOM error.
         RootedObject cancelResult(cx,
             ::ReadableStreamCancel(cx, unwrappedStream, compositeReasonVal));
         {
-            Rooted<PromiseObject*> promise(cx, unwrappedTeeState->promise());
-            AutoRealm ar(cx, promise);
+            Rooted<PromiseObject*> cancelPromise(cx, unwrappedTeeState->cancelPromise());
+            AutoRealm ar(cx, cancelPromise);
 
             if (!cancelResult) {
-                if (!RejectPromiseWithPendingError(cx, promise)) {
+                // Handle the OOM case mentioned above.
+                if (!RejectPromiseWithPendingError(cx, cancelPromise)) {
                     return nullptr;
                 }
             } else {
-                // Step c: Resolve teeState.[[promise]] with cancelResult.
+                // Step 13/14.c.iii: Resolve cancelPromise with cancelResult.
                 RootedValue resultVal(cx, ObjectValue(*cancelResult));
                 if (!cx->compartment()->wrap(cx, &resultVal)) {
                     return nullptr;
                 }
-                if (!PromiseObject::resolve(cx, promise, resultVal)) {
+                if (!PromiseObject::resolve(cx, cancelPromise, resultVal)) {
                     return nullptr;
                 }
             }
         }
     }
 
-    // Step 5: Return teeState.[[promise]].
-    RootedObject promise(cx, unwrappedTeeState->promise());
-    if (!cx->compartment()->wrap(cx, &promise)) {
+    // Step 13/14.d: Return cancelPromise.
+    RootedObject cancelPromise(cx, unwrappedTeeState->cancelPromise());
+    if (!cx->compartment()->wrap(cx, &cancelPromise)) {
         return nullptr;
     }
-    return promise;
+    return cancelPromise;
 }
 
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx,
                                              Handle<ReadableStreamDefaultController*> unwrappedController,
                                              HandleValue e);
 
 /**
@@ -1156,36 +1178,39 @@ ReadableStreamDefaultControllerErrorIfNe
  */
 static bool
 TeeReaderClosedHandler(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     Rooted<TeeState*> teeState(cx, TargetFromHandler<TeeState>(args));
     HandleValue reason = args.get(0);
 
-    // Step a: If teeState.[[closedOrErrored]] is false, then:
+    // Step a: If closedOrErrored is false, then:
     if (!teeState->closedOrErrored()) {
-        // Step a.iii: Set teeState.[[closedOrErrored]] to true.
+        // Step a.iii: Set closedOrErrored to true.
         // Reordered to ensure that internal errors in the other steps don't
         // leave the teeState in an undefined state.
         teeState->setClosedOrErrored();
 
-        // Step a.i: Perform ! ReadableStreamDefaultControllerErrorIfNeeded(pull.[[branch1]], r).
+        // Step a.i: Perform ! ReadableStreamDefaultControllerErrorIfNeeded(
+        //           branch1.[[readableStreamController]], r).
         Rooted<ReadableStreamDefaultController*> branch1(cx, teeState->branch1());
         if (!ReadableStreamDefaultControllerErrorIfNeeded(cx, branch1, reason)) {
             return false;
         }
 
-        // Step a.ii: Perform ! ReadableStreamDefaultControllerErrorIfNeeded(pull.[[branch2]], r).
+        // Step a.ii: Perform ! ReadableStreamDefaultControllerErrorIfNeeded(
+        //            branch2.[[readableStreamController]], r).
         Rooted<ReadableStreamDefaultController*> branch2(cx, teeState->branch2());
         if (!ReadableStreamDefaultControllerErrorIfNeeded(cx, branch2, reason)) {
             return false;
         }
     }
 
+    args.rval().setUndefined();
     return true;
 }
 
 /**
  * Streams spec, 3.3.9. ReadableStreamTee ( stream, cloneForBranch2 )
  */
 static MOZ_MUST_USE bool
 ReadableStreamTee(JSContext* cx,
@@ -1199,93 +1224,90 @@ ReadableStreamTee(JSContext* cx,
 
     // Step 3: Let reader be ? AcquireReadableStreamDefaultReader(stream).
     Rooted<ReadableStreamDefaultReader*> reader(cx,
         CreateReadableStreamDefaultReader(cx, unwrappedStream));
     if (!reader) {
         return false;
     }
 
-    // Step 4: Let teeState be Record {[[closedOrErrored]]: false,
-    //                                 [[canceled1]]: false,
-    //                                 [[canceled2]]: false,
-    //                                 [[reason1]]: undefined,
-    //                                 [[reason2]]: undefined,
-    //                                 [[promise]]: a new promise}.
+    // Several algorithms close over the variables initialized in the next few
+    // steps, so we allocate them in an object, the TeeState. The algorithms
+    // also close over `stream` and `reader`, so TeeState gets a reference to
+    // the stream.
+    //
+    // Step 4: Let closedOrErrored be false.
+    // Step 5: Let canceled1 be false.
+    // Step 6: Let canceled2 be false.
+    // Step 7: Let reason1 be undefined.
+    // Step 8: Let reason2 be undefined.
+    // Step 9: Let branch1 be undefined.
+    // Step 10: Let branch2 be undefined.
+    // Step 11: Let cancelPromise be a new promise.
     Rooted<TeeState*> teeState(cx, TeeState::create(cx, unwrappedStream));
     if (!teeState) {
         return false;
     }
 
-    // Steps 5-10 omitted because our implementation works differently.
-
-    // Step 5: Let pull be a new ReadableStreamTee pull function.
-    // Step 6: Set pull.[[reader]] to reader, pull.[[teeState]] to teeState, and
-    //         pull.[[cloneForBranch2]] to cloneForBranch2.
-    // Step 7: Let cancel1 be a new ReadableStreamTee branch 1 cancel function.
-    // Step 8: Set cancel1.[[stream]] to stream and cancel1.[[teeState]] to
-    //         teeState.
-
-    // Step 9: Let cancel2 be a new ReadableStreamTee branch 2 cancel function.
-    // Step 10: Set cancel2.[[stream]] to stream and cancel2.[[teeState]] to
-    //          teeState.
-
-    // Step 11: Let underlyingSource1 be ! ObjectCreate(%ObjectPrototype%).
-    // Step 12: Perform ! CreateDataProperty(underlyingSource1, "pull", pull).
-    // Step 13: Perform ! CreateDataProperty(underlyingSource1, "cancel", cancel1).
-
-    // Step 14: Let branch1Stream be ! Construct(ReadableStream, underlyingSource1).
+    // Step 12: Let pullAlgorithm be the following steps: [...]
+    // Step 13: Let cancel1Algorithm be the following steps: [...]
+    // Step 14: Let cancel2Algorithm be the following steps: [...]
+    // Step 15: Let startAlgorithm be an algorithm that returns undefined.
+    //
+    // Implicit. Our implementation does not use objects to represent
+    // [[pullAlgorithm]], [[cancelAlgorithm]], and so on. Instead, we decide
+    // which one to perform based on class checks. For example, our
+    // implementation of ReadableStreamControllerCallPullIfNeeded checks
+    // whether the stream's underlyingSource is a TeeState object.
+
+    // Step 16: Set branch1 to
+    //          ! CreateReadableStream(startAlgorithm, pullAlgorithm,
+    //                                 cancel1Algorithm).
     RootedValue hwmValue(cx, NumberValue(1));
     RootedValue underlyingSource(cx, ObjectValue(*teeState));
     branch1Stream.set(ReadableStream::createDefaultStream(cx, underlyingSource,
                                                           UndefinedHandleValue,
                                                           hwmValue));
     if (!branch1Stream) {
         return false;
     }
 
     Rooted<ReadableStreamDefaultController*> branch1(cx);
     branch1 = &branch1Stream->controller()->as<ReadableStreamDefaultController>();
     branch1->setTeeBranch1();
     teeState->setBranch1(branch1);
 
-    // Step 15: Let underlyingSource2 be ! ObjectCreate(%ObjectPrototype%).
-    // Step 16: Perform ! CreateDataProperty(underlyingSource2, "pull", pull).
-    // Step 17: Perform ! CreateDataProperty(underlyingSource2, "cancel", cancel2).
-
-    // Step 18: Let branch2Stream be ! Construct(ReadableStream, underlyingSource2).
+    // Step 17: Set branch2 to
+    //          ! CreateReadableStream(startAlgorithm, pullAlgorithm,
+    //                                 cancel2Algorithm).
     branch2Stream.set(ReadableStream::createDefaultStream(cx, underlyingSource,
                                                           UndefinedHandleValue,
                                                           hwmValue));
     if (!branch2Stream) {
         return false;
     }
 
     Rooted<ReadableStreamDefaultController*> branch2(cx);
     branch2 = &branch2Stream->controller()->as<ReadableStreamDefaultController>();
     branch2->setTeeBranch2();
     teeState->setBranch2(branch2);
 
-    // Step 19: Set pull.[[branch1]] to branch1Stream.[[readableStreamController]].
-    // Step 20: Set pull.[[branch2]] to branch2Stream.[[readableStreamController]].
-    // Our implementation stores the controllers on the TeeState instead.
-
-    // Step 21: Upon rejection of reader.[[closedPromise]] with reason r,
+    // Step 18: Upon rejection of reader.[[closedPromise]] with reason r, [...]
     RootedObject closedPromise(cx, reader->closedPromise());
 
     RootedObject onRejected(cx, NewHandler(cx, TeeReaderClosedHandler, teeState));
     if (!onRejected) {
         return false;
     }
 
     if (!JS::AddPromiseReactions(cx, closedPromise, nullptr, onRejected)) {
         return false;
     }
 
-    // Step 22: Return « branch1, branch2 ».
+    // Step 19: Return « branch1, branch2 ».
     return true;
 }
 
 
 /*** 3.4. The interface between readable streams and controllers *********************************/
 
 inline static MOZ_MUST_USE bool
 AppendToListAtSlot(JSContext* cx,
--- a/js/src/jit/BaselineIC.cpp
+++ b/js/src/jit/BaselineIC.cpp
@@ -1417,17 +1417,17 @@ DoTypeUpdateFallback(JSContext* cx, Base
                    ICStub::KindString(stub->kind()));
 
     MOZ_ASSERT(stub->isCacheIR_Updated());
 
     RootedScript script(cx, frame->script());
     RootedObject obj(cx, &objval.toObject());
 
     RootedId id(cx, stub->toCacheIR_Updated()->updateStubId());
-    MOZ_ASSERT(id != JSID_EMPTY);
+    MOZ_ASSERT(id.get() != JSID_EMPTY);
 
     // The group should match the object's group, except when the object is
     // an unboxed expando object: in that case, the group is the group of
     // the unboxed object.
     RootedObjectGroup group(cx, stub->toCacheIR_Updated()->updateStubGroup());
 #ifdef DEBUG
     if (obj->is<UnboxedExpandoObject>()) {
         MOZ_ASSERT(group->clasp() == &UnboxedPlainObject::class_);
--- a/js/src/vm/JSScript.cpp
+++ b/js/src/vm/JSScript.cpp
@@ -3167,20 +3167,16 @@ JSScript::JSScript(JS::Realm* realm, uin
     toStringStart_(toStringStart),
     toStringEnd_(toStringEnd)
 {
     // See JSScript.h for further details.
     MOZ_ASSERT(toStringStart <= sourceStart);
     MOZ_ASSERT(sourceStart <= sourceEnd);
     MOZ_ASSERT(sourceEnd <= toStringEnd);
 
-#ifdef MOZ_VTUNE
-    vtuneMethodId_ = vtune::GenerateUniqueMethodID();
-#endif
-
     setSourceObject(sourceObject);
 }
 
 /* static */ JSScript*
 JSScript::New(JSContext* cx, HandleObject sourceObject,
               uint32_t sourceStart, uint32_t sourceEnd,
               uint32_t toStringStart, uint32_t toStringEnd)
 {
@@ -3220,16 +3216,43 @@ JSScript::Create(JSContext* cx, const Re
         if (!script->initScriptName(cx)) {
             return nullptr;
         }
     }
 
     return script;
 }
 
+#ifdef MOZ_VTUNE
+uint32_t
+JSScript::vtuneMethodID()
+{
+    if (!realm()->scriptVTuneIdMap) {
+        auto map = MakeUnique<ScriptVTuneIdMap>();
+        if (!map) {
+            MOZ_CRASH("Failed to allocate ScriptVTuneIdMap");
+        }
+
+        realm()->scriptVTuneIdMap = std::move(map);
+    }
+
+    ScriptVTuneIdMap::AddPtr p = realm()->scriptVTuneIdMap->lookupForAdd(this);
+    if (p) {
+        return p->value();
+    }
+
+    uint32_t id = vtune::GenerateUniqueMethodID();
+    if (!realm()->scriptVTuneIdMap->add(p, this, id)) {
+        MOZ_CRASH("Failed to add vtune method id");
+    }
+
+    return id;
+}
+#endif
+
 bool
 JSScript::initScriptName(JSContext* cx)
 {
     MOZ_ASSERT(!hasScriptName());
 
     if (!filename()) {
         return true;
     }
@@ -3609,16 +3632,23 @@ JSScript::finalize(FreeOp* fop)
         types_->destroy();
     }
 
     jit::DestroyJitScripts(fop, this);
 
     destroyScriptCounts();
     destroyDebugScript(fop);
 
+#ifdef MOZ_VTUNE
+    if (realm()->scriptVTuneIdMap) {
+        // Note: we should only get here if the VTune JIT profiler is running.
+        realm()->scriptVTuneIdMap->remove(this);
+    }
+#endif
+
     if (data_) {
         JS_POISON(data_, 0xdb, computedSizeOfData(), MemCheckKind::MakeNoAccess);
         fop->free_(data_);
     }
 
     if (scriptData_) {
         scriptData_->decRefCount();
     }
--- a/js/src/vm/JSScript.h
+++ b/js/src/vm/JSScript.h
@@ -214,16 +214,23 @@ using ScriptCountsMap = HashMap<JSScript
                                 DefaultHasher<JSScript*>,
                                 SystemAllocPolicy>;
 
 using ScriptNameMap = HashMap<JSScript*,
                               JS::UniqueChars,
                               DefaultHasher<JSScript*>,
                               SystemAllocPolicy>;
 
+#ifdef MOZ_VTUNE
+using ScriptVTuneIdMap = HashMap<JSScript*,
+                                 uint32_t,
+                                 DefaultHasher<JSScript*>,
+                                 SystemAllocPolicy>;
+#endif
+
 class DebugScript
 {
     friend class ::JSScript;
     friend class JS::Realm;
 
     /*
      * When non-zero, compile script in single-step mode. The top bit is set and
      * cleared by setStepMode, as used by JSD. The lower bits are a count,
@@ -1621,22 +1628,16 @@ class JSScript : public js::gc::TenuredC
     //   |         sourceStart_                      sourceEnd_  |
     //   |                                                       |
     //   toStringStart_                                          toStringEnd_
     uint32_t sourceStart_ = 0;
     uint32_t sourceEnd_ = 0;
     uint32_t toStringStart_ = 0;
     uint32_t toStringEnd_ = 0;
 
-#ifdef MOZ_VTUNE
-    // Unique Method ID passed to the VTune profiler, or 0 if unset.
-    // Allows attribution of different jitcode to the same source script.
-    uint32_t vtuneMethodId_ = 0;
-#endif
-
     // Number of times the script has been called or has had backedges taken.
     // When running in ion, also increased for any inlined scripts. Reset if
     // the script's JIT code is forcibly discarded.
     mozilla::Atomic<uint32_t, mozilla::Relaxed,
                     mozilla::recordreplay::Behavior::DontPreserve> warmUpCount = {};
 
     // Immutable flags should not be modified after this script has been
     // initialized. These flags should likely be preserved when serializing
@@ -1700,16 +1701,18 @@ class JSScript : public js::gc::TenuredC
         IsAsync = 1 << 19,
 
         // Set if this function has a rest parameter.
         HasRest = 1 << 20,
 
         // See comments below.
         ArgsHasVarBinding = 1 << 21,
     };
+    // Note: don't make this a bitfield! It makes it hard to read these flags
+    // from JIT code.
     uint32_t immutableFlags_ = 0;
 
     // Mutable flags typically store information about runtime or deoptimization
     // behavior of this script. This is only public for the JITs.
   public:
     enum class MutableFlags : uint32_t {
         // Have warned about uses of undefined properties in this script.
         WarnedAboutUndefinedProp = 1 << 0,
@@ -1770,16 +1773,18 @@ class JSScript : public js::gc::TenuredC
         // See comments below.
         NeedsArgsAnalysis = 1 << 17,
         NeedsArgsObj = 1 << 18,
 
         // Set if the debugger's onNewScript hook has not yet been called.
         HideScriptFromDebugger = 1 << 19,
     };
   private:
+    // Note: don't make this a bitfield! It makes it hard to read these flags
+    // from JIT code.
     uint32_t mutableFlags_ = 0;
 
     // 16-bit fields.
 
     /**
      * Number of times the |warmUpCount| was forcibly discarded. The counter is
      * reset when a script is successfully jit-compiled.
      */
@@ -2389,17 +2394,19 @@ class JSScript : public js::gc::TenuredC
     void setDefaultClassConstructorSpan(JSObject* sourceObject, uint32_t start, uint32_t end,
                                         unsigned line, unsigned column);
 
     bool mutedErrors() const { return scriptSource()->mutedErrors(); }
     const char* filename() const { return scriptSource()->filename(); }
     const char* maybeForwardedFilename() const { return maybeForwardedScriptSource()->filename(); }
 
 #ifdef MOZ_VTUNE
-    uint32_t vtuneMethodID() const { return vtuneMethodId_; }
+    // Unique Method ID passed to the VTune profiler. Allows attribution of
+    // different jitcode to the same source script.
+    uint32_t vtuneMethodID();
 #endif
 
   public:
 
     /* Return whether this script was compiled for 'eval' */
     bool isForEval() const {
         MOZ_ASSERT_IF(isCachedEval() || isActiveEval(), bodyScope()->is<js::EvalScope>());
         return isCachedEval() || isActiveEval();
--- a/js/src/vm/Realm.cpp
+++ b/js/src/vm/Realm.cpp
@@ -400,16 +400,20 @@ Realm::finishRoots()
     if (debugEnvs_) {
         debugEnvs_->finish();
     }
 
     objects_.finishRoots();
 
     clearScriptCounts();
     clearScriptNames();
+
+#ifdef MOZ_VTUNE
+    scriptVTuneIdMap.reset();
+#endif
 }
 
 void
 ObjectRealm::sweepAfterMinorGC()
 {
     InnerViewTable& table = innerViews.get();
     if (table.needsSweepAfterMinorGC()) {
         table.sweepAfterMinorGC();
@@ -583,16 +587,27 @@ Realm::fixupScriptMapsAfterMovingGC()
     if (debugScriptMap) {
         for (DebugScriptMap::Enum e(*debugScriptMap); !e.empty(); e.popFront()) {
             JSScript* script = e.front().key();
             if (!IsAboutToBeFinalizedUnbarriered(&script) && script != e.front().key()) {
                 e.rekeyFront(script);
             }
         }
     }
+
+#ifdef MOZ_VTUNE
+    if (scriptVTuneIdMap) {
+        for (ScriptVTuneIdMap::Enum e(*scriptVTuneIdMap); !e.empty(); e.popFront()) {
+            JSScript* script = e.front().key();
+            if (!IsAboutToBeFinalizedUnbarriered(&script) && script != e.front().key()) {
+                e.rekeyFront(script);
+            }
+        }
+    }
+#endif
 }
 
 #ifdef JSGC_HASH_TABLE_CHECKS
 void
 Realm::checkScriptMapsAfterMovingGC()
 {
     if (scriptCountsMap) {
         for (auto r = scriptCountsMap->all(); !r.empty(); r.popFront()) {
@@ -625,16 +640,28 @@ Realm::checkScriptMapsAfterMovingGC()
                 if (site && site->type() == BreakpointSite::Type::JS) {
                     CheckGCThingAfterMovingGC(site->asJS()->script);
                 }
             }
             auto ptr = debugScriptMap->lookup(script);
             MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front());
         }
     }
+
+# ifdef MOZ_VTUNE
+    if (scriptVTuneIdMap) {
+        for (auto r = scriptVTuneIdMap->all(); !r.empty(); r.popFront()) {
+            JSScript* script = r.front().key();
+            MOZ_ASSERT(script->realm() == this);
+            CheckGCThingAfterMovingGC(script);
+            auto ptr = scriptVTuneIdMap->lookup(script);
+            MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front());
+        }
+    }
+# endif // MOZ_VTUNE
 }
 #endif
 
 void
 Realm::purge()
 {
     dtoaCache.purge();
     newProxyCache.purge();
--- a/js/src/vm/Realm.h
+++ b/js/src/vm/Realm.h
@@ -416,16 +416,19 @@ class JS::Realm : public JS::shadow::Rea
     js::ArraySpeciesLookup arraySpeciesLookup;
     js::PromiseLookup promiseLookup;
 
     js::PerformanceGroupHolder performanceMonitoring;
 
     js::UniquePtr<js::ScriptCountsMap> scriptCountsMap;
     js::UniquePtr<js::ScriptNameMap> scriptNameMap;
     js::UniquePtr<js::DebugScriptMap> debugScriptMap;
+#ifdef MOZ_VTUNE
+    js::UniquePtr<js::ScriptVTuneIdMap> scriptVTuneIdMap;
+#endif
 
     /*
      * Lazily initialized script source object to use for scripts cloned
      * from the self-hosting global.
      */
     js::ReadBarrieredScriptSourceObject selfHostingScriptSource { nullptr };
 
     // Last time at which an animation was played for this realm.
--- a/js/src/vm/TypeSet.h
+++ b/js/src/vm/TypeSet.h
@@ -16,25 +16,25 @@
 #include <stdint.h> // intptr_t, uintptr_t, uint8_t, uint32_t
 #include <stdio.h> // FILE
 
 #include "jstypes.h" // JS_BITS_PER_WORD
 #include "jsutil.h" // JS_CRASH_DIAGNOSTICS
 
 #include "jit/IonTypes.h" // jit::MIRType
 #include "js/GCAnnotations.h" // JS_HAZ_GC_POINTER
+#include "js/Id.h"
 #include "js/TracingAPI.h" // JSTracer
 #include "js/TypeDecls.h" // IF_BIGINT
 #include "js/Utility.h" // UniqueChars
 #include "js/Value.h" // JSVAL_TYPE_*
 #include "js/Vector.h" // js::Vector
 #include "vm/TaggedProto.h" // js::TaggedProto
 
 struct JSContext;
-struct jsid;
 class JSObject;
 
 namespace JS {
 
 class Compartment;
 class Realm;
 class Zone;
 
--- a/js/src/vtune/VTuneWrapper.cpp
+++ b/js/src/vtune/VTuneWrapper.cpp
@@ -114,17 +114,17 @@ MarkRegExp(const js::jit::JitCode* code,
     method.module_name = const_cast<char*>("irregexp");
 
     int ok = SafeNotifyEvent(iJVM_EVENT_TYPE_METHOD_LOAD_FINISHED_V2, (void*)&method);
     if (ok != 1)
         printf("[!] VTune Integration: Failed to load method.\n");
 }
 
 void
-MarkScript(const js::jit::JitCode* code, const JSScript* script, const char* module)
+MarkScript(const js::jit::JitCode* code, JSScript* script, const char* module)
 {
     if (!IsProfilingActive())
         return;
 
     iJIT_Method_Load_V2 method = {0};
     method.method_id = script->vtuneMethodID();
     method.method_load_address = code->raw();
     method.method_size = code->instructionsSize();
--- a/js/src/vtune/VTuneWrapper.h
+++ b/js/src/vtune/VTuneWrapper.h
@@ -28,17 +28,17 @@ bool IsProfilingActive();
 // Wrapper exists in case we need locking in the future.
 uint32_t GenerateUniqueMethodID();
 
 void MarkStub(const js::jit::JitCode* code, const char* name);
 
 void MarkRegExp(const js::jit::JitCode* code, bool match_only);
 
 void MarkScript(const js::jit::JitCode* code,
-                const JSScript* script,
+                JSScript* script,
                 const char* module);
 
 void MarkWasm(unsigned methodId,
               const char* name,
               void* start,
               uintptr_t size);
 
 void UnmarkCode(const js::jit::JitCode* code);
--- a/media/webrtc/trunk/webrtc/common_video/h264/sps_parser.cc
+++ b/media/webrtc/trunk/webrtc/common_video/h264/sps_parser.cc
@@ -89,33 +89,41 @@ rtc::Optional<SpsParser::SpsState> SpsPa
     // bit_depth_chroma_minus8: ue(v)
     RETURN_EMPTY_ON_FAIL(buffer->ReadExponentialGolomb(&golomb_ignored));
     // qpprime_y_zero_transform_bypass_flag: u(1)
     RETURN_EMPTY_ON_FAIL(buffer->ConsumeBits(1));
     // seq_scaling_matrix_present_flag: u(1)
     uint32_t seq_scaling_matrix_present_flag;
     RETURN_EMPTY_ON_FAIL(buffer->ReadBits(&seq_scaling_matrix_present_flag, 1));
     if (seq_scaling_matrix_present_flag) {
-      // seq_scaling_list_present_flags. Either 8 or 12, depending on
-      // chroma_format_idc.
-      uint32_t seq_scaling_list_present_flags;
-      if (chroma_format_idc != 3) {
-        RETURN_EMPTY_ON_FAIL(
-            buffer->ReadBits(&seq_scaling_list_present_flags, 8));
-      } else {
+      // Process the scaling lists just enough to be able to properly
+      // skip over them, so we can still read the resolution on streams
+      // where this is included.
+      int scaling_list_count = (chroma_format_idc == 3 ? 12 : 8);
+      for (int i = 0; i < scaling_list_count; ++i) {
+        // seq_scaling_list_present_flag[i]  : u(1)
+        uint32_t seq_scaling_list_present_flags;
         RETURN_EMPTY_ON_FAIL(
-            buffer->ReadBits(&seq_scaling_list_present_flags, 12));
-      }
-      // We don't support reading the sequence scaling list, and we don't really
-      // see/use them in practice, so we'll just reject the full sps if we see
-      // any provided.
-      if (seq_scaling_list_present_flags > 0) {
-        RTC_LOG(LS_WARNING)
-            << "SPS contains scaling lists, which are unsupported.";
-        return OptionalSps();
+            buffer->ReadBits(&seq_scaling_list_present_flags, 1));
+        if (seq_scaling_list_present_flags != 0) {
+          int last_scale = 8;
+          int next_scale = 8;
+          int size_of_scaling_list = i < 6 ? 16 : 64;
+          for (int j = 0; j < size_of_scaling_list; j++) {
+            if (next_scale != 0) {
+              int32_t delta_scale;
+              // delta_scale: se(v)
+              RETURN_EMPTY_ON_FAIL(
+                  buffer->ReadSignedExponentialGolomb(&delta_scale));
+              next_scale = (last_scale + delta_scale + 256) % 256;
+            }
+            if (next_scale != 0)
+              last_scale = next_scale;
+          }
+        }
       }
     }
   }
   // log2_max_frame_num_minus4: ue(v)
   RETURN_EMPTY_ON_FAIL(
       buffer->ReadExponentialGolomb(&sps.log2_max_frame_num_minus4));
   // pic_order_cnt_type: ue(v)
   RETURN_EMPTY_ON_FAIL(buffer->ReadExponentialGolomb(&sps.pic_order_cnt_type));
--- a/media/webrtc/trunk/webrtc/common_video/h264/sps_parser_unittest.cc
+++ b/media/webrtc/trunk/webrtc/common_video/h264/sps_parser_unittest.cc
@@ -166,9 +166,22 @@ TEST_F(H264SpsParserTest, TestSyntheticS
   GenerateFakeSps(156u, 122u, 2, &buffer);
   EXPECT_TRUE(static_cast<bool>(
       sps_ = SpsParser::ParseSps(buffer.data(), buffer.size())));
   EXPECT_EQ(156u, sps_->width);
   EXPECT_EQ(122u, sps_->height);
   EXPECT_EQ(2u, sps_->id);
 }
 
+TEST_F(H264SpsParserTest, TestSampleSPSWithScalingLists) {
+  // SPS from a 1920x1080 video. Contains scaling lists (and veritcal cropping).
+  const uint8_t buffer[] = {0x64, 0x00, 0x2a, 0xad, 0x84, 0x01, 0x0c, 0x20,
+                            0x08, 0x61, 0x00, 0x43, 0x08, 0x02, 0x18, 0x40,
+                            0x10, 0xc2, 0x00, 0x84, 0x3b, 0x50, 0x3c, 0x01,
+                            0x13, 0xf2, 0xcd, 0xc0, 0x40, 0x40, 0x50, 0x00,
+                            0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0xe8, 0x40};
+  EXPECT_TRUE(
+      static_cast<bool>(sps_ = SpsParser::ParseSps(buffer, arraysize(buffer))));
+  EXPECT_EQ(1920u, sps_->width);
+  EXPECT_EQ(1080u, sps_->height);
+}
+
 }  // namespace webrtc
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -223,18 +223,17 @@ pref("dom.keyboardevent.dispatch_during_
 #ifdef NIGHTLY_BUILD
 pref("dom.keyboardevent.keypress.dispatch_non_printable_keys_only_system_group_in_content", true);
 // Blacklist of domains of web apps which are not aware of strict keypress
 // dispatching behavior.  This is comma separated list.  If you need to match
 // all sub-domains, you can specify it as "*.example.com".  Additionally, you
 // can limit the path.  E.g., "example.com/foo" means "example.com/foo*".  So,
 // if you need to limit under a directory, the path should end with "/" like
 // "example.com/foo/".  Note that this cannot limit port number for now.
-pref("dom.keyboardevent.keypress.hack.dispatch_non_printable_keys",
-     "medium.com/p/");
+pref("dom.keyboardevent.keypress.hack.dispatch_non_printable_keys", "");
 #else
 pref("dom.keyboardevent.keypress.dispatch_non_printable_keys_only_system_group_in_content", false);
 #endif
 
 #ifdef NIGHTLY_BUILD
 // Blacklist of domains of web apps which handle keyCode and charCode of
 // keypress events with a path only for Firefox (i.e., broken if we set
 // non-zero keyCode or charCode value to the other).  The format is exactly
--- a/services/common/kinto-offline-client.js
+++ b/services/common/kinto-offline-client.js
@@ -28,17 +28,17 @@
 //
 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1394556#c3 for
 // more details.
 const global = this;
 
 var EXPORTED_SYMBOLS = ["Kinto"];
 
 /*
- * Version 12.2.0 - 266e100
+ * Version 12.2.4 - 8fb687a
  */
 
 (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Kinto = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
 /*
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -66,26 +66,25 @@ var _IDB = _interopRequireDefault(requir
 
 var _utils = require("../src/utils");
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 ChromeUtils.import("resource://gre/modules/Timer.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyGlobalGetters(global, ["fetch", "indexedDB"]);
-const {
-  EventEmitter
-} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm", {});
-const {
-  generateUUID
-} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); // Use standalone kinto-http module landed in FFx.
-
-const {
-  KintoHttpClient
-} = ChromeUtils.import("resource://services-common/kinto-http-client.js");
+ChromeUtils.defineModuleGetter(global, "EventEmitter", "resource://gre/modules/EventEmitter.jsm"); // Use standalone kinto-http module landed in FFx.
+
+ChromeUtils.defineModuleGetter(global, "KintoHttpClient", "resource://services-common/kinto-http-client.js");
+XPCOMUtils.defineLazyGetter(global, "generateUUID", () => {
+  const {
+    generateUUID
+  } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+  return generateUUID;
+});
 
 class Kinto extends _KintoBase.default {
   static get adapters() {
     return {
       BaseAdapter: _base.default,
       IDB: _IDB.default
     };
   }
@@ -485,55 +484,59 @@ const cursorHandlers = {
  * @return {IDBRequest}
  */
 
 function createListRequest(cid, store, filters, done) {
   const filterFields = Object.keys(filters); // If no filters, get all results in one bulk.
 
   if (filterFields.length == 0) {
     const request = store.index("cid").getAll(IDBKeyRange.only(cid));
+
     request.onsuccess = event => done(event.target.result);
+
     return request;
-  }
-  // Introspect filters and check if they leverage an indexed field.
+  } // Introspect filters and check if they leverage an indexed field.
+
+
   const indexField = filterFields.find(field => {
     return INDEXED_FIELDS.includes(field);
   });
 
   if (!indexField) {
     // Iterate on all records for this collection (ie. cid)
     const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
     request.onsuccess = cursorHandlers.all(filters, done);
     return request;
   } // If `indexField` was used already, don't filter again.
 
 
-  const remainingFilters = (0, _utils.omitKeys)(filters, indexField); // value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)
-
-  const value = filters[indexField];
-  // For the "id" field, use the primary key.
-  const indexStore = indexField == "id" ? store : store.index(indexField);
-
-  // WHERE IN equivalent clause
+  const remainingFilters = (0, _utils.omitKeys)(filters, [indexField]); // value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)
+
+  const value = filters[indexField]; // For the "id" field, use the primary key.
+
+  const indexStore = indexField == "id" ? store : store.index(indexField); // WHERE IN equivalent clause
+
   if (Array.isArray(value)) {
     if (value.length === 0) {
       return done([]);
     }
 
     const values = value.map(i => [cid, i]).sort();
     const range = IDBKeyRange.bound(values[0], values[values.length - 1]);
     const request = indexStore.openCursor(range);
     request.onsuccess = cursorHandlers.in(values, remainingFilters, done);
     return request;
-  }
-
-  // If no filters on custom attribute, get all results in one bulk.
+  } // If no filters on custom attribute, get all results in one bulk.
+
+
   if (remainingFilters.length == 0) {
     const request = indexStore.getAll(IDBKeyRange.only([cid, value]));
+
     request.onsuccess = event => done(event.target.result);
+
     return request;
   } // WHERE field = value clause
 
 
   const request = indexStore.openCursor(IDBKeyRange.only([cid, value]));
   request.onsuccess = cursorHandlers.all(remainingFilters, done);
   return request;
 }
@@ -762,20 +765,23 @@ class IDB extends _base.default {
       } // Preload specified records using a list request.
 
 
       const filters = {
         id: options.preload
       };
       createListRequest(this.cid, store, filters, records => {
         // Store obtained records by id.
-        const preloaded = records.reduce((acc, record) => {
-          acc[record.id] = (0, _utils.omitKeys)(record, ["_cid"]);
-          return acc;
-        }, {});
+        const preloaded = {};
+
+        for (const record of records) {
+          delete record["_cid"];
+          preloaded[record.id] = record;
+        }
+
         runCallback(preloaded);
       });
     }, {
       mode: "readwrite"
     });
     return result;
   }
   /**
@@ -815,17 +821,21 @@ class IDB extends _base.default {
     } = params;
 
     try {
       let results = [];
       await this.prepare("records", store => {
         createListRequest(this.cid, store, filters, _results => {
           // we have received all requested records that match the filters,
           // we now park them within current scope and hide the `_cid` attribute.
-          results = _results.map(r => (0, _utils.omitKeys)(r, ["_cid"]));
+          for (const result of _results) {
+            delete result["_cid"];
+          }
+
+          results = _results;
         });
       }); // The resulting list of records is sorted.
       // XXX: with some efforts, this could be fully implemented using IDB API.
 
       return params.order ? (0, _utils.sortObjects)(params.order, results) : results;
     } catch (e) {
       this._handleError("list", e);
     }
@@ -880,17 +890,31 @@ class IDB extends _base.default {
    * @param  {Array} records The records to load.
    * @return {Promise}
    */
 
 
   async loadDump(records) {
     try {
       await this.execute(transaction => {
-        records.forEach(record => transaction.update(record));
+        // Since the put operations are asynchronous, we chain
+        // them together. The last one will be waited for the
+        // `transaction.oncomplete` callback. (see #execute())
+        let i = 0;
+        putNext();
+
+        function putNext() {
+          if (i == records.length) {
+            return;
+          } // On error, `transaction.onerror` is called.
+
+
+          transaction.update(records[i]).onsuccess = putNext;
+          ++i;
+        }
       });
       const previousLastModified = await this.getLastModified();
       const lastModified = Math.max(...records.map(record => record.last_modified));
 
       if (lastModified > previousLastModified) {
         await this.saveLastModified(lastModified);
       }
 
@@ -919,17 +943,17 @@ function transactionProxy(adapter, store
   return {
     create(record) {
       store.add({ ...record,
         _cid
       });
     },
 
     update(record) {
-      store.put({ ...record,
+      return store.put({ ...record,
         _cid
       });
     },
 
     delete(id) {
       store.delete([_cid, id]);
     },
 
@@ -3111,23 +3135,24 @@ function deepEqual(a, b) {
  *
  * @param  {Object} obj        The original object.
  * @param  {Array}  keys       The list of keys to exclude.
  * @return {Object}            A copy without the specified keys.
  */
 
 
 function omitKeys(obj, keys = []) {
-  return Object.keys(obj).reduce((acc, key) => {
-    if (!keys.includes(key)) {
-      acc[key] = obj[key];
-    }
-
-    return acc;
-  }, {});
+  const result = { ...obj
+  };
+
+  for (const key of keys) {
+    delete result[key];
+  }
+
+  return result;
 }
 
 function arrayEqual(a, b) {
   if (a.length !== b.length) {
     return false;
   }
 
   for (let i = a.length; i--;) {
new file mode 100644
--- /dev/null
+++ b/services/settings/RemoteSettingsWorker.js
@@ -0,0 +1,188 @@
+/* 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/. */
+
+/* eslint-env mozilla/chrome-worker */
+
+"use strict";
+
+/**
+ * A worker dedicated to Remote Settings.
+ */
+
+importScripts("resource://gre/modules/workers/require.js",
+              "resource://gre/modules/CanonicalJSON.jsm",
+              "resource://gre/modules/third_party/jsesc/jsesc.js");
+
+const IDB_NAME = "remote-settings";
+const IDB_VERSION = 1;
+const IDB_RECORDS_STORE = "records";
+const IDB_TIMESTAMPS_STORE = "timestamps";
+
+const Agent = {
+  /**
+   * Return the canonical JSON serialization of the changes
+   * applied to the local records.
+   * It has to match what is done on the server (See Kinto/kinto-signer).
+   *
+   * @param {Array<Object>} localRecords
+   * @param {Array<Object>} remoteRecords
+   * @param {String} timestamp
+   * @returns {String}
+   */
+  async canonicalStringify(localRecords, remoteRecords, timestamp) {
+    // Sort list by record id.
+    let allRecords = localRecords.concat(remoteRecords).sort((a, b) => {
+      if (a.id < b.id) {
+        return -1;
+      }
+      return a.id > b.id ? 1 : 0;
+    });
+    // All existing records are replaced by the version from the server
+    // and deleted records are removed.
+    for (let i = 0; i < allRecords.length; /* no increment! */) {
+      const rec = allRecords[i];
+      const next = allRecords[i + 1];
+      if ((next && rec.id == next.id) || rec.deleted) {
+        allRecords.splice(i, 1); // remove local record
+      } else {
+        i++;
+      }
+    }
+    const toSerialize = {
+      last_modified: "" + timestamp,
+      data: allRecords,
+    };
+    return CanonicalJSON.stringify(toSerialize, jsesc);
+  },
+
+  /**
+   * If present, import the JSON file into the Remote Settings IndexedDB
+   * for the specified bucket and collection.
+   * (eg. blocklists/certificates, main/onboarding)
+   * @param {String} bucket
+   * @param {String} collection
+   */
+  async importJSONDump(bucket, collection) {
+    const { data: records } = await loadJSONDump(bucket, collection);
+    if (records.length > 0) {
+      await importDumpIDB(bucket, collection, records);
+    }
+    return records.length;
+  },
+};
+
+/**
+ * Wrap worker invocations in order to return the `callbackId` along
+ * the result. This will allow to transform the worker invocations
+ * into promises in `RemoteSettingsWorker.jsm`.
+ */
+self.onmessage = (event) => {
+  const { callbackId, method, args = [] } = event.data;
+  Agent[method](...args)
+    .then((result) => {
+      self.postMessage({ callbackId, result });
+    })
+    .catch(error => {
+      console.log(`RemoteSettingsWorker error: ${error}`);
+      self.postMessage({ callbackId, error: "" + error });
+    });
+};
+
+/**
+ * Load (from disk) the JSON file distributed with the release for this collection.
+ * @param {String}  bucket
+ * @param {String}  collection
+ */
+async function loadJSONDump(bucket, collection) {
+  const fileURI = `resource://app/defaults/settings/${bucket}/${collection}.json`;
+  let response;
+  try {
+    response = await fetch(fileURI);
+  } catch (e) {
+    // Return empty dataset if file is missing.
+    return { data: [] };
+  }
+  // Will throw if JSON is invalid.
+  return response.json();
+}
+
+/**
+ * Import the records into the Remote Settings Chrome IndexedDB.
+ *
+ * Note: This duplicates some logics from `kinto-offline-client.js`.
+ *
+ * @param {String} bucket
+ * @param {String} collection
+ * @param {Array<Object>} records
+ */
+async function importDumpIDB(bucket, collection, records) {
+  // Open the DB. It will exist since if we are running this, it means
+  // we already tried to read the timestamp in `remote-settings.js`
+  const db = await openIDB(IDB_NAME, IDB_VERSION);
+
+  // Each entry of the dump will be stored in the records store.
+  // They are indexed by `_cid`, and their status is `synced`.
+  const cid = bucket + "/" + collection;
+  await executeIDB(db, IDB_RECORDS_STORE, store => {
+    // Chain the put operations together, the last one will be waited by
+    // the `transaction.oncomplete` callback.
+    let i = 0;
+    putNext();
+
+    function putNext() {
+      if (i == records.length) {
+        return;
+      }
+      const entry = { ...records[i], _status: "synced", _cid: cid };
+      store.put(entry).onsuccess = putNext; // On error, `transaction.onerror` is called.
+      ++i;
+    }
+  });
+
+  // Store the highest timestamp as the collection timestamp.
+  const timestamp = Math.max(...records.map(record => record.last_modified));
+  await executeIDB(db, IDB_TIMESTAMPS_STORE, store => store.put({ cid, value: timestamp }));
+}
+
+/**
+ * Helper to wrap indexedDB.open() into a promise.
+ */
+async function openIDB(dbname, version) {
+  return new Promise((resolve, reject) => {
+    const request = indexedDB.open(dbname, version);
+    request.onupgradeneeded = () => {
+      // We should never have to initialize the DB here.
+      reject(new Error(`Error accessing ${dbname} Chrome IDB at version ${version}`));
+    };
+    request.onerror = event => reject(event.target.error);
+    request.onsuccess = event => {
+      const db = event.target.result;
+      resolve(db);
+    };
+  });
+}
+
+/**
+ * Helper to wrap some IDBObjectStore operations into a promise.
+ *
+ * @param {IDBDatabase} db
+ * @param {String} storeName
+ * @param {function} callback
+ */
+async function executeIDB(db, storeName, callback) {
+  const mode = "readwrite";
+  return new Promise((resolve, reject) => {
+    const transaction = db.transaction([storeName], mode);
+    const store = transaction.objectStore(storeName);
+    let result;
+    try {
+      result = callback(store);
+    } catch (e) {
+      transaction.abort();
+      reject(e);
+    }
+    transaction.onerror = event => reject(event.target.error);
+    transaction.oncomplete = event => resolve(result);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/services/settings/RemoteSettingsWorker.jsm
@@ -0,0 +1,53 @@
+/* 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";
+
+/**
+ * Interface to a dedicated thread handling for Remote Settings heavy operations.
+ */
+
+// ChromeUtils.import("resource://gre/modules/PromiseWorker.jsm", this);
+
+var EXPORTED_SYMBOLS = ["RemoteSettingsWorker"];
+
+class Worker {
+
+  constructor(source) {
+    this.worker = new ChromeWorker(source);
+    this.worker.onmessage = this._onWorkerMessage.bind(this);
+
+    this.callbacks = new Map();
+    this.lastCallbackId = 0;
+  }
+
+  async _execute(method, args = []) {
+    return new Promise(async (resolve, reject) => {
+      const callbackId = ++this.lastCallbackId;
+      this.callbacks.set(callbackId, [resolve, reject]);
+      this.worker.postMessage({ callbackId, method, args });
+    });
+  }
+
+  _onWorkerMessage(event) {
+    const { callbackId, result, error } = event.data;
+    const [resolve, reject] = this.callbacks.get(callbackId);
+    if (error) {
+      reject(new Error(error));
+    } else {
+      resolve(result);
+    }
+    this.callbacks.delete(callbackId);
+  }
+
+  async canonicalStringify(localRecords, remoteRecords, timestamp) {
+    return this._execute("canonicalStringify", [localRecords, remoteRecords, timestamp]);
+  }
+
+  async importJSONDump(bucket, collection) {
+    return this._execute("importJSONDump", [bucket, collection]);
+  }
+}
+
+var RemoteSettingsWorker = new Worker("resource://services-settings/RemoteSettingsWorker.js");
--- a/services/settings/moz.build
+++ b/services/settings/moz.build
@@ -11,11 +11,13 @@ DIRS += [
 
 EXTRA_COMPONENTS += [
     'RemoteSettingsComponents.js',
     'servicesSettings.manifest',
 ]
 
 EXTRA_JS_MODULES['services-settings'] += [
     'remote-settings.js',
+    'RemoteSettingsWorker.js',
+    'RemoteSettingsWorker.jsm',
 ]
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
--- a/services/settings/remote-settings.js
+++ b/services/settings/remote-settings.js
@@ -26,33 +26,35 @@ ChromeUtils.defineModuleGetter(this, "Ca
 ChromeUtils.defineModuleGetter(this, "UptakeTelemetry",
                                "resource://services-common/uptake-telemetry.js");
 ChromeUtils.defineModuleGetter(this, "ClientEnvironmentBase",
                                "resource://gre/modules/components-utils/ClientEnvironment.jsm");
 ChromeUtils.defineModuleGetter(this, "FilterExpressions",
                                "resource://gre/modules/components-utils/FilterExpressions.jsm");
 ChromeUtils.defineModuleGetter(this, "pushBroadcastService",
                                "resource://gre/modules/PushBroadcastService.jsm");
+ChromeUtils.defineModuleGetter(this, "RemoteSettingsWorker",
+                               "resource://services-settings/RemoteSettingsWorker.jsm");
 
 const PREF_SETTINGS_DEFAULT_BUCKET     = "services.settings.default_bucket";
 const PREF_SETTINGS_BRANCH             = "services.settings.";
 const PREF_SETTINGS_SERVER             = "server";
 const PREF_SETTINGS_DEFAULT_SIGNER     = "default_signer";
 const PREF_SETTINGS_VERIFY_SIGNATURE   = "verify_signature";
 const PREF_SETTINGS_SERVER_BACKOFF     = "server.backoff";
 const PREF_SETTINGS_CHANGES_PATH       = "changes.path";
 const PREF_SETTINGS_LAST_UPDATE        = "last_update_seconds";
 const PREF_SETTINGS_LAST_ETAG          = "last_etag";
 const PREF_SETTINGS_CLOCK_SKEW_SECONDS = "clock_skew_seconds";
 const PREF_SETTINGS_LOAD_DUMP          = "load_dump";
 
 // Telemetry update source identifier.
 const TELEMETRY_HISTOGRAM_KEY = "settings-changes-monitoring";
 
-const INVALID_SIGNATURE = "Invalid content/signature";
+const INVALID_SIGNATURE = "Invalid content signature";
 const MISSING_SIGNATURE = "Missing signature";
 
 XPCOMUtils.defineLazyGetter(this, "gPrefs", () => {
   return Services.prefs.getBranch(PREF_SETTINGS_BRANCH);
 });
 XPCOMUtils.defineLazyPreferenceGetter(this, "gVerifySignature", PREF_SETTINGS_BRANCH + PREF_SETTINGS_VERIFY_SIGNATURE, true);
 XPCOMUtils.defineLazyPreferenceGetter(this, "gServerURL", PREF_SETTINGS_BRANCH + PREF_SETTINGS_SERVER);
 XPCOMUtils.defineLazyPreferenceGetter(this, "gChangesPath", PREF_SETTINGS_BRANCH + PREF_SETTINGS_CHANGES_PATH);
@@ -101,42 +103,18 @@ async function jexlFilterFunc(entry, env
     };
     result = await FilterExpressions.eval(filter_expression, context);
   } catch (e) {
     Cu.reportError(e);
   }
   return result ? entry : null;
 }
 
-
-function mergeChanges(collection, localRecords, changes) {
-  const records = {};
-  // Local records by id.
-  localRecords.forEach((record) => records[record.id] = collection.cleanLocalFields(record));
-  // All existing records are replaced by the version from the server.
-  changes.forEach((record) => records[record.id] = record);
-
-  return Object.values(records)
-    // Filter out deleted records.
-    .filter((record) => !record.deleted)
-    // Sort list by record id.
-    .sort((a, b) => {
-      if (a.id < b.id) {
-        return -1;
-      }
-      return a.id > b.id ? 1 : 0;
-    });
-}
-
-
 async function fetchCollectionMetadata(remote, collection, expectedTimestamp) {
   const client = new KintoHttpClient(remote);
-  //
-  // XXX: https://github.com/Kinto/kinto-http.js/issues/307
-  //
   const { signature } = await client.bucket(collection.bucket)
                                     .collection(collection.name)
                                     .getData({ query: { _expected: expectedTimestamp }});
   return signature;
 }
 
 async function fetchRemoteCollection(collection, expectedTimestamp) {
   const client = new KintoHttpClient(gServerURL);
@@ -218,43 +196,16 @@ async function fetchLatestChanges(url, l
     if (!isNaN(value)) {
       backoffSeconds = value;
     }
   }
 
   return {changes, currentEtag, serverTimeMillis, backoffSeconds};
 }
 
-/**
- * Load the the JSON file distributed with the release for this collection.
- * @param {String}  bucket
- * @param {String}  collection
- * @param {Object}  options
- * @param {boolean} options.ignoreMissing Do not throw an error if the file is missing.
- */
-async function loadDumpFile(bucket, collection, { ignoreMissing = true } = {}) {
-  const fileURI = `resource://app/defaults/settings/${bucket}/${collection}.json`;
-  let response;
-  try {
-    // Will throw NetworkError is folder/file is missing.
-    response = await fetch(fileURI);
-    if (!response.ok) {
-      throw new Error(`Could not read from '${fileURI}'`);
-    }
-    // Will throw if JSON is invalid.
-    return response.json();
-  } catch (e) {
-    // A missing file is reported as "NetworError" (see Bug 1493709)
-    if (!ignoreMissing || !/NetworkError/.test(e.message)) {
-      throw e;
-    }
-  }
-  return { data: [] };
-}
-
 
 class RemoteSettingsClient {
 
   constructor(collectionName, { bucketNamePref, signerName, filterFunc = jexlFilterFunc, localFields = [], lastCheckTimePref }) {
     this.collectionName = collectionName;
     this.signerName = signerName;
     this.filterFunc = filterFunc;
     this.localFields = localFields;
@@ -347,26 +298,25 @@ class RemoteSettingsClient {
    * @param  {Object} options         The options object.
    * @param  {Object} options.filters Filter the results (default: `{}`).
    * @param  {Object} options.order   The order to apply   (default: `-last_modified`).
    * @return {Promise}
    */
   async get(options = {}) {
     // In Bug 1451031, we will do some jexl filtering to limit the list items
     // whose target is matched.
-    const { filters = {}, order } = options;
+    const { filters = {}, order = "" } = options; // not sorted by default.
     const c = await this.openCollection();
 
     const timestamp = await c.db.getLastModified();
     // If the local database was never synchronized, then we attempt to load
     // a packaged JSON dump.
     if (timestamp == null) {
       try {
-        const { data } = await loadDumpFile(this.bucketName, this.collectionName);
-        await c.loadDump(data);
+        await RemoteSettingsWorker.importJSONDump(this.bucketName, this.collectionName);
       } catch (e) {
         // Report but return an empty list since there will be no data anyway.
         Cu.reportError(e);
         return [];
       }
     }
 
     const { data } = await c.list({ filters, order });
@@ -393,18 +343,17 @@ class RemoteSettingsClient {
       let collectionLastModified = await collection.db.getLastModified();
 
       // If there is no data currently in the collection, attempt to import
       // initial data from the application defaults.
       // This allows to avoid synchronizing the whole collection content on
       // cold start.
       if (!collectionLastModified && loadDump) {
         try {
-          const initialData = await loadDumpFile(this.bucketName, this.collectionName);
-          await collection.loadDump(initialData.data);
+          await RemoteSettingsWorker.importJSONDump(this.bucketName, this.collectionName);
           collectionLastModified = await collection.db.getLastModified();
         } catch (e) {
           // Report but go-on.
           Cu.reportError(e);
         }
       }
 
       // If the data is up to date, there's no need to sync. We still need
@@ -413,18 +362,23 @@ class RemoteSettingsClient {
         this._updateLastCheck(serverTime);
         reportStatus = UptakeTelemetry.STATUS.UP_TO_DATE;
         return;
       }
 
       // If there is a `signerName` and collection signing is enforced, add a
       // hook for incoming changes that validates the signature.
       if (this.signerName && gVerifySignature) {
-        collection.hooks["incoming-changes"] = [(payload, collection) => {
-          return this._validateCollectionSignature(payload, collection, { expectedTimestamp });
+        collection.hooks["incoming-changes"] = [async (payload, collection) => {
+          await this._validateCollectionSignature(payload.changes,
+                                                  payload.lastModified,
+                                                  collection,
+                                                  { expectedTimestamp });
+          // In case the signature is valid, apply the changes locally.
+          return payload;
         }];
       }
 
       // Fetch changes from server.
       let syncResult;
       try {
         // Server changes have priority during synchronization.
         const strategy = Kinto.syncStrategy.SERVER_WINS;
@@ -434,34 +388,37 @@ class RemoteSettingsClient {
         syncResult = await collection.sync({ remote: gServerURL, strategy, expectedTimestamp });
         const { ok } = syncResult;
         if (!ok) {
           // Some synchronization conflicts occured.
           reportStatus = UptakeTelemetry.STATUS.CONFLICT_ERROR;
           throw new Error("Sync failed");
         }
       } catch (e) {
-        if (e.message == INVALID_SIGNATURE) {
+        if (e.message.includes(INVALID_SIGNATURE)) {
           // Signature verification failed during synchronzation.
           reportStatus = UptakeTelemetry.STATUS.SIGNATURE_ERROR;
           // if sync fails with a signature error, it's likely that our
           // local data has been modified in some way.
           // We will attempt to fix this by retrieving the whole
           // remote collection.
           const payload = await fetchRemoteCollection(collection, expectedTimestamp);
           try {
-            await this._validateCollectionSignature(payload, collection, { expectedTimestamp, ignoreLocal: true });
+            await this._validateCollectionSignature(payload.data,
+                                                    payload.last_modified,
+                                                    collection,
+                                                    { expectedTimestamp, ignoreLocal: true });
           } catch (e) {
             reportStatus = UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR;
             throw e;
           }
 
           // The signature is good (we haven't thrown).
           // Now we will Inspect what we had locally.
-          const { data: oldData } = await collection.list();
+          const { data: oldData } = await collection.list({ order: "" }); // no need to sort.
 
           // We build a sync result as if a diff-based sync was performed.
           syncResult = { created: [], updated: [], deleted: [] };
 
           // If the remote last_modified is newer than the local last_modified,
           // replace the local data
           const localLastModified = await collection.db.getLastModified();
           if (payload.last_modified >= localLastModified) {
@@ -510,17 +467,17 @@ class RemoteSettingsClient {
         );
       // For updates, keep entries whose updated form is matches the target.
       const updatedFilteredIds = new Set(updatedFiltered.map(e => e.id));
       const updated = allUpdated.filter(({ new: { id } }) => updatedFilteredIds.has(id));
 
       // If every changed entry is filtered, we don't even fire the event.
       if (created.length || updated.length || deleted.length) {
         // Read local collection of records (also filtered).
-        const { data: allData } = await collection.list();
+        const { data: allData } = await collection.list({ order: "" }); // no need to sort.
         const current = await this._filterEntries(allData);
         const payload = { data: { current, created, updated, deleted } };
         try {
           await this.emit("sync", payload);
         } catch (e) {
           reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
           throw e;
         }
@@ -540,54 +497,46 @@ class RemoteSettingsClient {
       if (reportStatus === null) {
         reportStatus = UptakeTelemetry.STATUS.SUCCESS;
       }
       // Report success/error status to Telemetry.
       UptakeTelemetry.report(this.identifier, reportStatus);
     }
   }
 
-  async _validateCollectionSignature(payload, collection, options = {}) {
-    const { expectedTimestamp, ignoreLocal } = options;
+  async _validateCollectionSignature(remoteRecords, timestamp, collection, options = {}) {
+    const { expectedTimestamp, ignoreLocal = false } = options;
     // this is a content-signature field from an autograph response.
     const signaturePayload = await fetchCollectionMetadata(gServerURL, collection, expectedTimestamp);
     if (!signaturePayload) {
       throw new Error(MISSING_SIGNATURE);
     }
     const {x5u, signature} = signaturePayload;
     const certChainResponse = await fetch(x5u);
     const certChain = await certChainResponse.text();
 
     const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
                        .createInstance(Ci.nsIContentSignatureVerifier);
 
-    let toSerialize;
-    if (ignoreLocal) {
-      toSerialize = {
-        last_modified: `${payload.last_modified}`,
-        data: payload.data,
-      };
-    } else {
-      const {data: localRecords} = await collection.list();
-      const records = mergeChanges(collection, localRecords, payload.changes);
-      toSerialize = {
-        last_modified: `${payload.lastModified}`,
-        data: records,
-      };
+    let localRecords = [];
+    if (!ignoreLocal) {
+      const { data } = await collection.list({ order: "" }); // no need to sort.
+      // Local fields are stripped to compute the collection signature (server does not have them).
+      localRecords = data.map(r => collection.cleanLocalFields(r));
     }
 
-    const serialized = CanonicalJSON.stringify(toSerialize);
-
-    if (verifier.verifyContentSignature(serialized, "p384ecdsa=" + signature,
-                                        certChain,
-                                        this.signerName)) {
-      // In case the hash is valid, apply the changes locally.
-      return payload;
+    const serialized = await RemoteSettingsWorker.canonicalStringify(localRecords,
+                                                                     remoteRecords,
+                                                                     timestamp);
+    if (!verifier.verifyContentSignature(serialized,
+                                         "p384ecdsa=" + signature,
+                                         certChain,
+                                         this.signerName)) {
+      throw new Error(INVALID_SIGNATURE + ` (${collection.bucket}/${collection.name})`);
     }
-    throw new Error(INVALID_SIGNATURE);
   }
 
   /**
    * Save last time server was checked in users prefs.
    *
    * @param {Date} serverTime   the current date return by server.
    */
   _updateLastCheck(serverTime) {
@@ -623,17 +572,17 @@ async function hasLocalData(client) {
  * Check if we ship a JSON dump for the specified bucket and collection.
  *
  * @param {String} bucket
  * @param {String} collection
  * @return {bool} Whether it is present or not.
  */
 async function hasLocalDump(bucket, collection) {
   try {
-    await loadDumpFile(bucket, collection, {ignoreMissing: false});
+    await fetch(`resource://app/defaults/settings/${bucket}/${collection}.json`);
     return true;
   } catch (e) {
     return false;
   }
 }
 
 
 function remoteSettingsFunction() {
new file mode 100644
--- /dev/null
+++ b/services/settings/test/unit/test_remote_settings_worker.js
@@ -0,0 +1,50 @@
+/* import-globals-from ../../../common/tests/unit/head_helpers.js */
+
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { RemoteSettingsWorker } = ChromeUtils.import("resource://services-settings/RemoteSettingsWorker.jsm", {});
+const { Kinto } = ChromeUtils.import("resource://services-common/kinto-offline-client.js", {});
+
+const IS_ANDROID = AppConstants.platform == "android";
+
+add_task(async function test_canonicaljson_merges_remote_into_local() {
+  const localRecords = [{ id: "1", title: "title 1" }, { id: "2", title: "title 2" }, { id: "3", title: "title 3" }];
+  const remoteRecords = [{ id: "2", title: "title b" }, { id: "3", deleted: true }];
+  const timestamp = 42;
+
+  const serialized = await RemoteSettingsWorker.canonicalStringify(localRecords, remoteRecords, timestamp);
+
+  Assert.equal('{"data":[{"id":"1","title":"title 1"},{"id":"2","title":"title b"}],"last_modified":"42"}', serialized);
+});
+
+
+add_task(async function test_import_json_dump_into_idb() {
+  if (IS_ANDROID) {
+    // Skip test: we don't ship remote settings dumps on Android (see package-manifest).
+    return;
+  }
+  const kintoCollection = new Kinto({
+    bucket: "main",
+    adapter: Kinto.adapters.IDB,
+    adapterOptions: { dbName: "remote-settings" },
+  }).collection("language-dictionaries");
+  const { data: before } = await kintoCollection.list();
+  Assert.equal(before.length, 0);
+
+  await RemoteSettingsWorker.importJSONDump("main", "language-dictionaries");
+
+  const { data: after } = await kintoCollection.list();
+  Assert.ok(after.length > 0);
+});
+
+
+add_task(async function test_throws_error_if_worker_fails() {
+  let error;
+  try {
+    await RemoteSettingsWorker.canonicalStringify(null, [], 42);
+  } catch (e) {
+    error = e;
+  }
+  Assert.equal("TypeError: localRecords is null; can't access its \"concat\" property", error.message);
+});
--- a/services/settings/test/unit/xpcshell.ini
+++ b/services/settings/test/unit/xpcshell.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 head = ../../../common/tests/unit/head_global.js ../../../common/tests/unit/head_helpers.js
 firefox-appdir = browser
 tags = remote-settings
 
 [test_remote_settings.js]
 [test_remote_settings_poll.js]
+[test_remote_settings_worker.js]
 [test_remote_settings_jexl_filters.js]
--- a/testing/web-platform/mach_commands.py
+++ b/testing/web-platform/mach_commands.py
@@ -161,20 +161,29 @@ class WebPlatformTestsRunnerSetup(Mozbui
 
 
 class WebPlatformTestsUpdater(MozbuildObject):
     """Update web platform tests."""
     def setup_logging(self, **kwargs):
         import update
         return update.setup_logging(kwargs, {"mach": sys.stdout})
 
+    def update_manifest(self, logger, **kwargs):
+        import manifestupdate
+        return manifestupdate.run(logger=logger,
+                                  src_root=self.topsrcdir,
+                                  obj_root=self.topobjdir,
+                                  **kwargs)
+
     def run_update(self, logger, **kwargs):
         import update
         from update import updatecommandline
 
+        self.update_manifest(logger, **kwargs)
+
         if kwargs["config"] is None:
             kwargs["config"] = os.path.join(self.topobjdir, '_tests', 'web-platform', 'wptrunner.local.ini')
         if kwargs["product"] is None:
             kwargs["product"] = "firefox"
 
         kwargs["store_state"] = False
 
         kwargs = updatecommandline.check_args(kwargs)
--- a/toolkit/components/build/nsToolkitCompsCID.h
+++ b/toolkit/components/build/nsToolkitCompsCID.h
@@ -58,19 +58,16 @@
   "@mozilla.org/browser/nav-history-service;1"
 
 #define NS_ANNOTATIONSERVICE_CONTRACTID \
   "@mozilla.org/browser/annotation-service;1"
 
 #define NS_NAVBOOKMARKSSERVICE_CONTRACTID \
   "@mozilla.org/browser/nav-bookmarks-service;1"
 
-#define NS_LIVEMARKSERVICE_CONTRACTID \
-  "@mozilla.org/browser/livemark-service;2"
-
 #define NS_TAGGINGSERVICE_CONTRACTID \
 "@mozilla.org/browser/tagging-service;1"
 
 #define NS_FAVICONSERVICE_CONTRACTID \
   "@mozilla.org/browser/favicon-service;1"
 
 #define NS_APPSTARTUP_CONTRACTID \
   "@mozilla.org/toolkit/app-startup;1"
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -87,23 +87,16 @@ async function promiseTagsFolderId() {
   );
   return gTagsFolderId = rows[0].getResultByName("id");
 }
 
 const MATCH_ANYWHERE_UNMODIFIED = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
 const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
 const SQLITE_MAX_VARIABLE_NUMBER = 999;
 
-// Annotations which insertTree currently accepts. These should be going away
-// soon, see bug 1460577.
-const ACCEPTED_ANNOTATIONS = [
-  PlacesUtils.LMANNO_FEEDURI,
-  PlacesUtils.LMANNO_SITEURI,
-];
-
 var Bookmarks = Object.freeze({
   /**
    * Item's type constants.
    * These should stay consistent with nsINavBookmarksService.idl
    */
   TYPE_BOOKMARK: 1,
   TYPE_FOLDER: 2,
   TYPE_SEPARATOR: 3,
@@ -438,18 +431,16 @@ var Bookmarks = Object.freeze({
           dateAdded: { defaultValue: time,
                        validIf: b => !b.lastModified ||
                                      b.dateAdded <= b.lastModified },
           lastModified: { defaultValue: time,
                           validIf: b => (!b.dateAdded && b.lastModified >= time) ||
                                         (b.dateAdded && b.lastModified >= b.dateAdded) },
           index: { replaceWith: indexToUse++ },
           source: { replaceWith: source },
-          annos: { validIf: b => false,
-                   fixup: b => b.annos = b.annos.filter(anno => ACCEPTED_ANNOTATIONS.includes(anno.name))},
           keyword: { validIf: b => b.type == TYPE_BOOKMARK },
           charset: { validIf: b => b.type == TYPE_BOOKMARK },
           postData: { validIf: b => b.type == TYPE_BOOKMARK },
           tags: { validIf: b => b.type == TYPE_BOOKMARK },
           children: { validIf: b => b.type == TYPE_FOLDER && Array.isArray(b.children) },
         };
         if (fixupOrSkipInvalidEntries) {
           insertInfo.guid.fixup = b => b.guid = PlacesUtils.history.makeGuid();
@@ -563,19 +554,16 @@ var Bookmarks = Object.freeze({
           title: item.title,
           dateAdded: item.dateAdded,
           guid: item.guid,
           parentGuid: item.parentGuid,
           source: item.source,
           isTagging: false,
         }));
 
-        // Note, annotations for livemark data are deleted from insertInfo
-        // within appendInsertionInfoForInfoArray, so we won't be duplicating
-        // the insertions here.
         try {
           await handleBookmarkItemSpecialData(itemId, item);
         } catch (ex) {
           // This is not critical, regardless the bookmark has been created
           // and we should continue notifying the next ones.
           Cu.reportError(`An error occured while handling special bookmark data: ${ex}`);
         }
 
--- a/toolkit/components/places/PlacesTransactions.jsm
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -54,19 +54,16 @@ var EXPORTED_SYMBOLS = ["PlacesTransacti
  * but optional for the NewBookmark transaction).
  *
  * To make things simple, a given input property has the same basic meaning and
  * valid values across all transactions which accept it in the input object.
  * Here is a list of all supported input properties along with their expected
  * values:
  *  - url: a URL object, an nsIURI object, or a href.
  *  - urls: an array of urls, as above.
- *  - feedUrl: an url (as above), holding the url for a live bookmark.
- *  - siteUrl an url (as above), holding the url for the site with which
- *            a live bookmark is associated.
  *  - tag - a string.
  *  - tags: an array of strings.
  *  - guid, parentGuid, newParentGuid: a valid Places GUID string.
  *  - guids: an array of valid Places GUID strings.
  *  - title: a string
  *  - index, newIndex: the position of an item in its containing folder,
  *    starting from 0.
  *    integer and PlacesUtils.bookmarks.DEFAULT_INDEX
@@ -894,17 +891,17 @@ DefineTransaction.verifyInput = function
     fixedInput[prop] = this.validatePropertyValue(prop, input, false);
   }
 
   return fixedInput;
 };
 
 // Update the documentation at the top of this module if you add or
 // remove properties.
-DefineTransaction.defineInputProps(["url", "feedUrl", "siteUrl"],
+DefineTransaction.defineInputProps(["url"],
                                    DefineTransaction.urlValidate, null);
 DefineTransaction.defineInputProps(["guid", "parentGuid", "newParentGuid"],
                                    DefineTransaction.guidValidate);
 DefineTransaction.defineInputProps(["title", "postData"],
                                    DefineTransaction.strOrNullValidate, null);
 DefineTransaction.defineInputProps(["keyword", "oldKeyword", "oldTag", "tag",
                                     "excludingAnnotation"],
                                    DefineTransaction.strValidate, "");
@@ -1384,28 +1381,23 @@ PT.Remove.prototype = {
         removedItems.push(await PlacesUtils.promiseBookmarksTree(guid));
       } catch (ex) {
         throw new Error("Failed to get info for the specified item (guid: " +
                           guid + "): " + ex);
       }
     }
 
     let removeThem = async function() {
-      let bmsToRemove = [];
-      for (let info of removedItems) {
-        if (info.annos &&
-            info.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
-          await PlacesUtils.livemarks.removeLivemark({ guid: info.guid });
-        } else {
-          bmsToRemove.push({guid: info.guid});
-        }
-      }
-
-      if (bmsToRemove.length) {
-        await PlacesUtils.bookmarks.remove(bmsToRemove);
+      if (removedItems.length) {
+        // We have to pass just the guids as although remove() accepts full
+        // info items, promiseBookmarksTree returns dateAdded and lastModified
+        // as PRTime rather than date types.
+        await PlacesUtils.bookmarks.remove(removedItems.map(info => {
+          return { guid: info.guid};
+        }));
       }
     };
     await removeThem();
 
     this.undo = async function() {
       for (let info of removedItems) {
         await createItemsFromBookmarksTree(info, true);
       }
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -254,18 +254,17 @@ const BOOKMARK_VALIDATORS = Object.freez
 });
 
 // Sync bookmark records can contain additional properties.
 const SYNC_BOOKMARK_VALIDATORS = Object.freeze({
   // Sync uses Places GUIDs for all records except roots.
   recordId: simpleValidateFunc(v => typeof v == "string" && (
                                 (PlacesSyncUtils.bookmarks.ROOTS.includes(v) || PlacesUtils.isValidGuid(v)))),
   parentRecordId: v => SYNC_BOOKMARK_VALIDATORS.recordId(v),
-  // Sync uses kinds instead of types, which distinguish between livemarks and
-  // queries.
+  // Sync uses kinds instead of types.
   kind: simpleValidateFunc(v => typeof v == "string" &&
                                 Object.values(PlacesSyncUtils.bookmarks.KINDS).includes(v)),
   query: simpleValidateFunc(v => v === null || (typeof v == "string" && v)),
   folder: simpleValidateFunc(v => typeof v == "string" && v &&
                                   v.length <= PlacesUtils.bookmarks.MAX_TAG_LENGTH),
   tags: v => {
     if (v === null) {
       return [];
@@ -395,16 +394,17 @@ var PlacesUtils = {
   TYPE_X_MOZ_URL: "text/x-moz-url",
   // Place entries formatted as HTML anchors
   TYPE_HTML: "text/html",
   // Place entries as raw URL text
   TYPE_UNICODE: "text/unicode",
   // Used to track the action that populated the clipboard.
   TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
 
+  // Deprecated: Remaining only for supporting migration of old livemarks.
   LMANNO_FEEDURI: "livemark/feedURI",
   LMANNO_SITEURI: "livemark/siteURI",
   CHARSET_ANNO: "URIProperties/characterSet",
   // Deprecated: This is only used for supporting import from older datasets.
   MOBILE_ROOT_ANNO: "mobile/bookmarksRoot",
 
   TOPIC_SHUTDOWN: "places-shutdown",
   TOPIC_INIT_COMPLETE: "places-init-complete",
@@ -1822,20 +1822,16 @@ XPCOMUtils.defineLazyGetter(PlacesUtils,
 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations",
                                    "@mozilla.org/browser/annotation-service;1",
                                    "nsIAnnotationService");
 
 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "tagging",
                                    "@mozilla.org/browser/tagging-service;1",
                                    "nsITaggingService");
 
-XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "livemarks",
-                                   "@mozilla.org/browser/livemark-service;2",
-                                   "mozIAsyncLivemarks");
-
 XPCOMUtils.defineLazyGetter(this, "bundle", function() {
   const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties";
   return Services.strings.createBundle(PLACES_STRING_BUNDLE_URI);
 });
 
 // This is just used as a reasonably-random value for copy & paste / drag operations.
 XPCOMUtils.defineLazyGetter(PlacesUtils, "instanceId", () => {
   return PlacesUtils.history.makeGuid();
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -11,17 +11,16 @@ XPIDL_SOURCES += [
     'nsINavHistoryService.idl',
 ]
 
 XPIDL_MODULE = 'places'
 
 if CONFIG['MOZ_PLACES']:
     XPIDL_SOURCES += [
         'mozIAsyncHistory.idl',
-        'mozIAsyncLivemarks.idl',
         'mozIColorAnalyzer.idl',
         'mozIPlacesAutoComplete.idl',
         'mozIPlacesPendingOperation.idl',
         'nsIAnnotationService.idl',
         'nsIFaviconService.idl',
         'nsINavBookmarksService.idl',
         'nsITaggingService.idl',
     ]
@@ -72,17 +71,16 @@ if CONFIG['MOZ_PLACES']:
         'PlacesSyncUtils.jsm',
         'PlacesTransactions.jsm',
         'PlacesUtils.jsm',
         'SyncedBookmarksMirror.jsm',
     ]
 
     EXTRA_COMPONENTS += [
         'ColorAnalyzer.js',
-        'nsLivemarkService.js',
         'nsPlacesExpiration.js',
         'nsTaggingService.js',
         'PageIconProtocolHandler.js',
         'PlacesCategoriesStarter.js',
         'toolkitplaces.manifest',
         'UnifiedComplete.js',
     ]
 
deleted file mode 100644
--- a/toolkit/components/places/mozIAsyncLivemarks.idl
+++ /dev/null
@@ -1,181 +0,0 @@
-/* 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/. */
-
-#include "nsISupports.idl"
-
-interface nsIURI;
-
-interface mozILivemarkInfo;
-interface mozILivemark;
-
-interface nsINavHistoryResultObserver;
-
-[scriptable, uuid(672387b7-a75d-4e8f-9b49-5c1dcbfff46b)]
-interface mozIAsyncLivemarks : nsISupports
-{
-  /**
-   * Removes an existing livemark.
-   *
-   * @param aLivemarkInfo
-   *        mozILivemarkInfo object containing either an id or a guid of the
-   *        livemark to remove.
-   *
-   * @return {Promise}
-   * @throws NS_ERROR_INVALID_ARG if the id/guid is invalid.
-   */
-  jsval removeLivemark(in jsval aLivemarkInfo);
-
-  /**
-   * Gets an existing livemark.
-   *
-   * @param aLivemarkInfo
-   *        mozILivemarkInfo object containing either an id or a guid of the
-   *        livemark to retrieve.
-   *
-   * @return {Promise}
-   * @throws NS_ERROR_INVALID_ARG if the id/guid is invalid or an invalid
-   *         callback is provided.
-   */
-  jsval getLivemark(in jsval aLivemarkInfo);
-
-  /**
-   * Reloads all livemarks if they are expired or if forced to do so.
-   *
-   * @param [optional]aForceUpdate
-   *        If set to true forces a reload even if contents are still valid.
-   *
-   * @note The update process is asynchronous, observers registered through
-   *       registerForUpdates will be notified of updated contents.
-   */
-  void reloadLivemarks([optional]in boolean aForceUpdate);
-
-  void handlePlacesEvents(in jsval aEvents);
-
-  jsval invalidateCachedLivemarks();
-};
-
-[scriptable, uuid(3a3c5e8f-ec4a-4086-ae0a-d16420d30c9f)]
-interface mozILivemarkInfo : nsISupports
-{
-  /**
-   * Id of the bookmarks folder representing this livemark.
-   *
-   * @deprecated Use guid instead.
-   */
-  readonly attribute long long id;
-
-  /**
-   * The globally unique identifier of this livemark.
-   */
-  readonly attribute ACString guid;
-
-  /**
-   * Title of this livemark.
-   */
-  readonly attribute AString title;
-
-  /**
-   * Id of the bookmarks parent folder containing this livemark.
-   *
-   * @deprecated Use parentGuid instead.
-   */
-  readonly attribute long long parentId;
-
-  /**
-   * Guid of the bookmarks parent folder containing this livemark.
-   */
-  readonly attribute long long parentGuid;
-
-  /**
-   * The position of this livemark in the bookmarks parent folder.
-   */
-  readonly attribute long index;
-
-  /**
-   * Time this livemark was created.
-   */
-  readonly attribute PRTime dateAdded;
-
-  /**
-   * Time this livemark's details were last modified.  Doesn't track changes to
-   * the livemark contents.
-   */
-  readonly attribute PRTime lastModified;
-
-  /**
-   * The URI of the syndication feed associated with this livemark.
-   */
-  readonly attribute nsIURI feedURI;
-
-  /**
-   * The URI of the website associated with this livemark.
-   */
-  readonly attribute nsIURI siteURI;
-};
-
-[scriptable, uuid(9f6fdfae-db9a-4bd8-bde1-148758cf1b18)]
-interface mozILivemark : mozILivemarkInfo
-{
-  // Indicates the livemark is inactive.
-  const unsigned short STATUS_READY = 0;
-  // Indicates the livemark is fetching new contents.
-  const unsigned short STATUS_LOADING = 1;
-  // Indicates the livemark failed to fetch new contents.
-  const unsigned short STATUS_FAILED = 2;
-
-  /**
-   * Status of this livemark.  One of the STATUS_* constants above.
-   */
-  readonly attribute unsigned short status;
-
-  /**
-   * Reload livemark contents if they are expired or if forced to do so.
-   *
-   * @param [optional]aForceUpdate
-   *        If set to true forces a reload even if contents are still valid.
-   *
-   * @note The update process is asynchronous, it's possible to register a
-   *       result observer to be notified of updated contents through
-   *       registerForUpdates.
-   */
-  void reload([optional]in boolean aForceUpdate);
-
-  /**
-   * Returns an array of nsINavHistoryResultNode objects, representing children
-   * of this livemark.  The nodes will have aContainerNode as parent.
-   *
-   * @param aContainerNode
-   *        Object implementing nsINavHistoryContainerResultNode, to be used as
-   *        parent of the livemark nodes.
-   */
-  jsval getNodesForContainer(in jsval aContainerNode);
-
-  /**
-   * Registers a container node for updates on this livemark.
-   * When the livemark contents change, an invalidateContainer(aContainerNode)
-   * request is sent to aResultObserver.
-   *
-   * @param aContainerNode
-   *        Object implementing nsINavHistoryContainerResultNode, representing
-   *        this livemark.
-   * @param aResultObserver
-   *        The nsINavHistoryResultObserver that should be notified of changes
-   *        to the livemark contents.
-   */
-  void registerForUpdates(in jsval aContainerNode,
-                          in nsINavHistoryResultObserver aResultObserver);
-
-  /**
-   * Unregisters a previously registered container node.
-   *
-   * @param aContainerNode
-   *        Object implementing nsINavHistoryContainerResultNode, representing
-   *        this livemark.
-   *
-   * @note it's suggested to always unregister containers that are no more used,
-   *       to free up the associated resources.  A good time to do so is when
-   *       the container gets closed.
-   */
-  void unregisterForUpdates(in jsval aContainerNode);
-};
deleted file mode 100644
--- a/toolkit/components/places/nsLivemarkService.js
+++ /dev/null
@@ -1,841 +0,0 @@
-/* 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/. */
-
-// Modules and services.
-
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.defineModuleGetter(this, "PlacesUtils",
-                               "resource://gre/modules/PlacesUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "NetUtil",
-                               "resource://gre/modules/NetUtil.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "history", function() {
-  let livemarks = PlacesUtils.livemarks;
-  // Lazily add an history observer when it's actually needed.
-  PlacesUtils.history.addObserver(livemarks, true);
-  return PlacesUtils.history;
-});
-
-// Constants
-
-// Delay between reloads of consecute livemarks.
-const RELOAD_DELAY_MS = 500;
-// Expire livemarks after this time.
-const EXPIRE_TIME_MS = 3600000; // 1 hour.
-// Expire livemarks after this time on error.
-const ONERROR_EXPIRE_TIME_MS = 300000; // 5 minutes.
-
-// Livemarks cache.
-
-XPCOMUtils.defineLazyGetter(this, "CACHE_SQL", () => {
-  function getAnnoSQLFragment(aAnnoParam) {
-    return `SELECT a.content
-            FROM moz_items_annos a
-            JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
-            WHERE a.item_id = b.id
-              AND n.name = ${aAnnoParam}`;
-  }
-
-  return `SELECT b.id, b.title, b.parent As parentId, b.position AS 'index',
-                 b.guid, b.dateAdded, b.lastModified, p.guid AS parentGuid,
-                 ( ${getAnnoSQLFragment(":feedURI_anno")} ) AS feedURI,
-                 ( ${getAnnoSQLFragment(":siteURI_anno")} ) AS siteURI
-          FROM moz_bookmarks b
-          JOIN moz_bookmarks p ON b.parent = p.id
-          JOIN moz_items_annos a ON a.item_id = b.id
-          JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
-          WHERE b.type = :folder_type
-            AND n.name = :feedURI_anno`;
-});
-
-/**
- * Convert a Date object to a PRTime (microseconds).
- *
- * @param date
- *        the Date object to convert.
- * @return microseconds from the epoch.
- */
-function toPRTime(date) {
-  return date * 1000;
-}
-
-/**
- * Convert a PRTime to a Date object.
- *
- * @param time
- *        microseconds from the epoch.
- * @return a Date object or undefined if time was not defined.
- */
-function toDate(time) {
-  return time ? new Date(parseInt(time / 1000)) : undefined;
-}
-
-// LivemarkService
-
-function LivemarkService() {
-  // Cleanup on shutdown.
-  Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);
-
-  // Observe bookmarks but don't init the service just for that.
-  PlacesUtils.bookmarks.addObserver(this, true);
-
-  this._placesListener = new PlacesWeakCallbackWrapper(
-    this.handlePlacesEvents.bind(this));
-  PlacesObservers.addListener(["page-visited"], this._placesListener);
-
-  this._livemarksMap = null;
-  this._promiseLivemarksMapReady = Promise.resolve();
-}
-
-LivemarkService.prototype = {
-  _withLivemarksMap(func) {
-    let promise = this._promiseLivemarksMapReady.then(async () => {
-      if (!this._livemarksMap) {
-        this._livemarksMap = new Map();
-        let conn = await PlacesUtils.promiseDBConnection();
-        let rows = await conn.executeCached(CACHE_SQL,
-          { folder_type: Ci.nsINavBookmarksService.TYPE_FOLDER,
-            feedURI_anno: PlacesUtils.LMANNO_FEEDURI,
-            siteURI_anno: PlacesUtils.LMANNO_SITEURI });
-        for (let row of rows) {
-          let siteURI = row.getResultByName("siteURI");
-          let livemark = new Livemark({
-            id: row.getResultByName("id"),
-            guid: row.getResultByName("guid"),
-            title: row.getResultByName("title"),
-            parentId: row.getResultByName("parentId"),
-            parentGuid: row.getResultByName("parentGuid"),
-            index: row.getResultByName("index"),
-            dateAdded: row.getResultByName("dateAdded"),
-            lastModified: row.getResultByName("lastModified"),
-            feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
-            siteURI: siteURI ? NetUtil.newURI(siteURI) : null,
-          });
-          this._livemarksMap.set(livemark.guid, livemark);
-        }
-      }
-      return func(this._livemarksMap);
-    });
-    this._promiseLivemarksMapReady = promise.catch(_ => {});
-    return promise;
-  },
-
-  _reloading: false,
-  _startReloadTimer(livemarksMap, forceUpdate, reloaded) {
-    if (this._reloadTimer) {
-      this._reloadTimer.cancel();
-    } else {
-      this._reloadTimer = Cc["@mozilla.org/timer;1"]
-                            .createInstance(Ci.nsITimer);
-    }
-
-    this._reloading = true;
-    this._reloadTimer.initWithCallback(() => {
-      // Find first livemark to be reloaded.
-      for (let [ guid, livemark ] of livemarksMap) {
-        if (!reloaded.has(guid)) {
-          reloaded.add(guid);
-          livemark.reload(forceUpdate);
-          this._startReloadTimer(livemarksMap, forceUpdate, reloaded);
-          return;
-        }
-      }
-      // All livemarks have been reloaded.
-      this._reloading = false;
-      this._forceUpdate = false;
-    }, RELOAD_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
-  },
-
-  // nsIObserver
-
-  observe(aSubject, aTopic, aData) {
-    if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) {
-      this._invalidateCachedLivemarks({
-        // No need to restart the reload timer on shutdown.
-        keepReloading: false,
-      }).catch(Cu.reportError);
-    }
-  },
-
-  // mozIAsyncLivemarks
-
-  removeLivemark(aLivemarkInfo) {
-    if (!aLivemarkInfo) {
-      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
-    }
-    // Accept either a guid or an id.
-    let hasGuid = "guid" in aLivemarkInfo;
-    let hasId = "id" in aLivemarkInfo;
-    if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
-        (hasId && aLivemarkInfo.id < 1) ||
-        (!hasId && !hasGuid)) {
-      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
-    }
-
-    return this._withLivemarksMap(async livemarksMap => {
-      if (!aLivemarkInfo.guid)
-        aLivemarkInfo.guid = await PlacesUtils.promiseItemGuid(aLivemarkInfo.id);
-
-      if (!livemarksMap.has(aLivemarkInfo.guid))
-        throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);
-
-      await PlacesUtils.bookmarks.remove(aLivemarkInfo.guid,
-                                         { source: aLivemarkInfo.source });
-    });
-  },
-
-  reloadLivemarks(aForceUpdate) {
-    // Check if there's a currently running reload, to save some useless work.
-    let notWorthRestarting =
-      this._forceUpdate || // We're already forceUpdating.
-      !aForceUpdate; // The caller didn't request a forced update.
-    if (this._reloading && notWorthRestarting) {
-      // Ignore this call.
-      return;
-    }
-
-    this._withLivemarksMap(livemarksMap => {
-      this._forceUpdate = !!aForceUpdate;
-      // Livemarks reloads happen on a timer for performance reasons.
-      this._startReloadTimer(livemarksMap, this._forceUpdate, new Set());
-    });
-  },
-
-  getLivemark(aLivemarkInfo) {
-    if (!aLivemarkInfo) {
-      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
-    }
-    // Accept either a guid or an id.
-    let hasGuid = "guid" in aLivemarkInfo;
-    let hasId = "id" in aLivemarkInfo;
-    if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
-        (hasId && aLivemarkInfo.id < 1) ||
-        (!hasId && !hasGuid)) {
-      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
-    }
-
-    return this._withLivemarksMap(async livemarksMap => {
-      if (!aLivemarkInfo.guid)
-        aLivemarkInfo.guid = await PlacesUtils.promiseItemGuid(aLivemarkInfo.id);
-
-      if (!livemarksMap.has(aLivemarkInfo.guid))
-        throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);
-
-      return livemarksMap.get(aLivemarkInfo.guid);
-    });
-  },
-
-  _invalidateCachedLivemarks({ keepReloading = true } = {}) {
-    // Cancel pending reloads, since any livemarks we're currently reloading
-    // might no longer be valid.
-    let wasReloading = this._reloading;
-    this._reloading = false;
-
-    let wasForceUpdating = this._forceUpdate;
-    this._forceUpdate = false;
-
-    if (this._reloadTimer) {
-      this._reloadTimer.cancel();
-    }
-
-    // Clear out the livemarks cache.
-    let promise = this._promiseLivemarksMapReady.then(() => {
-      let livemarksMap = this._livemarksMap;
-      this._livemarksMap = null;
-      if (livemarksMap) {
-        // Stop any ongoing network fetch.
-        for (let livemark of livemarksMap.values()) {
-          livemark.terminate();
-        }
-      }
-    });
-    this._promiseLivemarksMapReady = promise.catch(_ => {});
-
-    // Restart the timer if we were reloading before invalidating.
-    if (keepReloading) {
-      if (wasReloading) {
-        this.reloadLivemarks(wasForceUpdating);
-      }
-    } else {
-      delete this._reloadTimer;
-    }
-
-    return promise;
-  },
-
-  invalidateCachedLivemarks() {
-    return this._invalidateCachedLivemarks();
-  },
-
-  handlePlacesEvents(aEvents) {
-    if (!aEvents) {
-      throw new Components.Exception("Invalid arguments",
-                                     Cr.NS_ERROR_INVALID_ARG);
-    }
-
-    this._withLivemarksMap(livemarksMap => {
-      for (let event of aEvents) {
-        for (let livemark of livemarksMap.values()) {
-          livemark.updateURIVisitedStatus(event.url, true);
-        }
-      }
-    });
-  },
-
-  // nsINavBookmarkObserver
-
-  onBeginUpdateBatch() {},
-  onEndUpdateBatch() {},
-  onItemVisited() {},
-
-  onItemChanged(id, property, isAnno, value, lastModified, itemType, parentId,
-                guid, parentGuid) {
-    if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
-      return;
-
-    this._withLivemarksMap(livemarksMap => {
-      if (livemarksMap.has(guid)) {
-        let livemark = livemarksMap.get(guid);
-        if (property == "title") {
-          livemark.title = value;
-        }
-        livemark.lastModified = lastModified;
-      }
-    });
-  },
-
-  onItemMoved(id, parentId, oldIndex, newParentId, newIndex, itemType, guid,
-              oldParentGuid, newParentGuid) {
-    if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
-      return;
-
-    this._withLivemarksMap(livemarksMap => {
-      if (livemarksMap.has(guid)) {
-        let livemark = livemarksMap.get(guid);
-        livemark.parentId = newParentId;
-        livemark.parentGuid = newParentGuid;
-        livemark.index = newIndex;
-      }
-    });
-  },
-
-  onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) {
-    if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
-      return;
-
-    this._withLivemarksMap(livemarksMap => {
-      if (livemarksMap.has(guid)) {
-        let livemark = livemarksMap.get(guid);
-        livemark.terminate();
-        livemarksMap.delete(guid);
-      }
-    });
-  },
-
-  skipDescendantsOnItemRemoval: false,
-  skipTags: true,
-
-  // nsINavHistoryObserver
-
-  onPageChanged() {},
-  onTitleChanged() {},
-  onDeleteVisits() {},
-
-  onClearHistory() {
-    this._withLivemarksMap(livemarksMap => {
-      for (let livemark of livemarksMap.values()) {
-        livemark.updateURIVisitedStatus(null, false);
-      }
-    });
-  },
-
-  onDeleteURI(aURI) {
-    this._withLivemarksMap(livemarksMap => {
-      for (let livemark of livemarksMap.values()) {
-        livemark.updateURIVisitedStatus(aURI.spec, false);
-      }
-    });
-  },
-
-  // nsISupports
-
-  classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"),
-
-  _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService),
-
-  QueryInterface: ChromeUtils.generateQI([
-    Ci.mozIAsyncLivemarks,
-    Ci.nsINavBookmarkObserver,
-    Ci.nsINavHistoryObserver,
-    Ci.nsIObserver,
-    Ci.nsISupportsWeakReference,
-  ]),
-};
-
-// Livemark
-
-/**
- * Object used internally to represent a livemark.
- *
- * @param aLivemarkInfo
- *        Object containing information on the livemark.  If the livemark is
- *        not included in the object, a new livemark will be created.
- *
- * @note terminate() must be invoked before getting rid of this object.
- */
-function Livemark(aLivemarkInfo) {
-  this.id = aLivemarkInfo.id;
-  this.guid = aLivemarkInfo.guid;
-  this.feedURI = aLivemarkInfo.feedURI;
-  this.siteURI = aLivemarkInfo.siteURI || null;
-  this.title = aLivemarkInfo.title;
-  this.parentId = aLivemarkInfo.parentId;
-  this.parentGuid = aLivemarkInfo.parentGuid;
-  this.index = aLivemarkInfo.index;
-  this.dateAdded = aLivemarkInfo.dateAdded;
-  this.lastModified = aLivemarkInfo.lastModified;
-
-  this._status = Ci.mozILivemark.STATUS_READY;
-
-  // Hash of resultObservers, hashed by container.
-  this._resultObservers = new Map();
-
-  // Sorted array of objects representing livemark children in the form
-  // { uri, title, visited }.
-  this._children = [];
-
-  // Keeps a separate array of nodes for each requesting container, hashed by
-  // the container itself.
-  this._nodes = new Map();
-
-  this.loadGroup = null;
-  this.expireTime = 0;
-}
-
-Livemark.prototype = {
-  get status() {
-    return this._status;
-  },
-  set status(val) {
-    if (this._status != val) {
-      this._status = val;
-      this._invalidateRegisteredContainers();
-    }
-    return this._status;
-  },
-
-  writeSiteURI(aSiteURI, aSource) {
-    if (!aSiteURI) {
-      PlacesUtils.annotations.removeItemAnnotation(this.id,
-                                                   PlacesUtils.LMANNO_SITEURI,
-                                                   aSource);
-      this.siteURI = null;
-      return;
-    }
-
-    // Security check the site URI against the feed URI principal.
-    let secMan = Services.scriptSecurityManager;
-    let feedPrincipal = secMan.createCodebasePrincipal(this.feedURI, {});
-    try {
-      secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI,
-                                       Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
-    } catch (ex) {
-      return;
-    }
-
-    PlacesUtils.annotations
-               .setItemAnnotation(this.id, PlacesUtils.LMANNO_SITEURI,
-                                  aSiteURI.spec,
-                                  0, PlacesUtils.annotations.EXPIRE_NEVER,
-                                  aSource, true);
-    this.siteURI = aSiteURI;
-  },
-
-  /**
-   * Tries to updates the livemark if needed.
-   * The update process is asynchronous.
-   *
-   * @param [optional] aForceUpdate
-   *        If true will try to update the livemark even if its contents have
-   *        not yet expired.
-   */
-  updateChildren(aForceUpdate) {
-    // Check if the livemark is already updating.
-    if (this.status == Ci.mozILivemark.STATUS_LOADING)
-      return;
-
-    // Check the TTL/expiration on this, to check if there is no need to update
-    // this livemark.
-    if (!aForceUpdate && this.children.length && this.expireTime > Date.now())
-      return;
-
-    this.status = Ci.mozILivemark.STATUS_LOADING;
-
-    // Setting the status notifies observers that may remove the livemark.
-    if (this._terminated)
-      return;
-
-    try {
-      // Create a load group for the request.  This will allow us to
-      // automatically keep track of redirects, so we can always
-      // cancel the channel.
-      let loadgroup = Cc["@mozilla.org/network/load-group;1"].
-                      createInstance(Ci.nsILoadGroup);
-      // Creating a CodeBasePrincipal and using it as the loadingPrincipal
-      // is *not* desired and is only tolerated within this file.
-      // TODO: Find the right OriginAttributes and pass something other
-      // than {} to .createCodeBasePrincipal().
-      let channel = NetUtil.newChannel({
-        uri: this.feedURI,
-        loadingPrincipal: Services.scriptSecurityManager.createCodebasePrincipal(this.feedURI, {}),
-        securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-        contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_XMLHTTPREQUEST,
-      }).QueryInterface(Ci.nsIHttpChannel);
-      channel.loadGroup = loadgroup;
-      channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND |
-                           Ci.nsIRequest.LOAD_BYPASS_CACHE;
-      channel.requestMethod = "GET";
-      channel.setRequestHeader("X-Moz", "livebookmarks", false);
-
-      // Stream the result to the feed parser with this listener
-      let listener = new LivemarkLoadListener(this);
-      channel.notificationCallbacks = listener;
-      channel.asyncOpen2(listener);
-
-      this.loadGroup = loadgroup;
-    } catch (ex) {
-      this.status = Ci.mozILivemark.STATUS_FAILED;
-    }
-  },
-
-  reload(aForceUpdate) {
-    this.updateChildren(aForceUpdate);
-  },
-
-  get children() {
-    return this._children;
-  },
-  set children(val) {
-    this._children = val;
-
-    // Discard the previous cached nodes, new ones should be generated.
-    for (let container of this._resultObservers.keys()) {
-      this._nodes.delete(container);
-    }
-
-    // Update visited status for each entry.
-    for (let child of this._children) {
-      history.hasVisits(child.uri).then(isVisited => {
-        this.updateURIVisitedStatus(child.uri.spec, isVisited);
-      }).catch(Cu.reportError);
-    }
-
-    return this._children;
-  },
-
-  _isURIVisited(aURI) {
-    return this.children.some(child => child.uri.equals(aURI) && child.visited);
-  },
-
-  getNodesForContainer(aContainerNode) {
-    if (this._nodes.has(aContainerNode)) {
-      return this._nodes.get(aContainerNode);
-    }
-
-    let livemark = this;
-    let nodes = [];
-    let now = Date.now() * 1000;
-    for (let child of this.children) {
-      let node = {
-        // The QueryInterface is needed cause aContainerNode is a jsval.
-        // This is required to avoid issues with scriptable wrappers that would
-        // not allow the view to correctly set expandos.
-        get parent() {
-          return aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
-        },
-        get parentResult() {
-          return this.parent.parentResult;
-        },
-        get uri() {
-          return child.uri.spec;
-        },
-        get type() {
-          return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
-        },
-        get title() {
-          return child.title;
-        },
-        get accessCount() {
-          return Number(livemark._isURIVisited(NetUtil.newURI(this.uri)));
-        },
-        get time() {
-          return 0;
-        },
-        get icon() {
-          return "";
-        },
-        get indentLevel() {
-          return this.parent.indentLevel + 1;
-        },
-        get bookmarkIndex() {
-            return -1;
-        },
-        get itemId() {
-            return -1;
-        },
-        get dateAdded() {
-          return now;
-        },
-        get lastModified() {
-          return now;
-        },
-        get tags() {
-          return PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", ");
-        },
-        QueryInterface: ChromeUtils.generateQI([Ci.nsINavHistoryResultNode]),
-      };
-      nodes.push(node);
-    }
-    this._nodes.set(aContainerNode, nodes);
-    return nodes;
-  },
-
-  registerForUpdates(aContainerNode, aResultObserver) {
-    this._resultObservers.set(aContainerNode, aResultObserver);
-  },
-
-  unregisterForUpdates(aContainerNode) {
-    this._resultObservers.delete(aContainerNode);
-    this._nodes.delete(aContainerNode);
-  },
-
-  _invalidateRegisteredContainers() {
-    for (let [ container, observer ] of this._resultObservers) {
-      observer.invalidateContainer(container);
-    }
-  },
-
-  /**
-   * Updates the visited status of nodes observing this livemark.
-   *
-   * @param href
-   *        If provided will update nodes having the given uri,
-   *        otherwise any node.
-   * @param visitedStatus
-   *        Whether the nodes should be set as visited.
-   */
-  updateURIVisitedStatus(href, visitedStatus) {
-    let wasVisited = false;
-    for (let child of this.children) {
-      if (!href || child.uri.spec == href) {
-        wasVisited = child.visited;
-        child.visited = visitedStatus;
-      }
-    }
-
-    for (let [ container, observer ] of this._resultObservers) {
-      if (this._nodes.has(container)) {
-        let nodes = this._nodes.get(container);
-        for (let node of nodes) {
-          if (!href || node.uri == href) {
-            Services.tm.dispatchToMainThread(() => {
-              observer.nodeHistoryDetailsChanged(node, node.time, wasVisited);
-            });
-          }
-        }
-      }
-    }
-  },
-
-  /**
-   * Terminates the livemark entry, cancelling any ongoing load.
-   * Must be invoked before destroying the entry.
-   */
-  terminate() {
-    // Avoid handling any updateChildren request from now on.
-    this._terminated = true;
-    this.abort();
-  },
-
-  /**
-   * Aborts the livemark loading if needed.
-   */
-  abort() {
-    this.status = Ci.mozILivemark.STATUS_FAILED;
-    if (this.loadGroup) {
-      this.loadGroup.cancel(Cr.NS_BINDING_ABORTED);
-      this.loadGroup = null;
-    }
-  },
-
-  QueryInterface: ChromeUtils.generateQI([
-    Ci.mozILivemark,
-  ]),
-};
-
-// LivemarkLoadListener
-
-/**
- * Object used internally to handle loading a livemark's contents.
- *
- * @param aLivemark
- *        The Livemark that is loading.
- */
-function LivemarkLoadListener(aLivemark) {
-  this._livemark = aLivemark;
-  this._processor = null;
-  this._isAborted = false;
-  this._ttl = EXPIRE_TIME_MS;
-}
-
-LivemarkLoadListener.prototype = {
-  abort(aException) {
-    if (!this._isAborted) {
-      this._isAborted = true;
-      this._livemark.abort();
-      this._setResourceTTL(ONERROR_EXPIRE_TIME_MS);
-    }
-  },
-
-  // nsIFeedResultListener
-  handleResult(aResult) {
-    if (this._isAborted) {
-      return;
-    }
-
-    try {
-      // We need this to make sure the item links are safe
-      let feedPrincipal =
-        Services.scriptSecurityManager
-                .createCodebasePrincipal(this._livemark.feedURI, {});
-
-      // Enforce well-formedness because the existing code does
-      if (!aResult || !aResult.doc || aResult.bozo) {
-        throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
-      }
-
-      let feed = aResult.doc.QueryInterface(Ci.nsIFeed);
-      let siteURI = this._livemark.siteURI;
-      if (feed.link && (!siteURI || !feed.link.equals(siteURI))) {
-        siteURI = feed.link;
-        this._livemark.writeSiteURI(siteURI);
-      }
-
-      // Insert feed items.
-      let livemarkChildren = [];
-      for (let i = 0; i < feed.items.length; ++i) {
-        let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);
-        let uri = entry.link || siteURI;
-        if (!uri) {
-          continue;
-        }
-
-        try {
-          Services.scriptSecurityManager
-                  .checkLoadURIWithPrincipal(feedPrincipal, uri,
-                                             Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
-        } catch (ex) {
-          continue;
-        }
-
-        let title = entry.title ? entry.title.plainText() : "";
-        livemarkChildren.push({ uri, title, visited: false });
-      }
-
-      this._livemark.children = livemarkChildren;
-    } catch (ex) {
-      Cu.reportError(ex);
-      this.abort(ex);
-    } finally {
-      this._processor.listener = null;
-      this._processor = null;
-    }
-  },
-
-  onDataAvailable(aRequest, aContext, aInputStream, aSourceOffset, aCount) {
-    if (this._processor) {
-      this._processor.onDataAvailable(aRequest, aContext, aInputStream,
-                                      aSourceOffset, aCount);
-    }
-  },
-
-  onStartRequest(aRequest, aContext) {
-    if (this._isAborted) {
-      throw new Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
-    }
-
-    let channel = aRequest.QueryInterface(Ci.nsIChannel);
-    try {
-      // Parse feed data as it comes in
-      this._processor = Cc["@mozilla.org/feed-processor;1"].
-                        createInstance(Ci.nsIFeedProcessor);
-      this._processor.listener = this;
-      this._processor.parseAsync(null, channel.URI);
-      this._processor.onStartRequest(aRequest, aContext);
-    } catch (ex) {
-      Cu.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec);
-      this.abort(ex);
-    }
-  },
-
-  onStopRequest(aRequest, aContext, aStatus) {
-    if (!Components.isSuccessCode(aStatus)) {
-      this.abort();
-      return;
-    }
-
-    // Set an expiration on the livemark, to reloading the data in future.
-    try {
-      if (this._processor) {
-        this._processor.onStopRequest(aRequest, aContext, aStatus);
-      }
-
-      // Calculate a new ttl
-      let channel = aRequest.QueryInterface(Ci.nsICachingChannel);
-      if (channel) {
-        let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry);
-        if (entryInfo) {
-          // nsICacheEntry returns value as seconds.
-          let expireTime = entryInfo.expirationTime * 1000;
-          let nowTime = Date.now();
-          // Note, expireTime can be 0, see bug 383538.
-          if (expireTime > nowTime) {
-            this._setResourceTTL(Math.max((expireTime - nowTime),
-                                          EXPIRE_TIME_MS));
-            return;
-          }
-        }
-      }
-      this._setResourceTTL(EXPIRE_TIME_MS);
-    } catch (ex) {
-      this.abort(ex);
-    } finally {
-      if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) {
-        this._livemark.status = Ci.mozILivemark.STATUS_READY;
-      }
-      this._livemark.locked = false;
-      this._livemark.loadGroup = null;
-    }
-  },
-
-  _setResourceTTL(aMilliseconds) {
-    this._livemark.expireTime = Date.now() + aMilliseconds;
-  },
-
-  // nsIInterfaceRequestor
-  getInterface(aIID) {
-    return this.QueryInterface(aIID);
-  },
-
-  // nsISupports
-  QueryInterface: ChromeUtils.generateQI([
-    Ci.nsIFeedResultListener,
-    Ci.nsIStreamListener,
-    Ci.nsIRequestObserver,
-    Ci.nsIInterfaceRequestor,
-  ]),
-};
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -33,17 +33,16 @@ const int32_t nsNavBookmarks::kGetChildr
 const int32_t nsNavBookmarks::kGetChildrenIndex_SyncStatus = 22;
 
 using namespace mozilla::places;
 
 PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService)
 
 #define BOOKMARKS_ANNO_PREFIX "bookmarks/"
 #define BOOKMARKS_TOOLBAR_FOLDER_ANNO NS_LITERAL_CSTRING(BOOKMARKS_ANNO_PREFIX "toolbarFolder")
-#define FEED_URI_ANNO NS_LITERAL_CSTRING("livemark/feedURI")
 #define SYNC_PARENT_ANNO "sync/parent"
 #define SQLITE_MAX_VARIABLE_NUMBER 999
 
 
 namespace {
 
 #define SKIP_TAGS(condition) ((condition) ? SkipTags : DontSkip)
 
@@ -816,28 +815,16 @@ nsNavBookmarks::CreateFolder(int64_t aPa
     MOZ_RELEASE_ASSERT(success);
 
     PlacesObservers::NotifyListeners(events);
   }
 
   return NS_OK;
 }
 
-bool nsNavBookmarks::IsLivemark(int64_t aFolderId)
-{
-  nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
-  NS_ENSURE_TRUE(annosvc, false);
-  bool isLivemark;
-  nsresult rv = annosvc->ItemHasAnnotation(aFolderId,
-                                           FEED_URI_ANNO,
-                                           &isLivemark);
-  NS_ENSURE_SUCCESS(rv, false);
-  return isLivemark;
-}
-
 nsresult
 nsNavBookmarks::GetDescendantChildren(int64_t aFolderId,
                                       const nsACString& aFolderGuid,
                                       int64_t aGrandParentId,
                                       nsTArray<BookmarkData>& aFolderChildrenArray) {
   // New children will be added from this index on.
   uint32_t startIndex = aFolderChildrenArray.Length();
   nsresult rv;
@@ -1717,23 +1704,16 @@ nsNavBookmarks::ProcessFolderNodeRow(
     uint32_t nodeType;
     node->GetType(&nodeType);
     if (nodeType == nsINavHistoryResultNode::RESULT_TYPE_QUERY &&
         aOptions->ExcludeQueries()) {
       return NS_OK;
     }
   }
   else if (itemType == TYPE_FOLDER) {
-    // ExcludeReadOnlyFolders currently means "ExcludeLivemarks" (to be fixed in
-    // bug 1072833)
-    if (aOptions->ExcludeReadOnlyFolders()) {
-      if (IsLivemark(id))
-        return NS_OK;
-    }
-
     nsAutoCString title;
     bool isNull;
     rv = aRow->GetIsNull(nsNavHistory::kGetInfoIndex_Title, &isNull);
     NS_ENSURE_SUCCESS(rv, rv);
     if (!isNull) {
       rv = aRow->GetUTF8String(nsNavHistory::kGetInfoIndex_Title, title);
       NS_ENSURE_SUCCESS(rv, rv);
     }
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -230,25 +230,16 @@ public:
   static mozilla::Atomic<int64_t> sTotalSyncChanges;
   static void NoteSyncChange();
 
 private:
   static nsNavBookmarks* gBookmarksService;
 
   ~nsNavBookmarks();
 
-  /**
-   * Checks whether or not aFolderId points to a live bookmark.
-   *
-   * @param aFolderId
-   *        the item-id of the folder to check.
-   * @return true if aFolderId points to live bookmarks, false otherwise.
-   */
-  bool IsLivemark(int64_t aFolderId);
-
   nsresult AdjustIndices(int64_t aFolder,
                          int32_t aStartIndex,
                          int32_t aEndIndex,
                          int32_t aDelta);
 
   nsresult AdjustSeparatorsSyncCounter(int64_t aFolderId,
                                        int32_t aStartIndex,
                                        int64_t aSyncChangeDelta);
--- a/toolkit/components/places/tests/unit/test_multi_word_tags.js
+++ b/toolkit/components/places/tests/unit/test_multi_word_tags.js
@@ -38,17 +38,17 @@ add_task(async function run_test() {
 
   tagssvc.tagURI(uri1, ["foo"]);
   tagssvc.tagURI(uri2, ["bar"]);
   tagssvc.tagURI(uri3, ["cheese"]);
   tagssvc.tagURI(uri4, ["foo bar"]);
   tagssvc.tagURI(uri5, ["bar cheese"]);
   tagssvc.tagURI(uri6, ["foo bar cheese"]);
 
-  // exclude livemark items, search for "item", should get one result
+  // Search for "item", should get one result
   var options = histsvc.getNewQueryOptions();
   options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
 
   var query = histsvc.getNewQuery();
   query.searchTerms = "foo";
   var result = histsvc.executeQuery(query, options);
   var root = result.root;
   root.containerOpen = true;
--- a/toolkit/components/places/tests/unit/test_null_interfaces.js
+++ b/toolkit/components/places/tests/unit/test_null_interfaces.js
@@ -12,17 +12,16 @@ var testServices = [
   ["browser/nav-history-service;1",
     ["nsINavHistoryService"],
     ["queryStringToQuery", "removePagesByTimeframe", "removePagesFromHost", "getObservers"],
   ],
   ["browser/nav-bookmarks-service;1",
     ["nsINavBookmarksService", "nsINavHistoryObserver"],
     ["createFolder", "getObservers", "onFrecencyChanged", "onTitleChanged", "onDeleteURI"],
   ],
-  ["browser/livemark-service;2", ["mozIAsyncLivemarks"], ["reloadLivemarks"]],
   ["browser/favicon-service;1", ["nsIFaviconService"], []],
   ["browser/tagging-service;1", ["nsITaggingService"], []],
 ];
 info(testServices.join("\n"));
 
 function run_test() {
   for (let [cid, ifaces, nothrow] of testServices) {
     info(`Running test with ${cid} ${ifaces.join(", ")} ${nothrow}`);
--- a/toolkit/components/places/toolkitplaces.manifest
+++ b/toolkit/components/places/toolkitplaces.manifest
@@ -1,12 +1,8 @@
-# nsLivemarkService.js
-component {dca61eb5-c7cd-4df1-b0fb-d0722baba251} nsLivemarkService.js
-contract @mozilla.org/browser/livemark-service;2 {dca61eb5-c7cd-4df1-b0fb-d0722baba251}
-
 # nsTaggingService.js
 component {bbc23860-2553-479d-8b78-94d9038334f7} nsTaggingService.js
 contract @mozilla.org/browser/tagging-service;1 {bbc23860-2553-479d-8b78-94d9038334f7}
 component {1dcc23b0-d4cb-11dc-9ad6-479d56d89593} nsTaggingService.js
 contract @mozilla.org/autocomplete/search;1?name=places-tag-autocomplete {1dcc23b0-d4cb-11dc-9ad6-479d56d89593}
 
 # nsPlacesExpiration.js
 component {705a423f-2f69-42f3-b9fe-1517e0dee56f} nsPlacesExpiration.js
--- a/toolkit/components/resistfingerprinting/nsRFPService.h
+++ b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -44,16 +44,18 @@
 #define SPOOFED_OSCPU      "Linux x86_64"
 #define SPOOFED_PLATFORM   "Linux x86_64"
 #endif
 
 #define SPOOFED_APPNAME    "Netscape"
 #define LEGACY_BUILD_ID    "20181001000000"
 #define LEGACY_UA_GECKO_TRAIL "20100101"
 
+#define SPOOFED_POINTER_INTERFACE MouseEvent_Binding::MOZ_SOURCE_MOUSE
+
 // Forward declare LRUCache, defined in nsRFPService.cpp
 class LRUCache;
 
 namespace mozilla {
 
 enum KeyboardLang {
   EN = 0x01
 };
--- a/toolkit/modules/CanonicalJSON.jsm
+++ b/toolkit/modules/CanonicalJSON.jsm
@@ -1,46 +1,47 @@
 /* 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/. */
 
 var EXPORTED_SYMBOLS = ["CanonicalJSON"];
 
-ChromeUtils.defineModuleGetter(this, "jsesc",
-                               "resource://gre/modules/third_party/jsesc/jsesc.js");
-
 var CanonicalJSON = {
   /**
    * Return the canonical JSON form of the passed source, sorting all the object
    * keys recursively. Note that this method will cause an infinite loop if
    * cycles exist in the source (bug 1265357).
    *
    * @param source
    *        The elements to be serialized.
    *
    * The output will have all unicode chars escaped with the unicode codepoint
    * as lowercase hexadecimal.
    *
    * @usage
    *        CanonicalJSON.stringify(listOfRecords);
    **/
-  stringify: function stringify(source) {
+  stringify: function stringify(source, jsescFn) {
+    if (typeof jsescFn != "function") {
+      const { jsesc } = ChromeUtils.import("resource://gre/modules/third_party/jsesc/jsesc.js", {});
+      jsescFn = jsesc;
+    }
     if (Array.isArray(source)) {
       const jsonArray = source.map(x => typeof x === "undefined" ? null : x);
-      return `[${jsonArray.map(stringify).join(",")}]`;
+      return "[" + jsonArray.map(item => stringify(item, jsescFn)).join(",") + "]";
     }
 
     if (typeof source === "number") {
       if (source === 0) {
         return (Object.is(source, -0)) ? "-0" : "0";
       }
     }
 
     // Leverage jsesc library, mainly for unicode escaping.
-    const toJSON = (input) => jsesc(input, {lowercaseHex: true, json: true});
+    const toJSON = (input) => jsescFn(input, {lowercaseHex: true, json: true});
 
     if (typeof source !== "object" || source === null) {
       return toJSON(source);
     }
 
     // Dealing with objects, ordering keys.
     const sortedKeys = Object.keys(source).sort();
     const lastIndex = sortedKeys.length - 1;
@@ -48,12 +49,12 @@ var CanonicalJSON = {
       const value = source[key];
       // JSON.stringify drops keys with an undefined value.
       if (typeof value === "undefined") {
         return serial;
       }
       const jsonValue = value && value.toJSON ? value.toJSON() : value;
       const suffix = index !== lastIndex ? "," : "";
       const escapedKey = toJSON(key);
-      return serial + `${escapedKey}:${stringify(jsonValue)}${suffix}`;
+      return serial + escapedKey + ":" + stringify(jsonValue, jsescFn) + suffix;
     }, "{") + "}";
   },
 };
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -18,16 +18,17 @@
   "bogus_element_type.jsm": [],
   "bookmark_repair.js": ["BookmarkRepairRequestor", "BookmarkRepairResponder"],
   "bookmark_validator.js": ["BookmarkValidator", "BookmarkProblemData"],
   "bookmarks.js": ["BookmarksEngine", "PlacesItem", "Bookmark", "BookmarkFolder", "BookmarkQuery", "Livemark", "BookmarkSeparator", "BufferedBookmarksEngine"],
   "bookmarks.jsm": ["PlacesItem", "Bookmark", "Separator", "Livemark", "BookmarkFolder", "DumpBookmarks"],
   "BootstrapMonitor.jsm": ["monitor"],
   "browser-loader.js": ["BrowserLoader"],
   "browserid_identity.js": ["BrowserIDManager", "AuthenticationError"],
+  "CanonicalJSON.jsm": ["CanonicalJSON"],
   "CertUtils.jsm": ["CertUtils"],
   "clients.js": ["ClientEngine", "ClientsRec"],
   "collection_repair.js": ["getRepairRequestor", "getAllRepairRequestors", "CollectionRepairRequestor", "getRepairResponder", "CollectionRepairResponder"],
   "collection_validator.js": ["CollectionValidator", "CollectionProblemData"],
   "Console.jsm": ["console", "ConsoleAPI"],
   "constants.js": ["WEAVE_VERSION", "SYNC_API_VERSION", "STORAGE_VERSION", "PREFS_BRANCH", "DEFAULT_KEYBUNDLE_NAME", "SYNC_KEY_ENCODED_LENGTH", "SYNC_KEY_DECODED_LENGTH", "NO_SYNC_NODE_INTERVAL", "MAX_ERROR_COUNT_BEFORE_BACKOFF", "MINIMUM_BACKOFF_INTERVAL", "MAXIMUM_BACKOFF_INTERVAL", "HMAC_EVENT_INTERVAL", "MASTER_PASSWORD_LOCKED_RETRY_INTERVAL", "DEFAULT_GUID_FETCH_BATCH_SIZE", "DEFAULT_DOWNLOAD_BATCH_SIZE", "SINGLE_USER_THRESHOLD", "MULTI_DEVICE_THRESHOLD", "SCORE_INCREMENT_SMALL", "SCORE_INCREMENT_MEDIUM", "SCORE_INCREMENT_XLARGE", "SCORE_UPDATE_DELAY", "IDLE_OBSERVER_BACK_DELAY", "URI_LENGTH_MAX", "MAX_HISTORY_UPLOAD", "MAX_HISTORY_DOWNLOAD", "STATUS_OK", "SYNC_FAILED", "LOGIN_FAILED", "SYNC_FAILED_PARTIAL", "CLIENT_NOT_CONFIGURED", "STATUS_DISABLED", "MASTER_PASSWORD_LOCKED", "LOGIN_SUCCEEDED", "SYNC_SUCCEEDED", "ENGINE_SUCCEEDED", "LOGIN_FAILED_NO_USERNAME", "LOGIN_FAILED_NO_PASSPHRASE", "LOGIN_FAILED_NETWORK_ERROR", "LOGIN_FAILED_SERVER_ERROR", "LOGIN_FAILED_INVALID_PASSPHRASE", "LOGIN_FAILED_LOGIN_REJECTED", "METARECORD_DOWNLOAD_FAIL", "VERSION_OUT_OF_DATE", "CREDENTIALS_CHANGED", "ABORT_SYNC_COMMAND", "NO_SYNC_NODE_FOUND", "OVER_QUOTA", "SERVER_MAINTENANCE", "RESPONSE_OVER_QUOTA", "ENGINE_UPLOAD_FAIL", "ENGINE_DOWNLOAD_FAIL", "ENGINE_UNKNOWN_FAIL", "ENGINE_APPLY_FAIL", "ENGINE_BATCH_INTERRUPTED", "kSyncMasterPasswordLocked", "kSyncWeaveDisabled", "kSyncNetworkOffline", "kSyncBackoffNotMet", "kFirstSyncChoiceNotMade", "kSyncNotConfigured", "kFirefoxShuttingDown", "DEVICE_TYPE_DESKTOP", "DEVICE_TYPE_MOBILE", "SQLITE_MAX_VARIABLE_NUMBER"],
   "Constants.jsm": ["Roles", "Events", "Relations", "Filters", "States", "Prefilters"],
   "ContactDB.jsm": ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME", "REVISION_STORE", "DB_VERSION"],
@@ -95,16 +96,17 @@
   "history.jsm": ["HistoryEntry", "DumpHistory"],
   "Http.jsm": ["httpRequest", "percentEncode"],
   "httpd.js": ["HTTP_400", "HTTP_401", "HTTP_402", "HTTP_403", "HTTP_404", "HTTP_405", "HTTP_406", "HTTP_407", "HTTP_408", "HTTP_409", "HTTP_410", "HTTP_411", "HTTP_412", "HTTP_413", "HTTP_414", "HTTP_415", "HTTP_417", "HTTP_500", "HTTP_501", "HTTP_502", "HTTP_503", "HTTP_504", "HTTP_505", "HttpError", "HttpServer"],
   "import_module.jsm": ["MODULE_IMPORTED", "MODULE_URI", "SUBMODULE_IMPORTED", "same_scope", "SUBMODULE_IMPORTED_TO_SCOPE"],
   "import_sub_module.jsm": ["SUBMODULE_IMPORTED", "test_obj"],
   "InlineSpellChecker.jsm": ["InlineSpellChecker", "SpellCheckHelper"],
   "JSDOMParser.js": ["JSDOMParser"],
   "jsdebugger.jsm": ["addDebuggerToGlobal"],
+  "jsesc.js": ["jsesc"],
   "json2.js": ["JSON"],
   "keys.js": ["BulkKeyBundle", "SyncKeyBundle"],
   "KeyValueParser.jsm": ["parseKeyValuePairsFromLines", "parseKeyValuePairs", "parseKeyValuePairsFromFile", "parseKeyValuePairsFromFileAsync"],
   "kinto-http-client.js": ["KintoHttpClient"],
   "kinto-offline-client.js": ["Kinto"],
   "kinto-storage-adapter.js": ["FirefoxAdapter"],
   "L10nRegistry.jsm": ["L10nRegistry", "FileSource", "IndexedFileSource"],
   "loader-plugin-raw.jsm": ["requireRawId"],