Merge mozilla-central to autoland. a=merge CLOSED TREE
authorshindli <shindli@mozilla.com>
Fri, 01 Feb 2019 06:04:59 +0200
changeset 456388 9402c046690e11ebecc172778a24ec808ddbfb28
parent 456387 d6cd114bf585f724c94c3b97959785230911cd51 (current diff)
parent 456380 3bf0b7e7e89a005cbe843681384d49227f83a287 (diff)
child 456389 b325e24cae399094ec4f704a64905014b386bfae
push id19
push usermdeboer@mozilla.com
push dateFri, 01 Feb 2019 10:05:45 +0000
reviewersmerge
milestone67.0a1
Merge mozilla-central to autoland. a=merge CLOSED TREE
gfx/tests/reftest/1523776-ref.html
gfx/tests/reftest/1523776.html
--- a/devtools/client/debugger/new/src/client/firefox/commands.js
+++ b/devtools/client/debugger/new/src/client/firefox/commands.js
@@ -182,40 +182,31 @@ function getBreakpointByLocation(locatio
 function setXHRBreakpoint(path: string, method: string) {
   return threadClient.setXHRBreakpoint(path, method);
 }
 
 function removeXHRBreakpoint(path: string, method: string) {
   return threadClient.removeXHRBreakpoint(path, method);
 }
 
-// Source and breakpoint clients do not yet support an options structure, so
-// for now we transform options into condition strings when setting breakpoints.
-function transformOptionsToCondition(options) {
-  if (options.logValue) {
-    return `console.log(${options.logValue})`;
-  }
-  return options.condition;
-}
-
 function setBreakpoint(
   location: SourceActorLocation,
   options: BreakpointOptions,
   noSliding: boolean
 ): Promise<BreakpointResult> {
   const sourceThreadClient = lookupThreadClient(location.sourceActor.thread);
   const sourceClient = sourceThreadClient.source({
     actor: location.sourceActor.actor
   });
 
   return sourceClient
     .setBreakpoint({
       line: location.line,
       column: location.column,
-      condition: transformOptionsToCondition(options),
+      options,
       noSliding
     })
     .then(([{ actualLocation }, bpClient]) => {
       actualLocation = createBreakpointLocation(location, actualLocation);
 
       const id = makeBreakpointActorId(actualLocation);
       bpClients[id] = bpClient;
       bpClient.location.line = actualLocation.line;
@@ -243,24 +234,28 @@ function removeBreakpoint(
 }
 
 function setBreakpointOptions(
   location: SourceActorLocation,
   options: BreakpointOptions
 ) {
   const id = makeBreakpointActorId(location);
   const bpClient = bpClients[id];
-  delete bpClients[id];
 
-  const sourceThreadClient = bpClient.source._activeThread;
-  return bpClient
-    .setCondition(sourceThreadClient, transformOptionsToCondition(options))
-    .then(_bpClient => {
-      bpClients[id] = _bpClient;
-    });
+  if (debuggerClient.mainRoot.traits.nativeLogpoints) {
+    bpClient.setOptions(options);
+  } else {
+    // Older server breakpoints destroy themselves when changing options.
+    delete bpClients[id];
+    bpClient
+      .setOptions(options)
+      .then(_bpClient => {
+        bpClients[id] = _bpClient;
+      });
+  }
 }
 
 async function evaluateInFrame(script: Script, options: EvaluateParam) {
   return evaluate(script, options);
 }
 
 async function evaluateExpressions(scripts: Script[], options: EvaluateParam) {
   return Promise.all(scripts.map(script => evaluate(script, options)));
--- a/devtools/client/debugger/new/src/client/firefox/types.js
+++ b/devtools/client/debugger/new/src/client/firefox/types.js
@@ -393,17 +393,17 @@ export type BreakpointClient = {
   actor: ActorId,
   remove: () => void,
   location: {
     actor: string,
     url: string,
     line: number,
     column: ?number
   },
-  setCondition: (ThreadClient, ?string) => Promise<BreakpointClient>,
+  setOptions: (BreakpointOptions) => Promise<BreakpointClient>,
   // request: any,
   source: SourceClient,
   options: BreakpointOptions
 };
 
 export type BPClients = { [id: ActorId]: BreakpointClient };
 
 export type BreakpointResponse = [
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-cond.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-cond.js
@@ -67,39 +67,41 @@ add_task(async function() {
   await setConditionalBreakpoint(dbg, 5, "1");
   await waitForDispatch(dbg, "ADD_BREAKPOINT");
 
   let bp = findBreakpoint(dbg, "simple2", 5);
   is(bp.options.condition, "1", "breakpoint is created with the condition");
   assertEditorBreakpoint(dbg, 5, true);
 
   // Edit the conditional breakpoint set above
+  const bpCondition1 = waitForDispatch(dbg, "SET_BREAKPOINT_OPTIONS");
   await setConditionalBreakpoint(dbg, 5, "2");
-  await waitForDispatch(dbg, "SET_BREAKPOINT_OPTIONS");
+  await bpCondition1;
   bp = findBreakpoint(dbg, "simple2", 5);
   is(bp.options.condition, "12", "breakpoint is created with the condition");
   assertEditorBreakpoint(dbg, 5, true);
 
   clickElement(dbg, "gutter", 5);
   await waitForDispatch(dbg, "REMOVE_BREAKPOINT");
   bp = findBreakpoint(dbg, "simple2", 5);
   is(bp, null, "breakpoint was removed");
   assertEditorBreakpoint(dbg, 5, false);
 
   // Adding a condition to a breakpoint
   clickElement(dbg, "gutter", 5);
   await waitForDispatch(dbg, "ADD_BREAKPOINT");
+  const bpCondition2 = waitForDispatch(dbg, "SET_BREAKPOINT_OPTIONS");
   await setConditionalBreakpoint(dbg, 5, "1");
-  await waitForDispatch(dbg, "SET_BREAKPOINT_OPTIONS");
+  await bpCondition2;
 
   bp = findBreakpoint(dbg, "simple2", 5);
   is(bp.options.condition, "1", "breakpoint is created with the condition");
   assertEditorBreakpoint(dbg, 5, true);
 
-  const bpCondition = waitForDispatch(dbg, "SET_BREAKPOINT_OPTIONS");
+  const bpCondition3 = waitForDispatch(dbg, "SET_BREAKPOINT_OPTIONS");
   //right click breakpoint in breakpoints list
   rightClickElement(dbg, "breakpointItem", 3)
   // select "remove condition";
   selectContextMenuItem(dbg, selectors.breakpointContextMenu.removeCondition);
-  await bpCondition;
+  await bpCondition3;
   bp = findBreakpoint(dbg, "simple2", 5);
   is(bp.options.condition, undefined, "breakpoint condition removed");
 });
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -463,16 +463,22 @@ Toolbox.prototype = {
           "NetworkActivity",
         ]);
       }
 
       // Attach the thread
       this._threadClient = await attachThread(this);
       await domReady;
 
+      // The web console is immediately loaded when replaying, so that the
+      // timeline will always be populated with generated messages.
+      if (this.target.isReplayEnabled()) {
+        await this.loadTool("webconsole");
+      }
+
       this.isReady = true;
 
       const framesPromise = this._listFrames();
 
       Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings);
       Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled",
                                  this._applyServiceWorkersTestingSettings);
 
--- a/devtools/client/webconsole/webconsole-connection-proxy.js
+++ b/devtools/client/webconsole/webconsole-connection-proxy.js
@@ -25,16 +25,17 @@ const PREF_CONNECTION_TIMEOUT = "devtool
 function WebConsoleConnectionProxy(webConsoleFrame, target) {
   this.webConsoleFrame = webConsoleFrame;
   this.target = target;
   this.webConsoleClient = target.activeConsole;
 
   this._onPageError = this._onPageError.bind(this);
   this._onLogMessage = this._onLogMessage.bind(this);
   this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
+  this._onVirtualConsoleLog = this._onVirtualConsoleLog.bind(this);
   this._onNetworkEvent = this._onNetworkEvent.bind(this);
   this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
   this._onTabNavigated = this._onTabNavigated.bind(this);
   this._onTabWillNavigate = this._onTabWillNavigate.bind(this);
   this._onAttachConsole = this._onAttachConsole.bind(this);
   this._onCachedMessages = this._onCachedMessages.bind(this);
   this._connectionTimeout = this._connectionTimeout.bind(this);
   this._onLastPrivateContextExited =
@@ -122,16 +123,18 @@ WebConsoleConnectionProxy.prototype = {
 
     const client = this.client = this.target.client;
 
     client.addListener("logMessage", this._onLogMessage);
     client.addListener("pageError", this._onPageError);
     client.addListener("consoleAPICall", this._onConsoleAPICall);
     client.addListener("lastPrivateContextExited",
                        this._onLastPrivateContextExited);
+    client.addListener("virtualConsoleLog",
+                       this._onVirtualConsoleLog);
 
     this.target.on("will-navigate", this._onTabWillNavigate);
     this.target.on("navigate", this._onTabNavigated);
 
     if (this.target.isBrowsingContext) {
       this.webConsoleFrame.onLocationChange(this.target.url, this.target.title);
     }
     this.isAttached = this._attachConsole();
@@ -305,16 +308,30 @@ WebConsoleConnectionProxy.prototype = {
    *        The message received from the server.
    */
   _onConsoleAPICall: function(type, packet) {
     if (!this.webConsoleFrame || packet.from != this.webConsoleClient.actor) {
       return;
     }
     this.dispatchMessageAdd(packet);
   },
+
+  _onVirtualConsoleLog: function(type, packet) {
+    if (!this.webConsoleFrame) {
+      return;
+    }
+    this.dispatchMessageAdd({
+      type: "consoleAPICall",
+      message: {
+        executionPoint: packet.executionPoint,
+        "arguments": [packet.url + ":" + packet.line, packet.message],
+      },
+    });
+  },
+
   /**
    * The "networkEvent" message type handler. We redirect any message to
    * the UI for displaying.
    *
    * @private
    * @param object networkInfo
    *        The network request information.
    */
@@ -418,16 +435,18 @@ WebConsoleConnectionProxy.prototype = {
       return this._disconnecter.promise;
     }
 
     this.client.removeListener("logMessage", this._onLogMessage);
     this.client.removeListener("pageError", this._onPageError);
     this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
     this.client.removeListener("lastPrivateContextExited",
                                this._onLastPrivateContextExited);
+    this.client.removeListener("virtualConsoleLog",
+                               this._onVirtualConsoleLog);
     this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
     this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
     this.target.off("will-navigate", this._onTabWillNavigate);
     this.target.off("navigate", this._onTabNavigated);
 
     this.client = null;
     this.webConsoleClient = null;
     this.target = null;
--- a/devtools/server/actors/breakpoint.js
+++ b/devtools/server/actors/breakpoint.js
@@ -17,20 +17,17 @@ const { breakpointSpec } = require("devt
  *
  * @param BreakpointActor actor
  *        The actor handling the breakpoint hits.
  * @param Array entryPoints
  *        An array of objects of the form `{ script, offsets }`.
  */
 function setBreakpointAtEntryPoints(actor, entryPoints) {
   for (const { script, offsets } of entryPoints) {
-    actor.addScript(script);
-    for (const offset of offsets) {
-      script.setBreakpoint(offset, actor);
-    }
+    actor.addScript(script, offsets);
   }
 }
 
 exports.setBreakpointAtEntryPoints = setBreakpointAtEntryPoints;
 
 /**
  * BreakpointActors exist for the lifetime of their containing thread and are
  * responsible for deleting breakpoints, handling breakpoint hits and
@@ -41,72 +38,117 @@ const BreakpointActor = ActorClassWithSp
    * Create a Breakpoint actor.
    *
    * @param ThreadActor threadActor
    *        The parent thread actor that contains this breakpoint.
    * @param GeneratedLocation generatedLocation
    *        The generated location of the breakpoint.
    */
   initialize: function(threadActor, generatedLocation) {
-    // The set of Debugger.Script instances that this breakpoint has been set
-    // upon.
-    this.scripts = new Set();
+    // A map from Debugger.Script instances to the offsets which the breakpoint
+    // has been set for in that script.
+    this.scripts = new Map();
 
     this.threadActor = threadActor;
     this.generatedLocation = generatedLocation;
-    this.condition = null;
+    this.options = null;
     this.isPending = true;
   },
 
+  // Called when new breakpoint options are received from the client.
+  setOptions(options) {
+    for (const [script, offsets] of this.scripts) {
+      this._updateOptionsForScript(script, offsets, this.options, options);
+    }
+
+    this.options = options;
+  },
+
   destroy: function() {
     this.removeScripts();
   },
 
   hasScript: function(script) {
     return this.scripts.has(script);
   },
 
   /**
    * Called when this same breakpoint is added to another Debugger.Script
    * instance.
    *
    * @param script Debugger.Script
    *        The new source script on which the breakpoint has been set.
+   * @param offsets Array
+   *        Any offsets in the script the breakpoint is associated with.
    */
-  addScript: function(script) {
-    this.scripts.add(script);
+  addScript: function(script, offsets) {
+    this.scripts.set(script, offsets.concat(this.scripts.get(offsets) || []));
+    for (const offset of offsets) {
+      script.setBreakpoint(offset, this);
+    }
+
     this.isPending = false;
+    this._updateOptionsForScript(script, offsets, null, this.options);
   },
 
   /**
    * Remove the breakpoints from associated scripts and clear the script cache.
    */
   removeScripts: function() {
-    for (const script of this.scripts) {
+    for (const [script, offsets] of this.scripts) {
+      this._updateOptionsForScript(script, offsets, this.options, null);
       script.clearBreakpoint(this);
     }
     this.scripts.clear();
   },
 
+  // Update any state affected by changing options on a script this breakpoint
+  // is associated with.
+  _updateOptionsForScript(script, offsets, oldOptions, newOptions) {
+    if (this.threadActor.dbg.replaying) {
+      // When replaying, logging breakpoints are handled using an API to get logged
+      // messages from throughout the recording.
+      const oldLogValue = oldOptions && oldOptions.logValue;
+      const newLogValue = newOptions && newOptions.logValue;
+      if (oldLogValue != newLogValue) {
+        for (const offset of offsets) {
+          const { lineNumber, columnNumber } = script.getOffsetLocation(offset);
+          script.replayVirtualConsoleLog(offset, newLogValue, (point, rv) => {
+            const packet = {
+              from: this.actorID,
+              type: "virtualConsoleLog",
+              url: script.url,
+              line: lineNumber,
+              column: columnNumber,
+              executionPoint: point,
+              message: "return" in rv ? "" + rv.return : "" + rv.throw,
+            };
+            this.conn.send(packet);
+          });
+        }
+      }
+    }
+  },
+
   /**
    * Check if this breakpoint has a condition that doesn't error and
    * evaluates to true in frame.
    *
    * @param frame Debugger.Frame
    *        The frame to evaluate the condition in
    * @returns Object
    *          - result: boolean|undefined
    *            True when the conditional breakpoint should trigger a pause,
    *            false otherwise. If the condition evaluation failed/killed,
    *            `result` will be `undefined`.
    *          - message: string
    *            If the condition throws, this is the thrown message.
    */
-  checkCondition: function(frame) {
-    const completion = frame.eval(this.condition);
+  checkCondition: function(frame, condition) {
+    const completion = frame.eval(condition);
     if (completion) {
       if (completion.throw) {
         // The evaluation failed and threw
         let message = "Unknown exception";
         try {
           if (completion.throw.getOwnPropertyDescriptor) {
             message = completion.throw.getOwnPropertyDescriptor("message")
                       .value;
@@ -157,25 +199,39 @@ const BreakpointActor = ActorClassWithSp
     const locationAtFinish = frame.onPop && frame.onPop.generatedLocation;
     if (locationAtFinish &&
         locationAtFinish.generatedLine === generatedLine &&
         locationAtFinish.generatedColumn === generatedColumn) {
       return undefined;
     }
 
     const reason = {};
+    const { condition, logValue } = this.options || {};
 
     if (this.threadActor._hiddenBreakpoints.has(this.actorID)) {
       reason.type = "pauseOnDOMEvents";
-    } else if (!this.condition) {
+    } else if (!condition && !logValue) {
       reason.type = "breakpoint";
       // TODO: add the rest of the breakpoints on that line (bug 676602).
       reason.actors = [ this.actorID ];
     } else {
-      const { result, message } = this.checkCondition(frame);
+      // When replaying, breakpoints with log values are handled separately.
+      if (logValue && this.threadActor.dbg.replaying) {
+        return undefined;
+      }
+
+      let condstr = condition;
+      if (logValue) {
+        // In the non-replaying case, log values are handled by treating them as
+        // conditions. console.log() never returns true so we will not pause.
+        condstr = condition
+          ? `(${condition}) && console.log(${logValue})`
+          : `console.log(${logValue})`;
+      }
+      const { result, message } = this.checkCondition(frame, condstr);
 
       if (result) {
         if (!message) {
           reason.type = "breakpoint";
         } else {
           reason.type = "breakpointConditionThrown";
           reason.message = message;
         }
--- a/devtools/server/actors/replay/control.js
+++ b/devtools/server/actors/replay/control.js
@@ -686,16 +686,19 @@ function flushRecording() {
 
   for (const child of gChildren) {
     if (child && !child.recording) {
       child.pauseNeeded = false;
       child.role.poke();
     }
   }
 
+  // After flushing the recording there may be more search results.
+  maybeResumeSearch();
+
   gLastRecordingCheckpoint = gActiveChild.lastCheckpoint();
 
   // We now have a usable recording for replaying children.
   if (!gFirstReplayingChild) {
     spawnInitialReplayingChildren();
   }
 }
 
@@ -983,26 +986,102 @@ const gControl = {
   clearBreakpoints() { gActiveChild.sendClearBreakpoints(); },
   sendRequest(request) { return gActiveChild.sendDebuggerRequest(request); },
   markExplicitPause,
   maybeSwitchToReplayingChild,
   resume,
   timeWarp,
 };
 
+////////////////////////////////////////////////////////////////////////////////
+// Search Operations
+////////////////////////////////////////////////////////////////////////////////
+
+let gSearchChild;
+let gSearchRestartNeeded;
+
+function maybeRestartSearch() {
+  if (gSearchRestartNeeded && gSearchChild.paused) {
+    if (gSearchChild.lastPausePoint.checkpoint != FirstCheckpointId ||
+        gSearchChild.lastPausePoint.position) {
+      gSearchChild.sendRestoreCheckpoint(FirstCheckpointId);
+      gSearchChild.waitUntilPaused();
+    }
+    gSearchChild.sendClearBreakpoints();
+    gDebugger._forEachSearch(pos => gSearchChild.sendAddBreakpoint(pos));
+    gSearchRestartNeeded = false;
+    gSearchChild.sendResume({ forward: true });
+    return true;
+  }
+  return false;
+}
+
+function ChildRoleSearch() {}
+
+ChildRoleSearch.prototype = {
+  name: "Search",
+
+  initialize(child, { startup }) {
+    this.child = child;
+  },
+
+  hitExecutionPoint({ point, recordingEndpoint }) {
+    if (maybeRestartSearch()) {
+      return;
+    }
+
+    if (point.position) {
+      gDebugger._onSearchPause(point);
+    }
+
+    if (!recordingEndpoint) {
+      this.poke();
+    }
+  },
+
+  poke() {
+    if (!gSearchRestartNeeded && !this.child.pauseNeeded) {
+      this.child.sendResume({ forward: true });
+    }
+  },
+};
+
+function ensureHasSearchChild() {
+  if (!gSearchChild) {
+    gSearchChild = spawnReplayingChild(new ChildRoleSearch());
+  }
+}
+
+function maybeResumeSearch() {
+  if (gSearchChild && gSearchChild.paused) {
+    gSearchChild.sendResume({ forward: true });
+  }
+}
+
+const gSearchControl = {
+  reset() {
+    ensureHasSearchChild();
+    gSearchRestartNeeded = true;
+    maybeRestartSearch();
+  },
+
+  sendRequest(request) { return gSearchChild.sendDebuggerRequest(request); },
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// Utilities
+///////////////////////////////////////////////////////////////////////////////
+
 // eslint-disable-next-line no-unused-vars
 function ConnectDebugger(dbg) {
   gDebugger = dbg;
   dbg._control = gControl;
+  dbg._searchControl = gSearchControl;
 }
 
-///////////////////////////////////////////////////////////////////////////////
-// Utilities
-///////////////////////////////////////////////////////////////////////////////
-
 function dumpv(str) {
   //dump("[ReplayControl] " + str + "\n");
 }
 
 function assert(v) {
   if (!v) {
     ThrowError("Assertion Failed!");
   }
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -37,16 +37,17 @@ function ReplayDebugger() {
   if (existing) {
     // There is already a ReplayDebugger in existence, use that. There can only
     // be one ReplayDebugger in the process.
     return existing;
   }
 
   // We should have been connected to control.js by the call above.
   assert(this._control);
+  assert(this._searchControl);
 
   // Preferred direction of travel when not explicitly resumed.
   this._direction = Direction.NONE;
 
   // All breakpoint positions and handlers installed by this debugger.
   this._breakpoints = [];
 
   // All ReplayDebuggerFramees that have been created while paused at the
@@ -70,16 +71,19 @@ function ReplayDebugger() {
 
   // Flag set if the dispatched _performPause() call can be ignored because the
   // server entered a thread-wide pause first.
   this._cancelPerformPause = false;
 
   // After we are done pausing, callback describing how to resume.
   this._resumeCallback = null;
 
+  // Information about all searches that exist.
+  this._searches = [];
+
   // Handler called when hitting the beginning/end of the recording, or when
   // a time warp target has been reached.
   this.replayingOnForcedPause = null;
 
   // Handler called when the child pauses for any reason.
   this.replayingOnPositionChange = null;
 }
 
@@ -344,16 +348,57 @@ ReplayDebugger.prototype = {
     this._frames.forEach(frame => frame._invalidate());
     this._frames.length = 0;
 
     this._objects.forEach(obj => obj._invalidate());
     this._objects.length = 0;
   },
 
   /////////////////////////////////////////////////////////
+  // Search management
+  /////////////////////////////////////////////////////////
+
+  _forEachSearch(callback) {
+    for (const { position } of this._searches) {
+      callback(position);
+    }
+  },
+
+  _virtualConsoleLog(position, text, callback) {
+    this._searches.push({ position, text, callback, results: [] });
+    this._searchControl.reset();
+  },
+
+  _onSearchPause(point) {
+    for (const { position, text, callback, results } of this._searches) {
+      if (RecordReplayControl.positionSubsumes(position, point.position)) {
+        if (!results.some(existing => point.progress == existing.progress)) {
+          let evaluateResult;
+          if (text) {
+            const frameData = this._searchControl.sendRequest({
+              type: "getFrame",
+              index: NewestFrameIndex,
+            });
+            if ("index" in frameData) {
+              const rv = this._searchControl.sendRequest({
+                type: "frameEvaluate",
+                index: frameData.index,
+                text,
+              });
+              evaluateResult = this._convertCompletionValue(rv, { forSearch: true });
+            }
+          }
+          results.push(point);
+          callback(point, evaluateResult);
+        }
+      }
+    }
+  },
+
+  /////////////////////////////////////////////////////////
   // Breakpoint management
   /////////////////////////////////////////////////////////
 
   _setBreakpoint(handler, position, data) {
     this._ensurePaused();
     dumpv("AddBreakpoint " + JSON.stringify(position));
     this._control.addBreakpoint(position);
     this._breakpoints.push({handler, position, data});
@@ -480,63 +525,69 @@ ReplayDebugger.prototype = {
     const data = this._sendRequest({ type: "findSources" });
     return data.map(source => this._addSource(source));
   },
 
   /////////////////////////////////////////////////////////
   // Object methods
   /////////////////////////////////////////////////////////
 
-  // Objects which |forConsole| is set are objects that were logged in console
-  // messages, and had their properties recorded so that they can be inspected
-  // without switching to a replaying child.
-  _getObject(id, forConsole) {
+  _getObject(id, options) {
+    if (options && options.forSearch) {
+      // Returning objects through searches is NYI.
+      return "<UnknownSearchObject>";
+    }
+    const forConsole = options && options.forConsole;
+
     if (id && !this._objects[id]) {
       const data = this._sendRequest({ type: "getObject", id });
       switch (data.kind) {
       case "Object":
+        // Objects which |forConsole| is set are objects that were logged in
+        // console messages, and had their properties recorded so that they can
+        // be inspected without switching to a replaying child.
         this._objects[id] = new ReplayDebuggerObject(this, data, forConsole);
         break;
       case "Environment":
         this._objects[id] = new ReplayDebuggerEnvironment(this, data);
         break;
       default:
         ThrowError("Unknown object kind");
       }
     }
     const rv = this._objects[id];
     if (forConsole) {
       rv._forConsole = true;
     }
     return rv;
   },
 
-  _convertValue(value, forConsole) {
+  _convertValue(value, options) {
     if (isNonNullObject(value)) {
       if (value.object) {
-        return this._getObject(value.object, forConsole);
+        return this._getObject(value.object, options);
       } else if (value.special == "undefined") {
         return undefined;
       } else if (value.special == "NaN") {
         return NaN;
       } else if (value.special == "Infinity") {
         return Infinity;
       } else if (value.special == "-Infinity") {
         return -Infinity;
       }
     }
     return value;
   },
 
-  _convertCompletionValue(value) {
+  _convertCompletionValue(value, options) {
     if ("return" in value) {
-      return { return: this._convertValue(value.return) };
+      return { return: this._convertValue(value.return, options) };
     }
     if ("throw" in value) {
-      return { throw: this._convertValue(value.throw) };
+      return { throw: this._convertValue(value.throw, options) };
     }
     ThrowError("Unexpected completion value");
     return null; // For eslint
   },
 
   /////////////////////////////////////////////////////////
   // Frame methods
   /////////////////////////////////////////////////////////
@@ -577,17 +628,17 @@ ReplayDebugger.prototype = {
   /////////////////////////////////////////////////////////
 
   _convertConsoleMessage(message) {
     // Console API message arguments need conversion to debuggee values, but
     // other contents of the message can be left alone.
     if (message.messageType == "ConsoleAPI" && message.arguments) {
       for (let i = 0; i < message.arguments.length; i++) {
         message.arguments[i] = this._convertValue(message.arguments[i],
-                                                  /* forConsole = */ true);
+                                                  { forConsole: true });
       }
     }
     return message;
   },
 
   /////////////////////////////////////////////////////////
   // Handlers
   /////////////////////////////////////////////////////////
@@ -657,16 +708,17 @@ ReplayDebuggerScript.prototype = {
   get startLine() { return this._data.startLine; },
   get lineCount() { return this._data.lineCount; },
   get source() { return this._dbg._getSource(this._data.sourceId); },
   get sourceStart() { return this._data.sourceStart; },
   get sourceLength() { return this._data.sourceLength; },
   get format() { return this._data.format; },
 
   _forward(type, value) {
+    this._dbg._ensurePaused();
     return this._dbg._sendRequest({ type, id: this._data.id, value });
   },
 
   getLineOffsets(line) { return this._forward("getLineOffsets", line); },
   getOffsetLocation(pc) { return this._forward("getOffsetLocation", pc); },
   getSuccessorOffsets(pc) { return this._forward("getSuccessorOffsets", pc); },
   getPredecessorOffsets(pc) { return this._forward("getPredecessorOffsets", pc); },
   getAllColumnOffsets() { return this._forward("getAllColumnOffsets"); },
@@ -678,16 +730,21 @@ ReplayDebuggerScript.prototype = {
   },
 
   clearBreakpoint(handler) {
     this._dbg._clearMatchingBreakpoints(({position, data}) => {
       return position.script == this._data.id && handler == data;
     });
   },
 
+  replayVirtualConsoleLog(offset, text, callback) {
+    this._dbg._virtualConsoleLog({ kind: "Break", script: this._data.id, offset },
+                                 text, callback);
+  },
+
   get isGeneratorFunction() { NYI(); },
   get isAsyncFunction() { NYI(); },
   getChildScripts: NYI,
   getAllOffsets: NYI,
   getBreakpoints: NYI,
   clearAllBreakpoints: NYI,
   isInCatchScope: NYI,
 };
--- a/devtools/server/actors/root.js
+++ b/devtools/server/actors/root.js
@@ -165,16 +165,19 @@ RootActor.prototype = {
     // to retrieve the extension child process target actors.
     webExtensionAddonConnect: true,
     // Version of perf actor. Fx65+
     // Version 1 - Firefox 65: Introduces a duration-based buffer. It can be controlled
     // by adding a `duration` property (in seconds) to the options passed to
     // `front.startProfiler`. This is an optional parameter but it will throw an error if
     // the profiled Firefox doesn't accept it.
     perfActorVersion: 1,
+    // Supports native log points and modifying the condition/log of an existing
+    // breakpoints. Fx66+
+    nativeLogpoints: true,
   },
 
   /**
    * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    */
   sayHello: function() {
     return {
       from: this.actorID,
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -554,30 +554,30 @@ const SourceActor = ActorClassWithSpec(s
 
   /**
    * Handle a request to set a breakpoint.
    *
    * @param Number line
    *        Line to break on.
    * @param Number column
    *        Column to break on.
-   * @param String condition
-   *        A condition which must be true for breakpoint to be hit.
+   * @param Object options
+   *        Any options for the breakpoint.
    * @param Boolean noSliding
    *        If true, disables breakpoint sliding.
    *
    * @returns Promise
    *          A promise that resolves to a JSON object representing the
    *          response.
    */
-  setBreakpoint: function(line, column, condition, noSliding) {
+  setBreakpoint: function(line, column, options, noSliding) {
     const location = new GeneratedLocation(this, line, column);
     const actor = this._getOrCreateBreakpointActor(
       location,
-      condition,
+      options,
       noSliding
     );
 
     const response = {
       actor: actor.actorID,
       isPending: actor.isPending,
     };
 
@@ -592,34 +592,33 @@ const SourceActor = ActorClassWithSpec(s
   /**
    * Get or create a BreakpointActor for the given location in the generated
    * source, and ensure it is set as a breakpoint handler on all scripts that
    * match the given location.
    *
    * @param GeneratedLocation generatedLocation
    *        A GeneratedLocation representing the location of the breakpoint in
    *        the generated source.
-   * @param String condition
-   *        A string that is evaluated whenever the breakpoint is hit. If the
-   *        string evaluates to false, the breakpoint is ignored.
+   * @param Object options
+   *        Any options for the breakpoint.
    * @param Boolean noSliding
    *        If true, disables breakpoint sliding.
    *
    * @returns BreakpointActor
    *          A BreakpointActor representing the breakpoint.
    */
-  _getOrCreateBreakpointActor: function(generatedLocation, condition, noSliding) {
+  _getOrCreateBreakpointActor: function(generatedLocation, options, noSliding) {
     let actor = this.breakpointActorMap.getActor(generatedLocation);
     if (!actor) {
       actor = new BreakpointActor(this.threadActor, generatedLocation);
       this.threadActor.threadLifetimePool.addActor(actor);
       this.breakpointActorMap.setActor(generatedLocation, actor);
     }
 
-    actor.condition = condition;
+    actor.setOptions(options);
 
     return this._setBreakpoint(actor, noSliding);
   },
 
   /*
    * Ensure the given BreakpointActor is set as a breakpoint handler on all
    * scripts that match its location in the generated source.
    *
--- a/devtools/server/tests/unit/test_conditional_breakpoint-01.js
+++ b/devtools/server/tests/unit/test_conditional_breakpoint-01.js
@@ -22,26 +22,31 @@ function run_test() {
                              gThreadClient = threadClient;
                              test_simple_breakpoint();
                            });
   });
   do_test_pending();
 }
 
 function test_simple_breakpoint() {
+  let hitBreakpoint = false;
+
   gThreadClient.addOneTimeListener("paused", async function(event, packet) {
     const source = await getSourceById(
       gThreadClient,
       packet.frame.where.actor
     );
     source.setBreakpoint({
       line: 3,
-      condition: "a === 1",
+      options: { condition: "a === 1" },
     }).then(function([response, bpClient]) {
       gThreadClient.addOneTimeListener("paused", function(event, packet) {
+        Assert.equal(hitBreakpoint, false);
+        hitBreakpoint = true;
+
         // Check the return value.
         Assert.equal(packet.why.type, "breakpoint");
         Assert.equal(packet.frame.where.line, 3);
 
         // Remove the breakpoint.
         bpClient.remove(function(response) {
           gThreadClient.resume(function() {
             finishClient(gClient);
@@ -57,9 +62,11 @@ function test_simple_breakpoint() {
   Cu.evalInSandbox("debugger;\n" +   // 1
                    "var a = 1;\n" +  // 2
                    "var b = 2;\n",  // 3
                    gDebuggee,
                    "1.8",
                    "test.js",
                    1);
   /* eslint-enable */
+
+  Assert.equal(hitBreakpoint, true);
 }
--- a/devtools/server/tests/unit/test_conditional_breakpoint-02.js
+++ b/devtools/server/tests/unit/test_conditional_breakpoint-02.js
@@ -27,23 +27,27 @@ function run_test() {
 }
 
 function test_simple_breakpoint() {
   gThreadClient.addOneTimeListener("paused", async function(event, packet) {
     const source = await getSourceById(
       gThreadClient,
       packet.frame.where.actor
     );
-    source.setBreakpoint({
+    await source.setBreakpoint({
       line: 3,
-      condition: "a === 2",
+      options: { condition: "a === 2" },
+    });
+    source.setBreakpoint({
+      line: 4,
+      options: { condition: "a === 1" },
     }).then(function([response, bpClient]) {
       gThreadClient.addOneTimeListener("paused", function(event, packet) {
         // Check the return value.
-        Assert.equal(packet.why.type, "debuggerStatement");
+        Assert.equal(packet.why.type, "breakpoint");
         Assert.equal(packet.frame.where.line, 4);
 
         // Remove the breakpoint.
         bpClient.remove(function(response) {
           gThreadClient.resume(function() {
             finishClient(gClient);
           });
         });
@@ -52,15 +56,16 @@ function test_simple_breakpoint() {
       gThreadClient.resume();
     });
   });
 
   /* eslint-disable */
   Cu.evalInSandbox("debugger;\n" +   // 1
                    "var a = 1;\n" +  // 2
                    "var b = 2;\n" +  // 3
-                   "debugger;",      // 4
+                   "b++;" +          // 4
+                   "debugger;",      // 5
                    gDebuggee,
                    "1.8",
                    "test.js",
                    1);
   /* eslint-enable */
 }
--- a/devtools/server/tests/unit/test_conditional_breakpoint-03.js
+++ b/devtools/server/tests/unit/test_conditional_breakpoint-03.js
@@ -29,17 +29,17 @@ function run_test() {
 function test_simple_breakpoint() {
   gThreadClient.addOneTimeListener("paused", async function(event, packet) {
     const source = await getSourceById(
       gThreadClient,
       packet.frame.where.actor
     );
     source.setBreakpoint({
       line: 3,
-      condition: "throw new Error()",
+      options: { condition: "throw new Error()" },
     }).then(function([response, bpClient]) {
       gThreadClient.addOneTimeListener("paused", function(event, packet) {
         // Check the return value.
         Assert.equal(packet.why.type, "breakpointConditionThrown");
         Assert.equal(packet.frame.where.line, 3);
 
         // Remove the breakpoint.
         bpClient.remove(function(response) {
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_logpoint-01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that logpoints call console.log.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test() {
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-logpoint");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect().then(function() {
+    attachTestTabAndResume(gClient, "test-logpoint",
+                           function(response, targetFront, threadClient) {
+                             gThreadClient = threadClient;
+                             test_simple_breakpoint();
+                           });
+  });
+  do_test_pending();
+}
+
+function test_simple_breakpoint() {
+  gThreadClient.addOneTimeListener("paused", async function(event, packet) {
+    const source = await getSourceById(
+      gThreadClient,
+      packet.frame.where.actor
+    );
+
+    // Set a logpoint which should invoke console.log.
+    await source.setBreakpoint({
+      line: 4,
+      options: { logValue: "a" },
+    });
+
+    // Execute the rest of the code.
+    gThreadClient.resume();
+  });
+
+  // Sandboxes don't have a console available so we add our own.
+  /* eslint-disable */
+  Cu.evalInSandbox("var console = { log: v => { this.logValue = v } };\n" + // 1
+                   "debugger;\n" + // 2
+                   "var a = 'three';\n" +  // 3
+                   "var b = 2;\n", // 4
+                   gDebuggee,
+                   "1.8",
+                   "test.js",
+                   1);
+  /* eslint-enable */
+
+  Assert.equal(gDebuggee.logValue, "three");
+  finishClient(gClient);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_logpoint-02.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Check that conditions are respected when specified in a logpoint.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test() {
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-logpoint");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect().then(function() {
+    attachTestTabAndResume(gClient, "test-logpoint",
+                           function(response, targetFront, threadClient) {
+                             gThreadClient = threadClient;
+                             test_simple_breakpoint();
+                           });
+  });
+  do_test_pending();
+}
+
+function test_simple_breakpoint() {
+  gThreadClient.addOneTimeListener("paused", async function(event, packet) {
+    const source = await getSourceById(
+      gThreadClient,
+      packet.frame.where.actor
+    );
+
+    // Set a logpoint which should invoke console.log.
+    await source.setBreakpoint({
+      line: 5,
+      options: { logValue: "a", condition: "a === 5" },
+    });
+
+    // Execute the rest of the code.
+    gThreadClient.resume();
+  });
+
+  // Sandboxes don't have a console available so we add our own.
+  /* eslint-disable */
+  Cu.evalInSandbox("var console = { log: v => { this.logValue = v } };\n" + // 1
+                   "debugger;\n" + // 2
+                   "var a = 1;\n" +  // 3
+                   "while (a < 10) {\n" + // 4
+                   "  a++;\n" + // 5
+                   "}",
+                   gDebuggee,
+                   "1.8",
+                   "test.js",
+                   1);
+  /* eslint-enable */
+
+  Assert.equal(gDebuggee.logValue, 5);
+  finishClient(gClient);
+}
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -129,16 +129,18 @@ skip-if = coverage # bug 1336670
 skip-if = true
 reason = bug 1104838
 [test_breakpoint-20.js]
 [test_breakpoint-21.js]
 [test_breakpoint-22.js]
 [test_conditional_breakpoint-01.js]
 [test_conditional_breakpoint-02.js]
 [test_conditional_breakpoint-03.js]
+[test_logpoint-01.js]
+[test_logpoint-02.js]
 [test_listsources-01.js]
 [test_listsources-02.js]
 [test_listsources-03.js]
 [test_new_source-01.js]
 [test_new_source-02.js]
 [test_objectgrips-01.js]
 [test_objectgrips-02.js]
 [test_objectgrips-03.js]
--- a/devtools/shared/client/breakpoint-client.js
+++ b/devtools/shared/client/breakpoint-client.js
@@ -2,46 +2,42 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const promise = require("devtools/shared/deprecated-sync-thenables");
 
 const eventSource = require("devtools/shared/client/event-source");
-const {DebuggerClient} = require("devtools/shared/client/debugger-client");
+const {arg, DebuggerClient} = require("devtools/shared/client/debugger-client");
 
 /**
  * Breakpoint clients are used to remove breakpoints that are no longer used.
  *
  * @param client DebuggerClient
  *        The debugger client parent.
  * @param sourceClient SourceClient
  *        The source where this breakpoint exists
  * @param actor string
  *        The actor ID for this breakpoint.
  * @param location object
  *        The location of the breakpoint. This is an object with two properties:
  *        url and line.
- * @param condition string
- *        The conditional expression of the breakpoint
+ * @param options object
+ *        Any options associated with the breakpoint
  */
-function BreakpointClient(client, sourceClient, actor, location, condition) {
+function BreakpointClient(client, sourceClient, actor, location, options) {
   this._client = client;
   this._actor = actor;
   this.location = location;
   this.location.actor = sourceClient.actor;
   this.location.url = sourceClient.url;
   this.source = sourceClient;
   this.request = this._client.request;
-
-  // The condition property should only exist if it's a truthy value
-  if (condition) {
-    this.condition = condition;
-  }
+  this.options = options;
 }
 
 BreakpointClient.prototype = {
 
   _actor: null,
   get actor() {
     return this._actor;
   },
@@ -51,40 +47,54 @@ BreakpointClient.prototype = {
 
   /**
    * Remove the breakpoint from the server.
    */
   remove: DebuggerClient.requester({
     type: "delete",
   }),
 
-  /**
-   * Set the condition of this breakpoint
-   */
-  setCondition: function(gThreadClient, condition) {
-    const deferred = promise.defer();
-
-    const info = {
-      line: this.location.line,
-      column: this.location.column,
-      condition: condition,
-    };
+  // Send a setOptions request to newer servers.
+  setOptionsRequester: DebuggerClient.requester({
+    type: "setOptions",
+    options: arg(0),
+  }, {
+    before(packet) {
+      this.options = packet.options;
+      return packet;
+    },
+  }),
 
-    // Remove the current breakpoint and add a new one with the
-    // condition.
-    this.remove(response => {
-      if (response && response.error) {
-        deferred.reject(response);
-        return;
-      }
+  /**
+   * Set any options for this breakpoint.
+   */
+  setOptions: function(options) {
+    if (this._client.mainRoot.traits.nativeLogpoints) {
+      this.setOptionsRequester(options);
+    } else {
+      // Older servers need to reinstall breakpoints when the condition changes.
+      const deferred = promise.defer();
 
-      deferred.resolve(this.source.setBreakpoint(info).then(([, newBreakpoint]) => {
-        return newBreakpoint;
-      }));
-    });
+      const info = {
+        line: this.location.line,
+        column: this.location.column,
+        options,
+      };
 
-    return deferred.promise;
+      // Remove the current breakpoint and add a new one with the specified
+      // information.
+      this.remove(response => {
+        if (response && response.error) {
+          deferred.reject(response);
+          return;
+        }
+
+        deferred.resolve(this.source.setBreakpoint(info).then(([, newBreakpoint]) => {
+          return newBreakpoint;
+        }));
+      });
+    }
   },
 };
 
 eventSource(BreakpointClient.prototype);
 
 module.exports = BreakpointClient;
--- a/devtools/shared/client/constants.js
+++ b/devtools/shared/client/constants.js
@@ -32,16 +32,17 @@ const UnsolicitedNotifications = {
   "reflowActivity": "reflowActivity",
   "addonListChanged": "addonListChanged",
   "workerListChanged": "workerListChanged",
   "serviceWorkerRegistrationListChanged": "serviceWorkerRegistrationList",
   "pageError": "pageError",
   "evaluationResult": "evaluationResult",
   "updatedSource": "updatedSource",
   "inspectObject": "inspectObject",
+  "virtualConsoleLog": "virtualConsoleLog",
 
   // newSource is still emitted on the ThreadActor, in addition to the
   // BrowsingContextActor we have to keep it here until ThreadClient is converted to
   // ThreadFront and/or we stop emitting this duplicated events.
   // See ThreadActor.onNewSourceEvent.
   "newSource": "newSource",
 };
 
--- a/devtools/shared/client/source-client.js
+++ b/devtools/shared/client/source-client.js
@@ -170,46 +170,59 @@ SourceClient.prototype = {
       return newResponse;
     });
   },
 
   /**
    * Request to set a breakpoint in the specified location.
    *
    * @param object location
-   *        The location and condition of the breakpoint in
-   *        the form of { line[, column, condition] }.
+   *        The location and options of the breakpoint in
+   *        the form of { line[, column, options] }.
    */
-  setBreakpoint: function({ line, column, condition, noSliding }) {
+  setBreakpoint: function({ line, column, options, noSliding }) {
     // A helper function that sets the breakpoint.
     const doSetBreakpoint = callback => {
       const location = {
         line,
         column,
       };
 
       const packet = {
         to: this.actor,
         type: "setBreakpoint",
         location,
-        condition,
+        options,
         noSliding,
       };
 
+      // Older servers only support conditions, not a more general options
+      // object. Transform the packet to support the older format.
+      if (options && !this._client.mainRoot.traits.nativeLogpoints) {
+        delete packet.options;
+        if (options.logValue) {
+          // Emulate log points by setting a condition with a call to console.log,
+          // which always returns false so the server will never pause.
+          packet.condition = `console.log(${options.logValue})`;
+        } else {
+          packet.condition = options.condition;
+        }
+      }
+
       return this._client.request(packet).then(response => {
         // Ignoring errors, since the user may be setting a breakpoint in a
         // dead script that will reappear on a page reload.
         let bpClient;
         if (response.actor) {
           bpClient = new BreakpointClient(
             this._client,
             this,
             response.actor,
             location,
-            condition
+            options
           );
         }
         if (callback) {
           callback();
         }
         return [response, bpClient];
       });
     };
--- a/devtools/shared/specs/breakpoint.js
+++ b/devtools/shared/specs/breakpoint.js
@@ -1,16 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const {generateActorSpec} = require("devtools/shared/protocol");
+const {Arg, generateActorSpec} = require("devtools/shared/protocol");
 
 const breakpointSpec = generateActorSpec({
   typeName: "breakpoint",
 
   methods: {
     delete: {},
+
+    setOptions: {
+      request: {
+        options: Arg(0, "nullable:json"),
+      },
+    },
   },
 });
 
 exports.breakpointSpec = breakpointSpec;
--- a/devtools/shared/specs/source.js
+++ b/devtools/shared/specs/source.js
@@ -56,17 +56,17 @@ const sourceSpec = generateActorSpec({
       request: { range: Arg(0, "nullable:json") },
     },
     setBreakpoint: {
       request: {
         location: {
           line: Arg(0, "number"),
           column: Arg(1, "nullable:number"),
         },
-        condition: Arg(2, "nullable:string"),
+        options: Arg(2, "nullable:json"),
         noSliding: Arg(3, "nullable:boolean"),
       },
       response: RetVal("json"),
     },
   },
 });
 
 exports.sourceSpec = sourceSpec;
--- a/extensions/spellcheck/locales/en-US/hunspell/en-US.dic
+++ b/extensions/spellcheck/locales/en-US/hunspell/en-US.dic
@@ -1,9 +1,9 @@
-52987
+53000
 0/nm
 0th/pt
 1/n1
 1st/p
 1th/tc
 2/nm
 2nd/p
 2th/tc
@@ -15887,18 +15887,20 @@ backslapper/SM
 backslapping/M
 backslash/MS
 backslid
 backslide/RSZG
 backslider/M
 backspace/DSMG
 backspin/M
 backsplash/S
+backstab/S
 backstabber/MS
 backstabbing
+backstabby
 backstage/M
 backstair/S
 backstop/SM
 backstopped
 backstopping
 backstory/S
 backstreet/S
 backstretch/MS
@@ -17306,16 +17308,17 @@ bombardier/MS
 bombardment/SM
 bombast/M
 bombastic
 bombastically
 bomber/M
 bombproof
 bombshell/SM
 bombsite/S
+bon/S
 bona
 bonanza/MS
 bonbon/MS
 bonce/S
 bond/SGMD
 bondage/M
 bondholder/MS
 bonding/M
@@ -20149,16 +20152,18 @@ colloq
 colloquial/Y
 colloquialism/SM
 colloquies
 colloquium/MS
 colloquy/M
 collude/DSG
 collusion/M
 collusive
+colocate/DGS
+colocation/S
 cologne/SM
 colon/SM
 colonel/SM
 colonelcy/M
 colones
 colonial/SMY
 colonialism/M
 colonialist/MS
@@ -21236,17 +21241,17 @@ cosmogonist/SM
 cosmogony/SM
 cosmological
 cosmologist/SM
 cosmology/SM
 cosmonaut/SM
 cosmopolitan/MS
 cosmopolitanism/M
 cosmos/MS
-cosplay
+cosplay/DGSRZ
 cosponsor/GSMD
 cosset/SGD
 cossetted
 cossetting
 cost/MDYGSJ
 costar/SM
 costarred
 costarring
@@ -21841,16 +21846,17 @@ cuff/MDGS
 cuisine/SM
 cul-de-sac
 culinary
 cull/MDGS
 cullender/MS
 culminate/XDSGN
 culmination/M
 culotte/SM
+culpa/S
 culpability/M
 culpable/I
 culpably
 culprit/SM
 cult/MS
 cultism/M
 cultist/MS
 cultivable
@@ -25742,16 +25748,17 @@ extinguishable/I
 extinguisher/M
 extirpate/GNDS
 extirpation/M
 extol/S
 extolled
 extolling
 extort/SGD
 extortion/MRZ
+extortionary
 extortionate/Y
 extortioner/M
 extortionist/MS
 extra/SM
 extracellular
 extract/MDGVS
 extraction/SM
 extractor/MS
@@ -27975,17 +27982,17 @@ geomagnetic
 geomagnetism/M
 geometer
 geometric
 geometrical/Y
 geometry/SM
 geophysical
 geophysicist/SM
 geophysics/M
-geopolitical
+geopolitical/Y
 geopolitics/M
 geostationary
 geosynchronous
 geosyncline/MS
 geothermal
 geothermic
 geranium/MS
 gerbil/MS
@@ -34608,16 +34615,17 @@ mayoralty/M
 mayoress/MS
 maypole/SM
 mayst
 maze/MS
 mazurka/MS
 maņana/M
 mdse
 me/DSH
+mea
 mead/M
 meadow/MS
 meadowlark/MS
 meager/PY
 meagerness/M
 meal/MS
 mealiness/M
 mealtime/SM
@@ -39817,16 +39825,17 @@ pitted
 pitting
 pituitary/SM
 pity/GDSM
 pitying/Y
 pivot/MDGS
 pivotal
 pix/M
 pixel/MS
+pixelate/DS
 pixie/MS
 pizazz/M
 pizza/MS
 pizzazz/M
 pizzeria/SM
 pizzicati
 pizzicato/M
 piņata/MS
@@ -43852,17 +43861,17 @@ scad/MS
 scaffold/SMG
 scaffolding/M
 scag/S
 scagged
 scalar/S
 scalawag/MS
 scald/MDSG
 scale's
-scale/CGDS
+scale/CGDSB
 scaleless
 scalene
 scaliness/M
 scallion/MS
 scallop/GSMD
 scallywag/MS
 scalp/MDRSZG
 scalpel/SM
@@ -47071,16 +47080,17 @@ stonework/M
 stonily
 stoniness/M
 stonkered
 stonking
 stony/TRP
 stood
 stooge/MS
 stool/SM
+stoolie/SM
 stoop/GSMD
 stop's
 stop/US
 stopcock/SM
 stopgap/SM
 stoplight/MS
 stopover/MS
 stoppable/U
@@ -47760,16 +47770,17 @@ superstate/S
 superstition/MS
 superstitious/Y
 superstore/MS
 superstructure/MS
 supertanker/MS
 superuser/S
 supervene/GDS
 supervention/M
+supervillain/SM
 supervise/XGNDS
 supervised/U
 supervision/M
 supervisor/MS
 supervisory
 superwoman/M
 superwomen
 supine/Y
@@ -51471,16 +51482,17 @@ vitriolically
 vittles/M
 vituperate/GNVDS
 vituperation/M
 viva/MS
 vivace
 vivacious/PY
 vivaciousness/M
 vivacity/M
+vivant/S
 vivaria
 vivarium/SM
 vivid/RYTP
 vividness/M
 vivify/ADSG
 viviparous
 vivisect/DGS
 vivisection/M
@@ -52384,16 +52396,17 @@ wise/MYTGDRS
 wiseacre/SM
 wisecrack/MDSG
 wiseguy/S
 wish/MDRSZG
 wishbone/SM
 wisher/M
 wishful/Y
 wishlist's
+wishy-washy
 wisp/MS
 wispy/RT
 wist
 wisteria/SM
 wistful/YP
 wistfulness/M
 wit/SM
 witch/MDSG
--- a/gfx/layers/wr/ClipManager.cpp
+++ b/gfx/layers/wr/ClipManager.cpp
@@ -216,23 +216,16 @@ wr::WrSpaceAndClipChain ClipManager::Swi
   }
   Maybe<wr::WrSpaceAndClip> leafmostId =
       DefineScrollLayers(leafmostASR, aItem, aStackingContext);
 
   // Define all the clips in the item's clip chain, and obtain a clip chain id
   // for it.
   clips.mClipChainId = DefineClipChain(clip, auPerDevPixel, aStackingContext);
 
-  // If we didn't define a clip chain, inherit one from the stack. Eventually
-  // we should ensure stacking contexts always have a valid clip chain, and
-  // that should eliminate the need for this.
-  if (clips.mClipChainId.isNothing() && !mItemClipStack.empty()) {
-    clips.mClipChainId = mItemClipStack.top().mClipChainId;
-  }
-
   Maybe<wr::WrSpaceAndClip> spaceAndClip = GetScrollLayer(asr);
   MOZ_ASSERT(spaceAndClip.isSome());
   clips.mScrollId = SpatialIdAfterOverride(spaceAndClip->space);
   CLIP_LOG("\tassigning %d -> %d\n", (int)spaceAndClip->space.id,
            (int)clips.mScrollId.id);
 
   // Now that we have the scroll id and a clip id for the item, push it onto
   // the WR stack.
@@ -358,21 +351,32 @@ Maybe<wr::WrClipChainId> ClipManager::De
     spaceAndClip->space = SpatialIdAfterOverride(spaceAndClip->space);
     wr::WrClipId clipId = mBuilder->DefineClip(
         spaceAndClip, wr::ToRoundedLayoutRect(clip), &wrRoundedRects);
     clipIds.AppendElement(clipId);
     cache[chain] = clipId;
     CLIP_LOG("cache[%p] <= %zu\n", chain, clipId.id);
   }
 
-  if (clipIds.Length() == 0) {
-    return Nothing();
+  // Now find the parent display item's clipchain id
+  Maybe<wr::WrClipChainId> parentChainId;
+  if (!mItemClipStack.empty()) {
+    parentChainId = mItemClipStack.top().mClipChainId;
   }
 
-  return Some(mBuilder->DefineClipChain(clipIds));
+  // And define the current display item's clipchain using the clips and the
+  // parent. If the current item has no clips of its own, just use the parent
+  // item's clipchain.
+  Maybe<wr::WrClipChainId> chainId;
+  if (clipIds.Length() > 0) {
+    chainId = Some(mBuilder->DefineClipChain(parentChainId, clipIds));
+  } else {
+    chainId = parentChainId;
+  }
+  return chainId;
 }
 
 ClipManager::~ClipManager() {
   MOZ_ASSERT(!mBuilder);
   MOZ_ASSERT(mCacheStack.empty());
   MOZ_ASSERT(mItemClipStack.empty());
 }
 
deleted file mode 100644
--- a/gfx/tests/reftest/1523776-ref.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<style>
-html, body {
-    margin: 0;
-}
-</style>
-  <div style="filter: drop-shadow(50px 50px 20px red)">
-    <div style="width:256px; height:256px; border-radius:16px; overflow:hidden">
-      <div style="width:256px; height:256px; background-color:green;"></div>
-    </div>
-  </div>
-<!-- add white divs on top to simulate the clip from the test file -->
-<div style="background-color: white; position:absolute; left: 0; top: 0; width: 100px; height: 700px"></div>
-<div style="background-color: white; position:absolute; left: 0; top: 0; width: 700px; height: 100px"></div>
deleted file mode 100644
--- a/gfx/tests/reftest/1523776.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<style>
-html, body {
-    margin: 0;
-}
-</style>
-<div style="clip: rect(100px,1000px,1000px,100px); position:absolute">
-  <div style="filter: drop-shadow(50px 50px 20px red)">
-    <div style="width:256px; height:256px; border-radius:16px; overflow:hidden">
-      <div style="width:256px; height:256px; background-color:green;"></div>
-    </div>
-  </div>
-</div>
new file mode 100644
--- /dev/null
+++ b/gfx/tests/reftest/1524261-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<html>
+<div style="overflow-y:hidden; position:absolute; top:50px; width: 200px; height: 100px; border: solid 1px black">
+</div>
new file mode 100644
--- /dev/null
+++ b/gfx/tests/reftest/1524261.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html reftest-async-scroll>
+<div style="overflow-y:hidden; position:absolute; top:50px; width: 200px; height: 100px; border: solid 1px black"
+     reftest-displayport-x="0" reftest-displayport-y="0"
+     reftest-displayport-w="200" reftest-displayport-h="300"
+     reftest-async-scroll-x="0" reftest-async-scroll-y="50">
+    <div style="transform: translateY(0px); width: 100px; height: 300px">
+        <div style="background-color: green; height: 25px; width: 25px; border-radius: 5px"></div>
+    </div>
+</div>
new file mode 100644
--- /dev/null
+++ b/gfx/tests/reftest/1524353-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<div style="overflow:hidden; width: 300px; height: 200px; border: solid 1px black">
+    <div style="width: 600px; transform: translateX(0px); will-change: transform">
+        <svg style="height: 200px" width="600">
+            <path d="M0 0 H 300 V 25 Z" fill="red"></path>
+        </svg>
+    </div>
+</div>
new file mode 100644
--- /dev/null
+++ b/gfx/tests/reftest/1524353.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<div style="overflow:hidden; width: 300px; height: 200px; border: solid 1px black">
+    <div style="width: 600px; transform: translateX(0px); will-change: transform">
+        <svg style="height: 200px" width="600">
+            <path d="M0 0 H 600 V 50 Z" fill="red"></path>
+        </svg>
+    </div>
+</div>
--- a/gfx/tests/reftest/reftest.list
+++ b/gfx/tests/reftest/reftest.list
@@ -13,9 +13,10 @@ fuzzy(0-100,0-30) == 1149923.html 114992
 == 1435143.html 1435143-ref.html
 == 1444904.html 1444904-ref.html
 == 1451168.html 1451168-ref.html
 == 1461313.html 1461313-ref.html
 fuzzy(5-32,21908-26621) fuzzy-if(webrender,0-9,0-100) == 1463802.html 1463802-ref.html
 fuzzy(0-11,0-4) == 1474722.html 1474722-ref.html
 == 1501195.html 1501195-ref.html
 == 1519754.html 1519754-ref.html
-fuzzy-if(webrender,6-7,34741-36908) == 1523776.html 1523776-ref.html
+skip-if(!asyncPan) == 1524261.html 1524261-ref.html
+fuzzy-if(webrender,14-14,44-44) == 1524353.html 1524353-ref.html
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -696,20 +696,23 @@ Maybe<wr::WrSpatialId> DisplayListBuilde
 }
 
 void DisplayListBuilder::PopStackingContext(bool aIsReferenceFrame) {
   WRDL_LOG("PopStackingContext\n", mWrState);
   wr_dp_pop_stacking_context(mWrState, aIsReferenceFrame);
 }
 
 wr::WrClipChainId DisplayListBuilder::DefineClipChain(
+    const Maybe<wr::WrClipChainId>& aParent,
     const nsTArray<wr::WrClipId>& aClips) {
-  uint64_t clipchainId = wr_dp_define_clipchain(
-      mWrState, nullptr, aClips.Elements(), aClips.Length());
-  WRDL_LOG("DefineClipChain id=%" PRIu64 " clips=%zu\n", mWrState, clipchainId,
+  uint64_t clipchainId =
+      wr_dp_define_clipchain(mWrState, aParent ? &(aParent->id) : nullptr,
+                             aClips.Elements(), aClips.Length());
+  WRDL_LOG("DefineClipChain id=%" PRIu64 " p=%s clips=%zu\n", mWrState,
+           clipchainId, aParent ? Stringify(aParent->id).c_str() : "(nil)",
            aClips.Length());
   return wr::WrClipChainId{clipchainId};
 }
 
 wr::WrClipId DisplayListBuilder::DefineClip(
     const Maybe<wr::WrSpaceAndClip>& aParent, const wr::LayoutRect& aClipRect,
     const nsTArray<wr::ComplexClipRegion>* aComplex,
     const wr::WrImageMask* aMask) {
--- a/gfx/webrender_bindings/WebRenderAPI.h
+++ b/gfx/webrender_bindings/WebRenderAPI.h
@@ -357,17 +357,18 @@ class DisplayListBuilder {
   void Finalize(wr::LayoutSize& aOutContentSize,
                 wr::BuiltDisplayList& aOutDisplayList);
 
   Maybe<wr::WrSpatialId> PushStackingContext(
       const StackingContextParams& aParams, const wr::LayoutRect& aBounds,
       const wr::RasterSpace& aRasterSpace);
   void PopStackingContext(bool aIsReferenceFrame);
 
-  wr::WrClipChainId DefineClipChain(const nsTArray<wr::WrClipId>& aClips);
+  wr::WrClipChainId DefineClipChain(const Maybe<wr::WrClipChainId>& aParent,
+                                    const nsTArray<wr::WrClipId>& aClips);
 
   wr::WrClipId DefineClip(
       const Maybe<wr::WrSpaceAndClip>& aParent, const wr::LayoutRect& aClipRect,
       const nsTArray<wr::ComplexClipRegion>* aComplex = nullptr,
       const wr::WrImageMask* aMask = nullptr);
 
   wr::WrSpatialId DefineStickyFrame(const wr::LayoutRect& aContentRect,
                                     const float* aTopMargin,
--- a/js/src/builtin/TypedObject.cpp
+++ b/js/src/builtin/TypedObject.cpp
@@ -1536,20 +1536,33 @@ void OutlineTypedObject::setOwnerAndData
   MOZ_ASSERT_IF(owner && owner->is<ArrayBufferObject>(),
                 !owner->as<ArrayBufferObject>().forInlineTypedObject());
 
   // Typed objects cannot move from one owner to another, so don't worry
   // about pre barriers during this initialization.
   owner_ = owner;
   data_ = data;
 
-  // Trigger a post barrier when attaching an object outside the nursery to
-  // one that is inside it.
-  if (owner && !IsInsideNursery(this) && IsInsideNursery(owner)) {
-    owner->storeBuffer()->putWholeCell(this);
+  if (owner) {
+    if (!IsInsideNursery(this) && IsInsideNursery(owner)) {
+      // Trigger a post barrier when attaching an object outside the nursery to
+      // one that is inside it.
+      owner->storeBuffer()->putWholeCell(this);
+    } else if (IsInsideNursery(this) && !IsInsideNursery(owner)) {
+      // ...and also when attaching an object inside the nursery to one that is
+      // outside it, for a subtle reason -- the outline object now points to
+      // the memory owned by 'owner', and can modify object/string references
+      // stored in that memory, potentially storing nursery pointers in it. If
+      // the outline object is in the nursery, then the post barrier will do
+      // nothing; you will be writing a nursery pointer "into" a nursery
+      // object. But that will result in the tenured owner's data containing a
+      // nursery pointer, and thus we need a store buffer edge. Since we can't
+      // catch the actual write, register the owner preemptively now.
+      storeBuffer()->putWholeCell(owner);
+    }
   }
 }
 
 /*static*/ OutlineTypedObject* OutlineTypedObject::createUnattachedWithClass(
     JSContext* cx, const Class* clasp, HandleTypeDescr descr,
     gc::InitialHeap heap) {
   MOZ_ASSERT(clasp == &OutlineTransparentTypedObject::class_ ||
              clasp == &OutlineOpaqueTypedObject::class_);
--- a/js/src/gc/Memory.cpp
+++ b/js/src/gc/Memory.cpp
@@ -721,40 +721,62 @@ void UnmapPages(void* region, size_t len
 
   // ASan does not automatically unpoison memory, so we have to do this here.
   MOZ_MAKE_MEM_UNDEFINED(region, length);
 
   UnmapInternal(region, length);
 }
 
 bool MarkPagesUnused(void* region, size_t length) {
-  MOZ_RELEASE_ASSERT(region && OffsetFromAligned(region, pageSize) == 0);
-  MOZ_RELEASE_ASSERT(length > 0 && length % pageSize == 0);
+  MOZ_RELEASE_ASSERT(region);
+  MOZ_RELEASE_ASSERT(length > 0);
+
+  // pageSize == ArenaSize doesn't necessarily hold, but this function is
+  // used by the GC to decommit unused Arenas, so we don't want to assert
+  // if pageSize > ArenaSize.
+  MOZ_ASSERT(OffsetFromAligned(region, ArenaSize) == 0);
+  MOZ_ASSERT(length % ArenaSize == 0);
 
   MOZ_MAKE_MEM_NOACCESS(region, length);
 
   if (!DecommitEnabled()) {
     return true;
   }
+  // We can't decommit part of a page.
+  MOZ_RELEASE_ASSERT(OffsetFromAligned(region, pageSize) == 0);
+  MOZ_RELEASE_ASSERT(length % pageSize == 0);
 
 #if defined(XP_WIN)
   return VirtualAlloc(region, length, MEM_RESET,
                       DWORD(PageAccess::ReadWrite)) == region;
 #elif defined(XP_DARWIN)
   return madvise(region, length, MADV_FREE) == 0;
 #else
   return madvise(region, length, MADV_DONTNEED) == 0;
 #endif
 }
 
 void MarkPagesInUse(void* region, size_t length) {
-  MOZ_RELEASE_ASSERT(region && OffsetFromAligned(region, pageSize) == 0);
-  MOZ_RELEASE_ASSERT(length > 0 && length % pageSize == 0);
+  MOZ_RELEASE_ASSERT(region);
+  MOZ_RELEASE_ASSERT(length > 0);
+
+  // pageSize == ArenaSize doesn't necessarily hold, but this function is
+  // used by the GC to recommit Arenas that were previously decommitted,
+  // so we don't want to assert if pageSize > ArenaSize.
+  MOZ_ASSERT(OffsetFromAligned(region, ArenaSize) == 0);
+  MOZ_ASSERT(length % ArenaSize == 0);
 
   MOZ_MAKE_MEM_UNDEFINED(region, length);
+
+  if (!DecommitEnabled()) {
+    return;
+  }
+  // We can't commit part of a page.
+  MOZ_RELEASE_ASSERT(OffsetFromAligned(region, pageSize) == 0);
+  MOZ_RELEASE_ASSERT(length % pageSize == 0);
 }
 
 size_t GetPageFaultCount() {
   if (mozilla::recordreplay::IsRecordingOrReplaying()) {
     return 0;
   }
 #ifdef XP_WIN
   PROCESS_MEMORY_COUNTERS pmc;
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -7001,21 +7001,18 @@ bool nsDisplayStickyPosition::CreateWebR
         rightMargin.ptrOr(nullptr), bottomMargin.ptrOr(nullptr),
         leftMargin.ptrOr(nullptr), vBounds, hBounds, applied);
 
     saccHelper.emplace(aBuilder, spatialId);
     aManager->CommandBuilder().PushOverrideForASR(mContainerASR, spatialId);
   }
 
   {
-    wr::StackingContextParams params;
-    params.clip =
-        wr::WrStackingContextClip::ClipChain(aBuilder.CurrentClipChainId());
     StackingContextHelper sc(aSc, GetActiveScrolledRoot(), mFrame, this,
-                             aBuilder, params);
+                             aBuilder);
     nsDisplayWrapList::CreateWebRenderCommands(aBuilder, aResources, sc,
                                                aManager, aDisplayListBuilder);
   }
 
   if (stickyScrollContainer) {
     aManager->CommandBuilder().PopOverrideForASR(mContainerASR);
   }
 
--- a/layout/reftests/position-sticky/reftest.list
+++ b/layout/reftests/position-sticky/reftest.list
@@ -40,16 +40,16 @@ fuzzy-if(Android,0-4,0-810) == containin
 == inline-1.html inline-1-ref.html
 == inline-2.html inline-2-ref.html
 fuzzy-if(OSX,0-99,0-210) == inline-3.html inline-3-ref.html
 skip-if(!asyncPan) == inline-4.html inline-4-ref.html
 fails == column-contain-1a.html column-contain-1-ref.html
 == column-contain-1b.html column-contain-1-ref.html
 == column-contain-2.html column-contain-2-ref.html
 == block-in-inline-1.html block-in-inline-1-ref.html
-fuzzy-if(skiaContent,0-1,0-22) fuzzy-if((winWidget&&(webrender||!layersGPUAccelerated))||(OSX&&webrender),0-92,0-1369) fuzzy-if(Android,0-8,0-1533) == block-in-inline-2.html block-in-inline-2-ref.html
-fuzzy-if(Android,0-8,0-630) fuzzy-if(OSX,0-1,0-11) fuzzy-if(skiaContent,0-1,0-220) fuzzy-if((winWidget&&(webrender||!layersGPUAccelerated))||(OSX&&webrender),0-92,0-1343) == block-in-inline-3.html block-in-inline-3-ref.html
+fuzzy-if(skiaContent,0-1,0-22) fuzzy-if(winWidget&&!layersGPUAccelerated,0-116,0-1320) fuzzy-if(Android,0-8,0-1533) == block-in-inline-2.html block-in-inline-2-ref.html
+fuzzy-if(Android,0-8,0-630) fuzzy-if(OSX,0-1,0-11) fuzzy-if(skiaContent,0-1,0-220) fuzzy-if(winWidget&&!layersGPUAccelerated,0-116,0-1320) == block-in-inline-3.html block-in-inline-3-ref.html
 == block-in-inline-continuations.html block-in-inline-continuations-ref.html
 == iframe-1.html iframe-1-ref.html
 == transformed-1.html transformed-1-ref.html
 fuzzy-if(Android,0-2,0-4) skip-if(!asyncPan) == transformed-2.html transformed-2-ref.html
 skip-if(!asyncPan) fuzzy-if(Android,0-3,0-4) == nested-sticky-1.html nested-sticky-1-ref.html
 skip-if(!asyncPan) fuzzy-if(Android,0-3,0-4) fuzzy-if(/^Windows\x20NT\x206\.1/.test(http.oscpu),0-4,0-104) == nested-sticky-2.html nested-sticky-2-ref.html
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -1544,18 +1544,16 @@ public class BrowserApp extends GeckoApp
             delegate.onDestroy(this);
         }
 
         deleteTempFiles(getApplicationContext());
 
         NotificationHelper.destroy();
         GeckoNetworkManager.destroy();
 
-        EventDispatcher.getInstance().dispatch("Browser:ZombifyTabs", null);
-
         MmaDelegate.flushResources(this);
 
         super.onDestroy();
     }
 
     @Override
     protected void initializeChrome() {
         super.initializeChrome();
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -366,17 +366,16 @@ var BrowserApp = {
       "Tab:Move",
       "Tab:OpenUri",
       "Tab:ViewSource",
     ]);
 
     GlobalEventDispatcher.registerListener(this, [
       "Browser:LoadManifest",
       "Browser:Quit",
-      "Browser:ZombifyTabs",
       "Fonts:Reload",
       "FormHistory:Init",
       "FullScreen:Exit",
       "Locale:OS",
       "Locale:Changed",
       "Passwords:Init",
       "Sanitize:ClearData",
       "SaveAs:PDF",
@@ -1726,23 +1725,16 @@ var BrowserApp = {
         installManifest(browser, data);
         break;
       }
 
       case "Browser:Quit":
         this.quit(data);
         break;
 
-      case "Browser:ZombifyTabs":
-        let tabs = this._tabs;
-        for (let i = 0; i < tabs.length; i++) {
-          tabs[i].zombify();
-        }
-        break;
-
       case "Fonts:Reload":
         FontEnumerator.updateFontList();
         break;
 
       case "FormHistory:Init": {
         // Force creation/upgrade of formhistory.sqlite
         FormHistory.count({}, {
           handleCompletion() {
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -4165,16 +4165,17 @@ int AddPreCompleteActions(ActionList *li
 
     if (!action) {
       free(buf);
       return BAD_ACTION_ERROR;
     }
 
     rv = action->Parse(line);
     if (rv) {
+      delete action;
       free(buf);
       return rv;
     }
 
     list->Append(action);
   }
 
   free(buf);
--- a/toolkit/recordreplay/MiddlemanCall.cpp
+++ b/toolkit/recordreplay/MiddlemanCall.cpp
@@ -6,36 +6,52 @@
 
 #include "MiddlemanCall.h"
 
 #include <unordered_map>
 
 namespace mozilla {
 namespace recordreplay {
 
-// In a replaying or middleman process, all middleman calls that have been
-// encountered, indexed by their ID.
-static StaticInfallibleVector<MiddlemanCall*> gMiddlemanCalls;
+typedef std::unordered_map<const void*, MiddlemanCall*> MiddlemanCallMap;
+
+// State used for keeping track of middleman calls in either a replaying
+// process or middleman process.
+struct MiddlemanCallState {
+  // In a replaying or middleman process, all middleman calls that have been
+  // encountered, indexed by their ID.
+  InfallibleVector<MiddlemanCall*> mCalls;
+
+  // In a replaying or middleman process, association between values produced by
+  // a middleman call and the call itself.
+  MiddlemanCallMap mCallMap;
 
-// In a replaying or middleman process, association between values produced by
-// a middleman call and the call itself.
-typedef std::unordered_map<const void*, MiddlemanCall*> MiddlemanCallMap;
-static MiddlemanCallMap* gMiddlemanCallMap;
+  // In a middleman process, any buffers allocated for performed calls.
+  InfallibleVector<void*> mAllocatedBuffers;
+};
 
-// In a middleman process, any buffers allocated for performed calls.
-static StaticInfallibleVector<void*> gAllocatedBuffers;
+// In a replaying process, all middleman call state. In a middleman process,
+// state for the child currently being processed.
+static MiddlemanCallState* gState;
 
-// Lock protecting middleman call state.
+// In a middleman process, middleman call state for each child process, indexed
+// by the child ID.
+static StaticInfallibleVector<MiddlemanCallState*> gStatePerChild;
+
+// In a replaying process, lock protecting middleman call state. In the
+// middleman, all accesses occur on the main thread.
 static Monitor* gMonitor;
 
 void InitializeMiddlemanCalls() {
   MOZ_RELEASE_ASSERT(IsRecordingOrReplaying() || IsMiddleman());
 
-  gMiddlemanCallMap = new MiddlemanCallMap();
-  gMonitor = new Monitor();
+  if (IsReplaying()) {
+    gState = new MiddlemanCallState();
+    gMonitor = new Monitor();
+  }
 }
 
 // Apply the ReplayInput phase to aCall and any calls it depends on that have
 // not been sent to the middleman yet, filling aOutgoingCalls with the set of
 // such calls.
 static bool GatherDependentCalls(
     InfallibleVector<MiddlemanCall*>& aOutgoingCalls, MiddlemanCall* aCall) {
   MOZ_RELEASE_ASSERT(!aCall->mSent);
@@ -74,31 +90,31 @@ bool SendCallToMiddleman(size_t aCallId,
   MOZ_RELEASE_ASSERT(IsReplaying());
 
   const Redirection& redirection = GetRedirection(aCallId);
   MOZ_RELEASE_ASSERT(redirection.mMiddlemanCall);
 
   MonitorAutoLock lock(*gMonitor);
 
   // Allocate and fill in a new MiddlemanCall.
-  size_t id = gMiddlemanCalls.length();
+  size_t id = gState->mCalls.length();
   MiddlemanCall* newCall = new MiddlemanCall();
-  gMiddlemanCalls.emplaceBack(newCall);
+  gState->mCalls.emplaceBack(newCall);
   newCall->mId = id;
   newCall->mCallId = aCallId;
   newCall->mArguments.CopyFrom(aArguments);
 
   // Perform the ReplayPreface phase on the new call.
   {
     MiddlemanCallContext cx(newCall, aArguments,
                             MiddlemanCallPhase::ReplayPreface);
     redirection.mMiddlemanCall(cx);
     if (cx.mFailed) {
       delete newCall;
-      gMiddlemanCalls.popBack();
+      gState->mCalls.popBack();
       if (child::CurrentRepaintCannotFail()) {
         child::ReportFatalError(Nothing(),
                                 "Middleman call preface failed: %s\n",
                                 redirection.mName);
       }
       return false;
     }
   }
@@ -149,20 +165,28 @@ bool SendCallToMiddleman(size_t aCallId,
   // Perform the ReplayOutput phase to fill in outputs for the current call.
   newCall->mArguments.CopyTo(aArguments);
   MiddlemanCallContext cx(newCall, aArguments,
                           MiddlemanCallPhase::ReplayOutput);
   redirection.mMiddlemanCall(cx);
   return true;
 }
 
-void ProcessMiddlemanCall(const char* aInputData, size_t aInputSize,
+void ProcessMiddlemanCall(size_t aChildId, const char* aInputData, size_t aInputSize,
                           InfallibleVector<char>* aOutputData) {
   MOZ_RELEASE_ASSERT(IsMiddleman());
 
+  while (aChildId >= gStatePerChild.length()) {
+    gStatePerChild.append(nullptr);
+  }
+  if (!gStatePerChild[aChildId]) {
+    gStatePerChild[aChildId] = new MiddlemanCallState();
+  }
+  gState = gStatePerChild[aChildId];
+
   BufferStream inputStream(aInputData, aInputSize);
   BufferStream outputStream(aOutputData);
 
   while (!inputStream.IsEmpty()) {
     MiddlemanCall* call = new MiddlemanCall();
     call->DecodeInput(inputStream);
 
     const Redirection& redirection = GetRedirection(call->mCallId);
@@ -187,89 +211,102 @@ void ProcessMiddlemanCall(const char* aI
       MiddlemanCallContext cx(call, &arguments,
                               MiddlemanCallPhase::MiddlemanOutput);
       redirection.mMiddlemanCall(cx);
     }
 
     call->mArguments.CopyFrom(&arguments);
     call->EncodeOutput(outputStream);
 
-    while (call->mId >= gMiddlemanCalls.length()) {
-      gMiddlemanCalls.emplaceBack(nullptr);
+    while (call->mId >= gState->mCalls.length()) {
+      gState->mCalls.emplaceBack(nullptr);
     }
-    MOZ_RELEASE_ASSERT(!gMiddlemanCalls[call->mId]);
-    gMiddlemanCalls[call->mId] = call;
+    MOZ_RELEASE_ASSERT(!gState->mCalls[call->mId]);
+    gState->mCalls[call->mId] = call;
   }
+
+  gState = nullptr;
 }
 
 void* MiddlemanCallContext::AllocateBytes(size_t aSize) {
   void* rv = malloc(aSize);
 
   // In a middleman process, any buffers we allocate live until the calls are
   // reset. In a replaying process, the buffers will either live forever
   // (if they are allocated in the ReplayPreface phase, to match the lifetime
   // of the MiddlemanCall itself) or will be recovered when we rewind after we
   // are done with our divergence from the recording (any other phase).
   if (IsMiddleman()) {
-    gAllocatedBuffers.append(rv);
+    gState->mAllocatedBuffers.append(rv);
   }
 
   return rv;
 }
 
-void ResetMiddlemanCalls() {
+void ResetMiddlemanCalls(size_t aChildId) {
   MOZ_RELEASE_ASSERT(IsMiddleman());
 
-  for (MiddlemanCall* call : gMiddlemanCalls) {
+  if (aChildId >= gStatePerChild.length()) {
+    return;
+  }
+
+  gState = gStatePerChild[aChildId];
+  if (!gState) {
+    return;
+  }
+
+  for (MiddlemanCall* call : gState->mCalls) {
     if (call) {
       CallArguments arguments;
       call->mArguments.CopyTo(&arguments);
 
       MiddlemanCallContext cx(call, &arguments,
                               MiddlemanCallPhase::MiddlemanRelease);
       GetRedirection(call->mCallId).mMiddlemanCall(cx);
     }
   }
 
   // Delete the calls in a second pass. The MiddlemanRelease phase depends on
   // previous middleman calls still existing.
-  for (MiddlemanCall* call : gMiddlemanCalls) {
+  for (MiddlemanCall* call : gState->mCalls) {
     delete call;
   }
 
-  gMiddlemanCalls.clear();
-  for (auto buffer : gAllocatedBuffers) {
+  gState->mCalls.clear();
+  for (auto buffer : gState->mAllocatedBuffers) {
     free(buffer);
   }
-  gAllocatedBuffers.clear();
-  gMiddlemanCallMap->clear();
+  gState->mAllocatedBuffers.clear();
+  gState->mCallMap.clear();
+
+  gState = nullptr;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // System Values
 ///////////////////////////////////////////////////////////////////////////////
 
 static void AddMiddlemanCallValue(const void* aThing, MiddlemanCall* aCall) {
-  gMiddlemanCallMap->erase(aThing);
-  gMiddlemanCallMap->insert(MiddlemanCallMap::value_type(aThing, aCall));
+  gState->mCallMap.erase(aThing);
+  gState->mCallMap.insert(MiddlemanCallMap::value_type(aThing, aCall));
 }
 
 static MiddlemanCall* LookupMiddlemanCall(const void* aThing) {
-  MiddlemanCallMap::const_iterator iter = gMiddlemanCallMap->find(aThing);
-  if (iter != gMiddlemanCallMap->end()) {
+  MiddlemanCallMap::const_iterator iter = gState->mCallMap.find(aThing);
+  if (iter != gState->mCallMap.end()) {
     return iter->second;
   }
   return nullptr;
 }
 
 static const void* GetMiddlemanCallValue(size_t aId) {
   MOZ_RELEASE_ASSERT(IsMiddleman());
-  MOZ_RELEASE_ASSERT(aId < gMiddlemanCalls.length() && gMiddlemanCalls[aId] &&
-                     gMiddlemanCalls[aId]->mMiddlemanValue.isSome());
-  return gMiddlemanCalls[aId]->mMiddlemanValue.ref();
+  MOZ_RELEASE_ASSERT(aId < gState->mCalls.length() && gState->mCalls[aId] &&
+                     gState->mCalls[aId]->mMiddlemanValue.isSome());
+  return gState->mCalls[aId]->mMiddlemanValue.ref();
 }
 
 bool MM_SystemInput(MiddlemanCallContext& aCx, const void** aThingPtr) {
   MOZ_RELEASE_ASSERT(aCx.AccessPreface());
 
   if (!*aThingPtr) {
     // Null values are handled by the normal argument copying logic.
     return true;
@@ -288,17 +325,17 @@ bool MM_SystemInput(MiddlemanCallContext
   aCx.ReadOrWritePrefaceBytes(&callId, sizeof(callId));
 
   switch (aCx.mPhase) {
     case MiddlemanCallPhase::ReplayPreface:
       return true;
     case MiddlemanCallPhase::ReplayInput:
       if (callId.isSome()) {
         aCx.WriteInputScalar(callId.ref());
-        aCx.mDependentCalls->append(gMiddlemanCalls[callId.ref()]);
+        aCx.mDependentCalls->append(gState->mCalls[callId.ref()]);
         return true;
       }
       return false;
     case MiddlemanCallPhase::MiddlemanInput:
       if (callId.isSome()) {
         size_t callIndex = aCx.ReadInputScalar();
         *aThingPtr = GetMiddlemanCallValue(callIndex);
         return true;
--- a/toolkit/recordreplay/MiddlemanCall.h
+++ b/toolkit/recordreplay/MiddlemanCall.h
@@ -338,22 +338,24 @@ struct MiddlemanCallContext {
 // aDiverged is set if the current thread has diverged from the recording and
 // any outputs for the call must be filled in; otherwise, they already have
 // been filled in using data from the recording. Returns false if the call was
 // unable to be processed.
 bool SendCallToMiddleman(size_t aCallId, CallArguments* aArguments,
                          bool aDiverged);
 
 // In the middleman process, perform one or more calls encoded in aInputData
-// and encode their outputs to aOutputData.
-void ProcessMiddlemanCall(const char* aInputData, size_t aInputSize,
+// and encode their outputs to aOutputData. The calls are associated with the
+// specified child process ID.
+void ProcessMiddlemanCall(size_t aChildId,
+                          const char* aInputData, size_t aInputSize,
                           InfallibleVector<char>* aOutputData);
 
-// In the middleman process, reset all call state.
-void ResetMiddlemanCalls();
+// In the middleman process, reset all call state for a child process ID.
+void ResetMiddlemanCalls(size_t aChildId);
 
 ///////////////////////////////////////////////////////////////////////////////
 // Middleman Call Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
 // Capture the contents of an input buffer at BufferArg with element count at
 // CountArg.
 template <size_t BufferArg, size_t CountArg, typename ElemType = char>
--- a/toolkit/recordreplay/ipc/Channel.h
+++ b/toolkit/recordreplay/ipc/Channel.h
@@ -429,24 +429,16 @@ struct BinaryMessage : public Message {
 
 typedef BinaryMessage<MessageType::MiddlemanCallRequest>
     MiddlemanCallRequestMessage;
 typedef BinaryMessage<MessageType::MiddlemanCallResponse>
     MiddlemanCallResponseMessage;
 typedef EmptyMessage<MessageType::ResetMiddlemanCalls>
     ResetMiddlemanCallsMessage;
 
-static inline MiddlemanCallResponseMessage* ProcessMiddlemanCallMessage(
-    const MiddlemanCallRequestMessage& aMsg) {
-  InfallibleVector<char> outputData;
-  ProcessMiddlemanCall(aMsg.BinaryData(), aMsg.BinaryDataSize(), &outputData);
-  return MiddlemanCallResponseMessage::New(outputData.begin(),
-                                           outputData.length());
-}
-
 class Channel {
  public:
   // Note: the handler is responsible for freeing its input message. It will be
   // called on the channel's message thread.
   typedef std::function<void(Message::UniquePtr)> MessageHandler;
 
  private:
   // ID for this channel, unique for the middleman.
--- a/toolkit/recordreplay/ipc/ChildProcess.cpp
+++ b/toolkit/recordreplay/ipc/ChildProcess.cpp
@@ -89,22 +89,27 @@ void ChildProcessInfo::OnIncomingMessage
       js::OnDebuggerResponse(aMsg);
       break;
     case MessageType::RecordingFlushed:
       mPaused = true;
       break;
     case MessageType::MiddlemanCallRequest: {
       const MiddlemanCallRequestMessage& nmsg =
           static_cast<const MiddlemanCallRequestMessage&>(aMsg);
-      Message::UniquePtr response(ProcessMiddlemanCallMessage(nmsg));
+      InfallibleVector<char> outputData;
+      ProcessMiddlemanCall(GetId(), nmsg.BinaryData(), nmsg.BinaryDataSize(),
+                           &outputData);
+      Message::UniquePtr response(
+        MiddlemanCallResponseMessage::New(outputData.begin(),
+                                          outputData.length()));
       SendMessage(*response);
       break;
     }
     case MessageType::ResetMiddlemanCalls:
-      ResetMiddlemanCalls();
+      ResetMiddlemanCalls(GetId());
       break;
     default:
       break;
   }
 }
 
 void ChildProcessInfo::SendMessage(const Message& aMsg) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
--- a/toolkit/xre/nsUpdateDriver.cpp
+++ b/toolkit/xre/nsUpdateDriver.cpp
@@ -631,23 +631,18 @@ static void ApplyUpdate(nsIFile *greDir,
 #if defined(XP_UNIX) && !defined(XP_MACOSX)
   // We use execv to spawn the updater process on all UNIX systems except Mac
   // OSX since it is known to cause problems on the Mac.  Windows has execv, but
   // it is a faked implementation that doesn't really replace the current
   // process. Instead it spawns a new process, so we gain nothing from using
   // execv on Windows.
   if (restart) {
     exit(execv(updaterPath.get(), argv));
-  }
-  *outpid = fork();
-  if (*outpid == -1) {
-    delete[] argv;
-    return;
-  } else if (*outpid == 0) {
-    exit(execv(updaterPath.get(), argv));
+  } else {
+    *outpid = PR_CreateProcess(updaterPath.get(), argv, nullptr, nullptr);
   }
   delete[] argv;
 #elif defined(XP_WIN)
   if (isStaged) {
     // Launch the updater to replace the installation with the staged updated.
     if (!WinLaunchChild(updaterPathW.get(), argc, argv)) {
       delete[] argv;
       return;
@@ -702,35 +697,16 @@ static bool ProcessHasTerminated(Process
   if (WaitForSingleObject(pt, 1000)) {
     return false;
   }
   CloseHandle(pt);
   return true;
 #elif defined(XP_MACOSX)
   // We're waiting for the process to terminate in LaunchChildMac.
   return true;
-#elif defined(XP_UNIX)
-  int exitStatus;
-  pid_t exited = waitpid(pt, &exitStatus, WNOHANG);
-  if (exited == 0) {
-    // Process is still running.
-    sleep(1);
-    return false;
-  }
-  if (exited == -1) {
-    LOG(("Error while checking if the updater process is finished"));
-    // This shouldn't happen, but if it does, the updater process is lost to us,
-    // so the best we can do is pretend that it's exited.
-    return true;
-  }
-  // If we get here, the process has exited; make sure it exited normally.
-  if (WIFEXITED(exitStatus) && (WEXITSTATUS(exitStatus) != 0)) {
-    LOG(("Error while running the updater process, check update.log"));
-  }
-  return true;
 #else
   // No way to have a non-blocking implementation on these platforms,
   // because we're using NSPR and it only provides a blocking wait.
   int32_t exitCode;
   PR_WaitProcess(pt, &exitCode);
   if (exitCode != 0) {
     LOG(("Error while running the updater process, check update.log"));
   }
--- a/toolkit/xre/nsUpdateDriver.h
+++ b/toolkit/xre/nsUpdateDriver.h
@@ -14,17 +14,17 @@
 #include "nsString.h"
 #include "mozilla/Attributes.h"
 
 class nsIFile;
 
 #if defined(XP_WIN)
 #  include <windows.h>
 typedef HANDLE ProcessType;
-#elif defined(XP_UNIX)
+#elif defined(XP_MACOSX)
 typedef pid_t ProcessType;
 #else
 #  include "prproces.h"
 typedef PRProcess *ProcessType;
 #endif
 
 /**
  * This function processes any available updates.  As part of that process, it
--- a/xpcom/threads/nsThread.cpp
+++ b/xpcom/threads/nsThread.cpp
@@ -861,17 +861,18 @@ nsThread::Shutdown() {
   // XXX If we make this warn, then we hit that warning at xpcom shutdown while
   //     shutting down a thread in a thread pool.  That happens b/c the thread
   //     in the thread pool is already shutdown by the thread manager.
   if (!mThread) {
     return NS_OK;
   }
 
   nsThreadShutdownContext* maybeContext = ShutdownInternal(/* aSync = */ true);
-  NS_ENSURE_TRUE(maybeContext, NS_ERROR_UNEXPECTED);
+  if (!maybeContext) return NS_ERROR_UNEXPECTED;
+
   NotNull<nsThreadShutdownContext*> context = WrapNotNull(maybeContext);
 
   // Process events on the current thread until we receive a shutdown ACK.
   // Allows waiting; ensure no locks are held that would deadlock us!
   SpinEventLoopUntil([&, context]() { return !context->mAwaitingShutdownAck; },
                      context->mJoiningThread);
 
   ShutdownComplete(context);