Merge inbound to mozilla-central. a=merge
authorGurzau Raul <rgurzau@mozilla.com>
Sat, 20 Apr 2019 12:54:53 +0300
changeset 470293 a092972b53f0e566a36770e7b03363036ff820ec
parent 470278 6e082b6757630d3c57382feb106a674056881f86 (current diff)
parent 470292 6ffe4f5a3c4744adce3f62255cf91351c39a33a0 (diff)
child 470294 bbdaf05c9d16b95a9bea1bf1125d69fb58aacd81
child 470296 9bb46f41277cf0beec92dd551c86c500dad5c3cf
push id35892
push userrgurzau@mozilla.com
push dateSat, 20 Apr 2019 09:55:32 +0000
treeherdermozilla-central@a092972b53f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone68.0a1
first release with
nightly linux32
a092972b53f0 / 68.0a1 / 20190420095532 / files
nightly linux64
a092972b53f0 / 68.0a1 / 20190420095532 / files
nightly mac
a092972b53f0 / 68.0a1 / 20190420095532 / files
nightly win32
a092972b53f0 / 68.0a1 / 20190420095532 / files
nightly win64
a092972b53f0 / 68.0a1 / 20190420095532 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
devtools/client/debugger/src/client/firefox/commands.js
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/components/RequestListColumnTransferredSize.js
devtools/client/netmonitor/src/components/RequestListItem.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/constants.js
devtools/server/actors/network-event.js
toolkit/content/aboutProfiles.js
--- a/browser/installer/windows/nsis/shared.nsh
+++ b/browser/installer/windows/nsis/shared.nsh
@@ -1187,19 +1187,32 @@
             ; assume that unpinning unpinned a side by side installation from
             ; the Start Menu and pin this installation to the Start Menu.
             ${Unless} $R8 == $R9
               ; Pin the shortcut to the Start Menu. 5381 is the shell32.dll
               ; resource id for the "Pin to Start Menu" string.
               InvokeShellVerb::DoIt "$SMPROGRAMS" "$1" "5381"
             ${EndUnless}
 
-            ; Pin the shortcut to the TaskBar. 5386 is the shell32.dll resource
-            ; id for the "Pin to Taskbar" string.
-            InvokeShellVerb::DoIt "$SMPROGRAMS" "$1" "5386"
+            ${If} ${AtMostWin2012R2}
+              ; Pin the shortcut to the TaskBar. 5386 is the shell32.dll
+              ; resource id for the "Pin to Taskbar" string.
+              InvokeShellVerb::DoIt "$SMPROGRAMS" "$1" "5386"
+            ${Else}
+              ; In Windows 10 the "Pin to Taskbar" resource was removed, so we
+              ; can't access the verb that way anymore. We have a create a
+              ; command key using the GUID that's assigned to this action and
+              ; then invoke that as a verb.
+              ReadRegStr $R9 HKLM \
+                "Software\Microsoft\Windows\CurrentVersion\Explorer\CommandStore\shell\Windows.taskbarpin" \
+                "ExplorerCommandHandler"
+              WriteRegStr HKCU "Software\Classes\*\shell\${AppRegName}-$AppUserModelID" "ExplorerCommandHandler" $R9
+              InvokeShellVerb::DoIt "$SMPROGRAMS" "$1" "${AppRegName}-$AppUserModelID"
+              DeleteRegKey HKCU "Software\Classes\*\shell\${AppRegName}-$AppUserModelID"
+            ${EndIf}
 
             ; Delete the shortcut if it was created
             ${If} "$8" == "true"
               Delete "$SMPROGRAMS\$1"
             ${EndIf}
           ${EndIf}
 
           ${If} $TmpVal == "HKCU"
--- a/devtools/client/debugger/panel.js
+++ b/devtools/client/debugger/panel.js
@@ -134,16 +134,20 @@ DebuggerPanel.prototype = {
 
     frames.forEach(frame => {
       frame.actor = frame.id;
     });
 
     return { frames, selected };
   },
 
+  lookupConsoleClient: function(thread) {
+    return this._client.lookupConsoleClient(thread);
+  },
+
   getMappedExpression(expression) {
     return this._actions.getMappedExpression(expression);
   },
 
   isPaused() {
     const thread = this._selectors.getCurrentThread(this._getState());
     return this._selectors.getIsPaused(this._getState(), thread);
   },
--- a/devtools/client/debugger/src/client/firefox/commands.js
+++ b/devtools/client/debugger/src/client/firefox/commands.js
@@ -506,12 +506,13 @@ const clientCommands = {
   registerSourceActor,
   fetchWorkers,
   getMainThread,
   sendPacket,
   setSkipPausing,
   setEventListenerBreakpoints,
   waitForWorkers,
   detachWorkers,
-  hasWasmSupport
+  hasWasmSupport,
+  lookupConsoleClient
 };
 
 export { setupCommands, clientCommands };
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -140,16 +140,21 @@ jsonpScopeName=JSONP → callback %S()
 # in the response tab of the network details pane when the response is over
 # the truncation limit and thus was truncated.
 responseTruncated=Response has been truncated
 
 # LOCALIZATION NOTE (responsePreview): This is the text displayed
 # in the response tab of the network details pane for an HTML preview.
 responsePreview=Preview
 
+# LOCALIZATION NOTE (networkMenu.raced): This is the label displayed
+# in the network menu specifying the transfer or a request is
+# raced. %S refers to the current transfer size.
+networkMenu.raced=%S (raced)
+
 # LOCALIZATION NOTE (networkMenu.sortedAsc): This is the tooltip displayed
 # in the network table toolbar, for any column that is sorted ascending.
 networkMenu.sortedAsc=Sorted ascending
 
 # LOCALIZATION NOTE (networkMenu.sortedDesc): This is the tooltip displayed
 # in the network table toolbar, for any column that is sorted descending.
 networkMenu.sortedDesc=Sorted descending
 
@@ -216,17 +221,17 @@ networkMenu.sizeGB=%S GB
 networkMenu.sizeUnavailable=—
 
 # LOCALIZATION NOTE (networkMenu.sizeUnavailable.title): This is the tooltip
 # displayed in the network menu specifying that the transferred size of a
 # request is unavailable.
 networkMenu.sizeUnavailable.title=Transferred size is not available
 
 # LOCALIZATION NOTE (networkMenu.sizeCached): This is the label displayed
-# in the network menu specifying the transferred of a request is
+# in the network menu specifying the transfer or a request is
 # cached.
 networkMenu.sizeCached=cached
 
 # LOCALIZATION NOTE (networkMenu.sizeServiceWorker): This is the label displayed
 # in the network menu specifying the transferred of a request computed
 # by a service worker.
 networkMenu.sizeServiceWorker=service worker
 
--- a/devtools/client/netmonitor/src/components/RequestListColumnTransferredSize.js
+++ b/devtools/client/netmonitor/src/components/RequestListColumnTransferredSize.js
@@ -13,16 +13,17 @@ const { propertiesEqual } = require("../
 
 const SIZE_CACHED = L10N.getStr("networkMenu.sizeCached");
 const SIZE_SERVICE_WORKER = L10N.getStr("networkMenu.sizeServiceWorker");
 const SIZE_UNAVAILABLE = L10N.getStr("networkMenu.sizeUnavailable");
 const SIZE_UNAVAILABLE_TITLE = L10N.getStr("networkMenu.sizeUnavailable.title");
 const UPDATED_TRANSFERRED_PROPS = [
   "transferredSize",
   "fromCache",
+  "isRacing",
   "fromServiceWorker",
 ];
 
 class RequestListColumnTransferredSize extends Component {
   static get propTypes() {
     return {
       item: PropTypes.object.isRequired,
     };
@@ -34,27 +35,31 @@ class RequestListColumnTransferredSize e
 
   render() {
     const {
       blockedReason,
       fromCache,
       fromServiceWorker,
       status,
       transferredSize,
+      isRacing,
     } = this.props.item;
     let text;
 
     if (blockedReason) {
       text = L10N.getFormatStr("networkMenu.blockedBy", blockedReason);
     } else if (fromCache || status === "304") {
       text = SIZE_CACHED;
     } else if (fromServiceWorker) {
       text = SIZE_SERVICE_WORKER;
     } else if (typeof transferredSize == "number") {
       text = getFormattedSize(transferredSize);
+      if (isRacing && typeof isRacing == "boolean") {
+        text = L10N.getFormatStr("networkMenu.raced", text);
+      }
     } else if (transferredSize === null) {
       text = SIZE_UNAVAILABLE;
     }
 
     const title = text == SIZE_UNAVAILABLE ? SIZE_UNAVAILABLE_TITLE : text;
 
     return (
       dom.td({
--- a/devtools/client/netmonitor/src/components/RequestListItem.js
+++ b/devtools/client/netmonitor/src/components/RequestListItem.js
@@ -90,16 +90,17 @@ loader.lazyGetter(this, "RequestListColu
  */
 const UPDATED_REQ_ITEM_PROPS = [
   "mimeType",
   "eventTimings",
   "securityState",
   "status",
   "statusText",
   "fromCache",
+  "isRacing",
   "fromServiceWorker",
   "method",
   "url",
   "remoteAddress",
   "cause",
   "contentSize",
   "transferredSize",
   "startedMillis",
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -366,16 +366,17 @@ class FirefoxDataProvider {
     const { packet, networkInfo } = data;
     const { actor } = networkInfo;
     const { updateType } = packet;
 
     switch (updateType) {
       case "securityInfo":
         this.pushRequestToQueue(actor, {
           securityState: networkInfo.securityState,
+          isRacing: packet.isRacing,
         });
         break;
       case "responseStart":
         this.pushRequestToQueue(actor, {
           httpVersion: networkInfo.response.httpVersion,
           remoteAddress: networkInfo.response.remoteAddress,
           remotePort: networkInfo.response.remotePort,
           status: networkInfo.response.status,
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -114,16 +114,17 @@ const EVENTS = {
 const UPDATE_PROPS = [
   "method",
   "url",
   "remotePort",
   "remoteAddress",
   "status",
   "statusText",
   "httpVersion",
+  "isRacing",
   "securityState",
   "securityInfo",
   "securityInfoAvailable",
   "mimeType",
   "contentSize",
   "transferredSize",
   "totalTime",
   "eventTimings",
--- a/devtools/client/webconsole/actions/autocomplete.js
+++ b/devtools/client/webconsole/actions/autocomplete.js
@@ -21,17 +21,17 @@ const {
  */
 function autocompleteUpdate(force, getterPath) {
   return ({dispatch, getState, services}) => {
     if (services.inputHasSelection()) {
       return dispatch(autocompleteClear());
     }
 
     const inputValue = services.getInputValue();
-    const frameActorId = services.getFrameActor();
+    const { frameActor: frameActorId, client } = services.getFrameActor();
     const cursor = services.getInputCursor();
 
     const state = getState().autocomplete;
     const { cache } = state;
     if (!force && (
       !inputValue ||
       /^[a-zA-Z0-9_$]/.test(inputValue.substring(cursor))
     )) {
@@ -68,17 +68,17 @@ function autocompleteUpdate(force, gette
       } else {
         authorizedEvaluations = [getterPath];
       }
     }
 
     return dispatch(autocompleteDataFetch({
       input,
       frameActorId,
-      client: services.getWebConsoleClient(),
+      client,
       authorizedEvaluations,
       force,
     }));
   };
 }
 
 /**
  * Called when the autocompletion data should be cleared.
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -669,18 +669,21 @@ class JSTerm extends Component {
    */
   requestEvaluation(str, options = {}) {
     // Send telemetry event. If we are in the browser toolbox we send -1 as the
     // toolbox session id.
     this.props.serviceContainer.recordTelemetryEvent("execute_js", {
       "lines": str.split(/\n/).length,
     });
 
-    return this.webConsoleClient.evaluateJSAsync(str, {
-      frameActor: this.props.serviceContainer.getFrameActor(options.frame),
+    const { frameActor, client } =
+      this.props.serviceContainer.getFrameActor(options.frame);
+
+    return client.evaluateJSAsync(str, {
+      frameActor,
       ...options,
     });
   }
 
   /**
    * Copy the object/variable by invoking the server
    * which invokes the `copy(variable)` command and makes it
    * available in the clipboard
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -45,16 +45,18 @@ support-files =
   test-dynamic-import.html
   test-dynamic-import.js
   test-error.html
   test-error-worker.html
   test-error-worker.js
   test-error-worker2.js
   test-eval-in-stackframe.html
   test-eval-sources.html
+  test-evaluate-worker.html
+  test-evaluate-worker.js
   test-external-script-errors.html
   test-external-script-errors.js
   test-iframe-insecure-form-action.html
   test-iframe1.html
   test-iframe2.html
   test-iframe3.html
   test-iframe-wrong-hud-iframe.html
   test-iframe-wrong-hud.html
@@ -407,8 +409,9 @@ tags = trackingprotection
 [browser_webconsole_view_source.js]
 [browser_webconsole_visibility_messages.js]
 [browser_webconsole_warn_about_replaced_api.js]
 [browser_webconsole_warning_group_content_blocking.js]
 [browser_webconsole_warning_groups_outside_console_group.js]
 [browser_webconsole_warning_groups.js]
 [browser_webconsole_websocket.js]
 [browser_webconsole_worker_error.js]
+[browser_webconsole_worker_evaluate.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_worker_evaluate.js
@@ -0,0 +1,32 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// When the debugger is paused in a worker thread, console evaluations should
+// be performed in that worker's selected frame.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+                 "test/mochitest/test-evaluate-worker.html";
+
+add_task(async function() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const {jsterm} = hud;
+
+  await openDebugger();
+  const toolbox = gDevTools.getToolbox(hud.target);
+  const dbg = createDebuggerContext(toolbox);
+
+  jsterm.execute("pauseInWorker(42)");
+
+  await waitForPaused(dbg);
+  await openConsole();
+
+  const onMessage = waitForMessage(hud, "42");
+  jsterm.execute("data");
+  await onMessage;
+
+  ok(true, "Evaluated console message in worker thread");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/test-evaluate-worker.html
@@ -0,0 +1,9 @@
+<script>
+"use strict";
+var w = new Worker("test-evaluate-worker.js");
+
+// eslint-disable-next-line no-unused-vars
+function pauseInWorker(value) {
+  w.postMessage(value);
+}
+</script>
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/test-evaluate-worker.js
@@ -0,0 +1,8 @@
+"use strict";
+
+self.addEventListener("message", ({ data }) => foo(data));
+
+function foo(data) {
+  // eslint-disable-next-line no-debugger
+  debugger;
+}
--- a/devtools/client/webconsole/webconsole-wrapper.js
+++ b/devtools/client/webconsole/webconsole-wrapper.js
@@ -126,29 +126,40 @@ class WebConsoleWrapper {
           return webConsoleUI.webConsoleClient;
         },
 
         /**
          * Retrieve the FrameActor ID given a frame depth, or the selected one if no
          * frame depth given.
          *
          * @param {Number} frame: optional frame depth.
-         * @return {String|null}: The FrameActor ID for the given frame depth (or the
-         *                        selected frame if it exists).
+         * @return { frameActor: String|null, client: Object }:
+         *         frameActor is the FrameActor ID for the given frame depth
+         *         (or the selected frame if it exists), null if no frame was found.
+         *         client is the WebConsole client for the thread the frame is
+         *         associated with.
          */
         getFrameActor: (frame = null) => {
           const state = this.hud.getDebuggerFrames();
           if (!state) {
-            return null;
+            return { frameActor: null, client: webConsoleUI.webConsoleClient };
           }
 
           const grip = Number.isInteger(frame)
             ? state.frames[frame]
             : state.frames[state.selected];
-          return grip ? grip.actor : null;
+
+          if (!grip) {
+            return { frameActor: null, client: webConsoleUI.webConsoleClient };
+          }
+
+          return {
+            frameActor: grip.actor,
+            client: this.hud.lookupConsoleClient(grip.thread),
+          };
         },
 
         inputHasSelection: () => {
           const {editor, inputNode} = webConsoleUI.jsterm || {};
           return editor
             ? !!editor.getSelection()
             : (inputNode && inputNode.selectionStart !== inputNode.selectionEnd);
         },
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -248,16 +248,28 @@ class WebConsole {
     if (!panel) {
       return null;
     }
 
     return panel.getFrames();
   }
 
   /**
+   * Return the console client to use when interacting with a thread.
+   *
+   * @param {String} thread: The ID of the target thread.
+   * @returns {Object} The console client associated with the thread.
+   */
+  lookupConsoleClient(thread) {
+    const toolbox = gDevTools.getToolbox(this.target);
+    const panel = toolbox.getPanel("jsdebugger");
+    return panel.lookupConsoleClient(thread);
+  }
+
+  /**
    * Given an expression, returns an object containing a new expression, mapped by the
    * parser worker to provide additional feature for the user (top-level await,
    * original languages mapping, …).
    *
    * @param {String} expression: The input to maybe map.
    * @returns {Object|null}
    *          Returns null if the input can't be mapped.
    *          If it can, returns an object containing the following:
--- a/devtools/client/webreplay/mochitest/browser.ini
+++ b/devtools/client/webreplay/mochitest/browser.ini
@@ -14,16 +14,18 @@ support-files =
   !/devtools/client/debugger/test/mochitest/helpers/context.js
   !/devtools/client/inspector/test/shared-head.js
   examples/doc_rr_basic.html
   examples/doc_rr_continuous.html
   examples/doc_rr_logs.html
   examples/doc_rr_recovery.html
   examples/doc_rr_error.html
   examples/doc_inspector_basic.html
+  examples/doc_inspector_styles.html
+  examples/styles.css
 
 [browser_dbg_rr_breakpoints-01.js]
 [browser_dbg_rr_breakpoints-02.js]
 [browser_dbg_rr_breakpoints-03.js]
 [browser_dbg_rr_breakpoints-04.js]
 [browser_dbg_rr_breakpoints-05.js]
 [browser_dbg_rr_record.js]
 [browser_dbg_rr_stepping-01.js]
@@ -36,8 +38,9 @@ skip-if = true # See bug 1481009
 [browser_dbg_rr_replay-02.js]
 [browser_dbg_rr_replay-03.js]
 [browser_dbg_rr_console_warp-01.js]
 [browser_dbg_rr_console_warp-02.js]
 [browser_dbg_rr_logpoint-01.js]
 [browser_dbg_rr_logpoint-02.js]
 [browser_rr_inspector-01.js]
 [browser_rr_inspector-02.js]
+[browser_rr_inspector-03.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webreplay/mochitest/browser_rr_inspector-03.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-undef */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+  this
+);
+
+function getComputedViewProperty(view, name) {
+  let prop;
+  for (const property of view.styleDocument.querySelectorAll(
+      "#computed-container .computed-property-view")) {
+    const nameSpan = property.querySelector(".computed-property-name");
+    const valueSpan = property.querySelector(".computed-property-value");
+
+    if (nameSpan.firstChild.textContent === name) {
+      prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+      break;
+    }
+  }
+  return prop;
+}
+
+// Test that styles for elements can be viewed when using web replay.
+add_task(async function() {
+  const dbg = await attachRecordingDebugger(
+    "doc_inspector_styles.html",
+    { waitForRecording: true }
+  );
+  const {threadClient, tab, toolbox} = dbg;
+  await threadClient.resume();
+
+  await threadClient.interrupt();
+
+  const {inspector, view} = await openComputedView();
+  await checkBackgroundColor("body", "rgb(0, 128, 0)");
+  await checkBackgroundColor("#maindiv", "rgb(0, 0, 255)");
+
+  const bp = await setBreakpoint(threadClient, "doc_inspector_styles.html", 11);
+
+  await rewindToLine(threadClient, 11);
+  await checkBackgroundColor("#maindiv", "rgb(255, 0, 0)");
+
+  await threadClient.removeBreakpoint(bp);
+  await toolbox.closeToolbox();
+  await gBrowser.removeTab(tab);
+
+  async function checkBackgroundColor(node, color) {
+    await selectNode(node, inspector);
+
+    const value = getComputedViewProperty(view, "background-color").valueSpan;
+    const nodeInfo = view.getNodeInfo(value);
+    is(nodeInfo.value.property, "background-color");
+    is(nodeInfo.value.value, color);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/webreplay/mochitest/examples/doc_inspector_styles.html
@@ -0,0 +1,16 @@
+<body>
+<link rel="stylesheet" href="styles.css">
+<div id="maindiv">HELLO</div>
+<script>
+const cpmm = SpecialPowers.Services.cpmm;
+function recordingFinished() {
+  cpmm.sendAsyncMessage("RecordingFinished");
+}
+function foo() {
+  document.getElementById("maindiv").innerHTML = "GOODBYE";
+  document.getElementById("maindiv").style.backgroundColor = "blue";
+  window.setTimeout(recordingFinished);
+}
+window.setTimeout(foo);
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/devtools/client/webreplay/mochitest/examples/styles.css
@@ -0,0 +1,7 @@
+body {
+    background-color: green;
+}
+
+div {
+    background-color: red;
+}
--- a/devtools/server/actors/network-event.js
+++ b/devtools/server/actors/network-event.js
@@ -398,26 +398,26 @@ const NetworkEventActor = protocol.Actor
   },
 
   /**
    * Add connection security information.
    *
    * @param object info
    *        The object containing security information.
    */
-  addSecurityInfo(info) {
+  addSecurityInfo(info, isRacing) {
     // Ignore calls when this actor is already destroyed
     if (!this.actorID) {
       return;
     }
 
     this._securityInfo = info;
-
     this.emit("network-event-update:security-info", "securityInfo", {
       state: info.state,
+      isRacing: isRacing,
     });
   },
 
   /**
    * Add network response headers.
    *
    * @param array headers
    *        The response headers array.
--- a/devtools/server/actors/network-monitor/network-response-listener.js
+++ b/devtools/server/actors/network-monitor/network-response-listener.js
@@ -290,17 +290,23 @@ NetworkResponseListener.prototype = {
 
     // Take the security information from the original nsIHTTPChannel instead of
     // the nsIRequest received in onStartRequest. If response to this request
     // was a redirect from http to https, the request object seems to contain
     // security info for the https request after redirect.
     const secinfo = this.httpActivity.channel.securityInfo;
     const info = NetworkHelper.parseSecurityInfo(secinfo, this.httpActivity);
 
-    this.httpActivity.owner.addSecurityInfo(info);
+    let isRacing = false;
+    const channel = this.httpActivity.channel;
+    if (channel instanceof Ci.nsICacheInfoChannel) {
+      isRacing = channel.isRacing();
+    }
+
+    this.httpActivity.owner.addSecurityInfo(info, isRacing);
   }),
 
   /**
    * Fetches cache information from CacheEntry
    * @private
    */
   _fetchCacheInformation: function() {
     const httpActivity = this.httpActivity;
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -934,19 +934,16 @@ ReplayDebuggerObject.prototype = {
   get isGeneratorFunction() { return this._data.isGeneratorFunction; },
   get isAsyncFunction() { return this._data.isAsyncFunction; },
   get class() { return this._data.class; },
   get name() { return this._data.name; },
   get displayName() { return this._data.displayName; },
   get parameterNames() { return this._data.parameterNames; },
   get script() { return this._dbg._getScript(this._data.script); },
   get environment() { return this._dbg._getObject(this._data.environment); },
-  get boundTargetFunction() { return this.isBoundFunction ? NYI() : undefined; },
-  get boundThis() { return this.isBoundFunction ? NYI() : undefined; },
-  get boundArguments() { return this.isBoundFunction ? NYI() : undefined; },
   get isProxy() { return this._data.isProxy; },
   get proto() { return this._dbg._getObject(this._data.proto); },
 
   isExtensible() { return this._data.isExtensible; },
   isSealed() { return this._data.isSealed; },
   isFrozen() { return this._data.isFrozen; },
 
   unsafeDereference() {
@@ -1020,16 +1017,37 @@ ReplayDebuggerObject.prototype = {
     return this._dbg._convertValue(this._proxyData.target);
   },
 
   get proxyHandler() {
     this._ensureProxyData();
     return this._dbg._convertValue(this._proxyData.handler);
   },
 
+  get boundTargetFunction() {
+    if (this.isBoundFunction) {
+      return this._dbg._getObject(this._data.boundTargetFunction);
+    }
+    return undefined;
+  },
+
+  get boundThis() {
+    if (this.isBoundFunction) {
+      return this._dbg._convertValue(this._data.boundThis);
+    }
+    return undefined;
+  },
+
+  get boundArguments() {
+    if (this.isBoundFunction) {
+      return this._dbg._getObject(this._data.boundArguments);
+    }
+    return undefined;
+  },
+
   call(thisv, ...args) {
     return this.apply(thisv, args);
   },
 
   apply(thisv, args) {
     thisv = this._dbg._convertValueForChild(thisv);
     args = (args || []).map(v => this._dbg._convertValueForChild(v));
 
--- a/devtools/server/actors/replay/inspector.js
+++ b/devtools/server/actors/replay/inspector.js
@@ -34,28 +34,54 @@ function dbg() {
 
 ///////////////////////////////////////////////////////////////////////////////
 // Public Interface
 ///////////////////////////////////////////////////////////////////////////////
 
 const ReplayInspector = {
   // Return a proxy for the window in the replaying process.
   get window() {
-    return gWindow;
+    if (!gFixedProxy.window) {
+      updateFixedProxies();
+    }
+    return gFixedProxy.window;
   },
 
   // Create the InspectorUtils object to bind for other server users.
   createInspectorUtils(utils) {
-    // Overwrite some APIs that will fail if called on proxies from the
-    // replaying process.
+    return new Proxy({}, {
+      get(_, name) {
+        switch (name) {
+        case "getAllStyleSheets":
+        case "getCSSStyleRules":
+        case "getRuleLine":
+        case "getRuleColumn":
+        case "getRelativeRuleLine":
+        case "getSelectorCount":
+        case "getSelectorText":
+        case "selectorMatchesElement":
+        case "hasRulesModifiedByCSSOM":
+        case "getSpecificity":
+          return gFixedProxy.InspectorUtils[name];
+        case "hasPseudoClassLock":
+          return () => false;
+        default:
+          return utils[name];
+        }
+      },
+    });
+  },
+
+  // Create the CSSRule object to bind for other server users.
+  createCSSRule(rule) {
     return {
-      ...utils,
-      hasPseudoClassLock() { return false; },
-      getAllStyleSheets() { return []; },
-      getCSSStyleRules() { return []; },
+      ...rule,
+      isInstance(node) {
+        return gFixedProxy.CSSRule.isInstance(node);
+      },
     };
   },
 
   wrapRequireHook(requireHook) {
     return (id, require) => {
       const rv = requireHook(id, require);
       return substituteRequire(id, rv);
     };
@@ -133,23 +159,17 @@ function createSubstituteChrome(chrome) 
     }),
   };
 }
 
 function createSubstituteServices(Services) {
   return newSubstituteProxy(Services, {
     els: {
       getListenerInfoFor(node) {
-        const id = unwrapValue(node)._data.id;
-        const rv = dbg()._sendRequestAllowDiverge({
-          type: "getListenerInfoFor",
-          id,
-        });
-        const obj = dbg()._getObject(rv.id);
-        return wrapValue(obj);
+        return gFixedProxy.Services.els.getListenerInfoFor(node);
       },
     },
   });
 }
 
 function createSubstitute(id, rv) {
   switch (id) {
   case "chrome": return createSubstituteChrome(rv);
@@ -360,17 +380,17 @@ const ReplayInspectorProxyHandler = {
     ThrowError(rv.throw);
   },
 
   construct(target, args) {
     target = getTargetObject(target);
     const proxy = wrapObject(target);
 
     // Create fake MutationObservers to satisfy callers in the inspector.
-    if (proxy == gWindow.MutationObserver) {
+    if (proxy == gFixedProxy.window.MutationObserver) {
       return {
         observe: () => {},
         disconnect: () => {},
       };
     }
 
     NotAllowed();
   },
@@ -412,36 +432,36 @@ const ReplayInspectorProxyHandler = {
 };
 
 ///////////////////////////////////////////////////////////////////////////////
 // Fixed Proxies
 ///////////////////////////////////////////////////////////////////////////////
 
 // Proxies for the window and root document are reused to ensure consistent
 // actors are used for these objects.
-const gWindowTarget = { object: {} }, gDocumentTarget = { object: {} };
-const gWindow = new Proxy(gWindowTarget, ReplayInspectorProxyHandler);
-const gDocument = new Proxy(gDocumentTarget, ReplayInspectorProxyHandler);
+const gFixedProxyTargets = {};
+const gFixedProxy = {};
 
 function initFixedProxy(proxy, target, obj) {
   target.object = obj;
   proxyMap.set(proxy, obj);
   obj._inspectorObject = proxy;
 }
 
 function updateFixedProxies() {
   dbg()._ensurePaused();
 
-  const data = dbg()._sendRequestAllowDiverge({ type: "getWindow" });
-  const dbgWindow = dbg()._getObject(data.id);
-  initFixedProxy(gWindow, gWindowTarget, dbgWindow);
-
-  const rv = getObjectProperty(dbgWindow, "document");
-  assert(rv.return instanceof ReplayDebugger.Object);
-  initFixedProxy(gDocument, gDocumentTarget, rv.return);
+  const data = dbg()._sendRequestAllowDiverge({ type: "getFixedObjects" });
+  for (const [key, value] of Object.entries(data)) {
+    if (!gFixedProxyTargets[key]) {
+      gFixedProxyTargets[key] = { object: {} };
+      gFixedProxy[key] = new Proxy(gFixedProxyTargets[key], ReplayInspectorProxyHandler);
+    }
+    initFixedProxy(gFixedProxy[key], gFixedProxyTargets[key], dbg()._getObject(value));
+  }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Utilities
 ///////////////////////////////////////////////////////////////////////////////
 
 function NYI() {
   ThrowError("Not yet implemented");
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -21,28 +21,38 @@
 // any point where such interactions might occur.
 // eslint-disable spaced-comment
 
 "use strict";
 
 const CC = Components.Constructor;
 
 // Create a sandbox with the resources we need. require() doesn't work here.
-const sandbox = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
+const sandbox = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")(), {
+  wantGlobalProperties: [
+    "InspectorUtils",
+    "CSSRule",
+  ],
+});
 Cu.evalInSandbox(
   "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
   "Components.utils.import('resource://gre/modules/Services.jsm');" +
   "addDebuggerToGlobal(this);",
   sandbox
 );
-const Debugger = sandbox.Debugger;
-const RecordReplayControl = sandbox.RecordReplayControl;
-const Services = sandbox.Services;
+const {
+  Debugger,
+  RecordReplayControl,
+  Services,
+  InspectorUtils,
+  CSSRule,
+} = sandbox;
 
 const dbg = new Debugger();
+const firstGlobal = dbg.makeGlobalObjectReference(sandbox);
 
 // We are interested in debugging all globals in the process.
 dbg.onNewGlobalObject = function(global) {
   try {
     dbg.addDebuggee(global);
   } catch (e) {
     // Ignore errors related to adding a same-compartment debuggee.
     // See bug 1523755.
@@ -542,19 +552,26 @@ function convertValueFromParent(value) {
     }
   }
   return value;
 }
 
 function makeDebuggeeValue(value) {
   if (isNonNullObject(value)) {
     assert(!(value instanceof Debugger.Object));
-    const global = Cu.getGlobalForObject(value);
-    const dbgGlobal = dbg.makeGlobalObjectReference(global);
-    return dbgGlobal.makeDebuggeeValue(value);
+    try {
+      const global = Cu.getGlobalForObject(value);
+      const dbgGlobal = dbg.makeGlobalObjectReference(global);
+      return dbgGlobal.makeDebuggeeValue(value);
+    } catch (e) {
+      // Sometimes the global which Cu.getGlobalForObject finds has
+      // isInvisibleToDebugger set. Wrap the object into the first global we
+      // found in this case.
+      return firstGlobal.makeDebuggeeValue(value);
+    }
   }
   return value;
 }
 
 function getDebuggeeValue(value) {
   if (value && typeof value == "object") {
     assert(value instanceof Debugger.Object);
     return value.unsafeDereference();
@@ -713,17 +730,17 @@ const gRequestHandlers = {
 
   getSource(request) {
     return getSourceData(request.id);
   },
 
   getObject(request) {
     const object = gPausedObjects.getObject(request.id);
     if (object instanceof Debugger.Object) {
-      return {
+      const rv = {
         id: request.id,
         kind: "Object",
         callable: object.callable,
         isBoundFunction: object.isBoundFunction,
         isArrowFunction: object.isArrowFunction,
         isGeneratorFunction: object.isGeneratorFunction,
         isAsyncFunction: object.isAsyncFunction,
         proto: getObjectId(object.proto),
@@ -733,16 +750,22 @@ const gRequestHandlers = {
         parameterNames: object.parameterNames,
         script: gScripts.getId(object.script),
         environment: getObjectId(object.environment),
         isProxy: object.isProxy,
         isExtensible: object.isExtensible(),
         isSealed: object.isSealed(),
         isFrozen: object.isFrozen(),
       };
+      if (rv.isBoundFunction) {
+        rv.boundTargetFunction = getObjectId(object.boundTargetFunction);
+        rv.boundThis = convertValue(object.boundThis);
+        rv.boundArguments = getObjectId(makeDebuggeeValue(object.boundArguments));
+      }
+      return rv;
     }
     if (object instanceof Debugger.Environment) {
       return {
         id: request.id,
         kind: "Environment",
         type: object.type,
         parent: getObjectId(object.parent),
         object: object.type == "declarative" ? 0 : getObjectId(object.object),
@@ -876,23 +899,29 @@ const gRequestHandlers = {
   recordingEndpoint(request) {
     return RecordReplayControl.recordingEndpoint();
   },
 
   /////////////////////////////////////////////////////////
   // Inspector Requests
   /////////////////////////////////////////////////////////
 
-  getWindow(request) {
+  getFixedObjects(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return { throw: "Recording divergence in getWindow" };
     }
 
-    // Hopefully there is exactly one window in this enumerator.
-    return { id: getObjectId(makeDebuggeeValue(getWindow())) };
+    const window = getWindow();
+    return {
+      window: getObjectId(makeDebuggeeValue(window)),
+      document: getObjectId(makeDebuggeeValue(window.document)),
+      Services: getObjectId(makeDebuggeeValue(Services)),
+      InspectorUtils: getObjectId(makeDebuggeeValue(InspectorUtils)),
+      CSSRule: getObjectId(makeDebuggeeValue(CSSRule)),
+    };
   },
 
   newDeepTreeWalker(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return { throw: "Recording divergence in newDeepTreeWalker" };
     }
 
     const walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
@@ -941,26 +970,16 @@ const gRequestHandlers = {
     const element =
       getWindow().document.elementFromPoint(request.clientX, request.clientY);
     if (!element) {
       return { id: 0 };
     }
     const obj = makeDebuggeeValue(element);
     return { id: getObjectId(obj) };
   },
-
-  getListenerInfoFor(request) {
-    if (!RecordReplayControl.maybeDivergeFromRecording()) {
-      return { throw: "Recording divergence in getListenerInfoFor" };
-    }
-
-    const node = gPausedObjects.getObject(request.id).unsafeDereference();
-    const obj = makeDebuggeeValue(Services.els.getListenerInfoFor(node) || []);
-    return { id: getObjectId(obj) };
-  },
 };
 
 // eslint-disable-next-line no-unused-vars
 function ProcessRequest(request) {
   try {
     if (gRequestHandlers[request.type]) {
       return gRequestHandlers[request.type](request);
     }
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -14,16 +14,20 @@ const {mediaRuleSpec, styleSheetSpec,
 const InspectorUtils = require("InspectorUtils");
 
 loader.lazyRequireGetter(this, "CssLogic", "devtools/shared/inspector/css-logic");
 loader.lazyRequireGetter(this, "addPseudoClassLock",
   "devtools/server/actors/highlighters/utils/markup", true);
 loader.lazyRequireGetter(this, "removePseudoClassLock",
   "devtools/server/actors/highlighters/utils/markup", true);
 loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
+loader.lazyRequireGetter(this, "ReplayDebugger",
+  "devtools/server/actors/replay/debugger");
+loader.lazyRequireGetter(this, "ReplayInspector",
+  "devtools/server/actors/replay/inspector");
 
 var TRANSITION_PSEUDO_CLASS = ":-moz-styleeditor-transitioning";
 var TRANSITION_DURATION_MS = 500;
 var TRANSITION_BUFFER_MS = 1000;
 var TRANSITION_RULE_SELECTOR =
 `:root${TRANSITION_PSEUDO_CLASS}, :root${TRANSITION_PSEUDO_CLASS} *`;
 
 var TRANSITION_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
@@ -173,16 +177,23 @@ async function fetchStylesheet(sheet, co
   let result;
   if (consoleActor) {
     result = await consoleActor.getRequestContentForURL(href);
     if (result) {
       return result;
     }
   }
 
+  // When replaying, fetch the stylesheets from the replaying process, so that
+  // we get the same sheets which were used when recording.
+  if (isReplaying) {
+    const dbg = new ReplayDebugger();
+    return dbg.replayingContent(href);
+  }
+
   const options = {
     loadFromCache: true,
     policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET,
     charset: getCSSCharset(sheet),
   };
 
   // Bug 1282660 - We use the system principal to load the default internal
   // stylesheets instead of the content principal since such stylesheets
@@ -222,17 +233,17 @@ var StyleSheetActor = protocol.ActorClas
   toString: function() {
     return "[StyleSheetActor " + this.actorID + "]";
   },
 
   /**
    * Window of target
    */
   get window() {
-    return this.parentActor.window;
+    return isReplaying ? ReplayInspector.window : this.parentActor.window;
   },
 
   /**
    * Document of target.
    */
   get document() {
     return this.window.document;
   },
@@ -664,17 +675,18 @@ var StyleSheetsActor = protocol.ActorCla
 
   /**
    * Protocol method for getting a list of StyleSheetActors representing
    * all the style sheets in this document.
    */
   async getStyleSheets() {
     let actors = [];
 
-    for (const win of this.parentActor.windows) {
+    const windows = isReplaying ? [ReplayInspector.window] : this.parentActor.windows;
+    for (const win of windows) {
       const sheets = await this._addStyleSheets(win);
       actors = actors.concat(sheets);
     }
     return actors;
   },
 
   /**
    * Check if we should be showing this stylesheet.
--- a/devtools/shared/DevToolsUtils.js
+++ b/devtools/shared/DevToolsUtils.js
@@ -637,17 +637,17 @@ function mainThreadFetch(urlIn, aOptions
 
 /**
  * Opens a channel for given URL. Tries a bit harder than NetUtil.newChannel.
  *
  * @param {String} url - The URL to open a channel for.
  * @param {Object} options - The options object passed to @method fetch.
  * @return {nsIChannel} - The newly created channel. Throws on failure.
  */
-function newChannelForURL(url, { policy, window, principal }) {
+function newChannelForURL(url, { policy, window, principal }, recursing = false) {
   const securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL;
 
   let uri;
   try {
     uri = Services.io.newURI(url);
   } catch (e) {
     // In the xpcshell tests, the script url is the absolute path of the test
     // file, which will make a malformed URI error be thrown. Add the file
@@ -684,21 +684,27 @@ function newChannelForURL(url, { policy,
     }
 
     channelOptions.loadingPrincipal = prin;
   }
 
   try {
     return NetUtil.newChannel(channelOptions);
   } catch (e) {
+    // Don't infinitely recurse if newChannel keeps throwing.
+    if (recursing) {
+      throw e;
+    }
+
     // In xpcshell tests on Windows, nsExternalProtocolHandler::NewChannel()
     // can throw NS_ERROR_UNKNOWN_PROTOCOL if the external protocol isn't
     // supported by Windows, so we also need to handle the exception here if
     // parsing the URL above doesn't throw.
-    return newChannelForURL("file://" + url, { policy, window, principal });
+    return newChannelForURL("file://" + url, { policy, window, principal },
+                            /* recursing */ true);
   }
 }
 
 // Fetch is defined differently depending on whether we are on the main thread
 // or a worker thread.
 if (this.isWorker) {
   // Services is not available in worker threads, nor is there any other way
   // to fetch a URL. We need to enlist the help from the main thread here, by
--- a/devtools/shared/builtin-modules.js
+++ b/devtools/shared/builtin-modules.js
@@ -292,17 +292,16 @@ defineLazyGetter(exports.modules, "xpcIn
 // List of all custom globals exposed to devtools modules.
 // Changes here should be mirrored to devtools/.eslintrc.
 exports.globals = {
   atob,
   Blob,
   btoa,
   console,
   CSS,
-  CSSRule,
   // Make sure `define` function exists.  This allows defining some modules
   // in AMD format while retaining CommonJS compatibility through this hook.
   // JSON Viewer needs modules in AMD format, as it currently uses RequireJS
   // from a content document and can't access our usual loaders.  So, any
   // modules shared with the JSON Viewer should include a define wrapper:
   //
   //   // Make this available to both AMD and CJS environments
   //   define(function(require, exports, module) {
@@ -376,8 +375,15 @@ lazyGlobal("WebSocket", () => {
   return Services.appShell.hiddenDOMWindow.WebSocket;
 });
 lazyGlobal("indexedDB", () => {
   return require("devtools/shared/indexed-db").createDevToolsIndexedDB(indexedDB);
 });
 lazyGlobal("isReplaying", () => {
   return exports.modules.Debugger.recordReplayProcessKind() == "Middleman";
 });
+lazyGlobal("CSSRule", () => {
+  if (exports.modules.Debugger.recordReplayProcessKind() == "Middleman") {
+    const ReplayInspector = require("devtools/server/actors/replay/inspector");
+    return ReplayInspector.createCSSRule(CSSRule);
+  }
+  return CSSRule;
+});
--- a/devtools/shared/specs/network-event.js
+++ b/devtools/shared/specs/network-event.js
@@ -125,16 +125,17 @@ const networkEventSpec = generateActorSp
       response: Option(1, "json"),
     },
 
     "network-event-update:security-info": {
       type: "networkEventUpdate",
       updateType: Arg(0, "string"),
 
       state: Option(1, "string"),
+      isRacing: Option(1, "boolean"),
     },
 
     "network-event-update:response-content": {
       type: "networkEventUpdate",
       updateType: Arg(0, "string"),
 
       mimeType: Option(1, "string"),
       contentSize: Option(1, "number"),
--- a/layout/style/Loader.cpp
+++ b/layout/style/Loader.cpp
@@ -1571,16 +1571,25 @@ nsresult Loader::LoadSheet(SheetLoadData
 Loader::Completed Loader::ParseSheet(const nsACString& aBytes,
                                      SheetLoadData* aLoadData,
                                      AllowAsyncParse aAllowAsync) {
   LOG(("css::Loader::ParseSheet"));
   AUTO_PROFILER_LABEL("css::Loader::ParseSheet", LAYOUT_CSSParsing);
   MOZ_ASSERT(aLoadData);
   aLoadData->mIsBeingParsed = true;
 
+  // Tell the record/replay system about any sheets that are being parsed,
+  // so that devtools code can find them later.
+  if (recordreplay::IsRecordingOrReplaying() && aLoadData->mURI) {
+    recordreplay::NoteContentParse(
+        aLoadData, aLoadData->mURI->GetSpecOrDefault().get(), "text/css",
+        reinterpret_cast<const Utf8Unit*>(aBytes.BeginReading()),
+        aBytes.Length());
+  }
+
   StyleSheet* sheet = aLoadData->mSheet;
   MOZ_ASSERT(sheet);
 
   // Some cases, like inline style and UA stylesheets, need to be parsed
   // synchronously. The former may trigger child loads, the latter must not.
   if (aLoadData->mSyncLoad || aAllowAsync == AllowAsyncParse::No) {
     sheet->ParseSheetSync(this, aBytes, aLoadData, aLoadData->mLineNumber);
     aLoadData->mIsBeingParsed = false;
--- a/mfbt/RecordReplay.h
+++ b/mfbt/RecordReplay.h
@@ -311,17 +311,17 @@ MFBT_API ProgressCounter NewTimeWarpTarg
 // Return whether a script should update the progress counter when it runs.
 MFBT_API bool ShouldUpdateProgressCounter(const char* aURL);
 
 // Define a RecordReplayControl object on the specified global object, with
 // methods specialized to the current recording/replaying or middleman process
 // kind.
 MFBT_API bool DefineRecordReplayControlObject(JSContext* aCx, JSObject* aObj);
 
-// Notify the infrastructure that some URL which contains JavaScript is
+// Notify the infrastructure that some URL which contains JavaScript or CSS is
 // being parsed. This is used to provide the complete contents of the URL to
 // devtools code when it is inspecting the state of this process; that devtools
 // code can't simply fetch the URL itself since it may have been changed since
 // the recording was made or may no longer exist. The token for a parse may not
 // be used in other parses until after EndContentParse() is called.
 MFBT_API void BeginContentParse(const void* aToken, const char* aURL,
                                 const char* aContentType);
 
--- a/netwerk/base/nsICacheInfoChannel.idl
+++ b/netwerk/base/nsICacheInfoChannel.idl
@@ -51,16 +51,25 @@ interface nsICacheInfoChannel : nsISuppo
   /**
    * TRUE if this channel's data is being loaded from the cache.  This value
    * is undefined before the channel fires its OnStartRequest notification
    * and after the channel fires its OnStopRequest notification.
    */
   boolean isFromCache();
 
   /**
+   * Returns true if the channel raced the cache and network requests.
+   * In order to determine if the response is coming from the cache or the
+   * network, the consumer can check isFromCache().
+   * The method can only be called after the channel fires its OnStartRequest
+   * notification.
+   */
+  boolean isRacing();
+
+  /**
    * The unique ID of the corresponding nsICacheEntry from which the response is
    * retrieved. By comparing the returned value, we can judge whether the data
    * of two distinct nsICacheInfoChannels is from the same nsICacheEntry. This
    * scenario could be useful when verifying whether the alternative data from
    * one nsICacheInfochannel matches the main data from another one.
    *
    * Note: NS_ERROR_NOT_AVAILABLE is thrown when a nsICacheInfoChannel has no
    * valid corresponding nsICacheEntry.
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -169,16 +169,17 @@ HttpChannelChild::HttpChannelChild()
       mCacheFetchCount(0),
       mCacheExpirationTime(nsICacheEntry::NO_EXPIRATION_TIME),
       mDeletingChannelSent(false),
       mIPCOpen(false),
       mUnknownDecoderInvolved(false),
       mDivertingToParent(false),
       mFlushedForDiversion(false),
       mIsFromCache(false),
+      mIsRacing(false),
       mCacheNeedToReportBytesReadInitialized(false),
       mNeedToReportBytesRead(true),
       mCacheEntryAvailable(false),
       mAltDataCacheEntryAvailable(false),
       mSendResumeAt(false),
       mKeptAlive(false),
       mIPCActorDeleted(false),
       mSuspendSent(false),
@@ -394,31 +395,33 @@ void HttpChannelChild::AssociateApplicat
 
 class StartRequestEvent : public NeckoTargetChannelEvent<HttpChannelChild> {
  public:
   StartRequestEvent(
       HttpChannelChild* aChild, const nsresult& aChannelStatus,
       const nsHttpResponseHead& aResponseHead, const bool& aUseResponseHead,
       const nsHttpHeaderArray& aRequestHeaders,
       const ParentLoadInfoForwarderArgs& loadInfoForwarder,
-      const bool& aIsFromCache, const bool& aCacheEntryAvailable,
-      const uint64_t& aCacheEntryId, const int32_t& aCacheFetchCount,
-      const uint32_t& aCacheExpirationTime, const nsCString& aCachedCharset,
+      const bool& aIsFromCache, const bool& aIsRacing,
+      const bool& aCacheEntryAvailable, const uint64_t& aCacheEntryId,
+      const int32_t& aCacheFetchCount, const uint32_t& aCacheExpirationTime,
+      const nsCString& aCachedCharset,
       const nsCString& aSecurityInfoSerialization, const NetAddr& aSelfAddr,
       const NetAddr& aPeerAddr, const uint32_t& aCacheKey,
       const nsCString& altDataType, const int64_t& altDataLen,
       const bool& deliveringAltData, const bool& aApplyConversion,
       const ResourceTimingStruct& aTiming)
       : NeckoTargetChannelEvent<HttpChannelChild>(aChild),
         mChannelStatus(aChannelStatus),
         mResponseHead(aResponseHead),
         mRequestHeaders(aRequestHeaders),
         mUseResponseHead(aUseResponseHead),
         mApplyConversion(aApplyConversion),
         mIsFromCache(aIsFromCache),
+        mIsRacing(aIsRacing),
         mCacheEntryAvailable(aCacheEntryAvailable),
         mCacheEntryId(aCacheEntryId),
         mCacheFetchCount(aCacheFetchCount),
         mCacheExpirationTime(aCacheExpirationTime),
         mCachedCharset(aCachedCharset),
         mSecurityInfoSerialization(aSecurityInfoSerialization),
         mSelfAddr(aSelfAddr),
         mPeerAddr(aPeerAddr),
@@ -428,30 +431,31 @@ class StartRequestEvent : public NeckoTa
         mDeliveringAltData(deliveringAltData),
         mLoadInfoForwarder(loadInfoForwarder),
         mTiming(aTiming) {}
 
   void Run() override {
     LOG(("StartRequestEvent [this=%p]\n", mChild));
     mChild->OnStartRequest(
         mChannelStatus, mResponseHead, mUseResponseHead, mRequestHeaders,
-        mLoadInfoForwarder, mIsFromCache, mCacheEntryAvailable, mCacheEntryId,
-        mCacheFetchCount, mCacheExpirationTime, mCachedCharset,
+        mLoadInfoForwarder, mIsFromCache, mIsRacing, mCacheEntryAvailable,
+        mCacheEntryId, mCacheFetchCount, mCacheExpirationTime, mCachedCharset,
         mSecurityInfoSerialization, mSelfAddr, mPeerAddr, mCacheKey,
         mAltDataType, mAltDataLen, mDeliveringAltData, mApplyConversion,
         mTiming);
   }
 
  private:
   nsresult mChannelStatus;
   nsHttpResponseHead mResponseHead;
   nsHttpHeaderArray mRequestHeaders;
   bool mUseResponseHead;
   bool mApplyConversion;
   bool mIsFromCache;
+  bool mIsRacing;
   bool mCacheEntryAvailable;
   uint64_t mCacheEntryId;
   int32_t mCacheFetchCount;
   uint32_t mCacheExpirationTime;
   nsCString mCachedCharset;
   nsCString mSecurityInfoSerialization;
   NetAddr mSelfAddr;
   NetAddr mPeerAddr;
@@ -462,41 +466,42 @@ class StartRequestEvent : public NeckoTa
   ParentLoadInfoForwarderArgs mLoadInfoForwarder;
   ResourceTimingStruct mTiming;
 };
 
 mozilla::ipc::IPCResult HttpChannelChild::RecvOnStartRequest(
     const nsresult& channelStatus, const nsHttpResponseHead& responseHead,
     const bool& useResponseHead, const nsHttpHeaderArray& requestHeaders,
     const ParentLoadInfoForwarderArgs& loadInfoForwarder,
-    const bool& isFromCache, const bool& cacheEntryAvailable,
-    const uint64_t& cacheEntryId, const int32_t& cacheFetchCount,
-    const uint32_t& cacheExpirationTime, const nsCString& cachedCharset,
-    const nsCString& securityInfoSerialization, const NetAddr& selfAddr,
-    const NetAddr& peerAddr, const int16_t& redirectCount,
-    const uint32_t& cacheKey, const nsCString& altDataType,
-    const int64_t& altDataLen, const bool& deliveringAltData,
-    const bool& aApplyConversion, const ResourceTimingStruct& aTiming) {
+    const bool& isFromCache, const bool& isRacing,
+    const bool& cacheEntryAvailable, const uint64_t& cacheEntryId,
+    const int32_t& cacheFetchCount, const uint32_t& cacheExpirationTime,
+    const nsCString& cachedCharset, const nsCString& securityInfoSerialization,
+    const NetAddr& selfAddr, const NetAddr& peerAddr,
+    const int16_t& redirectCount, const uint32_t& cacheKey,
+    const nsCString& altDataType, const int64_t& altDataLen,
+    const bool& deliveringAltData, const bool& aApplyConversion,
+    const ResourceTimingStruct& aTiming) {
   AUTO_PROFILER_LABEL("HttpChannelChild::RecvOnStartRequest", NETWORK);
   LOG(("HttpChannelChild::RecvOnStartRequest [this=%p]\n", this));
   // mFlushedForDiversion and mDivertingToParent should NEVER be set at this
   // stage, as they are set in the listener's OnStartRequest.
   MOZ_RELEASE_ASSERT(
       !mFlushedForDiversion,
       "mFlushedForDiversion should be unset before OnStartRequest!");
   MOZ_RELEASE_ASSERT(
       !mDivertingToParent,
       "mDivertingToParent should be unset before OnStartRequest!");
 
   mRedirectCount = redirectCount;
 
   mEventQ->RunOrEnqueue(new StartRequestEvent(
       this, channelStatus, responseHead, useResponseHead, requestHeaders,
-      loadInfoForwarder, isFromCache, cacheEntryAvailable, cacheEntryId,
-      cacheFetchCount, cacheExpirationTime, cachedCharset,
+      loadInfoForwarder, isFromCache, isRacing, cacheEntryAvailable,
+      cacheEntryId, cacheFetchCount, cacheExpirationTime, cachedCharset,
       securityInfoSerialization, selfAddr, peerAddr, cacheKey, altDataType,
       altDataLen, deliveringAltData, aApplyConversion, aTiming));
 
   {
     // Child's mEventQ is to control the execution order of the IPC messages
     // from both main thread IPDL and PBackground IPDL.
     // To guarantee the ordering, PBackground IPC messages that are sent after
     // OnStartRequest will be throttled until OnStartRequest hits the Child's
@@ -515,21 +520,21 @@ mozilla::ipc::IPCResult HttpChannelChild
 
   return IPC_OK();
 }
 
 void HttpChannelChild::OnStartRequest(
     const nsresult& channelStatus, const nsHttpResponseHead& responseHead,
     const bool& useResponseHead, const nsHttpHeaderArray& requestHeaders,
     const ParentLoadInfoForwarderArgs& loadInfoForwarder,
-    const bool& isFromCache, const bool& cacheEntryAvailable,
-    const uint64_t& cacheEntryId, const int32_t& cacheFetchCount,
-    const uint32_t& cacheExpirationTime, const nsCString& cachedCharset,
-    const nsCString& securityInfoSerialization, const NetAddr& selfAddr,
-    const NetAddr& peerAddr, const uint32_t& cacheKey,
+    const bool& isFromCache, const bool& isRacing,
+    const bool& cacheEntryAvailable, const uint64_t& cacheEntryId,
+    const int32_t& cacheFetchCount, const uint32_t& cacheExpirationTime,
+    const nsCString& cachedCharset, const nsCString& securityInfoSerialization,
+    const NetAddr& selfAddr, const NetAddr& peerAddr, const uint32_t& cacheKey,
     const nsCString& altDataType, const int64_t& altDataLen,
     const bool& deliveringAltData, const bool& aApplyConversion,
     const ResourceTimingStruct& aTiming) {
   LOG(("HttpChannelChild::OnStartRequest [this=%p]\n", this));
 
   // mFlushedForDiversion and mDivertingToParent should NEVER be set at this
   // stage, as they are set in the listener's OnStartRequest.
   MOZ_RELEASE_ASSERT(
@@ -564,16 +569,17 @@ void HttpChannelChild::OnStartRequest(
     MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv),
                           "Deserializing security info should not fail");
     Unused << rv;  // So we don't get an unused error in release builds.
   }
 
   ipc::MergeParentLoadInfoForwarder(loadInfoForwarder, mLoadInfo);
 
   mIsFromCache = isFromCache;
+  mIsRacing = isRacing;
   mCacheEntryAvailable = cacheEntryAvailable;
   mCacheEntryId = cacheEntryId;
   mCacheFetchCount = cacheFetchCount;
   mCacheExpirationTime = cacheExpirationTime;
   mCachedCharset = cachedCharset;
   mSelfAddr = selfAddr;
   mPeerAddr = peerAddr;
 
@@ -2996,16 +3002,25 @@ HttpChannelChild::GetCacheEntryId(uint64
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   *aCacheEntryId = mCacheEntryId;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+HttpChannelChild::IsRacing(bool* aIsRacing) {
+  if (!mAfterOnStartRequestBegun) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+  *aIsRacing = mIsRacing;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 HttpChannelChild::GetCacheKey(uint32_t* cacheKey) {
   MOZ_ASSERT(NS_IsMainThread());
   if (mSynthesizedCacheInfo) {
     return mSynthesizedCacheInfo->GetCacheKey(cacheKey);
   }
 
   *cacheKey = mCacheKey;
   return NS_OK;
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -133,19 +133,20 @@ class HttpChannelChild final : public PH
 
   nsresult CrossProcessRedirectFinished(nsresult aStatus);
 
  protected:
   mozilla::ipc::IPCResult RecvOnStartRequest(
       const nsresult& channelStatus, const nsHttpResponseHead& responseHead,
       const bool& useResponseHead, const nsHttpHeaderArray& requestHeaders,
       const ParentLoadInfoForwarderArgs& loadInfoForwarder,
-      const bool& isFromCache, const bool& cacheEntryAvailable,
-      const uint64_t& cacheEntryId, const int32_t& cacheFetchCount,
-      const uint32_t& cacheExpirationTime, const nsCString& cachedCharset,
+      const bool& isFromCache, const bool& isRacing,
+      const bool& cacheEntryAvailable, const uint64_t& cacheEntryId,
+      const int32_t& cacheFetchCount, const uint32_t& cacheExpirationTime,
+      const nsCString& cachedCharset,
       const nsCString& securityInfoSerialization, const NetAddr& selfAddr,
       const NetAddr& peerAddr, const int16_t& redirectCount,
       const uint32_t& cacheKey, const nsCString& altDataType,
       const int64_t& altDataLen, const bool& deliveringAltData,
       const bool& aApplyConversion,
       const ResourceTimingStruct& aTiming) override;
   mozilla::ipc::IPCResult RecvFailedAsyncOpen(const nsresult& status) override;
   mozilla::ipc::IPCResult RecvRedirect1Begin(
@@ -397,16 +398,17 @@ class HttpChannelChild final : public PH
 
   // Once set, OnData and possibly OnStop will be diverted to the parent.
   Atomic<bool, ReleaseAcquire> mDivertingToParent;
   // Once set, no OnStart/OnData/OnStop callbacks should be received from the
   // parent channel, nor dequeued from the ChannelEventQueue.
   Atomic<bool, ReleaseAcquire> mFlushedForDiversion;
 
   Atomic<bool, SequentiallyConsistent> mIsFromCache;
+  Atomic<bool, SequentiallyConsistent> mIsRacing;
   // Set if we get the result and cache |mNeedToReportBytesRead|
   Atomic<bool, SequentiallyConsistent> mCacheNeedToReportBytesReadInitialized;
   // True if we need to tell the parent the size of unreported received data
   Atomic<bool, SequentiallyConsistent> mNeedToReportBytesRead;
 
   uint8_t mCacheEntryAvailable : 1;
   uint8_t mAltDataCacheEntryAvailable : 1;
 
@@ -458,19 +460,20 @@ class HttpChannelChild final : public PH
   bool RemoteChannelExists() { return mIPCOpen && !mKeptAlive; }
 
   void AssociateApplicationCache(const nsCString& groupID,
                                  const nsCString& clientID);
   void OnStartRequest(
       const nsresult& channelStatus, const nsHttpResponseHead& responseHead,
       const bool& useResponseHead, const nsHttpHeaderArray& requestHeaders,
       const ParentLoadInfoForwarderArgs& loadInfoForwarder,
-      const bool& isFromCache, const bool& cacheEntryAvailable,
-      const uint64_t& cacheEntryId, const int32_t& cacheFetchCount,
-      const uint32_t& cacheExpirationTime, const nsCString& cachedCharset,
+      const bool& isFromCache, const bool& isRacing,
+      const bool& cacheEntryAvailable, const uint64_t& cacheEntryId,
+      const int32_t& cacheFetchCount, const uint32_t& cacheExpirationTime,
+      const nsCString& cachedCharset,
       const nsCString& securityInfoSerialization, const NetAddr& selfAddr,
       const NetAddr& peerAddr, const uint32_t& cacheKey,
       const nsCString& altDataType, const int64_t& altDataLen,
       const bool& deliveringAltData, const bool& aApplyConversion,
       const ResourceTimingStruct& aTiming);
   void MaybeDivertOnData(const nsCString& data, const uint64_t& offset,
                          const uint32_t& count);
   void OnTransportAndData(const nsresult& channelStatus, const nsresult& status,
--- a/netwerk/protocol/http/HttpChannelParent.cpp
+++ b/netwerk/protocol/http/HttpChannelParent.cpp
@@ -1359,25 +1359,27 @@ HttpChannelParent::OnStartRequest(nsIReq
     MOZ_ASSERT(pcp, "We should have a manager if our IPC isn't closed");
     DebugOnly<nsresult> rv =
         static_cast<ContentParent*>(pcp)->AboutToLoadHttpFtpDocumentForChild(
             chan);
     MOZ_ASSERT(NS_SUCCEEDED(rv));
   }
 
   bool isFromCache = false;
+  bool isRacing = false;
   uint64_t cacheEntryId = 0;
   int32_t fetchCount = 0;
   uint32_t expirationTime = nsICacheEntry::NO_EXPIRATION_TIME;
   nsCString cachedCharset;
 
   RefPtr<nsHttpChannel> httpChannelImpl = do_QueryObject(chan);
 
   if (httpChannelImpl) {
     httpChannelImpl->IsFromCache(&isFromCache);
+    httpChannelImpl->IsRacing(&isRacing);
     httpChannelImpl->GetCacheEntryId(&cacheEntryId);
     httpChannelImpl->GetCacheTokenFetchCount(&fetchCount);
     httpChannelImpl->GetCacheTokenExpirationTime(&expirationTime);
     httpChannelImpl->GetCacheTokenCachedCharset(cachedCharset);
   }
 
   bool loadedFromApplicationCache = false;
 
@@ -1468,21 +1470,21 @@ HttpChannelParent::OnStartRequest(nsIReq
   ResourceTimingStruct timing;
   GetTimingAttributes(mChannel, timing);
 
   rv = NS_OK;
   if (mIPCClosed ||
       !SendOnStartRequest(
           channelStatus, *responseHead, useResponseHead,
           cleanedUpRequest ? cleanedUpRequestHeaders : requestHead->Headers(),
-          loadInfoForwarderArg, isFromCache, mCacheEntry ? true : false,
-          cacheEntryId, fetchCount, expirationTime, cachedCharset,
-          secInfoSerialization, chan->GetSelfAddr(), chan->GetPeerAddr(),
-          redirectCount, cacheKey, altDataType, altDataLen, deliveringAltData,
-          applyConversion, timing)) {
+          loadInfoForwarderArg, isFromCache, isRacing,
+          mCacheEntry ? true : false, cacheEntryId, fetchCount, expirationTime,
+          cachedCharset, secInfoSerialization, chan->GetSelfAddr(),
+          chan->GetPeerAddr(), redirectCount, cacheKey, altDataType, altDataLen,
+          deliveringAltData, applyConversion, timing)) {
     rv = NS_ERROR_UNEXPECTED;
   }
   requestHead->Exit();
 
   // OnStartRequest is sent to content process successfully.
   // Notify PHttpBackgroundChannelChild that all following IPC mesasges
   // should be run after OnStartRequest is handled.
   if (NS_SUCCEEDED(rv)) {
--- a/netwerk/protocol/http/InterceptedHttpChannel.cpp
+++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp
@@ -1171,16 +1171,25 @@ InterceptedHttpChannel::IsFromCache(bool
   if (mSynthesizedCacheInfo) {
     return mSynthesizedCacheInfo->IsFromCache(value);
   }
   *value = false;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+InterceptedHttpChannel::IsRacing(bool* value) {
+  if (mSynthesizedCacheInfo) {
+    return mSynthesizedCacheInfo->IsRacing(value);
+  }
+  *value = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 InterceptedHttpChannel::GetCacheEntryId(uint64_t* aCacheEntryId) {
   if (mSynthesizedCacheInfo) {
     return mSynthesizedCacheInfo->GetCacheEntryId(aCacheEntryId);
   }
   return NS_ERROR_NOT_AVAILABLE;
 }
 
 NS_IMETHODIMP
--- a/netwerk/protocol/http/PHttpChannel.ipdl
+++ b/netwerk/protocol/http/PHttpChannel.ipdl
@@ -107,16 +107,17 @@ parent:
 
 child:
   async OnStartRequest(nsresult            channelStatus,
                        nsHttpResponseHead  responseHead,
                        bool                useResponseHead,
                        nsHttpHeaderArray   requestHeaders,
                        ParentLoadInfoForwarderArgs loadInfoForwarder,
                        bool                isFromCache,
+                       bool                isRacing,
                        bool                cacheEntryAvailable,
                        uint64_t            cacheEntryId,
                        int32_t             cacheFetchCount,
                        uint32_t            cacheExpirationTime,
                        nsCString           cachedCharset,
                        nsCString           securityInfoSerialization,
                        NetAddr             selfAddr,
                        NetAddr             peerAddr,
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -8658,16 +8658,25 @@ nsHttpChannel::GetAltDataInputStream(con
   return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsICachingChannel
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
+nsHttpChannel::IsRacing(bool *aIsRacing) {
+  if (!mAfterOnStartRequestBegun) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+  *aIsRacing = mRaceCacheWithNetwork;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsHttpChannel::GetCacheToken(nsISupports **token) {
   NS_ENSURE_ARG_POINTER(token);
   if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE;
   return CallQueryInterface(mCacheEntry, token);
 }
 
 NS_IMETHODIMP
 nsHttpChannel::SetCacheToken(nsISupports *token) {
--- a/netwerk/test/unit/test_race_cache_with_network.js
+++ b/netwerk/test/unit/test_race_cache_with_network.js
@@ -53,16 +53,17 @@ function cached_handler(metadata, respon
   g200Counter++;
 }
 
 let gResponseCounter = 0;
 let gIsFromCache = 0;
 function checkContent(request, buffer, context, isFromCache)
 {
   Assert.equal(buffer, gResponseBody);
+  info("isRacing: " + request.QueryInterface(Ci.nsICacheInfoChannel).isRacing() + "\n");
   gResponseCounter++;
   if (isFromCache) {
     gIsFromCache++;
   }
   executeSoon(() => { testGenerator.next(); });
 }
 
 function run_test() {
index 36aded09f5f181fcc38f679a68060d512e6a1842..e1eab1bbca9ca7dd6213e323fd4778039d52455c
GIT binary patch
literal 4096
zc%1E5e{2(V6o1|7C{VT<TEia%Z)6%wu-<z32Mm+JcEEJVtzEkz6z$rBcHVl+^@<yT
zWHPGM(#1cJe=sC47mb?vBcdQNX+Q-MF>0onKSCtVrE`mxWHAe?p6~m;>(&SXVhH}T
z*L?52@8^BK@B7}n`|fU=pL!We0l<ty(*RC^F12uL8OBk5|HtL<!Td{=r%a8PDx1TR
z7}Kf9yOcl&6AVP7vdXlHj1rGBktkEY#lv*SA<?;L(Sj-?#`v`}C*L{x?Py_4pZaxl
z5VdspB|{$^eVzJ-M$e$$c;GU{Hy-$U^exoxNH9#+{W~{-2WT{zA^q9NwgUDulvge^
zmFdM=bZpsTyx4FsbZVn^iB18S)ga7HN(?IH&S+f;%K@H1-G%YGZq__(aO-XK_&Q;3
zzRszNT`KAiOO1rgP*lWBh-aKiD4+%aj+W>^aZid;taa)%7$N`*dhyolcp0Z6N^%h3
z8v~%c^6Pl(Z(ZP=+)eSOCZ3<L&I3sCR!{#07Hlcr-v0&Fw*IeB`_hU0HBD;~vg8j%
zB2$sLg7-F^n4HoyAv;l`W0xpq{)=G426mcY{7Cx(LHBS!3Q7ND(m#^)Ta*4=(r-`t
zr;>hK_Baxgd_KjG^nPlsIj64Mxq3-_ZTMZ>U31tN%q5pjST#*+4+&a4@6#M>5%i^|
zG=~+9n(25&_z;q_$1u>?dc1fvhk%|mZxzB}G}=v`a4SQ?)#DYDJ5*LeYpyw0abVhn
zd3tl{G^x{_jt})t*mj=hhb)lFCpZ5zkg^B^0|Q!96<K%MMAqfVx<O)E(q6ke{R-)Z
zThN+V!s(+NJK^-Ax!RwPx25?ZL36AkNzrEL8z96Ygd5_vxEqhV@pgSjt!aBAvu|El
z!pg?)32!F_enL{EkybMFRv}YsvWz`!q%uWvXeBvJ1Ib~E<d6g1nI+Dp#CaJ=R#}om
zDJ1>*ioQ1}J)id4j0RvTDZC?7Gu5w1WcIe>LxJ}vNTN-UXxNf{3lS7j7EELOas4*o
zO`6DljuE#eGF#)z(0`tv#0-JbFwnjQ;aXD8?}#B+bF84%7-3fXC*yXFJP-LAK3j1p
zgdq`A=g}TuD@aGO@9PEXORy6KBR7`)ThsLTeaIR6j!dicl|axgy3%UoY@`w-sW>*#
zXQUCYPw&`v@m#y9^{f@N2N(iBuoSPy$<gp7GU_8KHu0zWQ19L<3#g0J8_a^HR#`QN
zy+BHp`;r#TVa7R^8878BwL8w5q4zQ319i>u3-oN&gGFes)f_-}c$A*M90^1NIxI86
zAX!F@Buk>)Ia!16D?1J+j&LQwZXC;(>pnNyn<{l1-<m!1ap0-x!Pi6&!44a~VbmEM
zgE%G(j5*S|xclsv_r7+lp7%7P-;o>cDw<m>XLHz&Hue^B%q;c-@#B1D5$^=rc<^CW
z5tc_Ak2EYP!pboJ6-BY`M!i=`o(sl;9i3POu~b<C*zEDuxtpCK3GEzgP(-mY(xwEI
zZm2J5lmj7S)`DLQy!%PMk#p(Xizedrs7fTds|CCs-d*s7p-%QjBhiQ&2}qGWBK8dt
z64@J#$3!DyM2z7hRwvgfVn7ueqA@iP4I+09Vhu%WWl1ywZ)xP$d7BHnTs3W!<hFoB
z;$oGhQcP8Xog~lq3U;&welu9>B>LhnlEOFFe(Wnb^@Y)e^SwBFNWbyh<OuD1Lv*+N
ztmp}glH?PWHkv}c+@Rk0{>{mql?X&(Hv~XJD})CwgjxX+f}nzoR`(4!J5fu>A<}_b
z#phBsJORTOLSGEx7$=Cc82<3Ap`}0@@}kIBDPKlTl(MhiN1-bTL!nWC#tfq+;g16s
zIFU!}Ir-0G)Bm3+XYXT|v9Gcx*pJu@dy)Nt{e>N8ud;IW{_6hfH>*!qce`G64Z2Rc
uzISC^f4IzC1?S)%<W_RkTodQx0-VC_;SO?r+%c}7dz<^e%bJthlYap1#Gj)8
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -2110,26 +2110,30 @@ class EventManager {
   // about all primed listeners in the extension's persistentListeners Map.
   static primeListeners(extension) {
     EventManager._initPersistentListeners(extension);
 
     for (let [module, moduleEntry] of extension.persistentListeners) {
       let api = extension.apiManager.getAPI(module, extension, "addon_parent");
       for (let [event, eventEntry] of moduleEntry) {
         for (let listener of eventEntry.values()) {
-          let primed = {pendingEvents: []};
+          let primed = {pendingEvents: [], cleared: false};
           listener.primed = primed;
 
           let bgStartupPromise = new Promise(r => extension.once("startup", r));
           let wakeup = () => {
             extension.emit("background-page-event");
             return bgStartupPromise;
           };
 
           let fireEvent = (...args) => new Promise((resolve, reject) => {
+            if (primed.cleared) {
+              reject(new Error("listener not re-registered"));
+              return;
+            }
             primed.pendingEvents.push({args, resolve, reject});
             extension.emit("background-page-event");
           });
 
           let fire = {
             wakeup,
             sync: fireEvent,
             async: fireEvent,
@@ -2172,16 +2176,17 @@ class EventManager {
           for (let evt of primed.pendingEvents) {
             evt.reject(new Error("listener not re-registered"));
           }
 
           if (clearPersistent) {
             EventManager.clearPersistentListener(extension, module, event, key);
           }
           primed.unregister();
+          primed.cleared = true;
         }
       }
     }
   }
 
   // Record the fact that there is a listener for the given event in
   // the given extension.  `args` is an Array containing any extra
   // arguments that were passed to addListener().
--- a/toolkit/components/extensions/parent/ext-backgroundPage.js
+++ b/toolkit/components/extensions/parent/ext-backgroundPage.js
@@ -61,16 +61,23 @@ class BackgroundPage extends HiddenExten
 
     if (context) {
       // Wait until all event listeners registered by the script so far
       // to be handled.
       await Promise.all(context.listenerPromises);
       context.listenerPromises = null;
     }
 
+    if (extension.persistentListeners) {
+      // |this.extension| may be null if the extension was shut down.
+      // In that case, we still want to clear the primed listeners,
+      // but not update the persistent listeners in the startupData.
+      EventManager.clearPrimedListeners(extension, !!this.extension);
+    }
+
     extension.emit("startup");
   }
 
   shutdown() {
     this.extension._backgroundPageFrameLoader = null;
     super.shutdown();
   }
 }
@@ -108,20 +115,16 @@ this.backgroundPage = class extends Exte
 
     extension.once("start-background-page", async () => {
       if (!this.extension) {
         // Extension was shut down. Don't build the background page.
         // Primed listeners have been cleared in onShutdown.
         return;
       }
       await this.build();
-      // |this.extension| may be null if the extension was shut down.
-      // In that case, we still want to clear the primed listeners,
-      // but not update the persistent listeners in the startupData.
-      EventManager.clearPrimedListeners(extension, !!this.extension);
     });
 
     // There are two ways to start the background page:
     // 1. If a primed event fires, then start the background page as
     //    soon as we have painted a browser window.  Note that we have
     //    to touch browserPaintedPromise here to initialize the listener
     //    or else we can miss it if the event occurs after the first
     //    window is painted but before #2
--- a/toolkit/components/extensions/parent/ext-webRequest.js
+++ b/toolkit/components/extensions/parent/ext-webRequest.js
@@ -21,16 +21,20 @@ function registerEvent(extension, eventN
     if (filter.windowId != null && browserData.windowId != filter.windowId) {
       return;
     }
 
     let event = data.serialize(eventName);
     event.tabId = browserData.tabId;
 
     if (data.registerTraceableChannel) {
+      // If this is a primed listener, no tabParent was passed in here,
+      // but the convert() callback later in this function will be called
+      // when the background page is started.  Force that to happen here
+      // after which we'll have a valid tabParent.
       if (fire.wakeup) {
         await fire.wakeup();
       }
       data.registerTraceableChannel(extension.policy, tabParent);
     }
 
     return fire.sync(event);
   };
--- a/toolkit/components/extensions/test/xpcshell/test_ext_persistent_events.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_persistent_events.js
@@ -27,70 +27,72 @@ const SCHEMA = [
 // serialized into a string which is later loaded by the WebExtensions
 // framework in the same context as other extension APIs.  By writing it
 // this way rather than as a big string constant we get lint coverage.
 // But eslint doesn't understand that this code runs in a different context
 // where the EventManager class is available so just tell it here:
 /* global EventManager */
 const API = class extends ExtensionAPI {
   primeListener(extension, event, fire, params) {
-    let data = {wrappedJSObject: {event, params}};
-    Services.obs.notifyObservers(data, "prime-event-listener");
+    Services.obs.notifyObservers({event, params}, "prime-event-listener");
 
     const FIRE_TOPIC = `fire-${event}`;
 
-    async function listener(subject, topic, _data) {
+    async function listener(subject, topic, data) {
       try {
-        await fire.async(subject.wrappedJSObject);
+        if (subject.wrappedJSObject.waitForBackground) {
+          await fire.wakeup();
+        }
+        await fire.async(subject.wrappedJSObject.listenerArgs);
       } catch (err) {
-        Services.obs.notifyObservers(data, "listener-callback-exception");
+        Services.obs.notifyObservers({event}, "listener-callback-exception");
       }
     }
     Services.obs.addObserver(listener, FIRE_TOPIC);
 
     return {
       unregister() {
-        Services.obs.notifyObservers(data, "unregister-primed-listener");
+        Services.obs.notifyObservers({event, params}, "unregister-primed-listener");
         Services.obs.removeObserver(listener, FIRE_TOPIC);
       },
       convert(_fire) {
-        Services.obs.notifyObservers(data, "convert-event-listener");
+        Services.obs.notifyObservers({event, params}, "convert-event-listener");
         fire = _fire;
       },
     };
   }
 
   getAPI(context) {
     return {
       eventtest: {
         onEvent1: new EventManager({
           context,
           name: "test.event1",
           persistent: {
             module: "eventtest",
             event: "onEvent1",
           },
           register: (fire, ...params) => {
-            let data = {wrappedJSObject: {event: "onEvent1", params}};
+            let data = {event: "onEvent1", params};
             Services.obs.notifyObservers(data, "register-event-listener");
             return () => {
               Services.obs.notifyObservers(data, "unregister-event-listener");
             };
           },
         }).api(),
 
         onEvent2: new EventManager({
           context,
           name: "test.event1",
           persistent: {
             module: "eventtest",
             event: "onEvent2",
           },
           register: (fire, ...params) => {
-            let data = {wrappedJSObject: {event: "onEvent2", params}};
+            let data = {event: "onEvent2", params};
             Services.obs.notifyObservers(data, "register-event-listener");
             return () => {
               Services.obs.notifyObservers(data, "unregister-event-listener");
             };
           },
         }).api(),
       },
     };
@@ -247,24 +249,23 @@ add_task(async function() {
   info = await p;
 
   check(info, "convert");
 
   await extension.awaitMessage("ready");
 
   // Check that when the event is triggered, all the plumbing worked
   // correctly for the primed-then-converted listener.
-  let eventDetails = {test: "kaboom"};
-  let eventSubject = {wrappedJSObject: eventDetails};
-  Services.obs.notifyObservers(eventSubject, "fire-onEvent1");
+  let listenerArgs = {test: "kaboom"};
+  Services.obs.notifyObservers({listenerArgs}, "fire-onEvent1");
 
   let details = await extension.awaitMessage("listener1");
-  deepEqual(details, eventDetails, "Listener 1 fired");
+  deepEqual(details, listenerArgs, "Listener 1 fired");
   details = await extension.awaitMessage("listener2");
-  deepEqual(details, eventDetails, "Listener 2 fired");
+  deepEqual(details, listenerArgs, "Listener 2 fired");
 
   // Check that the converted listener is properly unregistered at
   // browser shutdown.
   [info] = await Promise.all([
     promiseObservable("unregister-primed-listener", 3),
     AddonTestUtils.promiseShutdownManager(),
   ]);
   check(info, "unregister");
@@ -275,24 +276,24 @@ add_task(async function() {
     AddonTestUtils.promiseStartupManager(),
   ]);
   check(info, "prime");
 
   // Check that triggering the event before the listener has been converted
   // causes the background page to be loaded and the listener to be converted,
   // and the listener is invoked.
   p = promiseObservable("convert-event-listener", 3);
-  eventDetails.test = "startup event";
-  Services.obs.notifyObservers(eventSubject, "fire-onEvent2");
+  listenerArgs.test = "startup event";
+  Services.obs.notifyObservers({listenerArgs}, "fire-onEvent2");
   info = await p;
 
   check(info, "convert");
 
   details = await extension.awaitMessage("listener3");
-  deepEqual(details, eventDetails, "Listener 3 fired for event during startup");
+  deepEqual(details, listenerArgs, "Listener 3 fired for event during startup");
 
   await extension.awaitMessage("ready");
 
   // Check that the unregister process works when we manually remove
   // a listener.
   p = promiseObservable("unregister-primed-listener", 1);
   extension.sendMessage("unregister2");
   info = await p;
@@ -315,20 +316,20 @@ add_task(async function() {
   // starts up.
   p = promiseObservable("unregister-primed-listener", 1,
                         () => extension.awaitMessage("ready"));
   Services.obs.notifyObservers(null, "sessionstore-windows-restored");
   info = await p;
   check(info, "unregister", {listener1: false, listener3: false});
 
   // Just listener1 should be registered now, fire event1 to confirm.
-  eventDetails.test = "third time";
-  Services.obs.notifyObservers(eventSubject, "fire-onEvent1");
+  listenerArgs.test = "third time";
+  Services.obs.notifyObservers({listenerArgs}, "fire-onEvent1");
   details = await extension.awaitMessage("listener1");
-  deepEqual(details, eventDetails, "Listener 1 fired");
+  deepEqual(details, listenerArgs, "Listener 1 fired");
 
   // Tell the extension not to re-register listener1 on the next startup
   extension.sendMessage("unregister1");
   await extension.awaitMessage("unregistered");
 
   // Shut down, start up
   info = await promiseObservable("unregister-primed-listener", 1,
                                  () => AddonTestUtils.promiseShutdownManager());
@@ -336,17 +337,17 @@ add_task(async function() {
 
   info = await promiseObservable("prime-event-listener", 1,
                                  () => AddonTestUtils.promiseStartupManager());
   check(info, "register", {listener2: false, listener3: false});
 
   // Check that firing event1 causes the listener fire callback to
   // reject.
   p = promiseObservable("listener-callback-exception", 1);
-  Services.obs.notifyObservers(eventSubject, "fire-onEvent1");
+  Services.obs.notifyObservers({listenerArgs, waitForBackground: true}, "fire-onEvent1");
   await p;
   ok(true, "Primed listener that was not re-registered received an error when event was triggered during startup");
 
   await extension.awaitMessage("ready");
 
   await extension.unload();
 
   await AddonTestUtils.promiseShutdownManager();
--- a/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_startup.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_startup.js
@@ -118,17 +118,17 @@ add_task(async function test_2() {
 
   Services.obs.notifyObservers(null, "sessionstore-windows-restored");
   await extension.awaitMessage("ready");
 
   await extension.unload();
   await promiseShutdownManager();
 });
 
-// Test that a block listener that uses filterResponseData() works
+// Test that a blocking listener that uses filterResponseData() works
 // properly (i.e., that the delayed call to registerTraceableChannel
 // works properly).
 add_task(async function test_3() {
   const DATA = `<!DOCTYPE html>
 <html>
 <body>
   <h1>This is a modified page</h1>
 </body>
--- a/toolkit/content/aboutProfiles.js
+++ b/toolkit/content/aboutProfiles.js
@@ -12,17 +12,17 @@ XPCOMUtils.defineLazyServiceGetter(
   "ProfileService",
   "@mozilla.org/toolkit/profile-service;1",
   "nsIToolkitProfileService"
 );
 
 async function flush() {
   try {
     ProfileService.flush();
-    refreshUI();
+    rebuildProfileList();
   } catch (e) {
     let [title, msg, button] = await document.l10n.formatValues([
       { id: "profiles-flush-fail-title" },
       { id: e.result == Cr.NS_ERROR_DATABASE_CHANGED ?
                         "profiles-flush-conflict" :
                         "profiles-flush-failed" },
       { id: "profiles-flush-restart-button" },
     ]);
@@ -33,17 +33,17 @@ async function flush() {
                                           (PS.BUTTON_POS_1 * PS.BUTTON_TITLE_IS_STRING),
                                           null, button, null, null, {});
     if (result == 1) {
       restart(false);
     }
   }
 }
 
-function refreshUI() {
+function rebuildProfileList() {
   let parent = document.getElementById("profiles");
   while (parent.firstChild) {
     parent.firstChild.remove();
   }
 
   let defaultProfile;
   try {
     defaultProfile = ProfileService.defaultProfile;
@@ -67,29 +67,16 @@ function refreshUI() {
     }
     display({
       profile,
       isDefault: profile == defaultProfile,
       isCurrentProfile,
       isInUse,
     });
   }
-
-  let createButton = document.getElementById("create-button");
-  createButton.onclick = createProfileWizard;
-
-  let restartSafeModeButton = document.getElementById("restart-in-safe-mode-button");
-  if (!Services.policies || Services.policies.isAllowed("safeMode")) {
-    restartSafeModeButton.onclick = function() { restart(true); };
-  } else {
-    restartSafeModeButton.setAttribute("disabled", "true");
-  }
-
-  let restartNormalModeButton = document.getElementById("restart-button");
-  restartNormalModeButton.onclick = function() { restart(false); };
 }
 
 function display(profileData) {
   let parent = document.getElementById("profiles");
 
   let div = document.createElement("div");
   parent.appendChild(div);
 
@@ -347,15 +334,28 @@ function restart(safeMode) {
   if (safeMode) {
     Services.startup.restartInSafeMode(flags);
   } else {
     Services.startup.quit(flags);
   }
 }
 
 window.addEventListener("DOMContentLoaded", function() {
+  let createButton = document.getElementById("create-button");
+  createButton.addEventListener("click", createProfileWizard);
+
+  let restartSafeModeButton = document.getElementById("restart-in-safe-mode-button");
+  if (!Services.policies || Services.policies.isAllowed("safeMode")) {
+    restartSafeModeButton.addEventListener("click", () => { restart(true); });
+  } else {
+    restartSafeModeButton.setAttribute("disabled", "true");
+  }
+
+  let restartNormalModeButton = document.getElementById("restart-button");
+  restartNormalModeButton.addEventListener("click", () => { restart(false); });
+
   if (ProfileService.isListOutdated) {
     document.getElementById("owned").hidden = true;
   } else {
     document.getElementById("conflict").hidden = true;
-    refreshUI();
+    rebuildProfileList();
   }
 }, {once: true});