Merge inbound to mozilla-central. a=merge
authorBrindusan Cristian <cbrindusan@mozilla.com>
Sun, 18 Nov 2018 13:06:19 +0200
changeset 446936 b3ceae83e290
parent 446929 e432617f7098 (current diff)
parent 446935 2b13a09333e7 (diff)
child 446937 7e9cac76980a
push id35059
push usercbrindusan@mozilla.com
push dateSun, 18 Nov 2018 11:17:46 +0000
treeherdermozilla-central@b3ceae83e290 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -14,25 +14,44 @@
 // methods and properties to those C++ objects. These replay objects are
 // created in the middleman process, and describe things that exist in the
 // recording/replaying process, inspecting them via the RecordReplayControl
 // interface.
 
 "use strict";
 
 const RecordReplayControl = !isWorker && require("RecordReplayControl");
+const Services = require("Services");
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebugger
 ///////////////////////////////////////////////////////////////////////////////
 
+// Possible preferred directions of travel.
+const Direction = {
+  FORWARD: "FORWARD",
+  BACKWARD: "BACKWARD",
+  NONE: "NONE",
+};
+
 function ReplayDebugger() {
-  RecordReplayControl.registerReplayDebugger(this);
+  const existing = RecordReplayControl.registerReplayDebugger(this);
+  if (existing) {
+    // There is already a ReplayDebugger in existence, use that. There can only
+    // be one ReplayDebugger in the process.
+    return existing;
+  }
 
-  // All breakpoints (per BreakpointPosition) installed by this debugger.
+  // Whether the process is currently paused.
+  this._paused = false;
+
+  // 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
   // current position, indexed by their index (zero is the oldest frame, with
   // the index increasing for newer frames). These are invalidated when
   // unpausing.
   this._frames = [];
 
@@ -40,100 +59,326 @@ function ReplayDebugger() {
   // created while paused at the current position, indexed by their id. These
   // are invalidated when unpausing.
   this._objects = [];
 
   // All ReplayDebuggerScripts and ReplayDebuggerScriptSources that have been
   // created, indexed by their id. These stay valid even after unpausing.
   this._scripts = [];
   this._scriptSources = [];
+
+  // How many nested thread-wide paused have been entered.
+  this._threadPauseCount = 0;
+
+  // 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;
+
+  // 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;
 }
 
 // Frame index used to refer to the newest frame in the child process.
 const NewestFrameIndex = -1;
 
 ReplayDebugger.prototype = {
 
   /////////////////////////////////////////////////////////
   // General methods
   /////////////////////////////////////////////////////////
 
   replaying: true,
 
   canRewind: RecordReplayControl.canRewind,
-  replayResumeBackward() { RecordReplayControl.resume(/* forward = */ false); },
-  replayResumeForward() { RecordReplayControl.resume(/* forward = */ true); },
-  replayTimeWarp: RecordReplayControl.timeWarp,
-
-  replayPause() {
-    RecordReplayControl.pause();
-    this._repaint();
-  },
 
   replayCurrentExecutionPoint() {
     return this._sendRequest({ type: "currentExecutionPoint" });
   },
 
   replayRecordingEndpoint() {
     return this._sendRequest({ type: "recordingEndpoint" });
   },
 
   replayIsRecording: RecordReplayControl.childIsRecording,
 
   addDebuggee() {},
   removeAllDebuggees() {},
 
   replayingContent(url) {
+    this._ensurePaused();
     return this._sendRequest({ type: "getContent", url });
   },
 
+  // Send a request object to the child process, and synchronously wait for it
+  // to respond.
   _sendRequest(request) {
+    assert(this._paused);
     const data = RecordReplayControl.sendRequest(request);
-    //dump("SendRequest: " +
-    //     JSON.stringify(request) + " -> " + JSON.stringify(data) + "\n");
+    dumpv("SendRequest: " +
+          JSON.stringify(request) + " -> " + JSON.stringify(data));
     if (data.exception) {
       ThrowError(data.exception);
     }
     return data;
   },
 
   // Send a request that requires the child process to perform actions that
   // diverge from the recording. In such cases we want to be interacting with a
   // replaying process (if there is one), as recording child processes won't
   // provide useful responses to such requests.
   _sendRequestAllowDiverge(request) {
+    assert(this._paused);
     RecordReplayControl.maybeSwitchToReplayingChild();
     return this._sendRequest(request);
   },
 
   // Update graphics according to the current state of the child process. This
   // should be done anytime we pause and allow the user to interact with the
   // debugger.
   _repaint() {
     const rv = this._sendRequestAllowDiverge({ type: "repaint" });
     if ("width" in rv && "height" in rv) {
       RecordReplayControl.hadRepaint(rv.width, rv.height);
     } else {
       RecordReplayControl.hadRepaintFailure();
     }
   },
 
+  /////////////////////////////////////////////////////////
+  // Paused/running state
+  /////////////////////////////////////////////////////////
+
+  // Paused State Management
+  //
+  // The single ReplayDebugger is exclusively responsible for controlling the
+  // position of the child process by keeping track of when it pauses and
+  // sending it commands to resume.
+  //
+  // The general goal of controlling this position is to make the child process
+  // execute at predictable times, similar to how it would execute if the
+  // debuggee was in the same process as this one (as is the case when not
+  // replaying), as described below:
+  //
+  // - After the child pauses, the it will only resume executing when an event
+  //   loop is running that is *not* associated with the thread actor's nested
+  //   pauses. As long as the thread actor has pushed a pause, the child will
+  //   remain paused.
+  //
+  // - After the child resumes, installed breakpoint handlers will only execute
+  //   when an event loop is running (which, because of the above point, cannot
+  //   be associated with a thread actor's nested pause).
+
+  replayResumeBackward() { this._resume(/* forward = */ false); },
+  replayResumeForward() { this._resume(/* forward = */ true); },
+
+  _resume(forward) {
+    this._ensurePaused();
+    this._setResume(() => {
+      this._paused = false;
+      this._direction = forward ? Direction.FORWARD : Direction.BACKWARD;
+      dumpv("Resuming " + this._direction);
+      RecordReplayControl.resume(forward);
+      if (this._paused) {
+        // If we resume and immediately pause, we are at an endpoint of the
+        // recording. Force the thread to pause.
+        this.replayingOnForcedPause(this.getNewestFrame());
+      }
+    });
+  },
+
+  replayTimeWarp(target) {
+    this._ensurePaused();
+    this._setResume(() => {
+      this._paused = false;
+      this._direction = Direction.NONE;
+      dumpv("Warping " + JSON.stringify(target));
+      RecordReplayControl.timeWarp(target);
+
+      // timeWarp() doesn't return until the child has reached the target of
+      // the warp, after which we force the thread to pause.
+      assert(this._paused);
+      this.replayingOnForcedPause(this.getNewestFrame());
+    });
+  },
+
+  replayPause() {
+    this._ensurePaused();
+
+    // Cancel any pending resume.
+    this._resumeCallback = null;
+  },
+
+  _ensurePaused() {
+    if (!this._paused) {
+      RecordReplayControl.waitUntilPaused();
+      assert(this._paused);
+    }
+  },
+
+  // This hook is called whenever the child has paused, which can happen
+  // within a RecordReplayControl method (resume, timeWarp, waitUntilPaused) or
+  // or be delivered via the event loop.
+  _onPause() {
+    this._paused = true;
+
+    // The position change handler is always called on pause notifications.
+    if (this.replayingOnPositionChange) {
+      this.replayingOnPositionChange();
+    }
+
+    // Call _performPause() soon via the event loop to check for breakpoint
+    // handlers at this point.
+    this._cancelPerformPause = false;
+    Services.tm.dispatchToMainThread(this._performPause.bind(this));
+  },
+
+  _performPause() {
+    // The child paused at some time in the past and any breakpoint handlers
+    // may still need to be called. If we've entered a thread-wide pause or
+    // have already told the child to resume, don't call handlers.
+    if (!this._paused || this._cancelPerformPause || this._resumeCallback) {
+      return;
+    }
+
+    const point = this.replayCurrentExecutionPoint();
+    dumpv("PerformPause " + JSON.stringify(point));
+
+    if (point.position.kind == "Invalid") {
+      // We paused at a checkpoint, and there are no handlers to call.
+    } else {
+      // Call any handlers for this point, unless one resumes execution.
+      for (const { handler, position } of this._breakpoints) {
+        if (RecordReplayControl.positionSubsumes(position, point.position)) {
+          handler();
+          assert(!this._threadPauseCount);
+          if (this._resumeCallback) {
+            break;
+          }
+        }
+      }
+    }
+
+    // If no handlers entered a thread-wide pause (resetting this._direction)
+    // or gave an explicit resume, continue traveling in the same direction
+    // we were going when we paused.
+    assert(!this._threadPauseCount);
+    if (!this._resumeCallback) {
+      switch (this._direction) {
+      case Direction.FORWARD: this.replayResumeForward(); break;
+      case Direction.BACKWARD: this.replayResumeBackward(); break;
+      }
+    }
+  },
+
+  // This hook is called whenever we switch between recording and replaying
+  // child processes.
+  _onSwitchChild() {
+    // The position change handler listens to changes to the current child.
+    if (this.replayingOnPositionChange) {
+      // Children are paused whenever we switch between them.
+      const paused = this._paused;
+      this._paused = true;
+      this.replayingOnPositionChange();
+      this._paused = paused;
+    }
+  },
+
+  replayPushThreadPause() {
+    // The thread has paused so that the user can interact with it. The child
+    // will stay paused until this thread-wide pause has been popped.
+    assert(this._paused);
+    assert(!this._resumeCallback);
+    if (++this._threadPauseCount == 1) {
+      // Save checkpoints near the current position in case the user rewinds.
+      RecordReplayControl.markExplicitPause();
+
+      // There is no preferred direction of travel after an explicit pause.
+      this._direction = Direction.NONE;
+
+      // Update graphics according to the current state of the child.
+      this._repaint();
+
+      // If breakpoint handlers for the pause haven't been called yet, don't
+      // call them at all.
+      this._cancelPerformPause = true;
+    }
+    const point = this.replayCurrentExecutionPoint();
+    dumpv("PushPause " + JSON.stringify(point));
+  },
+
+  replayPopThreadPause() {
+    dumpv("PopPause");
+
+    // After popping the last thread-wide pause, the child can resume.
+    if (--this._threadPauseCount == 0 && this._resumeCallback) {
+      Services.tm.dispatchToMainThread(this._performResume.bind(this));
+    }
+  },
+
+  _setResume(callback) {
+    assert(this._paused);
+
+    // Overwrite any existing resume direction.
+    this._resumeCallback = callback;
+
+    // The child can resume immediately if there is no thread-wide pause.
+    if (!this._threadPauseCount) {
+      Services.tm.dispatchToMainThread(this._performResume.bind(this));
+    }
+  },
+
+  _performResume() {
+    assert(this._paused && !this._threadPauseCount);
+    if (this._resumeCallback && !this._threadPauseCount) {
+      const callback = this._resumeCallback;
+      this._invalidateAfterUnpause();
+      this._resumeCallback = null;
+      callback();
+    }
+  },
+
+  // Clear out all data that becomes invalid when the child unpauses.
+  _invalidateAfterUnpause() {
+    this._frames.forEach(frame => frame._invalidate());
+    this._frames.length = 0;
+
+    this._objects.forEach(obj => obj._invalidate());
+    this._objects.length = 0;
+  },
+
+  /////////////////////////////////////////////////////////
+  // Breakpoint management
+  /////////////////////////////////////////////////////////
+
   _setBreakpoint(handler, position, data) {
-    const id = RecordReplayControl.setBreakpoint(handler, position);
-    this._breakpoints.push({id, position, data});
+    this._ensurePaused();
+    dumpv("AddBreakpoint " + JSON.stringify(position));
+    RecordReplayControl.addBreakpoint(position);
+    this._breakpoints.push({handler, position, data});
   },
 
   _clearMatchingBreakpoints(callback) {
-    this._breakpoints = this._breakpoints.filter(breakpoint => {
-      if (callback(breakpoint)) {
-        RecordReplayControl.clearBreakpoint(breakpoint.id);
-        return false;
+    this._ensurePaused();
+    const newBreakpoints = this._breakpoints.filter(bp => !callback(bp));
+    if (newBreakpoints.length != this._breakpoints.length) {
+      dumpv("ClearBreakpoints");
+      RecordReplayControl.clearBreakpoints();
+      for (const { position } of newBreakpoints) {
+        dumpv("AddBreakpoint " + JSON.stringify(position));
+        RecordReplayControl.addBreakpoint(position);
       }
-      return true;
-    });
+    }
+    this._breakpoints = newBreakpoints;
   },
 
   _searchBreakpoints(callback) {
     for (const breakpoint of this._breakpoints) {
       const v = callback(breakpoint);
       if (v) {
         return v;
       }
@@ -152,26 +397,16 @@ ReplayDebugger.prototype = {
   _breakpointKindSetter(kind, handler, callback) {
     if (handler) {
       this._setBreakpoint(callback, { kind }, handler);
     } else {
       this._clearMatchingBreakpoints(({position}) => position.kind == kind);
     }
   },
 
-  // This is called on all ReplayDebuggers whenever the child process is about
-  // to unpause. Clear out all data that is invalidated as a result.
-  invalidateAfterUnpause() {
-    this._frames.forEach(frame => frame._invalidate());
-    this._frames.length = 0;
-
-    this._objects.forEach(obj => obj._invalidate());
-    this._objects.length = 0;
-  },
-
   /////////////////////////////////////////////////////////
   // Script methods
   /////////////////////////////////////////////////////////
 
   _getScript(id) {
     if (!id) {
       return null;
     }
@@ -206,16 +441,17 @@ ReplayDebugger.prototype = {
     const data = this._sendRequest({
       type: "findScripts",
       query: this._convertScriptQuery(query),
     });
     return data.map(script => this._addScript(script));
   },
 
   findAllConsoleMessages() {
+    this._ensurePaused();
     const messages = this._sendRequest({ type: "findConsoleMessages" });
     return messages.map(this._convertConsoleMessage.bind(this));
   },
 
   /////////////////////////////////////////////////////////
   // ScriptSource methods
   /////////////////////////////////////////////////////////
 
@@ -230,16 +466,17 @@ ReplayDebugger.prototype = {
   _addSource(data) {
     if (!this._scriptSources[data.id]) {
       this._scriptSources[data.id] = new ReplayDebuggerScriptSource(this, data);
     }
     return this._scriptSources[data.id];
   },
 
   findSources() {
+    this._ensurePaused();
     const data = this._sendRequest({ type: "findSources" });
     return data.map(source => this._addSource(source));
   },
 
   /////////////////////////////////////////////////////////
   // Object methods
   /////////////////////////////////////////////////////////
 
@@ -358,55 +595,36 @@ ReplayDebugger.prototype = {
   set onNewScript(handler) {
     this._breakpointKindSetter("NewScript", handler,
                                () => handler.call(this, this._getNewScript()));
   },
 
   get onEnterFrame() { return this._breakpointKindGetter("EnterFrame"); },
   set onEnterFrame(handler) {
     this._breakpointKindSetter("EnterFrame", handler,
-                               () => { this._repaint();
-                                       handler.call(this, this.getNewestFrame()); });
+                               () => { handler.call(this, this.getNewestFrame()); });
   },
 
   get replayingOnPopFrame() {
     return this._searchBreakpoints(({position, data}) => {
       return (position.kind == "OnPop" && !position.script) ? data : null;
     });
   },
 
   set replayingOnPopFrame(handler) {
     if (handler) {
-      this._setBreakpoint(() => { this._repaint();
-                                  handler.call(this, this.getNewestFrame()); },
+      this._setBreakpoint(() => { handler.call(this, this.getNewestFrame()); },
                           { kind: "OnPop" }, handler);
     } else {
       this._clearMatchingBreakpoints(({position}) => {
         return position.kind == "OnPop" && !position.script;
       });
     }
   },
 
-  get replayingOnForcedPause() {
-    return this._breakpointKindGetter("ForcedPause");
-  },
-  set replayingOnForcedPause(handler) {
-    this._breakpointKindSetter("ForcedPause", handler,
-                               () => { this._repaint();
-                                       handler.call(this, this.getNewestFrame()); });
-  },
-
-  get replayingOnPositionChange() {
-    return this._breakpointKindGetter("PositionChange");
-  },
-  set replayingOnPositionChange(handler) {
-    this._breakpointKindSetter("PositionChange", handler,
-                               () => { handler.call(this); });
-  },
-
   getNewConsoleMessage() {
     const message = this._sendRequest({ type: "getNewConsoleMessage" });
     return this._convertConsoleMessage(message);
   },
 
   get onConsoleMessage() {
     return this._breakpointKindGetter("ConsoleMessage");
   },
@@ -442,18 +660,17 @@ ReplayDebuggerScript.prototype = {
   },
 
   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); },
 
   setBreakpoint(offset, handler) {
-    this._dbg._setBreakpoint(() => { this._dbg._repaint();
-                                     handler.hit(this._dbg.getNewestFrame()); },
+    this._dbg._setBreakpoint(() => { handler.hit(this._dbg.getNewestFrame()); },
                              { kind: "Break", script: this._data.id, offset },
                              handler);
   },
 
   clearBreakpoint(handler) {
     this._dbg._clearMatchingBreakpoints(({position, data}) => {
       return position.script == this._data.id && handler == data;
     });
@@ -560,18 +777,17 @@ ReplayDebuggerFrame.prototype = {
       ({position}) => this._positionMatches(position, "OnStep")
     );
   },
 
   setReplayingOnStep(handler, offsets) {
     this._clearOnStepBreakpoints();
     offsets.forEach(offset => {
       this._dbg._setBreakpoint(
-        () => { this._dbg._repaint();
-                handler.call(this._dbg.getNewestFrame()); },
+        () => { handler.call(this._dbg.getNewestFrame()); },
         { kind: "OnStep",
           script: this._data.script,
           offset,
           frameIndex: this._data.index },
         handler);
     });
   },
 
@@ -579,17 +795,16 @@ ReplayDebuggerFrame.prototype = {
     return this._dbg._searchBreakpoints(({position, data}) => {
       return this._positionMatches(position, "OnPop") ? data : null;
     });
   },
 
   set onPop(handler) {
     if (handler) {
       this._dbg._setBreakpoint(() => {
-          this._dbg._repaint();
           const result = this._dbg._sendRequest({ type: "popFrameResult" });
           handler.call(this._dbg.getNewestFrame(),
                        this._dbg._convertCompletionValue(result));
         },
         { kind: "OnPop", script: this._data.script, frameIndex: this._data.index },
         handler);
     } else {
       this._dbg._clearMatchingBreakpoints(
@@ -780,16 +995,20 @@ ReplayDebuggerEnvironment.prototype = {
   find: NYI,
   setVariable: NotAllowed,
 };
 
 ///////////////////////////////////////////////////////////////////////////////
 // Utilities
 ///////////////////////////////////////////////////////////////////////////////
 
+function dumpv(str) {
+  //dump("[ReplayDebugger] " + str + "\n");
+}
+
 function NYI() {
   ThrowError("Not yet implemented");
 }
 
 function NotAllowed() {
   ThrowError("Not allowed");
 }
 
@@ -797,17 +1016,17 @@ function ThrowError(msg)
 {
   const error = new Error(msg);
   dump("ReplayDebugger Server Error: " + msg + " Stack: " + error.stack + "\n");
   throw error;
 }
 
 function assert(v) {
   if (!v) {
-    throw new Error("Assertion Failed!");
+    ThrowError("Assertion Failed!");
   }
 }
 
 function isNonNullObject(obj) {
   return obj && (typeof obj == "object" || typeof obj == "function");
 }
 
 module.exports = ReplayDebugger;
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -109,18 +109,24 @@ const ThreadActor = ActorClassWithSpec(t
   get dbg() {
     if (!this._dbg) {
       this._dbg = this._parent.makeDebugger();
       this._dbg.uncaughtExceptionHook = this.uncaughtExceptionHook;
       this._dbg.onDebuggerStatement = this.onDebuggerStatement;
       this._dbg.onNewScript = this.onNewScript;
       if (this._dbg.replaying) {
         this._dbg.replayingOnForcedPause = this.replayingOnForcedPause.bind(this);
+        const sendProgress = throttle((recording, executionPoint) => {
+          if (this.attached) {
+            this.conn.send({ type: "progress", from: this.actorID,
+                             recording, executionPoint });
+          }
+        }, 100);
         this._dbg.replayingOnPositionChange =
-          throttle(this.replayingOnPositionChange.bind(this), 100);
+          this.replayingOnPositionChange.bind(this, sendProgress);
       }
       // Keep the debugger disabled until a client attaches.
       this._dbg.enabled = this._state != "detached";
     }
     return this._dbg;
   },
 
   get globalDebugObject() {
@@ -175,27 +181,33 @@ const ThreadActor = ActorClassWithSpec(t
   /**
    * Keep track of all of the nested event loops we use to pause the debuggee
    * when we hit a breakpoint/debugger statement/etc in one place so we can
    * resolve them when we get resume packets. We have more than one (and keep
    * them in a stack) because we can pause within client evals.
    */
   _threadPauseEventLoops: null,
   _pushThreadPause: function() {
+    if (this.dbg.replaying) {
+      this.dbg.replayPushThreadPause();
+    }
     if (!this._threadPauseEventLoops) {
       this._threadPauseEventLoops = [];
     }
     const eventLoop = this._nestedEventLoops.push();
     this._threadPauseEventLoops.push(eventLoop);
     eventLoop.enter();
   },
   _popThreadPause: function() {
     const eventLoop = this._threadPauseEventLoops.pop();
     assert(eventLoop, "Should have an event loop.");
     eventLoop.resolve();
+    if (this.dbg.replaying) {
+      this.dbg.replayPopThreadPause();
+    }
   },
 
   /**
    * Remove all debuggees and clear out the thread's sources.
    */
   clearDebuggees: function() {
     if (this._dbg) {
       this.dbg.removeAllDebuggees();
@@ -1781,20 +1793,20 @@ const ThreadActor = ActorClassWithSpec(t
     return { skip };
   },
 
   /*
    * A function that the engine calls when a recording/replaying process has
    * changed its position: a checkpoint was reached or a switch between a
    * recording and replaying child process occurred.
    */
-  replayingOnPositionChange: function() {
+  replayingOnPositionChange: function(sendProgress) {
     const recording = this.dbg.replayIsRecording();
     const executionPoint = this.dbg.replayCurrentExecutionPoint();
-    this.conn.send({ type: "progress", from: this.actorID, recording, executionPoint });
+    sendProgress(recording, executionPoint);
   },
 
   /**
    * A function that the engine calls when replay has hit a point where it will
    * pause, even if no breakpoint has been set. Such points include hitting the
    * beginning or end of the replay, or reaching the target of a time warp.
    *
    * @param frame Debugger.Frame
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -322,17 +322,17 @@ NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION
       CustomElementRegistry::CallGetCustomInterface(this, aIID);
     if (iface) {
       iface->QueryInterface(aIID, aInstancePtr);
       if (*aInstancePtr) {
         return NS_OK;
       }
     }
 
-NS_INTERFACE_MAP_END_INHERITING(Element)
+NS_INTERFACE_MAP_END_INHERITING(nsStyledElement)
 
 //----------------------------------------------------------------------
 // nsINode interface
 
 nsresult
 nsXULElement::Clone(mozilla::dom::NodeInfo* aNodeInfo, nsINode** aResult) const
 {
     *aResult = nullptr;
--- a/testing/web-platform/tests/css/vendor-imports/mozilla/mozilla-central-reftests/ui3/reftest.list
+++ b/testing/web-platform/tests/css/vendor-imports/mozilla/mozilla-central-reftests/ui3/reftest.list
@@ -2,9 +2,9 @@
 == box-sizing-border-box-002.xht box-sizing-border-box-002-ref.xht
 == box-sizing-border-box-003.xht box-sizing-border-box-003-ref.xht
 == box-sizing-border-box-004.xht box-sizing-border-box-004-ref.xht
 == box-sizing-content-box-001.xht box-sizing-content-box-001-ref.xht
 == box-sizing-content-box-002.xht box-sizing-content-box-002-ref.xht
 == box-sizing-content-box-003.xht box-sizing-content-box-003-ref.xht
 == box-sizing-replaced-001.xht box-sizing-replaced-001-ref.xht
 == box-sizing-replaced-002.xht box-sizing-replaced-002-ref.xht
-== box-sizing-replaced-003.xht box-sizing-replaced-003-ref.xht
+skip-if(cocoaWidget) == box-sizing-replaced-003.xht box-sizing-replaced-003-ref.xht # Bug 1383454
--- a/toolkit/recordreplay/ipc/Channel.cpp
+++ b/toolkit/recordreplay/ipc/Channel.cpp
@@ -249,35 +249,32 @@ Channel::PrintMessage(const char* aPrefi
     data.AppendPrintf("Id %d Endpoint %d Duration %.2f ms",
                       (int) nmsg.mCheckpointId, nmsg.mRecordingEndpoint,
                       nmsg.mDurationMicroseconds / 1000.0);
     break;
   }
   case MessageType::HitBreakpoint: {
     const HitBreakpointMessage& nmsg = (const HitBreakpointMessage&) aMsg;
     data.AppendPrintf("Endpoint %d", nmsg.mRecordingEndpoint);
-    for (size_t i = 0; i < nmsg.NumBreakpoints(); i++) {
-      data.AppendPrintf(" Id %d", nmsg.Breakpoints()[i]);
-    }
     break;
   }
   case MessageType::Resume: {
     const ResumeMessage& nmsg = (const ResumeMessage&) aMsg;
     data.AppendPrintf("Forward %d", nmsg.mForward);
     break;
   }
   case MessageType::RestoreCheckpoint: {
     const RestoreCheckpointMessage& nmsg = (const RestoreCheckpointMessage&) aMsg;
     data.AppendPrintf("Id %d", (int) nmsg.mCheckpoint);
     break;
   }
-  case MessageType::SetBreakpoint: {
-    const SetBreakpointMessage& nmsg = (const SetBreakpointMessage&) aMsg;
-    data.AppendPrintf("Id %d, Kind %s, Script %d, Offset %d, Frame %d",
-                      (int) nmsg.mId, nmsg.mPosition.KindString(), (int) nmsg.mPosition.mScript,
+  case MessageType::AddBreakpoint: {
+    const AddBreakpointMessage& nmsg = (const AddBreakpointMessage&) aMsg;
+    data.AppendPrintf("Kind %s, Script %d, Offset %d, Frame %d",
+                      nmsg.mPosition.KindString(), (int) nmsg.mPosition.mScript,
                       (int) nmsg.mPosition.mOffset, (int) nmsg.mPosition.mFrameIndex);
     break;
   }
   case MessageType::DebuggerRequest: {
     const DebuggerRequestMessage& nmsg = (const DebuggerRequestMessage&) aMsg;
     data = NS_ConvertUTF16toUTF8(nsDependentString(nmsg.Buffer(), nmsg.BufferSize()));
     break;
   }
--- a/toolkit/recordreplay/ipc/Channel.h
+++ b/toolkit/recordreplay/ipc/Channel.h
@@ -69,18 +69,24 @@ namespace recordreplay {
   /* Poke a child that is recording to create an artificial checkpoint, rather than */ \
   /* (potentially) idling indefinitely. This has no effect on a replaying process. */ \
   _Macro(CreateCheckpoint)                                     \
                                                                \
   /* Debugger JSON messages are initially sent from the parent. The child unpauses */ \
   /* after receiving the message and will pause after it sends a DebuggerResponse. */ \
   _Macro(DebuggerRequest)                                      \
                                                                \
-  /* Set or clear a JavaScript breakpoint. */                  \
-  _Macro(SetBreakpoint)                                        \
+  /* Add a breakpoint position to stop at. Because a single entry point is used for */ \
+  /* calling into the ReplayDebugger after pausing, the set of breakpoints is simply */ \
+  /* a set of positions at which the child process should pause and send a HitBreakpoint */ \
+  /* message. */                                               \
+  _Macro(AddBreakpoint)                                        \
+                                                               \
+  /* Clear all installed breakpoints. */                       \
+  _Macro(ClearBreakpoints)                                     \
                                                                \
   /* Unpause the child and play execution either to the next point when a */ \
   /* breakpoint is hit, or to the next checkpoint. Resumption may be either */ \
   /* forward or backward. */                                   \
   _Macro(Resume)                                               \
                                                                \
   /* Rewind to a particular saved checkpoint in the past. */   \
   _Macro(RestoreCheckpoint)                                    \
@@ -275,32 +281,28 @@ struct JSONMessage : public Message
     PodCopy(res->Data<JSONMessage<Type>, char16_t>(), aBuffer, aBufferSize);
     return res;
   }
 };
 
 typedef JSONMessage<MessageType::DebuggerRequest> DebuggerRequestMessage;
 typedef JSONMessage<MessageType::DebuggerResponse> DebuggerResponseMessage;
 
-struct SetBreakpointMessage : public Message
+struct AddBreakpointMessage : public Message
 {
-  // ID of the breakpoint to change.
-  size_t mId;
-
-  // New position of the breakpoint. If this is invalid then the breakpoint is
-  // being cleared.
   js::BreakpointPosition mPosition;
 
-  SetBreakpointMessage(size_t aId, const js::BreakpointPosition& aPosition)
-    : Message(MessageType::SetBreakpoint, sizeof(*this))
-    , mId(aId)
+  explicit AddBreakpointMessage(const js::BreakpointPosition& aPosition)
+    : Message(MessageType::AddBreakpoint, sizeof(*this))
     , mPosition(aPosition)
   {}
 };
 
+typedef EmptyMessage<MessageType::ClearBreakpoints> ClearBreakpointsMessage;
+
 struct ResumeMessage : public Message
 {
   // Whether to travel forwards or backwards.
   bool mForward;
 
   explicit ResumeMessage(bool aForward)
     : Message(MessageType::Resume, sizeof(*this))
     , mForward(aForward)
@@ -416,32 +418,20 @@ struct HitCheckpointMessage : public Mes
     , mDurationMicroseconds(aDurationMicroseconds)
   {}
 };
 
 struct HitBreakpointMessage : public Message
 {
   bool mRecordingEndpoint;
 
-  HitBreakpointMessage(uint32_t aSize, bool aRecordingEndpoint)
-    : Message(MessageType::HitBreakpoint, aSize)
+  explicit HitBreakpointMessage(bool aRecordingEndpoint)
+    : Message(MessageType::HitBreakpoint, sizeof(*this))
     , mRecordingEndpoint(aRecordingEndpoint)
   {}
-
-  const uint32_t* Breakpoints() const { return Data<HitBreakpointMessage, uint32_t>(); }
-  uint32_t NumBreakpoints() const { return DataSize<HitBreakpointMessage, uint32_t>(); }
-
-  static HitBreakpointMessage* New(bool aRecordingEndpoint,
-                                   const uint32_t* aBreakpoints, size_t aNumBreakpoints) {
-    HitBreakpointMessage* res =
-      NewWithData<HitBreakpointMessage, uint32_t>(aNumBreakpoints, aRecordingEndpoint);
-    MOZ_RELEASE_ASSERT(res->NumBreakpoints() == aNumBreakpoints);
-    PodCopy(res->Data<HitBreakpointMessage, uint32_t>(), aBreakpoints, aNumBreakpoints);
-    return res;
-  }
 };
 
 typedef EmptyMessage<MessageType::AlwaysMarkMajorCheckpoints> AlwaysMarkMajorCheckpointsMessage;
 
 template <MessageType Type>
 struct BinaryMessage : public Message
 {
   explicit BinaryMessage(uint32_t aSize)
--- a/toolkit/recordreplay/ipc/ChildIPC.cpp
+++ b/toolkit/recordreplay/ipc/ChildIPC.cpp
@@ -134,23 +134,27 @@ ChannelMessageHandler(Message* aMsg)
   }
   case MessageType::DebuggerRequest: {
     const DebuggerRequestMessage& nmsg = (const DebuggerRequestMessage&) *aMsg;
     js::CharBuffer* buf = new js::CharBuffer();
     buf->append(nmsg.Buffer(), nmsg.BufferSize());
     PauseMainThreadAndInvokeCallback([=]() { navigation::DebuggerRequest(buf); });
     break;
   }
-  case MessageType::SetBreakpoint: {
-    const SetBreakpointMessage& nmsg = (const SetBreakpointMessage&) *aMsg;
+  case MessageType::AddBreakpoint: {
+    const AddBreakpointMessage& nmsg = (const AddBreakpointMessage&) *aMsg;
     PauseMainThreadAndInvokeCallback([=]() {
-        navigation::SetBreakpoint(nmsg.mId, nmsg.mPosition);
+        navigation::AddBreakpoint(nmsg.mPosition);
       });
     break;
   }
+  case MessageType::ClearBreakpoints: {
+    PauseMainThreadAndInvokeCallback([=]() { navigation::ClearBreakpoints(); });
+    break;
+  }
   case MessageType::Resume: {
     const ResumeMessage& nmsg = (const ResumeMessage&) *aMsg;
     PauseMainThreadAndInvokeCallback([=]() {
         navigation::Resume(nmsg.mForward);
       });
     break;
   }
   case MessageType::RestoreCheckpoint: {
@@ -699,24 +703,21 @@ RespondToRequest(const js::CharBuffer& a
 {
   DebuggerResponseMessage* msg =
     DebuggerResponseMessage::New(aBuffer.begin(), aBuffer.length());
   gChannel->SendMessage(*msg);
   free(msg);
 }
 
 void
-HitBreakpoint(bool aRecordingEndpoint, const uint32_t* aBreakpoints, size_t aNumBreakpoints)
+HitBreakpoint(bool aRecordingEndpoint)
 {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
-  HitBreakpointMessage* msg =
-    HitBreakpointMessage::New(aRecordingEndpoint, aBreakpoints, aNumBreakpoints);
   PauseMainThreadAndInvokeCallback([=]() {
-      gChannel->SendMessage(*msg);
-      free(msg);
+      gChannel->SendMessage(HitBreakpointMessage(aRecordingEndpoint));
     });
 }
 
 void
 SendMiddlemanCallRequest(const char* aInputData, size_t aInputSize,
                          InfallibleVector<char>* aOutputData)
 {
   AutoPassThroughThreadEvents pt;
--- a/toolkit/recordreplay/ipc/ChildInternal.h
+++ b/toolkit/recordreplay/ipc/ChildInternal.h
@@ -39,17 +39,18 @@ js::ExecutionPoint GetRecordingEndpoint(
 // the recording file as it has been flushed.
 void SetRecordingEndpoint(size_t aIndex, const js::ExecutionPoint& aEndpoint);
 
 // Save temporary checkpoints at all opportunities during navigation.
 void AlwaysSaveTemporaryCheckpoints();
 
 // Process incoming requests from the middleman.
 void DebuggerRequest(js::CharBuffer* aBuffer);
-void SetBreakpoint(size_t aId, const js::BreakpointPosition& aPosition);
+void AddBreakpoint(const js::BreakpointPosition& aPosition);
+void ClearBreakpoints();
 void Resume(bool aForward);
 void RestoreCheckpoint(size_t aId);
 void RunToPoint(const js::ExecutionPoint& aPoint);
 
 // Attempt to diverge from the recording so that new recorded events cause
 // the process to rewind. Returns false if the divergence failed: either we
 // can't rewind, or already diverged here and then had an unhandled divergence.
 bool MaybeDivergeFromRecording();
@@ -81,17 +82,17 @@ size_t LastNormalCheckpoint();
 
 } // namespace navigation
 
 namespace child {
 
 // IPC activity that can be triggered by navigation.
 void RespondToRequest(const js::CharBuffer& aBuffer);
 void HitCheckpoint(size_t aId, bool aRecordingEndpoint);
-void HitBreakpoint(bool aRecordingEndpoint, const uint32_t* aBreakpoints, size_t aNumBreakpoints);
+void HitBreakpoint(bool aRecordingEndpoint);
 
 // Optional information about a crash that occurred. If not provided to
 // ReportFatalError, the current thread will be treated as crashed.
 struct MinidumpInfo
 {
   int mExceptionType;
   int mCode;
   int mSubcode;
--- a/toolkit/recordreplay/ipc/ChildNavigation.cpp
+++ b/toolkit/recordreplay/ipc/ChildNavigation.cpp
@@ -141,18 +141,16 @@ struct RequestInfo
     : mUnhandledDivergence(o.mUnhandledDivergence)
   {
     mRequestBuffer.append(o.mRequestBuffer.begin(), o.mRequestBuffer.length());
     mResponseBuffer.append(o.mResponseBuffer.begin(), o.mResponseBuffer.length());
   }
 };
 typedef InfallibleVector<RequestInfo, 4, UntrackedAllocPolicy> UntrackedRequestVector;
 
-typedef InfallibleVector<uint32_t> BreakpointVector;
-
 // Phase when the replaying process is paused.
 class PausedPhase final : public NavigationPhase
 {
   // Location of the pause.
   ExecutionPoint mPoint;
 
   // Whether we are paused at the end of the recording.
   bool mRecordingEndpoint;
@@ -172,17 +170,16 @@ class PausedPhase final : public Navigat
   // recording divergence, and haven't finished rehandling old requests.
   bool mRecoveringFromDivergence;
 
   // Set when we were told to resume forward and need to clean up our state.
   bool mResumeForward;
 
 public:
   void Enter(const ExecutionPoint& aPoint,
-             const BreakpointVector& aBreakpoints = BreakpointVector(),
              bool aRewind = false, bool aRecordingEndpoint = false);
 
   void ToString(nsAutoCString& aStr) override {
     aStr.AppendPrintf("Paused RecoveringFromDivergence %d", mRecoveringFromDivergence);
   }
 
   void AfterCheckpoint(const CheckpointId& aCheckpoint) override;
   void PositionHit(const ExecutionPoint& aPoint) override;
@@ -315,26 +312,19 @@ class NavigationState
   // The last checkpoint we ran forward or rewound to.
   CheckpointId mLastCheckpoint;
 
   // The locations of all temporary checkpoints we have saved. Temporary
   // checkpoints are taken immediately prior to reaching these points.
   InfallibleVector<ExecutionPoint, 0, UntrackedAllocPolicy> mTemporaryCheckpoints;
 
 public:
-  // All the currently installed breakpoints, indexed by their ID.
+  // All the currently installed breakpoints.
   InfallibleVector<BreakpointPosition, 4, UntrackedAllocPolicy> mBreakpoints;
 
-  BreakpointPosition& GetBreakpoint(size_t id) {
-    while (id >= mBreakpoints.length()) {
-      mBreakpoints.emplaceBack();
-    }
-    return mBreakpoints[id];
-  }
-
   CheckpointId LastCheckpoint() {
     return mLastCheckpoint;
   }
 
   // The current phase of the process.
   NavigationPhase* mPhase;
 
   void SetPhase(NavigationPhase* phase) {
@@ -486,63 +476,47 @@ public:
   ExecutionPoint CheckpointExecutionPoint(size_t aCheckpoint) {
     MOZ_RELEASE_ASSERT(aCheckpoint < mCheckpointProgress.length());
     return ExecutionPoint(aCheckpoint, mCheckpointProgress[aCheckpoint]);
   }
 };
 
 static NavigationState* gNavigation;
 
-static void
-GetAllBreakpointHits(const ExecutionPoint& aPoint, BreakpointVector& aHitBreakpoints)
-{
-  MOZ_RELEASE_ASSERT(aPoint.HasPosition());
-  for (size_t id = 0; id < gNavigation->mBreakpoints.length(); id++) {
-    const BreakpointPosition& breakpoint = gNavigation->mBreakpoints[id];
-    if (breakpoint.IsValid() && breakpoint.Subsumes(aPoint.mPosition)) {
-      aHitBreakpoints.append(id);
-    }
-  }
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Paused Phase
 ///////////////////////////////////////////////////////////////////////////////
 
 static bool
 ThisProcessCanRewind()
 {
   return HasSavedCheckpoint();
 }
 
 void
-PausedPhase::Enter(const ExecutionPoint& aPoint, const BreakpointVector& aBreakpoints,
-                   bool aRewind, bool aRecordingEndpoint)
+PausedPhase::Enter(const ExecutionPoint& aPoint, bool aRewind, bool aRecordingEndpoint)
 {
   mPoint = aPoint;
   mRecordingEndpoint = aRecordingEndpoint;
   mRequests.clear();
   mRequestIndex = 0;
   mSavedTemporaryCheckpoint = false;
   mRecoveringFromDivergence = false;
   mResumeForward = false;
 
   gNavigation->SetPhase(this);
 
-  // Breakpoints will never be hit if we are at a checkpoint.
-  MOZ_RELEASE_ASSERT(aPoint.HasPosition() || aBreakpoints.empty());
-
   if (aRewind) {
     MOZ_RELEASE_ASSERT(!aPoint.HasPosition());
     RestoreCheckpointAndResume(CheckpointId(aPoint.mCheckpoint));
     Unreachable();
   }
 
   if (aPoint.HasPosition()) {
-    child::HitBreakpoint(aRecordingEndpoint, aBreakpoints.begin(), aBreakpoints.length());
+    child::HitBreakpoint(aRecordingEndpoint);
   } else {
     child::HitCheckpoint(aPoint.mCheckpoint, aRecordingEndpoint);
   }
 }
 
 void
 PausedPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
 {
@@ -608,17 +582,17 @@ PausedPhase::Resume(bool aForward)
   Unreachable();
 }
 
 void
 PausedPhase::RestoreCheckpoint(size_t aCheckpoint)
 {
   ExecutionPoint target = gNavigation->CheckpointExecutionPoint(aCheckpoint);
   bool rewind = target != mPoint;
-  Enter(target, BreakpointVector(), rewind, /* aRecordingEndpoint = */ false);
+  Enter(target, rewind, /* aRecordingEndpoint = */ false);
 }
 
 void
 PausedPhase::RunToPoint(const ExecutionPoint& aTarget)
 {
   // This may only be used when we are paused at a normal checkpoint.
   MOZ_RELEASE_ASSERT(!mPoint.HasPosition());
   size_t checkpoint = mPoint.mCheckpoint;
@@ -803,54 +777,52 @@ void
 ForwardPhase::Enter(const ExecutionPoint& aPoint)
 {
   mPoint = aPoint;
 
   gNavigation->SetPhase(this);
 
   // Install handlers for all breakpoints.
   for (const BreakpointPosition& breakpoint : gNavigation->mBreakpoints) {
-    if (breakpoint.IsValid()) {
-      js::EnsurePositionHandler(breakpoint);
-    }
+    js::EnsurePositionHandler(breakpoint);
   }
 
   ResumeExecution();
 }
 
 void
 ForwardPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
 {
   MOZ_RELEASE_ASSERT(!aCheckpoint.mTemporary &&
                      aCheckpoint.mNormal == mPoint.mCheckpoint + 1);
   gNavigation->mPausedPhase.Enter(gNavigation->CheckpointExecutionPoint(aCheckpoint.mNormal));
 }
 
 void
 ForwardPhase::PositionHit(const ExecutionPoint& aPoint)
 {
-  BreakpointVector hitBreakpoints;
-  GetAllBreakpointHits(aPoint, hitBreakpoints);
+  bool hitBreakpoint = false;
+  for (const BreakpointPosition& breakpoint : gNavigation->mBreakpoints) {
+    if (breakpoint.Subsumes(aPoint.mPosition)) {
+      hitBreakpoint = true;
+    }
+  }
 
-  if (!hitBreakpoints.empty()) {
-    gNavigation->mPausedPhase.Enter(aPoint, hitBreakpoints);
+  if (hitBreakpoint) {
+    gNavigation->mPausedPhase.Enter(aPoint);
   }
 }
 
 void
 ForwardPhase::HitRecordingEndpoint(const ExecutionPoint& aPoint)
 {
   nsAutoCString str;
   ExecutionPointToString(aPoint, str);
 
-  // Use an empty vector even if there are breakpoints here. If we started
-  // running forward from aPoint and immediately hit the recording endpoint,
-  // we don't want to hit the breakpoints again.
-  gNavigation->mPausedPhase.Enter(aPoint, BreakpointVector(),
-                                  /* aRewind = */ false, /* aRecordingEndpoint = */ true);
+  gNavigation->mPausedPhase.Enter(aPoint, /* aRewind = */ false, /* aRecordingEndpoint = */ true);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReachBreakpointPhase
 ///////////////////////////////////////////////////////////////////////////////
 
 void
 ReachBreakpointPhase::Enter(const CheckpointId& aStart,
@@ -921,19 +893,17 @@ ReachBreakpointPhase::PositionHit(const 
         // We just restored the checkpoint, and could be in any phase.
         gNavigation->PositionHit(aPoint);
         return;
       }
     }
   }
 
   if (mPoint == aPoint) {
-    BreakpointVector hitBreakpoints;
-    GetAllBreakpointHits(aPoint, hitBreakpoints);
-    gNavigation->mPausedPhase.Enter(aPoint, hitBreakpoints);
+    gNavigation->mPausedPhase.Enter(aPoint);
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // FindLastHitPhase
 ///////////////////////////////////////////////////////////////////////////////
 
 void
@@ -1027,19 +997,16 @@ FindLastHitPhase::FindTrackedPosition(co
 }
 
 void
 FindLastHitPhase::OnRegionEnd()
 {
   // Find the point of the last hit which coincides with a breakpoint.
   Maybe<TrackedPosition> lastBreakpoint;
   for (const BreakpointPosition& breakpoint : gNavigation->mBreakpoints) {
-    if (!breakpoint.IsValid()) {
-      continue;
-    }
     const TrackedPosition& tracked = FindTrackedPosition(breakpoint);
     if (tracked.mLastHit.HasPosition() &&
         (lastBreakpoint.isNothing() ||
          lastBreakpoint.ref().mLastHitCount < tracked.mLastHitCount))
     {
       lastBreakpoint = Some(tracked);
     }
   }
@@ -1059,17 +1026,17 @@ FindLastHitPhase::OnRegionEnd()
         // The last temporary checkpoint may be at the same execution point as
         // the last normal checkpoint, if it was created while handling
         // debugger requests there.
       }
     }
 
     // Rewind to the last normal checkpoint and pause.
     gNavigation->mPausedPhase.Enter(gNavigation->CheckpointExecutionPoint(mStart.mNormal),
-                                    BreakpointVector(), /* aRewind = */ true);
+                                    /* aRewind = */ true);
     Unreachable();
   }
 
   // When running backwards, we don't want to place temporary checkpoints at
   // the breakpoint where we are going to stop at. If the user continues
   // rewinding then we will just have to discard the checkpoint and waste the
   // work we did in saving it.
   //
@@ -1144,19 +1111,27 @@ LastNormalCheckpoint()
 
 void
 DebuggerRequest(js::CharBuffer* aRequestBuffer)
 {
   gNavigation->HandleDebuggerRequest(aRequestBuffer);
 }
 
 void
-SetBreakpoint(size_t aId, const BreakpointPosition& aPosition)
+AddBreakpoint(const BreakpointPosition& aPosition)
 {
-  gNavigation->GetBreakpoint(aId) = aPosition;
+  gNavigation->mBreakpoints.append(aPosition);
+}
+
+void
+ClearBreakpoints()
+{
+  if (gNavigation) {
+    gNavigation->mBreakpoints.clear();
+  }
 }
 
 void
 Resume(bool aForward)
 {
   // For the primordial resume sent at startup, the navigation state will not
   // have been initialized yet.
   if (!gNavigation) {
--- a/toolkit/recordreplay/ipc/ChildProcess.cpp
+++ b/toolkit/recordreplay/ipc/ChildProcess.cpp
@@ -105,72 +105,24 @@ ChildProcessInfo::IsPausedAtRecordingEnd
   }
   if (mPausedMessage->mType == MessageType::HitBreakpoint) {
     return static_cast<HitBreakpointMessage*>(mPausedMessage)->mRecordingEndpoint;
   }
   return false;
 }
 
 void
-ChildProcessInfo::GetInstalledBreakpoints(Vector<SetBreakpointMessage*>& aBreakpoints)
-{
-  for (Message* msg : mMessages) {
-    if (msg->mType == MessageType::SetBreakpoint) {
-      SetBreakpointMessage* nmsg = static_cast<SetBreakpointMessage*>(msg);
-      for (SetBreakpointMessage*& existing : aBreakpoints) {
-        if (existing->mId == nmsg->mId) {
-          aBreakpoints.erase(&existing);
-          break;
-        }
-      }
-      if (nmsg->mPosition.IsValid()) {
-        if (!aBreakpoints.append(nmsg)) {
-          MOZ_CRASH("OOM");
-        }
-      }
-    }
-  }
-}
-
-bool
-ChildProcessInfo::IsPausedAtMatchingBreakpoint(const BreakpointFilter& aFilter)
+ChildProcessInfo::GetInstalledBreakpoints(InfallibleVector<AddBreakpointMessage*>& aBreakpoints)
 {
-  if (!IsPaused() || mPausedMessage->mType != MessageType::HitBreakpoint) {
-    return false;
-  }
-
-  Vector<SetBreakpointMessage*> installed;
-  GetInstalledBreakpoints(installed);
-
-  HitBreakpointMessage* npaused = static_cast<HitBreakpointMessage*>(mPausedMessage);
-  for (size_t i = 0; i < npaused->NumBreakpoints(); i++) {
-    uint32_t breakpointId = npaused->Breakpoints()[i];
-
-    // Note: this test isn't quite right if new breakpoints have been installed
-    // since the child paused, though this does not affect current callers.
-    for (SetBreakpointMessage* msg : installed) {
-      if (msg->mId == breakpointId && aFilter(msg->mPosition.mKind)) {
-        return true;
-      }
-    }
-  }
-
-  return false;
-}
-
-void
-ChildProcessInfo::GetMatchingInstalledBreakpoints(const BreakpointFilter& aFilter,
-                                                  Vector<uint32_t>& aBreakpointIds)
-{
-  Vector<SetBreakpointMessage*> installed;
-  GetInstalledBreakpoints(installed);
-
-  for (SetBreakpointMessage* msg : installed) {
-    if (aFilter(msg->mPosition.mKind) && !aBreakpointIds.append(msg->mId)) {
-      MOZ_CRASH("OOM");
+  MOZ_RELEASE_ASSERT(aBreakpoints.empty());
+  for (Message* msg : mMessages) {
+    if (msg->mType == MessageType::AddBreakpoint) {
+      aBreakpoints.append(static_cast<AddBreakpointMessage*>(msg));
+    } else if (msg->mType == MessageType::ClearBreakpoints) {
+      aBreakpoints.clear();
     }
   }
 }
 
 void
 ChildProcessInfo::AddMajorCheckpoint(size_t aId)
 {
   // Major checkpoints should be listed in order.
@@ -235,34 +187,28 @@ ChildProcessInfo::OnIncomingMessage(size
     break;
   }
 
   if (aMsg.mType == MessageType::HitCheckpoint) {
     const HitCheckpointMessage& nmsg = static_cast<const HitCheckpointMessage&>(aMsg);
     mLastCheckpoint = nmsg.mCheckpointId;
 
     // All messages sent since the last checkpoint are now obsolete, except
-    // SetBreakpoint messages.
+    // those which establish the set of installed breakpoints.
     InfallibleVector<Message*> newMessages;
     for (Message* msg : mMessages) {
-      if (msg->mType == MessageType::SetBreakpoint) {
-        // Look for an older SetBreakpoint on the same ID to overwrite.
-        bool found = false;
-        for (Message*& older : newMessages) {
-          if (static_cast<SetBreakpointMessage*>(msg)->mId ==
-              static_cast<SetBreakpointMessage*>(older)->mId) {
-            free(older);
-            older = msg;
-            found = true;
+      if (msg->mType == MessageType::AddBreakpoint) {
+        newMessages.append(msg);
+      } else {
+        if (msg->mType == MessageType::ClearBreakpoints) {
+          for (Message* existing : newMessages) {
+            free(existing);
           }
+          newMessages.clear();
         }
-        if (!found) {
-          newMessages.emplaceBack(msg);
-        }
-      } else {
         free(msg);
       }
     }
     mMessages = std::move(newMessages);
   }
 
   // The primordial HitCheckpoint messages is not forwarded to the role, as it
   // has not been initialized yet.
@@ -295,17 +241,18 @@ ChildProcessInfo::SendMessage(const Mess
   }
 
   // Keep track of messages which affect the child's behavior.
   switch (aMsg.mType) {
   case MessageType::Resume:
   case MessageType::RestoreCheckpoint:
   case MessageType::RunToPoint:
   case MessageType::DebuggerRequest:
-  case MessageType::SetBreakpoint:
+  case MessageType::AddBreakpoint:
+  case MessageType::ClearBreakpoints:
     mMessages.emplaceBack(aMsg.Clone());
     break;
   default:
     break;
   }
 
   // Keep track of the checkpoints the process will save.
   if (aMsg.mType == MessageType::SetSaveCheckpoint) {
@@ -333,23 +280,20 @@ ChildProcessInfo::Recover(bool aPaused, 
 
   SendMessageRaw(SetIsActiveMessage(false));
 
   size_t mostRecentCheckpoint = MostRecentCheckpoint();
   bool pausedAtCheckpoint = IsPausedAtCheckpoint();
 
   // Clear out all messages that have been sent to this process.
   for (Message* msg : mMessages) {
-    if (msg->mType == MessageType::SetBreakpoint) {
-      SetBreakpointMessage* nmsg = static_cast<SetBreakpointMessage*>(msg);
-      SendMessageRaw(SetBreakpointMessage(nmsg->mId, js::BreakpointPosition()));
-    }
     free(msg);
   }
   mMessages.clear();
+  SendMessageRaw(ClearBreakpointsMessage());
 
   mPaused = aPaused;
   mPausedMessage = aPausedMessage;
   mLastCheckpoint = aLastCheckpoint;
   for (size_t i = 0; i < aNumMessages; i++) {
     mMessages.append(aMessages[i]->Clone());
   }
 
@@ -445,19 +389,20 @@ ChildProcessInfo::SendNextRecoveryMessag
     if (mNumRecoveredMessages == mMessages.length()) {
       MOZ_RELEASE_ASSERT(IsPaused());
       mRecoveryStage = RecoveryStage::None;
       return;
     }
     msg = mMessages[mNumRecoveredMessages++];
     SendMessageRaw(*msg);
 
-    // If we just sent a SetBreakpoint message then the child process is still
-    // paused, so keep sending more messages.
-  } while (msg->mType == MessageType::SetBreakpoint);
+    // Messages operating on breakpoints preserve the paused state of the
+    // child, so keep sending more messages.
+  } while (msg->mType == MessageType::AddBreakpoint ||
+           msg->mType == MessageType::ClearBreakpoints);
 
   // If we have sent all messages and are in an unpaused state, we are done
   // recovering.
   if (mNumRecoveredMessages == mMessages.length() && !IsPaused()) {
     mRecoveryStage = RecoveryStage::None;
   }
 }
 
--- a/toolkit/recordreplay/ipc/JSControl.cpp
+++ b/toolkit/recordreplay/ipc/JSControl.cpp
@@ -172,76 +172,91 @@ ExecutionPoint::Decode(JSContext* aCx, H
       && GetNumberProperty(aCx, aObject, gCheckpointProperty, &mCheckpoint)
       && GetNumberProperty(aCx, aObject, gProgressProperty, &mProgress);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Middleman Methods
 ///////////////////////////////////////////////////////////////////////////////
 
-// Keep track of all replay debuggers in existence, so that they can all be
-// invalidated when the process is unpaused.
-static StaticInfallibleVector<PersistentRootedObject*> gReplayDebuggers;
+// There can be at most one replay debugger in existence.
+static PersistentRootedObject* gReplayDebugger;
 
 static bool
 Middleman_RegisterReplayDebugger(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  if (gReplayDebugger) {
+    args.rval().setObject(**gReplayDebugger);
+    return true;
+  }
+
   RootedObject obj(aCx, NonNullObject(aCx, args.get(0)));
   if (!obj) {
     return false;
   }
 
   obj = ::js::CheckedUnwrap(obj);
   if (!obj) {
     ::js::ReportAccessDenied(aCx);
     return false;
   }
 
-  PersistentRootedObject* root = new PersistentRootedObject(aCx);
-  *root = obj;
-  gReplayDebuggers.append(root);
+  gReplayDebugger = new PersistentRootedObject(aCx);
+  *gReplayDebugger = obj;
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool
-InvalidateReplayDebuggersAfterUnpause(JSContext* aCx)
+CallReplayDebuggerHook(const char* aMethod)
 {
-  RootedValue rval(aCx);
-  for (auto root : gReplayDebuggers) {
-    JSAutoRealm ar(aCx, *root);
-    if (!JS_CallFunctionName(aCx, *root, "invalidateAfterUnpause",
-                             HandleValueArray::empty(), &rval))
-    {
-      return false;
-    }
+  if (!gReplayDebugger) {
+    return false;
+  }
+
+  AutoSafeJSContext cx;
+  JSAutoRealm ar(cx, *gReplayDebugger);
+  RootedValue rval(cx);
+  if (!JS_CallFunctionName(cx, *gReplayDebugger, aMethod,
+                           HandleValueArray::empty(), &rval))
+  {
+    Print("Warning: ReplayDebugger hook %s threw an exception\n", aMethod);
   }
   return true;
 }
 
+bool
+DebuggerOnPause()
+{
+  return CallReplayDebuggerHook("_onPause");
+}
+
+void
+DebuggerOnSwitchChild()
+{
+  CallReplayDebuggerHook("_onSwitchChild");
+}
+
 static bool
 Middleman_CanRewind(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   args.rval().setBoolean(parent::CanRewind());
   return true;
 }
 
 static bool
 Middleman_Resume(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   bool forward = ToBoolean(args.get(0));
 
-  if (!InvalidateReplayDebuggersAfterUnpause(aCx)) {
-    return false;
-  }
-
   parent::Resume(forward);
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool
 Middleman_TimeWarp(JSContext* aCx, unsigned aArgc, Value* aVp)
@@ -252,38 +267,23 @@ Middleman_TimeWarp(JSContext* aCx, unsig
     return false;
   }
 
   ExecutionPoint target;
   if (!target.Decode(aCx, targetObject)) {
     return false;
   }
 
-  if (!InvalidateReplayDebuggersAfterUnpause(aCx)) {
-    return false;
-  }
-
   parent::TimeWarp(target);
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool
-Middleman_Pause(JSContext* aCx, unsigned aArgc, Value* aVp)
-{
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::Pause();
-
-  args.rval().setUndefined();
-  return true;
-}
-
-static bool
 Middleman_SendRequest(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   RootedObject requestObject(aCx, NonNullObject(aCx, args.get(0)));
   if (!requestObject) {
     return false;
   }
 
@@ -293,103 +293,43 @@ Middleman_SendRequest(JSContext* aCx, un
   }
 
   CharBuffer responseBuffer;
   parent::SendRequest(requestBuffer, &responseBuffer);
 
   return JS_ParseJSON(aCx, responseBuffer.begin(), responseBuffer.length(), args.rval());
 }
 
-struct InstalledBreakpoint
-{
-  PersistentRootedObject mHandler;
-  BreakpointPosition mPosition;
-
-  InstalledBreakpoint(JSContext* aCx, JSObject* aHandler, const BreakpointPosition& aPosition)
-    : mHandler(aCx, aHandler), mPosition(aPosition)
-  {}
-};
-static StaticInfallibleVector<InstalledBreakpoint*> gBreakpoints;
-
 static bool
-Middleman_SetBreakpoint(JSContext* aCx, unsigned aArgc, Value* aVp)
+Middleman_AddBreakpoint(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  RootedObject handler(aCx, NonNullObject(aCx, args.get(0)));
-  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(1)));
-  if (!handler || !positionObject) {
-    return false;
-  }
-
-  handler = ::js::CheckedUnwrap(handler);
-  if (!handler) {
-    ::js::ReportAccessDenied(aCx);
+  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(0)));
+  if (!positionObject) {
     return false;
   }
 
   BreakpointPosition position;
   if (!position.Decode(aCx, positionObject)) {
     return false;
   }
 
-  size_t breakpointId;
-  for (breakpointId = 0; breakpointId < gBreakpoints.length(); breakpointId++) {
-    if (!gBreakpoints[breakpointId]) {
-      break;
-    }
-  }
-  if (breakpointId == gBreakpoints.length()) {
-    gBreakpoints.append(nullptr);
-  }
+  parent::AddBreakpoint(position);
 
-  gBreakpoints[breakpointId] = new InstalledBreakpoint(aCx, handler, position);
-
-  parent::SetBreakpoint(breakpointId, position);
-
-  args.rval().setInt32(breakpointId);
+  args.rval().setUndefined();
   return true;
 }
 
-bool
-HitBreakpoint(JSContext* aCx, size_t aId)
-{
-  InstalledBreakpoint* breakpoint = gBreakpoints[aId];
-  MOZ_RELEASE_ASSERT(breakpoint);
-
-  JSAutoRealm ar(aCx, breakpoint->mHandler);
-
-  RootedValue handlerValue(aCx, ObjectValue(*breakpoint->mHandler));
-  RootedValue rval(aCx);
-  return JS_CallFunctionValue(aCx, nullptr, handlerValue,
-                              HandleValueArray::empty(), &rval)
-      // The replaying process will resume after this hook returns, if it
-      // hasn't already been explicitly resumed.
-      && InvalidateReplayDebuggersAfterUnpause(aCx);
-}
-
 /* static */ bool
-Middleman_ClearBreakpoint(JSContext* aCx, unsigned aArgc, Value* aVp)
+Middleman_ClearBreakpoints(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-  if (!args.get(0).isNumber()) {
-    JS_ReportErrorASCII(aCx, "Bad breakpoint ID");
-    return false;
-  }
 
-  size_t breakpointId = (size_t) args.get(0).toNumber();
-  if (breakpointId >= gBreakpoints.length() || !gBreakpoints[breakpointId]) {
-    JS_ReportErrorASCII(aCx, "Bad breakpoint ID");
-    return false;
-  }
-
-  delete gBreakpoints[breakpointId];
-  gBreakpoints[breakpointId] = nullptr;
-
-  parent::SetBreakpoint(breakpointId, BreakpointPosition());
+  parent::ClearBreakpoints();
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool
 Middleman_MaybeSwitchToReplayingChild(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
@@ -435,16 +375,67 @@ Middleman_HadRepaintFailure(JSContext* a
 static bool
 Middleman_ChildIsRecording(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   args.rval().setBoolean(parent::ActiveChildIsRecording());
   return true;
 }
 
+static bool
+Middleman_MarkExplicitPause(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::MarkActiveChildExplicitPause();
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+Middleman_WaitUntilPaused(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::WaitUntilActiveChildIsPaused();
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+Middleman_PositionSubsumes(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  RootedObject firstPositionObject(aCx, NonNullObject(aCx, args.get(0)));
+  if (!firstPositionObject) {
+    return false;
+  }
+
+  BreakpointPosition firstPosition;
+  if (!firstPosition.Decode(aCx, firstPositionObject)) {
+    return false;
+  }
+
+  RootedObject secondPositionObject(aCx, NonNullObject(aCx, args.get(1)));
+  if (!secondPositionObject) {
+    return false;
+  }
+
+  BreakpointPosition secondPosition;
+  if (!secondPosition.Decode(aCx, secondPositionObject)) {
+    return false;
+  }
+
+  args.rval().setBoolean(firstPosition.Subsumes(secondPosition));
+  return true;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Devtools Sandbox
 ///////////////////////////////////////////////////////////////////////////////
 
 static PersistentRootedObject* gDevtoolsSandbox;
 
 // URL of the root script that runs when recording/replaying.
 #define ReplayScriptURL "resource://devtools/server/actors/replay/replay.js"
@@ -985,24 +976,26 @@ RecordReplay_Dump(JSContext* aCx, unsign
 // Plumbing
 ///////////////////////////////////////////////////////////////////////////////
 
 static const JSFunctionSpec gMiddlemanMethods[] = {
   JS_FN("registerReplayDebugger", Middleman_RegisterReplayDebugger, 1, 0),
   JS_FN("canRewind", Middleman_CanRewind, 0, 0),
   JS_FN("resume", Middleman_Resume, 1, 0),
   JS_FN("timeWarp", Middleman_TimeWarp, 1, 0),
-  JS_FN("pause", Middleman_Pause, 0, 0),
   JS_FN("sendRequest", Middleman_SendRequest, 1, 0),
-  JS_FN("setBreakpoint", Middleman_SetBreakpoint, 2, 0),
-  JS_FN("clearBreakpoint", Middleman_ClearBreakpoint, 1, 0),
+  JS_FN("addBreakpoint", Middleman_AddBreakpoint, 1, 0),
+  JS_FN("clearBreakpoints", Middleman_ClearBreakpoints, 0, 0),
   JS_FN("maybeSwitchToReplayingChild", Middleman_MaybeSwitchToReplayingChild, 0, 0),
   JS_FN("hadRepaint", Middleman_HadRepaint, 2, 0),
   JS_FN("hadRepaintFailure", Middleman_HadRepaintFailure, 0, 0),
   JS_FN("childIsRecording", Middleman_ChildIsRecording, 0, 0),
+  JS_FN("markExplicitPause", Middleman_MarkExplicitPause, 0, 0),
+  JS_FN("waitUntilPaused", Middleman_WaitUntilPaused, 0, 0),
+  JS_FN("positionSubsumes", Middleman_PositionSubsumes, 2, 0),
   JS_FS_END
 };
 
 static const JSFunctionSpec gRecordReplayMethods[] = {
   JS_FN("areThreadEventsDisallowed", RecordReplay_AreThreadEventsDisallowed, 0, 0),
   JS_FN("maybeDivergeFromRecording", RecordReplay_MaybeDivergeFromRecording, 0, 0),
   JS_FN("advanceProgressCounter", RecordReplay_AdvanceProgressCounter, 0, 0),
   JS_FN("shouldUpdateProgressCounter", RecordReplay_ShouldUpdateProgressCounter, 1, 0),
--- a/toolkit/recordreplay/ipc/JSControl.h
+++ b/toolkit/recordreplay/ipc/JSControl.h
@@ -56,26 +56,17 @@ struct BreakpointPosition
 
     // Break when a new top-level script is created.
     NewScript,
 
     // Break when a message is logged to the web console.
     ConsoleMessage,
 
     // Break when NewTimeWarpTarget() is called.
-    WarpTarget,
-
-    // Break when the debugger should pause even if no breakpoint has been
-    // set: the beginning or end of the replay has been reached, or a time
-    // warp has reached its destination.
-    ForcedPause,
-
-    // Break when the child process reaches a checkpoint or we switch between
-    // recording and replaying child processes.
-    PositionChange
+    WarpTarget
   ));
 
   Kind mKind;
 
   // Optional information associated with the breakpoint.
   uint32_t mScript;
   uint32_t mOffset;
   uint32_t mFrameIndex;
@@ -118,18 +109,16 @@ struct BreakpointPosition
     case Invalid: return "Invalid";
     case Break: return "Break";
     case OnStep: return "OnStep";
     case OnPop: return "OnPop";
     case EnterFrame: return "EnterFrame";
     case NewScript: return "NewScript";
     case ConsoleMessage: return "ConsoleMessage";
     case WarpTarget: return "WarpTarget";
-    case ForcedPause: return "ForcedPause";
-    case PositionChange: return "PositionChange";
     }
     MOZ_CRASH("Bad BreakpointPosition kind");
   }
 
   const char* KindString() const {
     return StaticKindString(mKind);
   }
 
@@ -188,18 +177,23 @@ struct ExecutionPoint
 
   JSObject* Encode(JSContext* aCx) const;
   bool Decode(JSContext* aCx, JS::HandleObject aObject);
 };
 
 // Buffer type used for encoding object data.
 typedef InfallibleVector<char16_t> CharBuffer;
 
-// Called in the middleman when a breakpoint with the specified id has been hit.
-bool HitBreakpoint(JSContext* aCx, size_t id);
+// Called in the middleman when the child has hit a checkpoint or breakpoint.
+// The return value is whether there is a ReplayDebugger available which the
+// notification was sent to.
+bool DebuggerOnPause();
+
+// Called in the middleman when the child has changed.
+void DebuggerOnSwitchChild();
 
 // Set up the JS sandbox in the current recording/replaying process and load
 // its target script.
 void SetupDevtoolsSandbox();
 
 // The following hooks are used in the recording/replaying process to
 // call methods defined by the JS sandbox.
 
--- a/toolkit/recordreplay/ipc/ParentIPC.cpp
+++ b/toolkit/recordreplay/ipc/ParentIPC.cpp
@@ -579,49 +579,45 @@ SpawnReplayingChildren()
   }
   gFirstReplayingChild =
     new ChildProcessInfo(std::move(firstRole), Nothing());
   gSecondReplayingChild =
     new ChildProcessInfo(MakeUnique<ChildRoleStandby>(), Nothing());
   AssignMajorCheckpoint(gSecondReplayingChild, CheckpointId::First);
 }
 
-// Hit any installed breakpoints with the specified kind.
-static void HitBreakpointsWithKind(js::BreakpointPosition::Kind aKind);
-
 // Change the current active child, and select a new role for the old one.
 static void
 SwitchActiveChild(ChildProcessInfo* aChild, bool aRecoverPosition = true)
 {
   MOZ_RELEASE_ASSERT(aChild != gActiveChild);
   ChildProcessInfo* oldActiveChild = gActiveChild;
   aChild->WaitUntilPaused();
   if (!aChild->IsRecording()) {
     if (aRecoverPosition) {
       aChild->Recover(gActiveChild);
     } else {
-      Vector<SetBreakpointMessage*> breakpoints;
+      InfallibleVector<AddBreakpointMessage*> breakpoints;
       gActiveChild->GetInstalledBreakpoints(breakpoints);
-      for (SetBreakpointMessage* msg : breakpoints) {
+      for (AddBreakpointMessage* msg : breakpoints) {
         aChild->SendMessage(*msg);
       }
     }
   }
   aChild->SetRole(MakeUnique<ChildRoleActive>());
   if (oldActiveChild->IsRecording()) {
     oldActiveChild->SetRole(MakeUnique<ChildRoleInert>());
   } else {
     oldActiveChild->RecoverToCheckpoint(oldActiveChild->MostRecentSavedCheckpoint());
     oldActiveChild->SetRole(MakeUnique<ChildRoleStandby>());
   }
 
-  // Position state is affected when we switch between recording and
-  // replaying children.
+  // Notify the debugger when switching between recording and replaying children.
   if (aChild->IsRecording() != oldActiveChild->IsRecording()) {
-    HitBreakpointsWithKind(js::BreakpointPosition::Kind::PositionChange);
+    js::DebuggerOnSwitchChild();
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Preferences
 ///////////////////////////////////////////////////////////////////////////////
 
 static bool gPreferencesLoaded;
@@ -830,17 +826,17 @@ HasSavedCheckpointsInRange(ChildProcessI
   for (size_t i = aStart; i <= aEnd; i++) {
     if (!aChild->HasSavedCheckpoint(i)) {
       return false;
     }
   }
   return true;
 }
 
-static void
+void
 MarkActiveChildExplicitPause()
 {
   MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
   size_t targetCheckpoint = gActiveChild->RewindTargetCheckpoint();
 
   if (gActiveChild->IsRecording()) {
     // Make sure any replaying children can play forward to the same point as
     // the recording.
@@ -872,16 +868,30 @@ ActiveChildTargetCheckpoint()
   }
   if (gActiveChild->RewindTargetCheckpoint() <= gLastExplicitPause) {
     return Some(gActiveChild->RewindTargetCheckpoint());
   }
   return Nothing();
 }
 
 void
+WaitUntilActiveChildIsPaused()
+{
+  if (gActiveChild->IsPaused()) {
+    // The debugger expects an OnPause notification after calling this, even if
+    // it is already paused. This should only happen when attaching the
+    // debugger to a paused child process.
+    js::DebuggerOnPause();
+  } else {
+    MaybeCreateCheckpointInRecordingChild();
+    gActiveChild->WaitUntilPaused();
+  }
+}
+
+void
 MaybeSwitchToReplayingChild()
 {
   if (gActiveChild->IsRecording() && CanRewind()) {
     FlushRecording();
     size_t checkpoint = gActiveChild->RewindTargetCheckpoint();
     ChildProcessInfo* child =
       OtherReplayingChild(ReplayingChildResponsibleForSavingCheckpoint(checkpoint));
     SwitchActiveChild(child);
@@ -958,57 +968,59 @@ RecvDebuggerResponse(const DebuggerRespo
 {
   MOZ_RELEASE_ASSERT(gResponseBuffer && gResponseBuffer->empty());
   gResponseBuffer->append(aMsg.Buffer(), aMsg.BufferSize());
 }
 
 void
 SendRequest(const js::CharBuffer& aBuffer, js::CharBuffer* aResponse)
 {
-  MaybeCreateCheckpointInRecordingChild();
-  gActiveChild->WaitUntilPaused();
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
 
   MOZ_RELEASE_ASSERT(!gResponseBuffer);
   gResponseBuffer = aResponse;
 
   DebuggerRequestMessage* msg = DebuggerRequestMessage::New(aBuffer.begin(), aBuffer.length());
   gActiveChild->SendMessage(*msg);
   free(msg);
 
   // Wait for the child to respond to the query.
   gActiveChild->WaitUntilPaused();
   MOZ_RELEASE_ASSERT(gResponseBuffer == aResponse);
   MOZ_RELEASE_ASSERT(gResponseBuffer->length() != 0);
   gResponseBuffer = nullptr;
 }
 
 void
-SetBreakpoint(size_t aId, const js::BreakpointPosition& aPosition)
+AddBreakpoint(const js::BreakpointPosition& aPosition)
 {
-  MaybeCreateCheckpointInRecordingChild();
-  gActiveChild->WaitUntilPaused();
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
 
-  gActiveChild->SendMessage(SetBreakpointMessage(aId, aPosition));
+  gActiveChild->SendMessage(AddBreakpointMessage(aPosition));
 
   // Also set breakpoints in any recording child that is not currently active.
   // We can't recover recording processes so need to keep their breakpoints up
   // to date.
   if (!gActiveChild->IsRecording() && gRecordingChild) {
-    gRecordingChild->SendMessage(SetBreakpointMessage(aId, aPosition));
+    gRecordingChild->SendMessage(AddBreakpointMessage(aPosition));
   }
 }
 
-// Flags for the preferred direction of travel when execution unpauses,
-// according to the last direction we were explicitly given.
-static bool gChildExecuteForward = true;
-static bool gChildExecuteBackward = false;
+void
+ClearBreakpoints()
+{
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
 
-// Whether there is a ResumeForwardOrBackward task which should execute on the
-// main thread. This will continue execution in the preferred direction.
-static bool gResumeForwardOrBackward = false;
+  gActiveChild->SendMessage(ClearBreakpointsMessage());
+
+  // Clear breakpoints in the recording child, as for AddBreakpoint().
+  if (!gActiveChild->IsRecording() && gRecordingChild) {
+    gRecordingChild->SendMessage(ClearBreakpointsMessage());
+  }
+}
 
 static void
 MaybeSendRepaintMessage()
 {
   // In repaint stress mode, we want to trigger a repaint at every checkpoint,
   // so before resuming after the child pauses at each checkpoint, send it a
   // repaint message. There might not be a debugger open, so manually craft the
   // same message which the debugger would send to trigger a repaint and parse
@@ -1035,34 +1047,29 @@ MaybeSendRepaintMessage()
       }
     }
   }
 }
 
 void
 Resume(bool aForward)
 {
-  gActiveChild->WaitUntilPaused();
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
 
   MaybeSendRepaintMessage();
 
-  // Set the preferred direction of travel.
-  gResumeForwardOrBackward = false;
-  gChildExecuteForward = aForward;
-  gChildExecuteBackward = !aForward;
-
   // When rewinding, make sure the active child can rewind to the previous
   // checkpoint.
   if (!aForward && !gActiveChild->HasSavedCheckpoint(gActiveChild->RewindTargetCheckpoint())) {
     size_t targetCheckpoint = gActiveChild->RewindTargetCheckpoint();
 
     // Don't rewind if we are at the beginning of the recording.
     if (targetCheckpoint == CheckpointId::Invalid) {
       SendMessageToUIProcess("HitRecordingBeginning");
-      HitBreakpointsWithKind(js::BreakpointPosition::Kind::ForcedPause);
+      js::DebuggerOnPause();
       return;
     }
 
     // Find the replaying child responsible for saving the target checkpoint.
     // We should have explicitly paused before rewinding and given fill roles
     // to the replaying children.
     ChildProcessInfo* targetChild = ReplayingChildResponsibleForSavingCheckpoint(targetCheckpoint);
     MOZ_RELEASE_ASSERT(targetChild != gActiveChild);
@@ -1079,17 +1086,17 @@ Resume(bool aForward)
 
   if (aForward) {
     // Don't send a replaying process past the recording endpoint.
     if (gActiveChild->IsPausedAtRecordingEndpoint()) {
       // Look for a recording child we can transition into.
       MOZ_RELEASE_ASSERT(!gActiveChild->IsRecording());
       if (!gRecordingChild) {
         SendMessageToUIProcess("HitRecordingEndpoint");
-        HitBreakpointsWithKind(js::BreakpointPosition::Kind::ForcedPause);
+        js::DebuggerOnPause();
         return;
       }
 
       // Switch to the recording child as the active child and continue execution.
       SwitchActiveChild(gRecordingChild);
     }
 
     EnsureMajorCheckpointSaved(gActiveChild, gActiveChild->LastCheckpoint() + 1);
@@ -1099,22 +1106,17 @@ Resume(bool aForward)
   }
 
   gActiveChild->SendMessage(ResumeMessage(aForward));
 }
 
 void
 TimeWarp(const js::ExecutionPoint& aTarget)
 {
-  gActiveChild->WaitUntilPaused();
-
-  // There is no preferred direction of travel after warping.
-  gResumeForwardOrBackward = false;
-  gChildExecuteForward = false;
-  gChildExecuteBackward = false;
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
 
   // Make sure the active child can rewind to the checkpoint prior to the
   // warp target.
   MOZ_RELEASE_ASSERT(gTimeWarpTarget.isNothing());
   gTimeWarpTarget.emplace(aTarget.mCheckpoint);
 
   PokeChildren();
 
@@ -1144,149 +1146,60 @@ TimeWarp(const js::ExecutionPoint& aTarg
     gActiveChild->SendMessage(RestoreCheckpointMessage(aTarget.mCheckpoint));
     gActiveChild->WaitUntilPaused();
   }
 
   gActiveChild->SendMessage(RunToPointMessage(aTarget));
 
   gActiveChild->WaitUntilPaused();
   SendMessageToUIProcess("TimeWarpFinished");
-  HitBreakpointsWithKind(js::BreakpointPosition::Kind::ForcedPause);
-}
-
-void
-Pause()
-{
-  MaybeCreateCheckpointInRecordingChild();
-  gActiveChild->WaitUntilPaused();
-
-  // If the debugger has explicitly paused then there is no preferred direction
-  // of travel.
-  gChildExecuteForward = false;
-  gChildExecuteBackward = false;
-
-  MarkActiveChildExplicitPause();
-}
-
-static void
-ResumeForwardOrBackward()
-{
-  MOZ_RELEASE_ASSERT(!gChildExecuteForward || !gChildExecuteBackward);
-
-  if (gResumeForwardOrBackward && (gChildExecuteForward || gChildExecuteBackward)) {
-    Resume(gChildExecuteForward);
-  }
 }
 
 void
 ResumeBeforeWaitingForIPDLReply()
 {
   MOZ_RELEASE_ASSERT(gActiveChild->IsRecording());
 
   // The main thread is about to block while it waits for a sync reply from the
   // recording child process. If the child is paused, resume it immediately so
   // that we don't deadlock.
   if (gActiveChild->IsPaused()) {
-    MOZ_RELEASE_ASSERT(gChildExecuteForward);
     Resume(true);
   }
 }
 
 static void
 RecvHitCheckpoint(const HitCheckpointMessage& aMsg)
 {
   UpdateCheckpointTimes(aMsg);
   MaybeUpdateGraphicsAtCheckpoint(aMsg.mCheckpointId);
 
-  // Position state is affected when new checkpoints are reached.
-  HitBreakpointsWithKind(js::BreakpointPosition::Kind::PositionChange);
-
-  // Resume either forwards or backwards. Break the resume off into a separate
-  // runnable, to avoid starving any code already on the stack and waiting for
-  // the process to pause. Immediately resume if the main thread is blocked.
+  // Immediately resume if the main thread is blocked. If there is no
+  // debugger attached a resume is needed as well, but post a runnable so that
+  // callers waiting for the child to pause (e.g. SaveRecording) don't starve.
   if (MainThreadIsWaitingForIPDLReply()) {
-    MOZ_RELEASE_ASSERT(gChildExecuteForward);
     Resume(true);
-  } else if (!gResumeForwardOrBackward) {
-    gResumeForwardOrBackward = true;
-    gMainThreadMessageLoop->PostTask(NewRunnableFunction("ResumeForwardOrBackward",
-                                                         ResumeForwardOrBackward));
-  }
-}
-
-static void
-HitBreakpoint(uint32_t* aBreakpoints, size_t aNumBreakpoints,
-              js::BreakpointPosition::Kind aSharedKind)
-{
-  if (!gActiveChild->IsPaused()) {
-    delete[] aBreakpoints;
-    return;
+  } else if (!js::DebuggerOnPause()) {
+    gMainThreadMessageLoop->PostTask(NewRunnableFunction("RecvHitCheckpointResume", Resume, true));
   }
-
-  switch (aSharedKind) {
-  case js::BreakpointPosition::ForcedPause:
-    MarkActiveChildExplicitPause();
-    MOZ_FALLTHROUGH;
-  case js::BreakpointPosition::PositionChange:
-    // Call all breakpoint handlers.
-    for (size_t i = 0; i < aNumBreakpoints; i++) {
-      AutoSafeJSContext cx;
-      if (!js::HitBreakpoint(cx, aBreakpoints[i])) {
-        Print("Warning: hitBreakpoint hook threw an exception.\n");
-      }
-    }
-    break;
-  default:
-    gResumeForwardOrBackward = true;
-
-    MarkActiveChildExplicitPause();
-
-    // Call breakpoint handlers until one of them explicitly resumes forward or
-    // backward travel.
-    for (size_t i = 0; i < aNumBreakpoints && gResumeForwardOrBackward; i++) {
-      AutoSafeJSContext cx;
-      if (!js::HitBreakpoint(cx, aBreakpoints[i])) {
-        Print("Warning: hitBreakpoint hook threw an exception.\n");
-      }
-    }
-
-    // If the child was not explicitly resumed by any breakpoint handler,
-    // resume travel in whichever direction we were going previously.
-    if (gResumeForwardOrBackward) {
-      ResumeForwardOrBackward();
-    }
-    break;
-  }
-
-  delete[] aBreakpoints;
 }
 
 static void
 RecvHitBreakpoint(const HitBreakpointMessage& aMsg)
 {
-  uint32_t* breakpoints = new uint32_t[aMsg.NumBreakpoints()];
-  PodCopy(breakpoints, aMsg.Breakpoints(), aMsg.NumBreakpoints());
-  gMainThreadMessageLoop->PostTask(NewRunnableFunction("HitBreakpoint", HitBreakpoint,
-                                                       breakpoints, aMsg.NumBreakpoints(),
-                                                       js::BreakpointPosition::Invalid));
-}
-
-static void
-HitBreakpointsWithKind(js::BreakpointPosition::Kind aKind)
-{
-  Vector<uint32_t> breakpoints;
-  gActiveChild->GetMatchingInstalledBreakpoints([=](js::BreakpointPosition::Kind aInstalled) {
-      return aInstalled == aKind;
-    }, breakpoints);
-  if (!breakpoints.empty()) {
-    uint32_t* newBreakpoints = new uint32_t[breakpoints.length()];
-    PodCopy(newBreakpoints, breakpoints.begin(), breakpoints.length());
-    gMainThreadMessageLoop->PostTask(NewRunnableFunction("HitBreakpoint", HitBreakpoint,
-                                                         newBreakpoints, breakpoints.length(),
-                                                         aKind));
+  // HitBreakpoint messages will be sent both when hitting user breakpoints and
+  // when hitting the endpoint of the recording, if it is at a breakpoint
+  // position. Don't send an OnPause notification in the latter case: if the
+  // user installed a breakpoint here we will have already gotten a
+  // HitBreakpoint message *without* mRecordingEndpoint set, and we don't want
+  // to pause twice at the same point.
+  if (aMsg.mRecordingEndpoint) {
+    Resume(true);
+  } else if (!js::DebuggerOnPause()) {
+    gMainThreadMessageLoop->PostTask(NewRunnableFunction("RecvHitBreakpointResume", Resume, true));
   }
 }
 
 static void
 RecvMiddlemanCallRequest(const MiddlemanCallRequestMessage& aMsg)
 {
   MiddlemanCallResponseMessage* response = ProcessMiddlemanCallMessage(aMsg);
   gActiveChild->SendMessage(*response);
--- a/toolkit/recordreplay/ipc/ParentInternal.h
+++ b/toolkit/recordreplay/ipc/ParentInternal.h
@@ -53,34 +53,39 @@ void InitializeForwarding();
 void Shutdown();
 
 // Monitor used for synchronizing between the main and channel or message loop threads.
 static Monitor* gMonitor;
 
 // Allow the child process to resume execution.
 void Resume(bool aForward);
 
-// Pause the child process at the next opportunity.
-void Pause();
-
 // Direct the child process to warp to a specific point.
 void TimeWarp(const js::ExecutionPoint& target);
 
 // Send a JSON request to the child process, and synchronously wait for a
 // response.
 void SendRequest(const js::CharBuffer& aBuffer, js::CharBuffer* aResponse);
 
-// Set or clear a breakpoint in the child process.
-void SetBreakpoint(size_t aId, const js::BreakpointPosition& aPosition);
+// Set the breakpoints installed in the child process.
+void AddBreakpoint(const js::BreakpointPosition& aPosition);
+void ClearBreakpoints();
 
 // If possible, make sure the active child is replaying, and that requests
 // which might trigger an unhandled divergence can be processed (recording
 // children cannot process such requests).
 void MaybeSwitchToReplayingChild();
 
+// Block until the active child has paused somewhere.
+void WaitUntilActiveChildIsPaused();
+
+// Notify the parent that the debugger has paused and will allow the user to
+// interact with it and potentially start rewinding.
+void MarkActiveChildExplicitPause();
+
 ///////////////////////////////////////////////////////////////////////////////
 // Graphics
 ///////////////////////////////////////////////////////////////////////////////
 
 extern void* gGraphicsMemory;
 
 void InitializeGraphicsMemory();
 void SendGraphicsMemoryToChild();
@@ -307,27 +312,20 @@ public:
   bool PauseNeeded() { return mPauseNeeded; }
   const InfallibleVector<size_t>& MajorCheckpoints() { return mMajorCheckpoints; }
 
   bool IsPaused() { return mPaused; }
   bool IsPausedAtCheckpoint();
   bool IsPausedAtRecordingEndpoint();
 
   // Get all breakpoints currently installed for this process.
-  void GetInstalledBreakpoints(Vector<SetBreakpointMessage*>& aBreakpoints);
+  void GetInstalledBreakpoints(InfallibleVector<AddBreakpointMessage*>& aBreakpoints);
 
   typedef std::function<bool(js::BreakpointPosition::Kind)> BreakpointFilter;
 
-  // Return whether this process is paused at a breakpoint matching a filter.
-  bool IsPausedAtMatchingBreakpoint(const BreakpointFilter& aFilter);
-
-  // Get the ids of all installed breakpoints matching a filter.
-  void GetMatchingInstalledBreakpoints(const BreakpointFilter& aFilter,
-                                       Vector<uint32_t>& aBreakpointIds);
-
   // Get the checkpoint at or earlier to the process' position. This is either
   // the last reached checkpoint or the previous one.
   size_t MostRecentCheckpoint() {
     return (GetDisposition() == BeforeLastCheckpoint) ? mLastCheckpoint - 1 : mLastCheckpoint;
   }
 
   // Get the checkpoint which needs to be saved in order for this process
   // (or another at the same place) to rewind.