Backed out 12 changesets (bug 1516578, bug 1513118, bug 1516694) for failing at browser_toolbox_remoteness_change.js on a CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Sun, 13 Jan 2019 01:30:05 +0200
changeset 510768 9e3564442734c89fa1b735ff8662588576cf0115
parent 510767 0ed38259d2d8e55dd6e00c5ba53cf21a6a390d78
child 510769 306f0d268fa7f2fe7814c8cf0aae8d08f962427e
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1516578, 1513118, 1516694
milestone66.0a1
backs out4abb81088a9b49d700cfea840848a9dac6a0010d
5ce216ffcb1da139187cf8c62473d150df35dc5e
9417ce02d4f2702923d48ca9ef8019bb09c6aa6a
ec9a7aa1898ccb5b0e227847ba9c4434236524a9
ab90c34ff486cc6b637b234b7d801987b8364090
6689773a4e1caafd89480b2c6182c5a41fc081ce
91d5c6ff3ee769f779668b19f09b76cba00b8142
1701613c165d52565cc8dcf48b5e8a8bfebf7a23
9ddc5bc1e961af52dbd772f66559f3fe4199e572
fb64ff37f6345cfa2e8efc7d692bc3dc3a1cd6f5
0426a61d27a9c9c047b4d489e4a2586b4c7a6491
cf0578ce6aa3b10e00b877c50e0c2ab517b07c2c
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
Backed out 12 changesets (bug 1516578, bug 1513118, bug 1516694) for failing at browser_toolbox_remoteness_change.js on a CLOSED TREE Backed out changeset 4abb81088a9b (bug 1513118) Backed out changeset 5ce216ffcb1d (bug 1513118) Backed out changeset 9417ce02d4f2 (bug 1513118) Backed out changeset ec9a7aa1898c (bug 1516694) Backed out changeset ab90c34ff486 (bug 1516578) Backed out changeset 6689773a4e1c (bug 1516578) Backed out changeset 91d5c6ff3ee7 (bug 1516578) Backed out changeset 1701613c165d (bug 1516578) Backed out changeset 9ddc5bc1e961 (bug 1516578) Backed out changeset fb64ff37f634 (bug 1516578) Backed out changeset 0426a61d27a9 (bug 1516578) Backed out changeset cf0578ce6aa3 (bug 1516578)
devtools/client/framework/toolbox.js
devtools/client/webconsole/webconsole-connection-proxy.js
devtools/server/actors/replay/control.js
devtools/server/actors/replay/debugger.js
devtools/server/actors/replay/moz.build
devtools/server/actors/replay/replay.js
devtools/server/actors/replay/rrIControl.idl
devtools/server/actors/replay/rrIReplay.idl
devtools/server/actors/source.js
devtools/shared/client/constants.js
devtools/shared/client/source-client.js
devtools/shared/specs/source.js
toolkit/recordreplay/MiddlemanCall.cpp
toolkit/recordreplay/MiddlemanCall.h
toolkit/recordreplay/ipc/Channel.cpp
toolkit/recordreplay/ipc/Channel.h
toolkit/recordreplay/ipc/ChildIPC.cpp
toolkit/recordreplay/ipc/ChildInternal.h
toolkit/recordreplay/ipc/ChildNavigation.cpp
toolkit/recordreplay/ipc/ChildProcess.cpp
toolkit/recordreplay/ipc/JSControl.cpp
toolkit/recordreplay/ipc/JSControl.h
toolkit/recordreplay/ipc/ParentForwarding.cpp
toolkit/recordreplay/ipc/ParentIPC.cpp
toolkit/recordreplay/ipc/ParentInternal.h
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -464,22 +464,16 @@ 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,17 +25,16 @@ 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 =
@@ -117,18 +116,16 @@ 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._attachConsole();
@@ -300,30 +297,16 @@ 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.
    */
deleted file mode 100644
--- a/devtools/server/actors/replay/control.js
+++ /dev/null
@@ -1,1104 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* eslint-disable spaced-comment, brace-style, indent-legacy */
-
-"use strict";
-
-// This file provides an interface which the ReplayDebugger uses to interact
-// with a middleman's child recording and replaying processes. There can be
-// several child processes in existence at once; this is largely hidden from the
-// ReplayDebugger, and the position of each child is managed to provide a fast
-// and stable experience when rewinding or running forward.
-
-const CC = Components.Constructor;
-
-// Create a sandbox with the resources we need. require() doesn't work here.
-const sandbox = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
-Cu.evalInSandbox(
-  "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
-  "Components.utils.import('resource://gre/modules/Services.jsm');" +
-  "addDebuggerToGlobal(this);",
-  sandbox
-);
-const RecordReplayControl = sandbox.RecordReplayControl;
-const Services = sandbox.Services;
-
-const InvalidCheckpointId = 0;
-const FirstCheckpointId = 1;
-
-const gChildren = [];
-
-let gDebugger;
-
-function ChildProcess(id, recording, role) {
-  assert(!gChildren[id]);
-  gChildren[id] = this;
-
-  this.id = id;
-  this.recording = recording;
-  this.role = role;
-  this.paused = false;
-
-  this.lastPausePoint = null;
-  this.lastPauseAtRecordingEndpoint = false;
-  this.pauseNeeded = false;
-
-  // All currently installed breakpoints
-  this.breakpoints = [];
-
-  // Any debugger requests sent while paused at the current point.
-  this.debuggerRequests = [];
-
-  this._willSaveCheckpoints = [];
-  this._majorCheckpoints = [];
-
-  // Replaying processes always save the first checkpoint.
-  if (!recording) {
-    this._willSaveCheckpoints.push(FirstCheckpointId);
-  }
-
-  dumpv(`InitRole #${this.id} ${role.name}`);
-  this.role.initialize(this, { startup: true });
-}
-
-ChildProcess.prototype = {
-  hitExecutionPoint(msg) {
-    assert(!this.paused);
-    this.paused = true;
-    this.lastPausePoint = msg.point;
-    this.lastPauseAtRecordingEndpoint = msg.recordingEndpoint;
-
-    this.role.hitExecutionPoint(msg);
-  },
-
-  setRole(role) {
-    dumpv(`SetRole #${this.id} ${role.name}`);
-
-    this.role = role;
-    this.role.initialize(this, { startup: false });
-  },
-
-  addMajorCheckpoint(checkpointId) {
-    this._majorCheckpoints.push(checkpointId);
-  },
-
-  _unpause() {
-    this.paused = false;
-    this.debuggerRequests.length = 0;
-  },
-
-  sendResume({ forward }) {
-    assert(this.paused);
-    this._unpause();
-    RecordReplayControl.sendResume(this.id, forward);
-  },
-
-  sendRestoreCheckpoint(checkpoint) {
-    assert(this.paused);
-    this._unpause();
-    RecordReplayControl.sendRestoreCheckpoint(this.id, checkpoint);
-  },
-
-  sendRunToPoint(point) {
-    assert(this.paused);
-    this._unpause();
-    RecordReplayControl.sendRunToPoint(this.id, point);
-  },
-
-  sendFlushRecording() {
-    assert(this.paused);
-    RecordReplayControl.sendFlushRecording(this.id);
-  },
-
-  waitUntilPaused(maybeCreateCheckpoint) {
-    if (this.paused) {
-      return;
-    }
-    const msg =
-      RecordReplayControl.waitUntilPaused(this.id, maybeCreateCheckpoint);
-    this.hitExecutionPoint(msg);
-    assert(this.paused);
-  },
-
-  lastCheckpoint() {
-    return this.lastPausePoint.checkpoint;
-  },
-
-  rewindTargetCheckpoint() {
-    return this.lastPausePoint.position
-           ? this.lastCheckpoint()
-           : this.lastCheckpoint() - 1;
-  },
-
-  // Get the last major checkpoint at or before id.
-  lastMajorCheckpointPreceding(id) {
-    let last = InvalidCheckpointId;
-    for (const major of this._majorCheckpoints) {
-      if (major > id) {
-        break;
-      }
-      last = major;
-    }
-    return last;
-  },
-
-  isMajorCheckpoint(id) {
-    return this._majorCheckpoints.some(major => major == id);
-  },
-
-  ensureCheckpointSaved(id, shouldSave) {
-    const willSaveIndex = this._willSaveCheckpoints.indexOf(id);
-    if (shouldSave != (willSaveIndex != -1)) {
-      if (shouldSave) {
-        this._willSaveCheckpoints.push(id);
-      } else {
-        const last = this._willSaveCheckpoints.pop();
-        if (willSaveIndex != this._willSaveCheckpoints.length) {
-          this._willSaveCheckpoints[willSaveIndex] = last;
-        }
-      }
-      RecordReplayControl.sendSetSaveCheckpoint(this.id, id, shouldSave);
-    }
-  },
-
-  // Ensure a checkpoint is saved in this child iff it is a major one.
-  ensureMajorCheckpointSaved(id) {
-    // The first checkpoint is always saved, even if not marked as major.
-    this.ensureCheckpointSaved(id, this.isMajorCheckpoint(id) || id == FirstCheckpointId);
-  },
-
-  hasSavedCheckpoint(id) {
-    return (id <= this.lastCheckpoint()) &&
-           this._willSaveCheckpoints.includes(id);
-  },
-
-  hasSavedCheckpointsInRange(startId, endId) {
-    for (let i = startId; i <= endId; i++) {
-      if (!this.hasSavedCheckpoint(i)) {
-        return false;
-      }
-    }
-    return true;
-  },
-
-  lastSavedCheckpointPriorTo(id) {
-    while (!this.hasSavedCheckpoint(id)) {
-      id--;
-    }
-    return id;
-  },
-
-  sendAddBreakpoint(pos) {
-    assert(this.paused);
-    this.breakpoints.push(pos);
-    RecordReplayControl.sendAddBreakpoint(this.id, pos);
-  },
-
-  sendClearBreakpoints() {
-    assert(this.paused);
-    this.breakpoints.length = 0;
-    RecordReplayControl.sendClearBreakpoints(this.id);
-  },
-
-  sendDebuggerRequest(request) {
-    assert(this.paused);
-    this.debuggerRequests.push(request);
-    return RecordReplayControl.sendDebuggerRequest(this.id, request);
-  },
-};
-
-const FlushMs = .5 * 1000;
-const MajorCheckpointMs = 2 * 1000;
-
-// This section describes the strategy used for managing child processes. When
-// recording, there is a single recording process and two replaying processes.
-// When replaying, there are two replaying processes. The main advantage of
-// using two replaying processes is to provide a smooth experience when
-// rewinding.
-//
-// At any time there is one active child: the process which the user is
-// interacting with. This may be any of the two or three children in existence,
-// depending on the user's behavior. The other processes do not interact with
-// the user: inactive recording processes are inert, and sit idle until
-// recording is ready to resume, while inactive replaying processes are on
-// standby, staying close to the active process in the recording's execution
-// space and saving checkpoints in case the user starts rewinding.
-//
-// Below are some scenarios showing the state we attempt to keep the children
-// in, and ways in which the active process switches from one to another.
-// The execution diagrams show the position of each process, with '*' and '-'
-// indicating checkpoints the process reached and, respectively, whether
-// the checkpoint was saved or not.
-//
-// When the recording process is actively recording, flushes are issued to it
-// every FlushMs to keep the recording reasonably current and allow the
-// replaying processes to stay behind but close to the position of the
-// recording process. Additionally, one replaying process saves a checkpoint
-// every MajorCheckpointMs with the process saving the checkpoint alternating
-// back and forth so that individual processes save checkpoints every
-// MajorCheckpointMs*2. These are the major checkpoints for each replaying
-// process.
-//
-// Active  Recording:    -----------------------
-// Standby Replaying #1: *---------*---------*
-// Standby Replaying #2: -----*---------*-----
-//
-// When the recording process is explicitly paused (via the debugger UI) at a
-// checkpoint or breakpoint, it is flushed and the replaying processes will
-// navigate around the recording to ensure all checkpoints going back at least
-// MajorCheckpointMs have been saved. These are the intermediate checkpoints.
-// No replaying process needs to rewind past its last major checkpoint, and a
-// given intermediate checkpoint will only ever be saved by the replaying
-// process with the most recent major checkpoint.
-//
-// Active  Recording:    -----------------------
-// Standby Replaying #1: *---------*---------***
-// Standby Replaying #2: -----*---------*****
-//
-// If the user starts rewinding, the replaying process with the most recent
-// major checkpoint (and which has been saving the most recent intermediate
-// checkpoints) becomes the active child.
-//
-// Inert   Recording:    -----------------------
-// Active  Replaying #1: *---------*---------**
-// Standby Replaying #2: -----*---------*****
-//
-// As the user continues rewinding, the replaying process stays active until it
-// goes past its most recent major checkpoint. At that time the other replaying
-// process (which has been saving checkpoints prior to that point) becomes the
-// active child and allows continuous rewinding. The first replaying process
-// rewinds to its last major checkpoint and begins saving older intermediate
-// checkpoints, attempting to maintain the invariant that we have saved (or are
-// saving) all checkpoints going back MajorCheckpointMs.
-//
-// Inert   Recording:    -----------------------
-// Standby Replaying #1: *---------*****
-// Active  Replaying #2: -----*---------**
-//
-// Rewinding continues in this manner, alternating back and forth between the
-// replaying processes as the user continues going back in time.
-//
-// Inert   Recording:    -----------------------
-// Active  Replaying #1: *---------**
-// Standby Replaying #2: -----*****
-//
-// If the user starts navigating forward, the replaying processes both run
-// forward and save checkpoints at the same major checkpoints as earlier.
-// Note that this is how all forward execution works when there is no recording
-// process (i.e. we started from a saved recording).
-//
-// Inert   Recording:    -----------------------
-// Active  Replaying #1: *---------**------
-// Standby Replaying #2: -----*****-----*--
-//
-// If the user pauses at a checkpoint or breakpoint in the replay, we again
-// want to fill in all the checkpoints going back MajorCheckpointMs to allow
-// smooth rewinding. This cannot be done simultaneously -- as it was when the
-// recording process was active -- since we need to keep one of the replaying
-// processes at an up to date point and be the active one. This falls on the one
-// whose most recent major checkpoint is oldest, as the other is responsible for
-// saving the most recent intermediate checkpoints.
-//
-// Inert   Recording:    -----------------------
-// Active  Replaying #1: *---------**------
-// Standby Replaying #2: -----*****-----***
-//
-// After the recent intermediate checkpoints have been saved the process which
-// took them can become active so the older intermediate checkpoints can be
-// saved.
-//
-// Inert   Recording:    -----------------------
-// Standby Replaying #1: *---------*****
-// Active  Replaying #2: -----*****-----***
-//
-// Finally, if the replay plays forward to the end of the recording (the point
-// where the recording process is situated), the recording process takes over
-// again as the active child and the user can resume interacting with a live
-// process.
-//
-// Active  Recording:    ----------------------------------------
-// Standby Replaying #1: *---------*****-----*---------*-------
-// Standby Replaying #2: -----*****-----***-------*---------*--
-
-// Child processes that can participate in the above management.
-let gRecordingChild;
-let gFirstReplayingChild;
-let gSecondReplayingChild;
-let gActiveChild;
-
-function otherReplayingChild(child) {
-  assert(child == gFirstReplayingChild || child == gSecondReplayingChild);
-  return child == gFirstReplayingChild
-         ? gSecondReplayingChild
-         : gFirstReplayingChild;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Child Roles
-////////////////////////////////////////////////////////////////////////////////
-
-function ChildRoleActive() {}
-
-ChildRoleActive.prototype = {
-  name: "Active",
-
-  initialize(child, { startup }) {
-    this.child = child;
-    gActiveChild = child;
-
-    // Mark the child as active unless we are starting up, in which case it is
-    // unpaused and we can't send messages to it.
-    if (!startup) {
-      RecordReplayControl.setActiveChild(child.id);
-    }
-  },
-
-  hitExecutionPoint(msg) {
-    // Ignore HitCheckpoint messages received while doing a time warp.
-    // timeWarp() will immediately resume the child and we don't want to tell
-    // the debugger it ever paused.
-    if (gTimeWarpInProgress) {
-      return;
-    }
-
-    // Make sure the active child is marked as such when starting up.
-    if (msg.point.checkpoint == FirstCheckpointId) {
-      RecordReplayControl.setActiveChild(this.child.id);
-    }
-
-    updateCheckpointTimes(msg);
-
-    // When at the endpoint of the recording, immediately resume. We don't
-    // want to notify the debugger about this: if the user installed a
-    // breakpoint here we will have already gotten a HitExecutionPoint message
-    // *without* mRecordingEndpoint set, and we don't want to pause twice at
-    // the same point.
-    if (msg.recordingEndpoint) {
-      resume(true);
-      return;
-    }
-
-    // Run forward by default if there is no debugger attached, but post a
-    // runnable so that callers waiting for the child to pause don't starve.
-    if (!gDebugger) {
-      Services.tm.dispatchToMainThread(() => this.child.sendResume({ forward: true }));
-      return;
-    }
-
-    gDebugger._onPause();
-  },
-
-  poke() {},
-};
-
-// The last checkpoint included in the recording.
-let gLastRecordingCheckpoint;
-
-// The role taken by replaying children trying to stay close to the active
-// child and save either major or intermediate checkpoints, depending on
-// whether the active child is paused or rewinding.
-function ChildRoleStandby() {}
-
-ChildRoleStandby.prototype = {
-  name: "Standby",
-
-  initialize(child, { startup }) {
-    this.child = child;
-    if (!startup) {
-      this.poke();
-    }
-  },
-
-  hitExecutionPoint(msg) {
-    assert(!msg.point.position);
-    this.poke();
-  },
-
-  poke() {
-    assert(this.child.paused && !this.child.lastPausePoint.position);
-    const currentCheckpoint = this.child.lastCheckpoint();
-
-    // Stay paused if we need to while the recording is flushed.
-    if (this.child.pauseNeeded) {
-      return;
-    }
-
-    // Intermediate checkpoints are only saved when the active child is paused
-    // or rewinding.
-    let targetCheckpoint = getActiveChildTargetCheckpoint();
-    if (targetCheckpoint == undefined) {
-      // Intermediate checkpoints do not need to be saved. Run forward until we
-      // reach either the active child's position, or the last checkpoint
-      // included in the on-disk recording. Only save major checkpoints.
-      if ((currentCheckpoint < gActiveChild.lastCheckpoint()) &&
-          (!gRecordingChild || currentCheckpoint < gLastRecordingCheckpoint)) {
-        this.child.ensureMajorCheckpointSaved(currentCheckpoint + 1);
-        this.child.sendResume({ forward: true });
-      }
-      return;
-    }
-
-    // The startpoint of the range is the most recent major checkpoint prior
-    // to the target.
-    const lastMajorCheckpoint =
-      this.child.lastMajorCheckpointPreceding(targetCheckpoint);
-
-    // If there is no major checkpoint prior to the target, just idle.
-    if (lastMajorCheckpoint == InvalidCheckpointId) {
-      return;
-    }
-
-    // If we haven't reached the last major checkpoint, we need to run forward
-    // without saving intermediate checkpoints.
-    if (currentCheckpoint < lastMajorCheckpoint) {
-      this.child.ensureMajorCheckpointSaved(currentCheckpoint + 1);
-      this.child.sendResume({ forward: true });
-      return;
-    }
-
-    // The endpoint of the range is the checkpoint prior to either the active
-    // child's current position, or the other replaying child's most recent
-    // major checkpoint.
-    const otherChild = otherReplayingChild(this.child);
-    const otherMajorCheckpoint =
-      otherChild.lastMajorCheckpointPreceding(targetCheckpoint);
-    if (otherMajorCheckpoint > lastMajorCheckpoint) {
-      assert(otherMajorCheckpoint <= targetCheckpoint);
-      targetCheckpoint = otherMajorCheckpoint - 1;
-    }
-
-    // Find the first checkpoint in the fill range which we have not saved.
-    let missingCheckpoint;
-    for (let i = lastMajorCheckpoint; i <= targetCheckpoint; i++) {
-      if (!this.child.hasSavedCheckpoint(i)) {
-        missingCheckpoint = i;
-        break;
-      }
-    }
-
-    // If we have already saved everything we need to, we can idle.
-    if (missingCheckpoint == undefined) {
-      return;
-    }
-
-    // We must have saved the checkpoint prior to the missing one and can
-    // restore it. missingCheckpoint cannot be lastMajorCheckpoint, because we
-    // always save major checkpoints, and the loop above checked that all
-    // prior checkpoints going back to lastMajorCheckpoint have been saved.
-    const restoreTarget = missingCheckpoint - 1;
-    assert(this.child.hasSavedCheckpoint(restoreTarget));
-
-    // If we need to rewind to the restore target, do so.
-    if (currentCheckpoint != restoreTarget) {
-      this.child.sendRestoreCheckpoint(restoreTarget);
-      return;
-    }
-
-    // Make sure the process will save the next checkpoint.
-    this.child.ensureCheckpointSaved(missingCheckpoint, true);
-
-    // Run forward to the next checkpoint.
-    this.child.sendResume({ forward: true });
-  },
-};
-
-// The role taken by a child that always sits idle.
-function ChildRoleInert() {}
-
-ChildRoleInert.prototype = {
-  name: "Inert",
-
-  initialize() {},
-  hitExecutionPoint() {},
-  poke() {},
-};
-
-function pokeChildren() {
-  for (const child of gChildren) {
-    if (child && !child.recording && child.paused) {
-      child.pauseNeeded = false;
-      child.role.poke();
-    }
-  }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Child Switching
-////////////////////////////////////////////////////////////////////////////////
-
-// Change the current active child, and select a new role for the old one.
-function switchActiveChild(child, recoverPosition = true) {
-  assert(child != gActiveChild);
-  assert(gActiveChild.paused);
-
-  const oldActiveChild = gActiveChild;
-  child.pauseNeeded = true;
-  child.waitUntilPaused();
-  child.pauseNeeded = false;
-
-  // Move the installed breakpoints from the old child to the new child.
-  assert(child.breakpoints.length == 0);
-  for (const pos of oldActiveChild.breakpoints) {
-    child.sendAddBreakpoint(pos);
-  }
-  oldActiveChild.sendClearBreakpoints();
-
-  if (recoverPosition && !child.recording) {
-    child.setRole(new ChildRoleInert());
-    const targetCheckpoint = oldActiveChild.lastCheckpoint();
-    if (child.lastCheckpoint() > targetCheckpoint) {
-      const restoreCheckpoint =
-        child.lastSavedCheckpointPriorTo(targetCheckpoint);
-      child.sendRestoreCheckpoint(restoreCheckpoint);
-      child.waitUntilPaused();
-    }
-    while (child.lastCheckpoint() < targetCheckpoint) {
-      child.ensureMajorCheckpointSaved(child.lastCheckpoint() + 1);
-      child.sendResume({ forward: true });
-      child.waitUntilPaused();
-    }
-    assert(!child.lastPausePoint.position);
-    if (oldActiveChild.lastPausePoint.position) {
-      child.sendRunToPoint(oldActiveChild.lastPausePoint);
-      child.waitUntilPaused();
-    }
-    for (const request of oldActiveChild.debuggerRequests) {
-      child.sendDebuggerRequest(request);
-    }
-  }
-
-  child.setRole(new ChildRoleActive());
-  oldActiveChild.setRole(new ChildRoleInert());
-
-  if (!oldActiveChild.recording) {
-    if (oldActiveChild.lastPausePoint.position) {
-      // Standby replaying children must be paused at a checkpoint.
-      const oldCheckpoint = oldActiveChild.lastCheckpoint();
-      const restoreCheckpoint =
-        oldActiveChild.lastSavedCheckpointPriorTo(oldCheckpoint);
-      oldActiveChild.sendRestoreCheckpoint(restoreCheckpoint);
-      oldActiveChild.waitUntilPaused();
-    }
-    oldActiveChild.setRole(new ChildRoleStandby());
-  }
-
-  // Notify the debugger when switching between recording and replaying
-  // children.
-  if (child.recording != oldActiveChild.recording) {
-    gDebugger._onSwitchChild();
-  }
-}
-
-function maybeSwitchToReplayingChild() {
-  if (gActiveChild.recording && RecordReplayControl.canRewind()) {
-    flushRecording();
-    const checkpoint = gActiveChild.rewindTargetCheckpoint();
-    const child = otherReplayingChild(
-      replayingChildResponsibleForSavingCheckpoint(checkpoint));
-    switchActiveChild(child);
-  }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Major Checkpoints
-////////////////////////////////////////////////////////////////////////////////
-
-// For each checkpoint N, this vector keeps track of the time intervals taken
-// for the active child (excluding idle time) to run from N to N+1.
-const gCheckpointTimes = [];
-
-// How much time has elapsed (per gCheckpointTimes) since the last flush or
-// major checkpoint was noted.
-let gTimeSinceLastFlush;
-let gTimeSinceLastMajorCheckpoint;
-
-// The replaying process that was given the last major checkpoint.
-let gLastAssignedMajorCheckpoint;
-
-function assignMajorCheckpoint(child, checkpointId) {
-  dumpv(`AssignMajorCheckpoint: #${child.id} Checkpoint ${checkpointId}`);
-  child.addMajorCheckpoint(checkpointId);
-  gLastAssignedMajorCheckpoint = child;
-}
-
-function updateCheckpointTimes(msg) {
-  if (msg.point.checkpoint != gCheckpointTimes.length + 1 ||
-      msg.point.position) {
-    return;
-  }
-  gCheckpointTimes.push(msg.duration);
-
-  if (gActiveChild.recording) {
-    gTimeSinceLastFlush += msg.duration;
-
-    // Occasionally flush while recording so replaying processes stay
-    // reasonably current.
-    if (msg.point.checkpoint == FirstCheckpointId ||
-        gTimeSinceLastFlush >= FlushMs) {
-      if (maybeFlushRecording()) {
-        gTimeSinceLastFlush = 0;
-      }
-    }
-  }
-
-  gTimeSinceLastMajorCheckpoint += msg.duration;
-
-  if (gTimeSinceLastMajorCheckpoint >= MajorCheckpointMs) {
-    // Alternate back and forth between assigning major checkpoints to the
-    // two replaying processes.
-    const child = otherReplayingChild(gLastAssignedMajorCheckpoint);
-    assignMajorCheckpoint(child, msg.point.checkpoint + 1);
-    gTimeSinceLastMajorCheckpoint = 0;
-  }
-}
-
-// Get the replaying process responsible for saving id when rewinding: the one
-// with the most recent major checkpoint preceding id.
-function replayingChildResponsibleForSavingCheckpoint(id) {
-  assert(gFirstReplayingChild && gSecondReplayingChild);
-  const firstMajor = gFirstReplayingChild.lastMajorCheckpointPreceding(id);
-  const secondMajor = gSecondReplayingChild.lastMajorCheckpointPreceding(id);
-  return (firstMajor < secondMajor)
-         ? gSecondReplayingChild
-         : gFirstReplayingChild;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Saving Recordings
-////////////////////////////////////////////////////////////////////////////////
-
-// Synchronously flush the recording to disk.
-function flushRecording() {
-  assert(gActiveChild.recording && gActiveChild.paused);
-
-  // All replaying children must be paused while the recording is flushed.
-  for (const child of gChildren) {
-    if (child && !child.recording) {
-      child.pauseNeeded = true;
-      child.waitUntilPaused();
-    }
-  }
-
-  gActiveChild.sendFlushRecording();
-
-  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();
-  }
-}
-
-// Get the replaying children to pause, and flush the recording if they already
-// are.
-function maybeFlushRecording() {
-  assert(gActiveChild.recording && gActiveChild.paused);
-
-  let allPaused = true;
-  for (const child of gChildren) {
-    if (child && !child.recording) {
-      child.pauseNeeded = true;
-      allPaused &= child.paused;
-    }
-  }
-
-  if (allPaused) {
-    flushRecording();
-    return true;
-  }
-  return false;
-}
-
-// eslint-disable-next-line no-unused-vars
-function BeforeSaveRecording() {
-  if (gActiveChild.recording) {
-    // The recording might not be up to date, flush it now.
-    gActiveChild.waitUntilPaused(true);
-    flushRecording();
-  }
-}
-
-// eslint-disable-next-line no-unused-vars
-function AfterSaveRecording() {
-  Services.cpmm.sendAsyncMessage("SaveRecordingFinished");
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Child Management
-////////////////////////////////////////////////////////////////////////////////
-
-function spawnReplayingChild(role) {
-  const id = RecordReplayControl.spawnReplayingChild();
-  return new ChildProcess(id, false, role);
-}
-
-function spawnInitialReplayingChildren() {
-  gFirstReplayingChild = spawnReplayingChild(gRecordingChild
-                                             ? new ChildRoleStandby()
-                                             : new ChildRoleActive());
-  gSecondReplayingChild = spawnReplayingChild(new ChildRoleStandby());
-
-  assignMajorCheckpoint(gSecondReplayingChild, FirstCheckpointId);
-}
-
-// eslint-disable-next-line no-unused-vars
-function Initialize(recordingChildId) {
-  try {
-    if (recordingChildId != undefined) {
-      gRecordingChild = new ChildProcess(recordingChildId, true,
-                                         new ChildRoleActive());
-    } else {
-      // If there is no recording child, we have now initialized enough state
-      // that we can start spawning replaying children.
-      spawnInitialReplayingChildren();
-    }
-    return gControl;
-  } catch (e) {
-    dump(`ERROR: Initialize threw exception: ${e}\n`);
-  }
-}
-
-// eslint-disable-next-line no-unused-vars
-function HitExecutionPoint(id, msg) {
-  try {
-    dumpv(`HitExecutionPoint #${id} ${JSON.stringify(msg)}`);
-    gChildren[id].hitExecutionPoint(msg);
-  } catch (e) {
-    dump(`ERROR: HitExecutionPoint threw exception: ${e}\n`);
-  }
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Explicit Pauses
-///////////////////////////////////////////////////////////////////////////////
-
-// At the last time the active child was explicitly paused, the ID of the
-// checkpoint that needs to be saved for the child to rewind.
-let gLastExplicitPause = FirstCheckpointId;
-
-// Any checkpoint we are trying to warp to and pause.
-let gTimeWarpTarget;
-
-// Returns a checkpoint if the active child is explicitly paused somewhere,
-// has started rewinding after being explicitly paused, or is attempting to
-// warp to an execution point. The checkpoint returned is the latest one which
-// should be saved, and standby roles must save all intermediate checkpoints
-// they are responsible for, in the range from their most recent major
-// checkpoint up to the returned checkpoint.
-function getActiveChildTargetCheckpoint() {
-  if (gTimeWarpTarget != undefined) {
-    return gTimeWarpTarget;
-  }
-  if (gActiveChild.rewindTargetCheckpoint() <= gLastExplicitPause) {
-    return gActiveChild.rewindTargetCheckpoint();
-  }
-  return undefined;
-}
-
-function markExplicitPause() {
-  assert(gActiveChild.paused);
-  const targetCheckpoint = gActiveChild.rewindTargetCheckpoint();
-
-  if (gActiveChild.recording) {
-    // Make sure any replaying children can play forward to the same point as
-    // the recording.
-    flushRecording();
-  } else if (RecordReplayControl.canRewind()) {
-    // Make sure we have a replaying child that can rewind from this point.
-    // Switch to the other one if (a) this process is responsible for rewinding
-    // from this point, and (b) this process has not saved all intermediate
-    // checkpoints going back to its last major checkpoint.
-    if (gActiveChild ==
-        replayingChildResponsibleForSavingCheckpoint(targetCheckpoint)) {
-      const lastMajorCheckpoint =
-        gActiveChild.lastMajorCheckpointPreceding(targetCheckpoint);
-      if (!gActiveChild.hasSavedCheckpointsInRange(lastMajorCheckpoint,
-                                                   targetCheckpoint)) {
-        switchActiveChild(otherReplayingChild(gActiveChild));
-      }
-    }
-  }
-
-  gLastExplicitPause = targetCheckpoint;
-  dumpv(`MarkActiveChildExplicitPause ${gLastExplicitPause}`);
-
-  pokeChildren();
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Debugger Operations
-////////////////////////////////////////////////////////////////////////////////
-
-function 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
-  // the result.
-  if (RecordReplayControl.inRepaintStressMode()) {
-    maybeSwitchToReplayingChild();
-    const rv = gActiveChild.sendRequest({ type: "repaint" });
-    if ("width" in rv && "height" in rv) {
-      RecordReplayControl.hadRepaint(rv.width, rv.height);
-    }
-  }
-}
-
-function waitUntilChildHasSavedCheckpoint(child, checkpoint) {
-  while (true) {
-    child.pauseNeeded = true;
-    child.waitUntilPaused();
-    child.pauseNeeded = false;
-    if (child.hasSavedCheckpoint(checkpoint)) {
-      return;
-    }
-    child.role.poke();
-  }
-}
-
-function resume(forward) {
-  assert(gActiveChild.paused);
-
-  maybeSendRepaintMessage();
-
-  // When rewinding, make sure the active child can rewind to the previous
-  // checkpoint.
-  if (!forward &&
-      !gActiveChild.hasSavedCheckpoint(gActiveChild.rewindTargetCheckpoint())) {
-    const targetCheckpoint = gActiveChild.rewindTargetCheckpoint();
-
-    // Don't rewind if we are at the beginning of the recording.
-    if (targetCheckpoint == InvalidCheckpointId) {
-      Services.cpmm.sendAsyncMessage("HitRecordingBeginning");
-      gDebugger._onPause(gActiveChild.lastPausePoint);
-      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.
-    const targetChild =
-      replayingChildResponsibleForSavingCheckpoint(targetCheckpoint);
-    assert(targetChild != gActiveChild);
-
-    waitUntilChildHasSavedCheckpoint(targetChild, targetCheckpoint);
-    switchActiveChild(targetChild);
-  }
-
-  if (forward) {
-    // Don't send a replaying process past the recording endpoint.
-    if (gActiveChild.lastPauseAtRecordingEndpoint) {
-      // Look for a recording child we can transition into.
-      assert(!gActiveChild.recording);
-      if (!gRecordingChild) {
-        Services.cpmm.sendAsyncMessage("HitRecordingEndpoint");
-        if (gDebugger) {
-          gDebugger._onPause(gActiveChild.lastPausePoint);
-        }
-        return;
-      }
-
-      // Switch to the recording child as the active child and continue
-      // execution.
-      switchActiveChild(gRecordingChild);
-    }
-
-    gActiveChild.ensureMajorCheckpointSaved(gActiveChild.lastCheckpoint() + 1);
-
-    // Idle children might change their behavior as we run forward.
-    pokeChildren();
-  }
-
-  gActiveChild.sendResume({ forward });
-}
-
-let gTimeWarpInProgress;
-
-function timeWarp(targetPoint) {
-  assert(gActiveChild.paused);
-  const targetCheckpoint = targetPoint.checkpoint;
-
-  // Make sure the active child can rewind to the checkpoint prior to the
-  // warp target.
-  assert(gTimeWarpTarget == undefined);
-  gTimeWarpTarget = targetCheckpoint;
-
-  pokeChildren();
-
-  if (!gActiveChild.hasSavedCheckpoint(targetCheckpoint)) {
-    // Find the replaying child responsible for saving the target checkpoint.
-    const targetChild =
-      replayingChildResponsibleForSavingCheckpoint(targetCheckpoint);
-
-    if (targetChild == gActiveChild) {
-      // Switch to the other replaying child while this one saves the necessary
-      // checkpoint.
-      switchActiveChild(otherReplayingChild(gActiveChild));
-    }
-
-    waitUntilChildHasSavedCheckpoint(targetChild, targetCheckpoint);
-    switchActiveChild(targetChild, /* aRecoverPosition = */ false);
-  }
-
-  gTimeWarpTarget = undefined;
-
-  if (gActiveChild.lastPausePoint.position ||
-      gActiveChild.lastCheckpoint() != targetCheckpoint) {
-    assert(!gTimeWarpInProgress);
-    gTimeWarpInProgress = true;
-
-    gActiveChild.sendRestoreCheckpoint(targetCheckpoint);
-    gActiveChild.waitUntilPaused();
-
-    gTimeWarpInProgress = false;
-  }
-
-  gActiveChild.sendRunToPoint(targetPoint);
-  gActiveChild.waitUntilPaused();
-
-  Services.cpmm.sendAsyncMessage("TimeWarpFinished");
-}
-
-const gControl = {
-  pausePoint() { return gActiveChild.paused ? gActiveChild.lastPausePoint : null; },
-  childIsRecording() { return gActiveChild.recording; },
-  waitUntilPaused() {
-    // Use a loop because the active child can change while running if a
-    // replaying active child hits the end of the recording.
-    while (!gActiveChild.paused) {
-      gActiveChild.waitUntilPaused(true);
-    }
-  },
-  addBreakpoint(pos) { gActiveChild.sendAddBreakpoint(pos); },
-  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;
-}
-
-function dumpv(str) {
-  //dump("[ReplayControl] " + str + "\n");
-}
-
-function assert(v) {
-  if (!v) {
-    ThrowError("Assertion Failed!");
-  }
-}
-
-function ThrowError(msg)
-{
-  const error = new Error(msg);
-  dump("ReplayControl Server Error: " + msg + " Stack: " + error.stack + "\n");
-  throw error;
-}
-
-// eslint-disable-next-line no-unused-vars
-var EXPORTED_SYMBOLS = [
-  "Initialize",
-  "ConnectDebugger",
-  "HitExecutionPoint",
-  "BeforeSaveRecording",
-  "AfterSaveRecording",
-];
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -8,18 +8,18 @@
 // When recording/replaying an execution with Web Replay, Devtools server code
 // runs in the middleman process instead of the recording/replaying process the
 // code is interested in.
 //
 // This file defines replay objects analogous to those constructed by the
 // C++ Debugger (Debugger, Debugger.Object, etc.), which implement similar
 // 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 interface provided by
-// control.js.
+// recording/replaying process, inspecting them via the RecordReplayControl
+// interface.
 
 "use strict";
 
 const RecordReplayControl = !isWorker && require("RecordReplayControl");
 const Services = require("Services");
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebugger
@@ -35,19 +35,18 @@ const Direction = {
 function ReplayDebugger() {
   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;
   }
 
-  // We should have been connected to control.js by the call above.
-  assert(this._control);
-  assert(this._searchControl);
+  // 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
@@ -71,19 +70,16 @@ 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;
 }
 
@@ -96,54 +92,53 @@ ReplayDebugger.prototype = {
   // General methods
   /////////////////////////////////////////////////////////
 
   replaying: true,
 
   canRewind: RecordReplayControl.canRewind,
 
   replayCurrentExecutionPoint() {
-    assert(this._paused);
-    return this._control.pausePoint();
+    return this._sendRequest({ type: "currentExecutionPoint" });
   },
 
   replayRecordingEndpoint() {
     return this._sendRequest({ type: "recordingEndpoint" });
   },
 
-  replayIsRecording() {
-    return this._control.childIsRecording();
-  },
+  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) {
-    const data = this._control.sendRequest(request);
+    assert(this._paused);
+    const data = RecordReplayControl.sendRequest(request);
     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) {
-    this._control.maybeSwitchToReplayingChild();
+    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" });
@@ -173,43 +168,41 @@ ReplayDebugger.prototype = {
   //   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).
 
-  get _paused() {
-    return !!this._control.pausePoint();
-  },
-
   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);
-      this._control.resume(forward);
+      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));
-      this._control.timeWarp(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());
     });
   },
 
@@ -217,25 +210,27 @@ ReplayDebugger.prototype = {
     this._ensurePaused();
 
     // Cancel any pending resume.
     this._resumeCallback = null;
   },
 
   _ensurePaused() {
     if (!this._paused) {
-      this._control.waitUntilPaused();
+      RecordReplayControl.waitUntilPaused();
       assert(this._paused);
     }
   },
 
   // This hook is called whenever the child has paused, which can happen
-  // within a control method (resume, timeWarp, waitUntilPaused) or be
-  // delivered via the event loop.
+  // 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;
@@ -248,17 +243,17 @@ ReplayDebugger.prototype = {
     // 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) {
+    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) {
@@ -280,28 +275,32 @@ ReplayDebugger.prototype = {
     }
   },
 
   // 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.
-      this._control.markExplicitPause();
+      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
@@ -348,76 +347,35 @@ 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);
+    RecordReplayControl.addBreakpoint(position);
     this._breakpoints.push({handler, position, data});
   },
 
   _clearMatchingBreakpoints(callback) {
     this._ensurePaused();
     const newBreakpoints = this._breakpoints.filter(bp => !callback(bp));
     if (newBreakpoints.length != this._breakpoints.length) {
       dumpv("ClearBreakpoints");
-      this._control.clearBreakpoints();
+      RecordReplayControl.clearBreakpoints();
       for (const { position } of newBreakpoints) {
         dumpv("AddBreakpoint " + JSON.stringify(position));
-        this._control.addBreakpoint(position);
+        RecordReplayControl.addBreakpoint(position);
       }
     }
     this._breakpoints = newBreakpoints;
   },
 
   _searchBreakpoints(callback) {
     for (const breakpoint of this._breakpoints) {
       const v = callback(breakpoint);
@@ -482,17 +440,16 @@ ReplayDebugger.prototype = {
     }
     if ("source" in query) {
       rv.source = query.source._data.id;
     }
     return rv;
   },
 
   findScripts(query) {
-    this._ensurePaused();
     const data = this._sendRequest({
       type: "findScripts",
       query: this._convertScriptQuery(query),
     });
     return data.map(script => this._addScript(script));
   },
 
   findAllConsoleMessages() {
@@ -525,69 +482,63 @@ ReplayDebugger.prototype = {
     const data = this._sendRequest({ type: "findSources" });
     return data.map(source => this._addSource(source));
   },
 
   /////////////////////////////////////////////////////////
   // Object methods
   /////////////////////////////////////////////////////////
 
-  _getObject(id, options) {
-    if (options && options.forSearch) {
-      // Returning objects through searches is NYI.
-      return "<UnknownSearchObject>";
-    }
-    const forConsole = options && options.forConsole;
-
+  // 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) {
     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, options) {
+  _convertValue(value, forConsole) {
     if (isNonNullObject(value)) {
       if (value.object) {
-        return this._getObject(value.object, options);
+        return this._getObject(value.object, forConsole);
       } 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, options) {
+  _convertCompletionValue(value) {
     if ("return" in value) {
-      return { return: this._convertValue(value.return, options) };
+      return { return: this._convertValue(value.return) };
     }
     if ("throw" in value) {
-      return { throw: this._convertValue(value.throw, options) };
+      return { throw: this._convertValue(value.throw) };
     }
     ThrowError("Unexpected completion value");
     return null; // For eslint
   },
 
   /////////////////////////////////////////////////////////
   // Frame methods
   /////////////////////////////////////////////////////////
@@ -628,17 +579,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
   /////////////////////////////////////////////////////////
@@ -705,49 +656,44 @@ function ReplayDebuggerScript(dbg, data)
 ReplayDebuggerScript.prototype = {
   get displayName() { return this._data.displayName; },
   get url() { return this._data.url; },
   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) {
     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"); },
 
   setBreakpoint(offset, handler) {
     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;
     });
   },
 
-  replayVirtualConsoleLog(offset, text, callback) {
-    this._dbg._virtualConsoleLog({ kind: "Break", script: this._data.id, offset },
-                                 text, callback);
-  },
-
   get isGeneratorFunction() { NYI(); },
   get isAsyncFunction() { NYI(); },
+  get format() { NYI(); },
   getChildScripts: NYI,
   getAllOffsets: NYI,
+  getAllColumnOffsets: NYI,
   getBreakpoints: NYI,
   clearAllBreakpoints: NYI,
   isInCatchScope: NYI,
 };
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerScriptSource
 ///////////////////////////////////////////////////////////////////////////////
--- a/devtools/server/actors/replay/moz.build
+++ b/devtools/server/actors/replay/moz.build
@@ -1,20 +1,18 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
-    'control.js',
     'debugger.js',
     'graphics.js',
     'replay.js',
 )
 
 XPIDL_MODULE = 'devtools_rr'
 
 XPIDL_SOURCES = [
-    'rrIControl.idl',
     'rrIGraphics.idl',
     'rrIReplay.idl',
 ]
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -514,17 +514,16 @@ function getScriptData(id) {
     id,
     sourceId: gScriptSources.getId(script.source),
     startLine: script.startLine,
     lineCount: script.lineCount,
     sourceStart: script.sourceStart,
     sourceLength: script.sourceLength,
     displayName: script.displayName,
     url: script.url,
-    format: script.format,
   };
 }
 
 function getSourceData(id) {
   const source = gScriptSources.getObject(id);
   const introductionScript = gScripts.getId(source.introductionScript);
   return {
     id: id,
@@ -734,17 +733,16 @@ const gRequestHandlers = {
       arguments: _arguments,
     };
   },
 
   getLineOffsets: forwardToScript("getLineOffsets"),
   getOffsetLocation: forwardToScript("getOffsetLocation"),
   getSuccessorOffsets: forwardToScript("getSuccessorOffsets"),
   getPredecessorOffsets: forwardToScript("getPredecessorOffsets"),
-  getAllColumnOffsets: forwardToScript("getAllColumnOffsets"),
 
   frameEvaluate(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return { throw: "Recording divergence in frameEvaluate" };
     }
 
     const frame = scriptFrameForIndex(request.index);
     const rv = frame.eval(request.text, request.options);
deleted file mode 100644
--- a/devtools/server/actors/replay/rrIControl.idl
+++ /dev/null
@@ -1,17 +0,0 @@
-/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#include "nsISupports.idl"
-
-// This interface defines the methods used for calling into control.js in a
-// middleman process.
-[scriptable, uuid(c296e7c3-8a27-4fd0-94c2-b6e5126909ba)]
-interface rrIControl : nsISupports {
-  void Initialize(in jsval recordingChildId);
-  void ConnectDebugger(in jsval replayDebugger);
-  void HitExecutionPoint(in long childId, in jsval msg);
-  void BeforeSaveRecording();
-  void AfterSaveRecording();
-};
--- a/devtools/server/actors/replay/rrIReplay.idl
+++ b/devtools/server/actors/replay/rrIReplay.idl
@@ -1,18 +1,19 @@
 /* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
-// This interface defines the methods used for calling into replay.js in a
-// recording/replaying process. See JSControl.h for the documentation of these
-// methods.
 [scriptable, uuid(8b86b71f-8471-472e-9997-c5f21f9d0598)]
 interface rrIReplay : nsISupports {
   jsval ProcessRequest(in jsval request);
+
   void EnsurePositionHandler(in jsval position);
+
   void ClearPositionHandlers();
+
   void ClearPausedState();
+
   jsval GetEntryPosition(in jsval position);
 };
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -731,30 +731,46 @@ const SourceActor = ActorClassWithSpec(s
         generatedLocation
       )) {
         success = true;
       }
     }
     return success;
   },
 
-  _findEntryPointsForLocation: function(generatedLocation) {
+  /*
+   * Ensure the given BreakpointActor is set as breakpoint handler on all
+   * scripts that match the given location in the generated source.
+   *
+   * @param BreakpointActor actor
+   *        The BreakpointActor to be set as a breakpoint handler.
+   * @param GeneratedLocation generatedLocation
+   *        A GeneratedLocation representing the location in the generated
+   *        source for which the given BreakpointActor is to be set as a
+   *        breakpoint handler.
+   *
+   * @returns A Boolean that is true if the BreakpointActor was set as a
+   *          breakpoint handler on at least one script, and false otherwise.
+   */
+  _setBreakpointAtGeneratedLocation: function(actor, generatedLocation) {
     const {
       generatedSourceActor,
       generatedLine,
       generatedColumn,
       generatedLastColumn,
     } = generatedLocation;
 
     // Find all scripts that match the given source actor and line
     // number.
-    const scripts = generatedSourceActor._findDebuggeeScripts(
+    let scripts = generatedSourceActor._findDebuggeeScripts(
       { line: generatedLine }
     );
 
+    scripts = scripts.filter((script) => !actor.hasScript(script));
+
     // Find all entry points that correspond to the given location.
     const entryPoints = [];
     if (generatedColumn === undefined) {
       // This is a line breakpoint, so we are interested in all offsets
       // that correspond to the given line number.
       for (const script of scripts) {
         const offsets = script.getLineOffsets(generatedLine);
         if (offsets.length > 0) {
@@ -810,61 +826,18 @@ const SourceActor = ActorClassWithSpec(s
             if (generatedColumn > lastColumnOffset.columnNumber) {
               entryPoints.push({ script, offsets: [lastColumnOffset.offset] });
             }
           }
         }
       }
     }
 
-    return entryPoints;
-  },
-
-  /*
-   * Ensure the given BreakpointActor is set as breakpoint handler on all
-   * scripts that match the given location in the generated source.
-   *
-   * @param BreakpointActor actor
-   *        The BreakpointActor to be set as a breakpoint handler.
-   * @param GeneratedLocation generatedLocation
-   *        A GeneratedLocation representing the location in the generated
-   *        source for which the given BreakpointActor is to be set as a
-   *        breakpoint handler.
-   *
-   * @returns A Boolean that is true if the BreakpointActor was set as a
-   *          breakpoint handler on at least one script, and false otherwise.
-   */
-  _setBreakpointAtGeneratedLocation: function(actor, generatedLocation) {
-    let entryPoints = this._findEntryPointsForLocation(generatedLocation);
-    entryPoints = entryPoints.filter(({script}) => !actor.hasScript(script));
-
     if (entryPoints.length === 0) {
       return false;
     }
 
     setBreakpointAtEntryPoints(actor, entryPoints);
     return true;
   },
-
-  setVirtualLog(line, column, text) {
-    const location = new GeneratedLocation(this, line, column);
-    const entryPoints = this._findEntryPointsForLocation(location);
-
-    for (const { script, offsets } of entryPoints) {
-      for (const offset of offsets) {
-        script.replayVirtualConsoleLog(offset, text, (point, rv) => {
-          const packet = {
-            from: this.actorID,
-            type: "virtualConsoleLog",
-            url: this.url,
-            line,
-            column,
-            executionPoint: point,
-            message: "return" in rv ? "" + rv.return : "" + rv.throw,
-          };
-          this.conn.send(packet);
-        });
-      }
-    }
-  },
 });
 
 exports.SourceActor = SourceActor;
--- a/devtools/shared/client/constants.js
+++ b/devtools/shared/client/constants.js
@@ -32,17 +32,16 @@ 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
@@ -243,22 +243,16 @@ SourceClient.prototype = {
       const cleanUp = type == "paused" && why.type == "interrupted"
             ? () => this._activeThread.resume()
             : noop;
 
       return doSetBreakpoint(cleanUp);
     });
   },
 
-  setVirtualLog: DebuggerClient.requester({
-    type: "setVirtualLog",
-    location: arg(0),
-    text: arg(1),
-  }),
-
   setPausePoints: function(pausePoints) {
     const packet = {
       to: this._form.actor,
       type: "setPausePoints",
       pausePoints,
     };
     return this._client.request(packet);
   },
--- a/devtools/shared/specs/source.js
+++ b/devtools/shared/specs/source.js
@@ -61,21 +61,12 @@ const sourceSpec = generateActorSpec({
           line: Arg(0, "number"),
           column: Arg(1, "nullable:number"),
         },
         condition: Arg(2, "nullable:string"),
         noSliding: Arg(3, "nullable:boolean"),
       },
       response: RetVal("json"),
     },
-    setVirtualLog: {
-      request: {
-        location: {
-          line: Arg(0, "number"),
-          column: Arg(1, "nullable:number"),
-        },
-        text: Arg(2, "string"),
-      },
-    },
   },
 });
 
 exports.sourceSpec = sourceSpec;
--- a/toolkit/recordreplay/MiddlemanCall.cpp
+++ b/toolkit/recordreplay/MiddlemanCall.cpp
@@ -6,52 +6,36 @@
 
 #include "MiddlemanCall.h"
 
 #include <unordered_map>
 
 namespace mozilla {
 namespace recordreplay {
 
-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, all middleman calls that have been
+// encountered, indexed by their ID.
+static StaticInfallibleVector<MiddlemanCall*> gMiddlemanCalls;
 
-  // In a middleman process, any buffers allocated for performed calls.
-  InfallibleVector<void*> mAllocatedBuffers;
-};
+// 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 replaying process, all middleman call state. In a middleman process,
-// state for the child currently being processed.
-static MiddlemanCallState* gState;
+// In a middleman process, any buffers allocated for performed calls.
+static StaticInfallibleVector<void*> gAllocatedBuffers;
 
-// 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.
+// Lock protecting middleman call state.
 static Monitor* gMonitor;
 
 void InitializeMiddlemanCalls() {
   MOZ_RELEASE_ASSERT(IsRecordingOrReplaying() || IsMiddleman());
 
-  if (IsReplaying()) {
-    gState = new MiddlemanCallState();
-    gMonitor = new Monitor();
-  }
+  gMiddlemanCallMap = new MiddlemanCallMap();
+  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);
@@ -90,31 +74,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 = gState->mCalls.length();
+  size_t id = gMiddlemanCalls.length();
   MiddlemanCall* newCall = new MiddlemanCall();
-  gState->mCalls.emplaceBack(newCall);
+  gMiddlemanCalls.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;
-      gState->mCalls.popBack();
+      gMiddlemanCalls.popBack();
       if (child::CurrentRepaintCannotFail()) {
         child::ReportFatalError(Nothing(),
                                 "Middleman call preface failed: %s\n",
                                 redirection.mName);
       }
       return false;
     }
   }
@@ -165,28 +149,20 @@ 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(size_t aChildId, const char* aInputData, size_t aInputSize,
+void ProcessMiddlemanCall(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);
@@ -211,102 +187,89 @@ void ProcessMiddlemanCall(size_t aChildI
       MiddlemanCallContext cx(call, &arguments,
                               MiddlemanCallPhase::MiddlemanOutput);
       redirection.mMiddlemanCall(cx);
     }
 
     call->mArguments.CopyFrom(&arguments);
     call->EncodeOutput(outputStream);
 
-    while (call->mId >= gState->mCalls.length()) {
-      gState->mCalls.emplaceBack(nullptr);
+    while (call->mId >= gMiddlemanCalls.length()) {
+      gMiddlemanCalls.emplaceBack(nullptr);
     }
-    MOZ_RELEASE_ASSERT(!gState->mCalls[call->mId]);
-    gState->mCalls[call->mId] = call;
+    MOZ_RELEASE_ASSERT(!gMiddlemanCalls[call->mId]);
+    gMiddlemanCalls[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()) {
-    gState->mAllocatedBuffers.append(rv);
+    gAllocatedBuffers.append(rv);
   }
 
   return rv;
 }
 
-void ResetMiddlemanCalls(size_t aChildId) {
+void ResetMiddlemanCalls() {
   MOZ_RELEASE_ASSERT(IsMiddleman());
 
-  if (aChildId >= gStatePerChild.length()) {
-    return;
-  }
-
-  gState = gStatePerChild[aChildId];
-  if (!gState) {
-    return;
-  }
-
-  for (MiddlemanCall* call : gState->mCalls) {
+  for (MiddlemanCall* call : gMiddlemanCalls) {
     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 : gState->mCalls) {
+  for (MiddlemanCall* call : gMiddlemanCalls) {
     delete call;
   }
 
-  gState->mCalls.clear();
-  for (auto buffer : gState->mAllocatedBuffers) {
+  gMiddlemanCalls.clear();
+  for (auto buffer : gAllocatedBuffers) {
     free(buffer);
   }
-  gState->mAllocatedBuffers.clear();
-  gState->mCallMap.clear();
-
-  gState = nullptr;
+  gAllocatedBuffers.clear();
+  gMiddlemanCallMap->clear();
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // System Values
 ///////////////////////////////////////////////////////////////////////////////
 
 static void AddMiddlemanCallValue(const void* aThing, MiddlemanCall* aCall) {
-  gState->mCallMap.erase(aThing);
-  gState->mCallMap.insert(MiddlemanCallMap::value_type(aThing, aCall));
+  gMiddlemanCallMap->erase(aThing);
+  gMiddlemanCallMap->insert(MiddlemanCallMap::value_type(aThing, aCall));
 }
 
 static MiddlemanCall* LookupMiddlemanCall(const void* aThing) {
-  MiddlemanCallMap::const_iterator iter = gState->mCallMap.find(aThing);
-  if (iter != gState->mCallMap.end()) {
+  MiddlemanCallMap::const_iterator iter = gMiddlemanCallMap->find(aThing);
+  if (iter != gMiddlemanCallMap->end()) {
     return iter->second;
   }
   return nullptr;
 }
 
 static const void* GetMiddlemanCallValue(size_t aId) {
   MOZ_RELEASE_ASSERT(IsMiddleman());
-  MOZ_RELEASE_ASSERT(aId < gState->mCalls.length() && gState->mCalls[aId] &&
-                     gState->mCalls[aId]->mMiddlemanValue.isSome());
-  return gState->mCalls[aId]->mMiddlemanValue.ref();
+  MOZ_RELEASE_ASSERT(aId < gMiddlemanCalls.length() && gMiddlemanCalls[aId] &&
+                     gMiddlemanCalls[aId]->mMiddlemanValue.isSome());
+  return gMiddlemanCalls[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;
@@ -325,17 +288,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(gState->mCalls[callId.ref()]);
+        aCx.mDependentCalls->append(gMiddlemanCalls[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,24 +338,22 @@ 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. The calls are associated with the
-// specified child process ID.
-void ProcessMiddlemanCall(size_t aChildId,
-                          const char* aInputData, size_t aInputSize,
+// and encode their outputs to aOutputData.
+void ProcessMiddlemanCall(const char* aInputData, size_t aInputSize,
                           InfallibleVector<char>* aOutputData);
 
-// In the middleman process, reset all call state for a child process ID.
-void ResetMiddlemanCalls(size_t aChildId);
+// In the middleman process, reset all call state.
+void ResetMiddlemanCalls();
 
 ///////////////////////////////////////////////////////////////////////////////
 // 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.cpp
+++ b/toolkit/recordreplay/ipc/Channel.cpp
@@ -130,21 +130,21 @@ Channel::Channel(size_t aId, bool aMiddl
 
   {
     MonitorAutoLock lock(channel->mMonitor);
     channel->mInitialized = true;
     channel->mMonitor.Notify();
   }
 
   while (true) {
-    Message::UniquePtr msg = channel->WaitForMessage();
+    Message* msg = channel->WaitForMessage();
     if (!msg) {
       break;
     }
-    channel->mHandler(std::move(msg));
+    channel->mHandler(msg);
   }
 }
 
 void Channel::SendMessage(const Message& aMsg) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread() ||
                      aMsg.mType == MessageType::BeginFatalError ||
                      aMsg.mType == MessageType::FatalError ||
                      aMsg.mType == MessageType::MiddlemanCallRequest);
@@ -170,17 +170,17 @@ void Channel::SendMessage(const Message&
       MOZ_RELEASE_ASSERT(errno == EPIPE);
       return;
     }
     ptr += rv;
     nbytes -= rv;
   }
 }
 
-Message::UniquePtr Channel::WaitForMessage() {
+Message* Channel::WaitForMessage() {
   if (!mMessageBuffer) {
     mMessageBuffer = (MessageBuffer*)AllocateMemory(sizeof(MessageBuffer),
                                                     MemoryKind::Generic);
     mMessageBuffer->appendN(0, PageSize);
   }
 
   size_t messageSize = 0;
   while (true) {
@@ -211,17 +211,17 @@ Message::UniquePtr Channel::WaitForMessa
       }
       PrintSpew("Channel disconnected, exiting...\n");
       _exit(0);
     }
 
     mMessageBytes += nbytes;
   }
 
-  Message::UniquePtr res = ((Message*)mMessageBuffer->begin())->Clone();
+  Message* res = ((Message*)mMessageBuffer->begin())->Clone();
 
   // Remove the message we just received from the incoming buffer.
   size_t remaining = mMessageBytes - messageSize;
   if (remaining) {
     memmove(mMessageBuffer->begin(), &mMessageBuffer->begin()[messageSize],
             remaining);
   }
   mMessageBytes = remaining;
@@ -232,25 +232,28 @@ Message::UniquePtr Channel::WaitForMessa
 
 void Channel::PrintMessage(const char* aPrefix, const Message& aMsg) {
   if (!SpewEnabled()) {
     return;
   }
   AutoEnsurePassThroughThreadEvents pt;
   nsCString data;
   switch (aMsg.mType) {
-    case MessageType::HitExecutionPoint: {
-      const HitExecutionPointMessage& nmsg =
-        (const HitExecutionPointMessage&)aMsg;
-      nmsg.mPoint.ToString(data);
-      data.AppendPrintf(" Endpoint %d Duration %.2f ms",
-                        nmsg.mRecordingEndpoint,
+    case MessageType::HitCheckpoint: {
+      const HitCheckpointMessage& nmsg = (const HitCheckpointMessage&)aMsg;
+      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);
+      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;
--- a/toolkit/recordreplay/ipc/Channel.h
+++ b/toolkit/recordreplay/ipc/Channel.h
@@ -6,17 +6,16 @@
 
 #ifndef mozilla_recordreplay_Channel_h
 #define mozilla_recordreplay_Channel_h
 
 #include "base/process.h"
 
 #include "mozilla/gfx/Types.h"
 #include "mozilla/Maybe.h"
-#include "mozilla/UniquePtr.h"
 
 #include "File.h"
 #include "JSControl.h"
 #include "MiddlemanCall.h"
 #include "Monitor.h"
 
 namespace mozilla {
 namespace recordreplay {
@@ -72,18 +71,18 @@ namespace recordreplay {
   _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)                                      \
                                                                \
   /* 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 */ \
-  /* HitExecutionPoint message. */                             \
+  /* 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. */                                   \
@@ -122,18 +121,20 @@ namespace recordreplay {
                                                                \
   /* Sent when a fatal error has occurred, but before the minidump has been */ \
   /* generated. */                                             \
   _Macro(BeginFatalError)                                      \
                                                                \
   /* The child's graphics were repainted. */                   \
   _Macro(Paint)                                                \
                                                                \
-  /* Notify the middleman that the child has hit an execution point and paused. */ \
-  _Macro(HitExecutionPoint)                                    \
+  /* Notify the middleman that a checkpoint or breakpoint was hit. */ \
+  /* The child will pause after sending these messages. */     \
+  _Macro(HitCheckpoint)                                        \
+  _Macro(HitBreakpoint)                                        \
                                                                \
   /* Send a response to a DebuggerRequest message. */          \
   _Macro(DebuggerResponse)                                     \
                                                                \
   /* Call a system function from the middleman process which the child has */ \
   /* encountered after diverging from the recording. */        \
   _Macro(MiddlemanCallRequest)                                 \
                                                                \
@@ -156,23 +157,20 @@ struct Message {
   uint32_t mSize;
 
  protected:
   Message(MessageType aType, uint32_t aSize) : mType(aType), mSize(aSize) {
     MOZ_RELEASE_ASSERT(mSize >= sizeof(*this));
   }
 
  public:
-  struct FreePolicy { void operator()(Message* msg) { /*free(msg);*/ } };
-  typedef UniquePtr<Message, FreePolicy> UniquePtr;
-
-  UniquePtr Clone() const {
-    Message* res = static_cast<Message*>(malloc(mSize));
+  Message* Clone() const {
+    char* res = (char*)malloc(mSize);
     memcpy(res, this, mSize);
-    return UniquePtr(res);
+    return (Message*)res;
   }
 
   const char* TypeString() const {
     switch (mType) {
 #define EnumToString(Kind) \
   case MessageType::Kind:  \
     return #Kind;
       ForEachMessageType(EnumToString)
@@ -378,36 +376,40 @@ struct PaintMessage : public Message {
 
   PaintMessage(uint32_t aCheckpointId, uint32_t aWidth, uint32_t aHeight)
       : Message(MessageType::Paint, sizeof(*this)),
         mCheckpointId(aCheckpointId),
         mWidth(aWidth),
         mHeight(aHeight) {}
 };
 
-struct HitExecutionPointMessage : public Message {
-  // The point the child paused at.
-  js::ExecutionPoint mPoint;
-
-  // Whether the pause occurred due to hitting the end of the recording.
+struct HitCheckpointMessage : public Message {
+  uint32_t mCheckpointId;
   bool mRecordingEndpoint;
 
-  // The amount of non-idle time taken to get to this pause from the last time
-  // the child paused.
+  // When recording, the amount of non-idle time taken to get to this
+  // checkpoint from the previous one.
   double mDurationMicroseconds;
 
-  HitExecutionPointMessage(const js::ExecutionPoint& aPoint,
-                           bool aRecordingEndpoint,
-                           double aDurationMicroseconds)
-      : Message(MessageType::HitExecutionPoint, sizeof(*this)),
-        mPoint(aPoint),
+  HitCheckpointMessage(uint32_t aCheckpointId, bool aRecordingEndpoint,
+                       double aDurationMicroseconds)
+      : Message(MessageType::HitCheckpoint, sizeof(*this)),
+        mCheckpointId(aCheckpointId),
         mRecordingEndpoint(aRecordingEndpoint),
         mDurationMicroseconds(aDurationMicroseconds) {}
 };
 
+struct HitBreakpointMessage : public Message {
+  bool mRecordingEndpoint;
+
+  explicit HitBreakpointMessage(bool aRecordingEndpoint)
+      : Message(MessageType::HitBreakpoint, sizeof(*this)),
+        mRecordingEndpoint(aRecordingEndpoint) {}
+};
+
 typedef EmptyMessage<MessageType::AlwaysMarkMajorCheckpoints>
     AlwaysMarkMajorCheckpointsMessage;
 
 template <MessageType Type>
 struct BinaryMessage : public Message {
   explicit BinaryMessage(uint32_t aSize) : Message(Type, aSize) {}
 
   const char* BinaryData() const { return Data<BinaryMessage<Type>, char>(); }
@@ -426,21 +428,29 @@ 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;
+  typedef std::function<void(Message*)> MessageHandler;
 
  private:
   // ID for this channel, unique for the middleman.
   size_t mId;
 
   // Callback to invoke off thread on incoming messages.
   MessageHandler mHandler;
 
@@ -464,17 +474,17 @@ class Channel {
   // The number of bytes of data already in the message buffer.
   size_t mMessageBytes;
 
   // If spew is enabled, print a message and associated info to stderr.
   void PrintMessage(const char* aPrefix, const Message& aMsg);
 
   // Block until a complete message is received from the other side of the
   // channel.
-  Message::UniquePtr WaitForMessage();
+  Message* WaitForMessage();
 
   // Main routine for the channel's thread.
   static void ThreadMain(void* aChannel);
 
  public:
   // Initialize this channel, connect to the other side, and spin up a thread
   // to process incoming messages by calling aHandler.
   Channel(size_t aId, bool aMiddlemanRecording, const MessageHandler& aHandler);
--- a/toolkit/recordreplay/ipc/ChildIPC.cpp
+++ b/toolkit/recordreplay/ipc/ChildIPC.cpp
@@ -53,38 +53,36 @@ static StaticInfallibleVector<char*> gPa
 
 // File descriptors used by a pipe to create checkpoints when instructed by the
 // parent process.
 static FileHandle gCheckpointWriteFd;
 static FileHandle gCheckpointReadFd;
 
 // Copy of the introduction message we got from the middleman. This is saved on
 // receipt and then processed during InitRecordingOrReplayingProcess.
-static UniquePtr<IntroductionMessage, Message::FreePolicy> gIntroductionMessage;
+static IntroductionMessage* gIntroductionMessage;
 
 // When recording, whether developer tools server code runs in the middleman.
 static bool gDebuggerRunsInMiddleman;
 
 // Any response received to the last MiddlemanCallRequest message.
-static UniquePtr<MiddlemanCallResponseMessage, Message::FreePolicy>
-  gCallResponseMessage;
+static MiddlemanCallResponseMessage* gCallResponseMessage;
 
 // Whether some thread has sent a MiddlemanCallRequest and is waiting for
 // gCallResponseMessage to be filled in.
 static bool gWaitingForCallResponse;
 
 // Processing routine for incoming channel messages.
-static void ChannelMessageHandler(Message::UniquePtr aMsg) {
+static void ChannelMessageHandler(Message* aMsg) {
   MOZ_RELEASE_ASSERT(MainThreadShouldPause() || aMsg->CanBeSentWhileUnpaused());
 
   switch (aMsg->mType) {
     case MessageType::Introduction: {
       MOZ_RELEASE_ASSERT(!gIntroductionMessage);
-      gIntroductionMessage.reset(
-          static_cast<IntroductionMessage*>(aMsg.release()));
+      gIntroductionMessage = (IntroductionMessage*)aMsg->Clone();
       break;
     }
     case MessageType::CreateCheckpoint: {
       MOZ_RELEASE_ASSERT(IsRecording());
 
       // Ignore requests to create checkpoints before we have reached the first
       // paint and finished initializing.
       if (navigation::IsInitialized()) {
@@ -174,24 +172,26 @@ static void ChannelMessageHandler(Messag
       PauseMainThreadAndInvokeCallback(
           [=]() { navigation::RunToPoint(nmsg.mTarget); });
       break;
     }
     case MessageType::MiddlemanCallResponse: {
       MonitorAutoLock lock(*gMonitor);
       MOZ_RELEASE_ASSERT(gWaitingForCallResponse);
       MOZ_RELEASE_ASSERT(!gCallResponseMessage);
-      gCallResponseMessage.reset(
-          static_cast<MiddlemanCallResponseMessage*>(aMsg.release()));
+      gCallResponseMessage = (MiddlemanCallResponseMessage*)aMsg;
+      aMsg = nullptr;  // Avoid freeing the message below.
       gMonitor->NotifyAll();
       break;
     }
     default:
       MOZ_CRASH();
   }
+
+  free(aMsg);
 }
 
 // Main routine for a thread whose sole purpose is to listen to requests from
 // the middleman process to create a new checkpoint. This is separate from the
 // channel thread because this thread is recorded and the latter is not
 // recorded. By communicating between the two threads with a pipe, this
 // thread's behavior will be replicated exactly when replaying and new
 // checkpoints will be created at the same point as during recording.
@@ -289,17 +289,17 @@ void InitRecordingOrReplayingProcess(int
   // performed by another child.
   AddInitialUntrackedMemoryRegion((uint8_t*)gGraphicsShmem,
                                   parent::GraphicsMemorySize);
 
   pt.reset();
 
   // We are ready to receive initialization messages from the middleman, pause
   // so they can be sent.
-  HitExecutionPoint(js::ExecutionPoint(), /* aRecordingEndpoint = */ false);
+  HitCheckpoint(CheckpointId::Invalid, /* aRecordingEndpoint = */ false);
 
   // If we failed to initialize then report it to the user.
   if (gInitializationFailureMessage) {
     ReportFatalError(Nothing(), "%s", gInitializationFailureMessage);
     Unreachable();
   }
 
   // Process the introduction message to fill in arguments.
@@ -317,16 +317,17 @@ void InitRecordingOrReplayingProcess(int
     for (size_t i = 0; i < msg->mArgc; i++) {
       gParentArgv.append(strdup(pos));
       pos += strlen(pos) + 1;
     }
 
     free(msg);
   }
 
+  free(gIntroductionMessage);
   gIntroductionMessage = nullptr;
 
   // Some argument manipulation code expects a null pointer at the end.
   gParentArgv.append(nullptr);
 
   MOZ_RELEASE_ASSERT(*aArgc >= 1);
   MOZ_RELEASE_ASSERT(gParentArgv.back() == nullptr);
 
@@ -628,63 +629,69 @@ void Repaint(size_t* aWidth, size_t* aHe
 bool CurrentRepaintCannotFail() {
   return gRepainting && !gAllowRepaintFailures;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Checkpoint Messages
 ///////////////////////////////////////////////////////////////////////////////
 
-// The time when the last HitExecutionPoint message was sent.
-static double gLastPauseTime;
+// The time when the last HitCheckpoint message was sent.
+static double gLastCheckpointTime;
 
 // When recording and we are idle, the time when we became idle.
 static double gIdleTimeStart;
 
 void BeginIdleTime() {
   MOZ_RELEASE_ASSERT(IsRecording() && NS_IsMainThread() && !gIdleTimeStart);
   gIdleTimeStart = CurrentTime();
 }
 
 void EndIdleTime() {
   MOZ_RELEASE_ASSERT(IsRecording() && NS_IsMainThread() && gIdleTimeStart);
 
   // Erase the idle time from our measurements by advancing the last checkpoint
   // time.
-  gLastPauseTime += CurrentTime() - gIdleTimeStart;
+  gLastCheckpointTime += CurrentTime() - gIdleTimeStart;
   gIdleTimeStart = 0;
 }
 
-void HitExecutionPoint(const js::ExecutionPoint& aPoint,
-                       bool aRecordingEndpoint) {
+void HitCheckpoint(size_t aId, bool aRecordingEndpoint) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
   double time = CurrentTime();
   PauseMainThreadAndInvokeCallback([=]() {
     double duration = 0;
-    if (gLastPauseTime) {
-      duration = time - gLastPauseTime;
+    if (aId > CheckpointId::First) {
+      duration = time - gLastCheckpointTime;
       MOZ_RELEASE_ASSERT(duration > 0);
     }
     gChannel->SendMessage(
-        HitExecutionPointMessage(aPoint, aRecordingEndpoint, duration));
+        HitCheckpointMessage(aId, aRecordingEndpoint, duration));
   });
-  gLastPauseTime = time;
+  gLastCheckpointTime = time;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Message Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
 void RespondToRequest(const js::CharBuffer& aBuffer) {
   DebuggerResponseMessage* msg =
       DebuggerResponseMessage::New(aBuffer.begin(), aBuffer.length());
   gChannel->SendMessage(*msg);
   free(msg);
 }
 
+void HitBreakpoint(bool aRecordingEndpoint) {
+  MOZ_RELEASE_ASSERT(NS_IsMainThread());
+  PauseMainThreadAndInvokeCallback([=]() {
+    gChannel->SendMessage(HitBreakpointMessage(aRecordingEndpoint));
+  });
+}
+
 void SendMiddlemanCallRequest(const char* aInputData, size_t aInputSize,
                               InfallibleVector<char>* aOutputData) {
   AutoPassThroughThreadEvents pt;
   MonitorAutoLock lock(*gMonitor);
 
   while (gWaitingForCallResponse) {
     gMonitor->Wait();
   }
@@ -697,16 +704,17 @@ void SendMiddlemanCallRequest(const char
 
   while (!gCallResponseMessage) {
     gMonitor->Wait();
   }
 
   aOutputData->append(gCallResponseMessage->BinaryData(),
                       gCallResponseMessage->BinaryDataSize());
 
+  free(gCallResponseMessage);
   gCallResponseMessage = nullptr;
   gWaitingForCallResponse = false;
 
   gMonitor->Notify();
 }
 
 void SendResetMiddlemanCalls() {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
--- a/toolkit/recordreplay/ipc/ChildInternal.h
+++ b/toolkit/recordreplay/ipc/ChildInternal.h
@@ -82,17 +82,18 @@ void AfterCheckpoint(const CheckpointId&
 size_t LastNormalCheckpoint();
 
 }  // namespace navigation
 
 namespace child {
 
 // IPC activity that can be triggered by navigation.
 void RespondToRequest(const js::CharBuffer& aBuffer);
-void HitExecutionPoint(const js::ExecutionPoint& aPoint, bool aRecordingEndpoint);
+void HitCheckpoint(size_t aId, bool aRecordingEndpoint);
+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;
   mach_port_t mThread;
--- a/toolkit/recordreplay/ipc/ChildNavigation.cpp
+++ b/toolkit/recordreplay/ipc/ChildNavigation.cpp
@@ -10,16 +10,33 @@
 
 namespace mozilla {
 namespace recordreplay {
 namespace navigation {
 
 typedef js::BreakpointPosition BreakpointPosition;
 typedef js::ExecutionPoint ExecutionPoint;
 
+static void BreakpointPositionToString(const BreakpointPosition& aPos,
+                                       nsAutoCString& aStr) {
+  aStr.AppendPrintf("{ Kind: %s, Script: %d, Offset: %d, Frame: %d }",
+                    aPos.KindString(), (int)aPos.mScript, (int)aPos.mOffset,
+                    (int)aPos.mFrameIndex);
+}
+
+static void ExecutionPointToString(const ExecutionPoint& aPoint,
+                                   nsAutoCString& aStr) {
+  aStr.AppendPrintf("{ Checkpoint %d", (int)aPoint.mCheckpoint);
+  if (aPoint.HasPosition()) {
+    aStr.AppendPrintf(" Progress %llu Position ", aPoint.mProgress);
+    BreakpointPositionToString(aPoint.mPosition, aStr);
+  }
+  aStr.AppendPrintf(" }");
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Navigation State
 ///////////////////////////////////////////////////////////////////////////////
 
 // The navigation state of a recording/replaying process describes where the
 // process currently is and what it is doing in order to respond to messages
 // from the middleman process.
 //
@@ -210,20 +227,20 @@ class ReachBreakpointPhase final : publi
 
  public:
   void Enter(const CheckpointId& aStart, bool aRewind,
              const ExecutionPoint& aPoint,
              const Maybe<ExecutionPoint>& aTemporaryCheckpoint);
 
   void ToString(nsAutoCString& aStr) override {
     aStr.AppendPrintf("ReachBreakpoint: ");
-    mPoint.ToString(aStr);
+    ExecutionPointToString(mPoint, aStr);
     if (mTemporaryCheckpoint.isSome()) {
       aStr.AppendPrintf(" TemporaryCheckpoint: ");
-      mTemporaryCheckpoint.ref().ToString(aStr);
+      ExecutionPointToString(mTemporaryCheckpoint.ref(), aStr);
     }
   }
 
   void AfterCheckpoint(const CheckpointId& aCheckpoint) override;
   void PositionHit(const ExecutionPoint& aPoint) override;
 };
 
 // Phase when the replaying process is searching forward from a checkpoint to
@@ -472,26 +489,30 @@ void PausedPhase::Enter(const ExecutionP
   gNavigation->SetPhase(this);
 
   if (aRewind) {
     MOZ_RELEASE_ASSERT(!aPoint.HasPosition());
     RestoreCheckpointAndResume(CheckpointId(aPoint.mCheckpoint));
     Unreachable();
   }
 
-  child::HitExecutionPoint(aPoint, aRecordingEndpoint);
+  if (aPoint.HasPosition()) {
+    child::HitBreakpoint(aRecordingEndpoint);
+  } else {
+    child::HitCheckpoint(aPoint.mCheckpoint, aRecordingEndpoint);
+  }
 }
 
 void PausedPhase::AfterCheckpoint(const CheckpointId& aCheckpoint) {
   MOZ_RELEASE_ASSERT(!mRecoveringFromDivergence);
   if (!aCheckpoint.mTemporary) {
     // We just rewound here, and are now where we should pause.
     MOZ_RELEASE_ASSERT(
         mPoint == gNavigation->CheckpointExecutionPoint(aCheckpoint.mNormal));
-    child::HitExecutionPoint(mPoint, mRecordingEndpoint);
+    child::HitCheckpoint(mPoint.mCheckpoint, mRecordingEndpoint);
   } else {
     // We just saved or restored the temporary checkpoint taken while
     // processing debugger requests here.
     MOZ_RELEASE_ASSERT(ThisProcessCanRewind());
     MOZ_RELEASE_ASSERT(mSavedTemporaryCheckpoint);
   }
 }
 
@@ -759,17 +780,17 @@ void ForwardPhase::PositionHit(const Exe
 
   if (hitBreakpoint) {
     gNavigation->mPausedPhase.Enter(aPoint);
   }
 }
 
 void ForwardPhase::HitRecordingEndpoint(const ExecutionPoint& aPoint) {
   nsAutoCString str;
-  aPoint.ToString(str);
+  ExecutionPointToString(aPoint, str);
 
   gNavigation->mPausedPhase.Enter(aPoint, /* aRewind = */ false,
                                   /* aRecordingEndpoint = */ true);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReachBreakpointPhase
 ///////////////////////////////////////////////////////////////////////////////
--- a/toolkit/recordreplay/ipc/ChildProcess.cpp
+++ b/toolkit/recordreplay/ipc/ChildProcess.cpp
@@ -24,119 +24,377 @@ static size_t gNumChannels;
 static bool gChildrenAreDebugging;
 
 /* static */ void ChildProcessInfo::SetIntroductionMessage(
     IntroductionMessage* aMessage) {
   gIntroductionMessage = aMessage;
 }
 
 ChildProcessInfo::ChildProcessInfo(
+    UniquePtr<ChildRole> aRole,
     const Maybe<RecordingProcessData>& aRecordingProcessData)
     : mChannel(nullptr),
       mRecording(aRecordingProcessData.isSome()),
+      mRecoveryStage(RecoveryStage::None),
       mPaused(false),
+      mPausedMessage(nullptr),
+      mLastCheckpoint(CheckpointId::Invalid),
+      mNumRecoveredMessages(0),
+      mRole(std::move(aRole)),
+      mPauseNeeded(false),
       mHasBegunFatalError(false),
       mHasFatalError(false) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
   static bool gFirst = false;
   if (!gFirst) {
     gFirst = true;
     gChildrenAreDebugging = !!getenv("WAIT_AT_START");
   }
 
+  mRole->SetProcess(this);
+
   LaunchSubprocess(aRecordingProcessData);
+
+  // Replaying processes always save the first checkpoint, if saving
+  // checkpoints is allowed. This is currently assumed by the rewinding
+  // mechanism in the replaying process, and would be nice to investigate
+  // removing.
+  if (!IsRecording() && CanRewind()) {
+    SendMessage(SetSaveCheckpointMessage(CheckpointId::First, true));
+  }
+
+  mRole->Initialize();
 }
 
 ChildProcessInfo::~ChildProcessInfo() {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
   if (IsRecording()) {
     SendMessage(TerminateMessage());
   }
 }
 
-void ChildProcessInfo::OnIncomingMessage(const Message& aMsg,
-                                         bool aForwardToControl) {
+ChildProcessInfo::Disposition ChildProcessInfo::GetDisposition() {
+  // We can determine the disposition of the child by looking at the first
+  // resume message sent since the last time it reached a checkpoint.
+  for (Message* msg : mMessages) {
+    if (msg->mType == MessageType::Resume) {
+      const ResumeMessage& nmsg = static_cast<const ResumeMessage&>(*msg);
+      return nmsg.mForward ? AfterLastCheckpoint : BeforeLastCheckpoint;
+    }
+    if (msg->mType == MessageType::RunToPoint) {
+      return AfterLastCheckpoint;
+    }
+  }
+  return AtLastCheckpoint;
+}
+
+bool ChildProcessInfo::IsPausedAtCheckpoint() {
+  return IsPaused() && mPausedMessage->mType == MessageType::HitCheckpoint;
+}
+
+bool ChildProcessInfo::IsPausedAtRecordingEndpoint() {
+  if (!IsPaused()) {
+    return false;
+  }
+  if (mPausedMessage->mType == MessageType::HitCheckpoint) {
+    return static_cast<HitCheckpointMessage*>(mPausedMessage)
+        ->mRecordingEndpoint;
+  }
+  if (mPausedMessage->mType == MessageType::HitBreakpoint) {
+    return static_cast<HitBreakpointMessage*>(mPausedMessage)
+        ->mRecordingEndpoint;
+  }
+  return false;
+}
+
+void ChildProcessInfo::GetInstalledBreakpoints(
+    InfallibleVector<AddBreakpointMessage*>& aBreakpoints) {
+  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.
+  MOZ_RELEASE_ASSERT(mMajorCheckpoints.empty() ||
+                     aId > mMajorCheckpoints.back());
+  mMajorCheckpoints.append(aId);
+}
+
+void ChildProcessInfo::SetRole(UniquePtr<ChildRole> aRole) {
+  MOZ_RELEASE_ASSERT(!IsRecovering());
+
+  PrintSpew("SetRole:%d %s\n", (int)GetId(),
+            ChildRole::TypeString(aRole->GetType()));
+
+  mRole = std::move(aRole);
+  mRole->SetProcess(this);
+  mRole->Initialize();
+}
+
+void ChildProcessInfo::OnIncomingMessage(size_t aChannelId,
+                                         const Message& aMsg) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+  // Ignore messages from channels for subprocesses we terminated already.
+  if (aChannelId != mChannel->GetId()) {
+    return;
+  }
+
+  // Always handle fatal errors in the same way.
+  if (aMsg.mType == MessageType::BeginFatalError) {
+    mHasBegunFatalError = true;
+    return;
+  } else if (aMsg.mType == MessageType::FatalError) {
+    mHasFatalError = true;
+    const FatalErrorMessage& nmsg = static_cast<const FatalErrorMessage&>(aMsg);
+    OnCrash(nmsg.Error());
+    return;
+  }
+
   mLastMessageTime = TimeStamp::Now();
 
+  if (IsRecovering()) {
+    OnIncomingRecoveryMessage(aMsg);
+    return;
+  }
+
+  // Update paused state.
+  MOZ_RELEASE_ASSERT(!IsPaused());
   switch (aMsg.mType) {
-    case MessageType::BeginFatalError:
-      mHasBegunFatalError = true;
-      return;
-    case MessageType::FatalError: {
-      mHasFatalError = true;
-      const FatalErrorMessage& nmsg =
-        static_cast<const FatalErrorMessage&>(aMsg);
-      OnCrash(nmsg.Error());
-      return;
-    }
-    case MessageType::HitExecutionPoint: {
-      const HitExecutionPointMessage& nmsg =
-        static_cast<const HitExecutionPointMessage&>(aMsg);
+    case MessageType::HitCheckpoint:
+    case MessageType::HitBreakpoint:
+      MOZ_RELEASE_ASSERT(!mPausedMessage);
+      mPausedMessage = aMsg.Clone();
+      MOZ_FALLTHROUGH;
+    case MessageType::DebuggerResponse:
+    case MessageType::RecordingFlushed:
+      MOZ_RELEASE_ASSERT(mPausedMessage);
       mPaused = true;
-      if (this == GetActiveChild() && !nmsg.mPoint.HasPosition()) {
-        MaybeUpdateGraphicsAtCheckpoint(nmsg.mPoint.mCheckpoint);
-      }
-      if (aForwardToControl) {
-        js::ForwardHitExecutionPointMessage(GetId(), nmsg);
-      }
-      break;
-    }
-    case MessageType::Paint:
-      MaybeUpdateGraphicsAtPaint(static_cast<const PaintMessage&>(aMsg));
-      break;
-    case MessageType::DebuggerResponse:
-      mPaused = true;
-      js::OnDebuggerResponse(aMsg);
-      break;
-    case MessageType::RecordingFlushed:
-      mPaused = true;
-      break;
-    case MessageType::MiddlemanCallRequest: {
-      const MiddlemanCallRequestMessage& nmsg =
-        static_cast<const MiddlemanCallRequestMessage&>(aMsg);
-      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(GetId());
       break;
     default:
       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
+    // those which establish the set of installed breakpoints.
+    InfallibleVector<Message*> newMessages;
+    for (Message* msg : mMessages) {
+      if (msg->mType == MessageType::AddBreakpoint) {
+        newMessages.append(msg);
+      } else {
+        if (msg->mType == MessageType::ClearBreakpoints) {
+          for (Message* existing : newMessages) {
+            free(existing);
+          }
+          newMessages.clear();
+        }
+        free(msg);
+      }
+    }
+    mMessages = std::move(newMessages);
+  }
+
+  // The primordial HitCheckpoint messages is not forwarded to the role, as it
+  // has not been initialized yet.
+  if (aMsg.mType != MessageType::HitCheckpoint || mLastCheckpoint) {
+    mRole->OnIncomingMessage(aMsg);
+  }
 }
 
 void ChildProcessInfo::SendMessage(const Message& aMsg) {
+  MOZ_RELEASE_ASSERT(!IsRecovering());
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
   // Update paused state.
   MOZ_RELEASE_ASSERT(IsPaused() || aMsg.CanBeSentWhileUnpaused());
   switch (aMsg.mType) {
     case MessageType::Resume:
     case MessageType::RestoreCheckpoint:
     case MessageType::RunToPoint:
+      free(mPausedMessage);
+      mPausedMessage = nullptr;
+      MOZ_FALLTHROUGH;
     case MessageType::DebuggerRequest:
     case MessageType::FlushRecording:
       mPaused = false;
       break;
     default:
       break;
   }
 
+  // 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::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) {
+    const SetSaveCheckpointMessage& nmsg =
+        static_cast<const SetSaveCheckpointMessage&>(aMsg);
+    MOZ_RELEASE_ASSERT(nmsg.mCheckpoint > MostRecentCheckpoint());
+    VectorAddOrRemoveEntry(mShouldSaveCheckpoints, nmsg.mCheckpoint,
+                           nmsg.mSave);
+  }
+
+  SendMessageRaw(aMsg);
+}
+
+void ChildProcessInfo::SendMessageRaw(const Message& aMsg) {
+  MOZ_RELEASE_ASSERT(NS_IsMainThread());
   mLastMessageTime = TimeStamp::Now();
   mChannel->SendMessage(aMsg);
 }
 
+void ChildProcessInfo::Recover(bool aPaused, Message* aPausedMessage,
+                               size_t aLastCheckpoint, Message** aMessages,
+                               size_t aNumMessages) {
+  MOZ_RELEASE_ASSERT(IsPaused());
+
+  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) {
+    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());
+  }
+
+  mNumRecoveredMessages = 0;
+
+  if (mostRecentCheckpoint < mLastCheckpoint) {
+    mRecoveryStage = RecoveryStage::ReachingCheckpoint;
+    SendMessageRaw(ResumeMessage(/* aForward = */ true));
+  } else if (mostRecentCheckpoint > mLastCheckpoint || !pausedAtCheckpoint) {
+    mRecoveryStage = RecoveryStage::ReachingCheckpoint;
+    // Rewind to the last saved checkpoint at or prior to the target.
+    size_t targetCheckpoint = CheckpointId::Invalid;
+    for (size_t saved : mShouldSaveCheckpoints) {
+      if (saved <= mLastCheckpoint && saved > targetCheckpoint) {
+        targetCheckpoint = saved;
+      }
+    }
+    MOZ_RELEASE_ASSERT(targetCheckpoint != CheckpointId::Invalid);
+    SendMessageRaw(RestoreCheckpointMessage(targetCheckpoint));
+  } else {
+    mRecoveryStage = RecoveryStage::PlayingMessages;
+    SendNextRecoveryMessage();
+  }
+
+  WaitUntil([=]() { return !IsRecovering(); });
+}
+
+void ChildProcessInfo::Recover(ChildProcessInfo* aTargetProcess) {
+  MOZ_RELEASE_ASSERT(aTargetProcess->IsPaused());
+  Recover(true, aTargetProcess->mPausedMessage->Clone(),
+          aTargetProcess->mLastCheckpoint, aTargetProcess->mMessages.begin(),
+          aTargetProcess->mMessages.length());
+}
+
+void ChildProcessInfo::RecoverToCheckpoint(size_t aCheckpoint) {
+  HitCheckpointMessage pausedMessage(aCheckpoint,
+                                     /* aRecordingEndpoint = */ false,
+                                     /* aDuration = */ 0);
+  Recover(true, pausedMessage.Clone(), aCheckpoint, nullptr, 0);
+}
+
+void ChildProcessInfo::OnIncomingRecoveryMessage(const Message& aMsg) {
+  switch (aMsg.mType) {
+    case MessageType::HitCheckpoint: {
+      MOZ_RELEASE_ASSERT(mRecoveryStage == RecoveryStage::ReachingCheckpoint);
+      const HitCheckpointMessage& nmsg =
+          static_cast<const HitCheckpointMessage&>(aMsg);
+      if (nmsg.mCheckpointId < mLastCheckpoint) {
+        SendMessageRaw(ResumeMessage(/* aForward = */ true));
+      } else {
+        MOZ_RELEASE_ASSERT(nmsg.mCheckpointId == mLastCheckpoint);
+        mRecoveryStage = RecoveryStage::PlayingMessages;
+        SendNextRecoveryMessage();
+      }
+      break;
+    }
+    case MessageType::HitBreakpoint:
+    case MessageType::DebuggerResponse:
+      SendNextRecoveryMessage();
+      break;
+    case MessageType::MiddlemanCallRequest: {
+      // Middleman call messages can arrive in different orders when recovering
+      // than they originally did in the original process, so handle them afresh
+      // even when recovering.
+      MiddlemanCallResponseMessage* response =
+          ProcessMiddlemanCallMessage((MiddlemanCallRequestMessage&)aMsg);
+      SendMessageRaw(*response);
+      free(response);
+      break;
+    }
+    case MessageType::ResetMiddlemanCalls:
+      ResetMiddlemanCalls();
+      break;
+    default:
+      MOZ_CRASH("Unexpected message during recovery");
+  }
+}
+
+void ChildProcessInfo::SendNextRecoveryMessage() {
+  MOZ_RELEASE_ASSERT(mRecoveryStage == RecoveryStage::PlayingMessages);
+
+  // Keep sending messages to the child as long as it stays paused.
+  Message* msg;
+  do {
+    // Check if we have recovered to the desired paused state.
+    if (mNumRecoveredMessages == mMessages.length()) {
+      MOZ_RELEASE_ASSERT(IsPaused());
+      mRecoveryStage = RecoveryStage::None;
+      return;
+    }
+    msg = mMessages[mNumRecoveredMessages++];
+    SendMessageRaw(*msg);
+
+    // 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;
+  }
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Subprocess Management
 ///////////////////////////////////////////////////////////////////////////////
 
 ipc::GeckoChildProcessHost* gRecordingProcess;
 
 void GetArgumentsForChildProcess(base::ProcessId aMiddlemanPid,
                                  uint32_t aChannelId,
@@ -163,20 +421,19 @@ void GetArgumentsForChildProcess(base::P
 void ChildProcessInfo::LaunchSubprocess(
     const Maybe<RecordingProcessData>& aRecordingProcessData) {
   size_t channelId = gNumChannels++;
 
   // Create a new channel every time we launch a new subprocess, without
   // deleting or tearing down the old one's state. This is pretty lame and it
   // would be nice if we could do something better here, especially because
   // with restarts we could create any number of channels over time.
-  mChannel = new Channel(channelId, IsRecording(),
-                         [=](Message::UniquePtr aMsg) {
-                           ReceiveChildMessageOnMainThread(std::move(aMsg));
-                         });
+  mChannel = new Channel(channelId, IsRecording(), [=](Message* aMsg) {
+    ReceiveChildMessageOnMainThread(channelId, aMsg);
+  });
 
   MOZ_RELEASE_ASSERT(IsRecording() == aRecordingProcessData.isSome());
   if (IsRecording()) {
     std::vector<std::string> extraArgs;
     GetArgumentsForChildProcess(base::GetCurrentProcId(), channelId,
                                 gRecordingFilename, /* aRecording = */ true,
                                 extraArgs);
 
@@ -199,30 +456,21 @@ void ChildProcessInfo::LaunchSubprocess(
   } else {
     dom::ContentChild::GetSingleton()->SendCreateReplayingProcess(channelId);
   }
 
   mLastMessageTime = TimeStamp::Now();
 
   SendGraphicsMemoryToChild();
 
-  // The child should send us a HitExecutionPoint message with an invalid point
-  // to pause.
+  // The child should send us a HitCheckpoint with an invalid ID to pause.
   WaitUntilPaused();
 
   MOZ_RELEASE_ASSERT(gIntroductionMessage);
   SendMessage(*gIntroductionMessage);
-
-  // Always save the first checkpoint in replaying child processes.
-  if (!IsRecording()) {
-    SendMessage(SetSaveCheckpointMessage(CheckpointId::First, true));
-  }
-
-  // Always run forward to the first checkpoint after the primordial one.
-  SendMessage(ResumeMessage(/* aForward = */ true));
 }
 
 void ChildProcessInfo::OnCrash(const char* aWhy) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
   // If a child process crashes or hangs then annotate the crash report.
   CrashReporter::AnnotateCrashReport(
       CrashReporter::Annotation::RecordReplayError, nsAutoCString(aWhy));
@@ -249,77 +497,58 @@ void ChildProcessInfo::OnCrash(const cha
 // When messages are received from child processes, we want their handler to
 // execute on the main thread. The main thread might be blocked in WaitUntil,
 // so runnables associated with child processes have special handling.
 
 // All messages received on a channel thread which the main thread has not
 // processed yet. This is protected by gMonitor.
 struct PendingMessage {
   ChildProcessInfo* mProcess;
-  Message::UniquePtr mMsg;
-
-  PendingMessage() : mProcess(nullptr) {}
-
-  PendingMessage& operator=(PendingMessage&& aOther) {
-    mProcess = aOther.mProcess;
-    mMsg = std::move(aOther.mMsg);
-    return *this;
-  }
-
-  PendingMessage(PendingMessage&& aOther) {
-    *this = std::move(aOther);
-  }
+  size_t mChannelId;
+  Message* mMsg;
 };
 static StaticInfallibleVector<PendingMessage> gPendingMessages;
 
-static Message::UniquePtr ExtractChildMessage(ChildProcessInfo** aProcess) {
-  MOZ_RELEASE_ASSERT(NS_IsMainThread());
-
-  for (size_t i = 0; i < gPendingMessages.length(); i++) {
-    PendingMessage& pending = gPendingMessages[i];
-    if (!*aProcess || pending.mProcess == *aProcess) {
-      *aProcess = pending.mProcess;
-      Message::UniquePtr msg = std::move(pending.mMsg);
-      gPendingMessages.erase(&pending);
-      return msg;
-    }
-  }
-
-  return nullptr;
-}
-
 // Whether there is a pending task on the main thread's message loop to handle
 // all pending messages.
 static bool gHasPendingMessageRunnable;
 
+// Process a pending message from aProcess (or any process if aProcess is null)
+// and return whether such a message was found. This must be called on the main
+// thread with gMonitor held.
+/* static */ bool ChildProcessInfo::MaybeProcessPendingMessage(
+    ChildProcessInfo* aProcess) {
+  MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+  for (size_t i = 0; i < gPendingMessages.length(); i++) {
+    if (!aProcess || gPendingMessages[i].mProcess == aProcess) {
+      PendingMessage copy = gPendingMessages[i];
+      gPendingMessages.erase(&gPendingMessages[i]);
+
+      MonitorAutoUnlock unlock(*gMonitor);
+      copy.mProcess->OnIncomingMessage(copy.mChannelId, *copy.mMsg);
+      free(copy.mMsg);
+      return true;
+    }
+  }
+
+  return false;
+}
+
 // How many seconds to wait without hearing from an unpaused child before
 // considering that child to be hung.
 static const size_t HangSeconds = 30;
 
-Message::UniquePtr ChildProcessInfo::WaitUntilPaused() {
+void ChildProcessInfo::WaitUntil(const std::function<bool()>& aCallback) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
-  if (IsPaused()) {
-    return nullptr;
-  }
-
   bool sentTerminateMessage = false;
-  while (true) {
+  while (!aCallback()) {
     MonitorAutoLock lock(*gMonitor);
-
-    // Search for the first message received from this process.
-    ChildProcessInfo* process = this;
-    Message::UniquePtr msg = ExtractChildMessage(&process);
-
-    if (msg) {
-      OnIncomingMessage(*msg, /* aForwardToControl = */ false);
-      if (IsPaused()) {
-        return msg;
-      }
-    } else {
+    if (!MaybeProcessPendingMessage(this)) {
       if (gChildrenAreDebugging || IsRecording()) {
         // Don't watch for hangs when children are being debugged. Recording
         // children are never treated as hanged both because they cannot be
         // restarted and because they may just be idling.
         gMonitor->Wait();
       } else {
         TimeStamp deadline =
             mLastMessageTime + TimeDuration::FromSeconds(HangSeconds);
@@ -327,17 +556,17 @@ Message::UniquePtr ChildProcessInfo::Wai
           MonitorAutoUnlock unlock(*gMonitor);
           if (!sentTerminateMessage) {
             // Try to get the child to crash, so that we can get a minidump.
             // Sending the message will reset mLastMessageTime so we get to
             // wait another HangSeconds before hitting the restart case below.
             // Use SendMessageRaw to avoid problems if we are recovering.
             CrashReporter::AnnotateCrashReport(
                 CrashReporter::Annotation::RecordReplayHang, true);
-            SendMessage(TerminateMessage());
+            SendMessageRaw(TerminateMessage());
             sentTerminateMessage = true;
           } else {
             // The child is still non-responsive after sending the terminate
             // message.
             OnCrash("Child process non-responsive");
           }
         }
         gMonitor->WaitUntil(deadline);
@@ -348,43 +577,36 @@ Message::UniquePtr ChildProcessInfo::Wai
 
 // Runnable created on the main thread to handle any tasks sent by the replay
 // message loop thread which were not handled while the main thread was blocked.
 /* static */ void ChildProcessInfo::MaybeProcessPendingMessageRunnable() {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
   MonitorAutoLock lock(*gMonitor);
   MOZ_RELEASE_ASSERT(gHasPendingMessageRunnable);
   gHasPendingMessageRunnable = false;
-  while (true) {
-    ChildProcessInfo* process = nullptr;
-    Message::UniquePtr msg = ExtractChildMessage(&process);
-
-    if (msg) {
-      MonitorAutoUnlock unlock(*gMonitor);
-      process->OnIncomingMessage(*msg, /* aForwardToControl = */ true);
-    } else {
-      break;
-    }
+  while (MaybeProcessPendingMessage(nullptr)) {
   }
 }
 
 // Execute a task that processes a message received from the child. This is
 // called on a channel thread, and the function executes asynchronously on
 // the main thread.
-void ChildProcessInfo::ReceiveChildMessageOnMainThread(Message::UniquePtr aMsg) {
+void ChildProcessInfo::ReceiveChildMessageOnMainThread(size_t aChannelId,
+                                                       Message* aMsg) {
   MOZ_RELEASE_ASSERT(!NS_IsMainThread());
 
   MonitorAutoLock lock(*gMonitor);
 
   PendingMessage pending;
   pending.mProcess = this;
-  pending.mMsg = std::move(aMsg);
-  gPendingMessages.append(std::move(pending));
+  pending.mChannelId = aChannelId;
+  pending.mMsg = aMsg;
+  gPendingMessages.append(pending);
 
-  // Notify the main thread, if it is waiting in WaitUntilPaused.
+  // Notify the main thread, if it is waiting in WaitUntil.
   gMonitor->NotifyAll();
 
   // Make sure there is a task on the main thread's message loop that can
   // process this task if necessary.
   if (!gHasPendingMessageRunnable) {
     gHasPendingMessageRunnable = true;
     MainThreadMessageLoop()->PostTask(
         NewRunnableFunction("MaybeProcessPendingMessageRunnable",
--- a/toolkit/recordreplay/ipc/JSControl.cpp
+++ b/toolkit/recordreplay/ipc/JSControl.cpp
@@ -10,17 +10,16 @@
 #include "mozilla/StaticPtr.h"
 #include "js/CharacterEncoding.h"
 #include "js/Conversions.h"
 #include "js/JSON.h"
 #include "js/PropertySpec.h"
 #include "ChildInternal.h"
 #include "ParentInternal.h"
 #include "nsImportModule.h"
-#include "rrIControl.h"
 #include "rrIReplay.h"
 #include "xpcprivate.h"
 
 using namespace JS;
 
 namespace mozilla {
 namespace recordreplay {
 namespace js {
@@ -65,31 +64,16 @@ static bool GetNumberProperty(JSContext*
   if (!v.isNumber()) {
     JS_ReportErrorASCII(aCx, "Object missing required property");
     return false;
   }
   *aResult = v.toNumber();
   return true;
 }
 
-static parent::ChildProcessInfo* GetChildById(JSContext* aCx,
-                                              const Value& aValue,
-                                              bool aAllowUnpaused = false) {
-  if (!aValue.isNumber()) {
-    JS_ReportErrorASCII(aCx, "Expected child ID");
-    return nullptr;
-  }
-  parent::ChildProcessInfo* child = parent::GetChildProcess(aValue.toNumber());
-  if (!child || (!aAllowUnpaused && !child->IsPaused())) {
-    JS_ReportErrorASCII(aCx, "Unpaused or bad child ID");
-    return nullptr;
-  }
-  return child;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // BreakpointPosition Conversion
 ///////////////////////////////////////////////////////////////////////////////
 
 // Names of properties which JS code uses to specify the contents of a
 // BreakpointPosition.
 static const char gKindProperty[] = "kind";
 static const char gScriptProperty[] = "script";
@@ -117,17 +101,17 @@ JSObject* BreakpointPosition::Encode(JSC
 }
 
 bool BreakpointPosition::Decode(JSContext* aCx, HandleObject aObject) {
   RootedValue v(aCx);
   if (!JS_GetProperty(aCx, aObject, gKindProperty, &v)) {
     return false;
   }
 
-  RootedString str(aCx, ::ToString(aCx, v));
+  RootedString str(aCx, ToString(aCx, v));
   for (size_t i = BreakpointPosition::Invalid + 1;
        i < BreakpointPosition::sKindCount; i++) {
     BreakpointPosition::Kind kind = (BreakpointPosition::Kind)i;
     bool match;
     if (!JS_StringEqualsAscii(
             aCx, str, BreakpointPosition::StaticKindString(kind), &match))
       return false;
     if (match) {
@@ -145,435 +129,195 @@ bool BreakpointPosition::Decode(JSContex
       !MaybeGetNumberProperty(aCx, aObject, gFrameIndexProperty,
                               &mFrameIndex)) {
     return false;
   }
 
   return true;
 }
 
-void
-BreakpointPosition::ToString(nsCString& aStr) const
-{
-  aStr.AppendPrintf("{ Kind: %s, Script: %d, Offset: %d, Frame: %d }",
-                    KindString(), (int) mScript, (int) mOffset, (int) mFrameIndex);
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // ExecutionPoint Conversion
 ///////////////////////////////////////////////////////////////////////////////
 
 // Names of properties which JS code uses to specify the contents of an
 // ExecutionPoint.
 static const char gCheckpointProperty[] = "checkpoint";
 static const char gProgressProperty[] = "progress";
 static const char gPositionProperty[] = "position";
 
 JSObject* ExecutionPoint::Encode(JSContext* aCx) const {
   RootedObject obj(aCx, JS_NewObject(aCx, nullptr));
-  if (!obj ||
+  RootedObject position(aCx, mPosition.Encode(aCx));
+  if (!obj || !position ||
       !JS_DefineProperty(aCx, obj, gCheckpointProperty, (double)mCheckpoint,
                          JSPROP_ENUMERATE) ||
       !JS_DefineProperty(aCx, obj, gProgressProperty, (double)mProgress,
+                         JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(aCx, obj, gPositionProperty, position,
                          JSPROP_ENUMERATE)) {
     return nullptr;
   }
-  if (HasPosition()) {
-    RootedObject position(aCx, mPosition.Encode(aCx));
-    if (!position ||
-        !JS_DefineProperty(aCx, obj, gPositionProperty, position,
-                           JSPROP_ENUMERATE)) {
-      return nullptr;
-    }
-  }
   return obj;
 }
 
 bool ExecutionPoint::Decode(JSContext* aCx, HandleObject aObject) {
   RootedValue v(aCx);
   if (!JS_GetProperty(aCx, aObject, gPositionProperty, &v)) {
     return false;
   }
 
-  if (v.isUndefined()) {
-    MOZ_RELEASE_ASSERT(!HasPosition());
-  } else {
-    RootedObject positionObject(aCx, NonNullObject(aCx, v));
-    if (!positionObject || !mPosition.Decode(aCx, positionObject)) {
-      return false;
-    }
-  }
-  return GetNumberProperty(aCx, aObject, gCheckpointProperty, &mCheckpoint) &&
+  RootedObject positionObject(aCx, NonNullObject(aCx, v));
+  return positionObject && mPosition.Decode(aCx, positionObject) &&
+         GetNumberProperty(aCx, aObject, gCheckpointProperty, &mCheckpoint) &&
          GetNumberProperty(aCx, aObject, gProgressProperty, &mProgress);
 }
 
-void
-ExecutionPoint::ToString(nsCString& aStr) const
-{
-  aStr.AppendPrintf("{ Checkpoint %d", (int) mCheckpoint);
-  if (HasPosition()) {
-    aStr.AppendPrintf(" Progress %llu Position ", mProgress);
-    mPosition.ToString(aStr);
-  }
-  aStr.AppendPrintf(" }");
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Message Conversion
-///////////////////////////////////////////////////////////////////////////////
-
-static JSObject* EncodeChannelMessage(JSContext* aCx,
-                                      const HitExecutionPointMessage& aMsg) {
-  RootedObject obj(aCx, JS_NewObject(aCx, nullptr));
-  if (!obj) {
-    return nullptr;
-  }
-
-  RootedObject pointObject(aCx, aMsg.mPoint.Encode(aCx));
-  if (!pointObject ||
-      !JS_DefineProperty(aCx, obj, "point", pointObject,
-                         JSPROP_ENUMERATE) ||
-      !JS_DefineProperty(aCx, obj, "recordingEndpoint",
-                         aMsg.mRecordingEndpoint, JSPROP_ENUMERATE) ||
-      !JS_DefineProperty(aCx, obj, "duration",
-                         aMsg.mDurationMicroseconds / 1000.0,
-                         JSPROP_ENUMERATE)) {
-    return nullptr;
-  }
-
-  return obj;
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Middleman Control
-///////////////////////////////////////////////////////////////////////////////
-
-static StaticRefPtr<rrIControl> gControl;
-
-void SetupMiddlemanControl(const Maybe<size_t>& aRecordingChildId) {
-  MOZ_RELEASE_ASSERT(!gControl);
-
-  nsCOMPtr<rrIControl> control =
-    do_ImportModule("resource://devtools/server/actors/replay/control.js");
-  gControl = control.forget();
-  ClearOnShutdown(&gControl);
-
-  MOZ_RELEASE_ASSERT(gControl);
-
-  AutoSafeJSContext cx;
-  JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
-
-  RootedValue recordingChildValue(cx);
-  if (aRecordingChildId.isSome()) {
-    recordingChildValue.setInt32(aRecordingChildId.ref());
-  }
-  if (NS_FAILED(gControl->Initialize(recordingChildValue))) {
-    MOZ_CRASH("SetupMiddlemanControl");
-  }
-}
-
-void ForwardHitExecutionPointMessage(size_t aId, const HitExecutionPointMessage& aMsg) {
-  MOZ_RELEASE_ASSERT(gControl);
-
-  AutoSafeJSContext cx;
-  JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
-
-  JSObject* obj = EncodeChannelMessage(cx, aMsg);
-  MOZ_RELEASE_ASSERT(obj);
-
-  RootedValue value(cx, ObjectValue(*obj));
-  if (NS_FAILED(gControl->HitExecutionPoint(aId, value))) {
-    MOZ_CRASH("ForwardMessageToMiddlemanControl");
-  }
-}
-
-void BeforeSaveRecording() {
-  MOZ_RELEASE_ASSERT(gControl);
-
-  AutoSafeJSContext cx;
-  JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
-
-  if (NS_FAILED(gControl->BeforeSaveRecording())) {
-    MOZ_CRASH("BeforeSaveRecording");
-  }
-}
-
-void AfterSaveRecording() {
-  MOZ_RELEASE_ASSERT(gControl);
-
-  AutoSafeJSContext cx;
-  JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
-
-  if (NS_FAILED(gControl->AfterSaveRecording())) {
-    MOZ_CRASH("AfterSaveRecording");
-  }
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Middleman Methods
 ///////////////////////////////////////////////////////////////////////////////
 
 // 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 JS_WrapValue(aCx, args.rval());
+    return true;
   }
 
   RootedObject obj(aCx, NonNullObject(aCx, args.get(0)));
   if (!obj) {
     return false;
   }
 
-  {
-    JSAutoRealm ar(aCx, xpc::PrivilegedJunkScope());
-
-    RootedValue debuggerValue(aCx, ObjectValue(*obj));
-    if (!JS_WrapValue(aCx, &debuggerValue)) {
-      return false;
-    }
-
-    if (NS_FAILED(gControl->ConnectDebugger(debuggerValue))) {
-      JS_ReportErrorASCII(aCx, "ConnectDebugger failed\n");
-      return false;
-    }
-  }
-
   obj = ::js::CheckedUnwrap(obj);
   if (!obj) {
     ::js::ReportAccessDenied(aCx);
     return false;
   }
 
   gReplayDebugger = new PersistentRootedObject(aCx);
   *gReplayDebugger = obj;
 
   args.rval().setUndefined();
   return true;
 }
 
+static bool CallReplayDebuggerHook(const char* aMethod) {
+  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_SpawnReplayingChild(JSContext* aCx,
-                                          unsigned aArgc, Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  size_t id = parent::SpawnReplayingChild();
-  args.rval().setInt32(id);
-  return true;
-}
-
-static bool Middleman_SetActiveChild(JSContext* aCx,
-                                     unsigned aArgc, Value* aVp) {
+static bool Middleman_Resume(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
-    return false;
-  }
-
-  parent::SetActiveChild(child);
-
-  args.rval().setUndefined();
-  return true;
-}
-
-static bool Middleman_SendSetSaveCheckpoint(JSContext* aCx,
-                                            unsigned aArgc, Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  bool forward = ToBoolean(args.get(0));
 
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
-    return false;
-  }
-
-  double checkpoint;
-  if (!ToNumber(aCx, args.get(1), &checkpoint)) {
-    return false;
-  }
-
-  bool shouldSave = ToBoolean(args.get(2));
-
-  child->SendMessage(SetSaveCheckpointMessage(checkpoint, shouldSave));
-
-  args.rval().setUndefined();
-  return true;
-}
-
-static bool Middleman_SendFlushRecording(JSContext* aCx,
-                                         unsigned aArgc, Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
-    return false;
-  }
-
-  child->SendMessage(FlushRecordingMessage());
-
-  // The child will unpause until the flush finishes.
-  child->WaitUntilPaused();
+  parent::Resume(forward);
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_SendResume(JSContext* aCx, unsigned aArgc, Value* aVp) {
+static bool Middleman_TimeWarp(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
+  RootedObject targetObject(aCx, NonNullObject(aCx, args.get(0)));
+  if (!targetObject) {
     return false;
   }
 
-  bool forward = ToBoolean(args.get(1));
-
-  child->SendMessage(ResumeMessage(forward));
-
-  args.rval().setUndefined();
-  return true;
-}
-
-static bool Middleman_SendRestoreCheckpoint(JSContext* aCx, unsigned aArgc, Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
+  ExecutionPoint target;
+  if (!target.Decode(aCx, targetObject)) {
     return false;
   }
 
-  double checkpoint;
-  if (!ToNumber(aCx, args.get(1), &checkpoint)) {
-    return false;
-  }
-
-  child->SendMessage(RestoreCheckpointMessage(checkpoint));
+  parent::TimeWarp(target);
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_SendRunToPoint(JSContext* aCx, unsigned aArgc, Value* aVp) {
+static bool Middleman_SendRequest(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
-    return false;
-  }
-
-  RootedObject pointObject(aCx, NonNullObject(aCx, args.get(1)));
-  if (!pointObject) {
-    return false;
-  }
-
-  ExecutionPoint point;
-  if (!point.Decode(aCx, pointObject)) {
-    return false;
-  }
-
-  child->SendMessage(RunToPointMessage(point));
-
-  args.rval().setUndefined();
-  return true;
-}
-
-// Buffer for receiving the next debugger response.
-static js::CharBuffer* gResponseBuffer;
-
-void OnDebuggerResponse(const Message& aMsg) {
-  const DebuggerResponseMessage& nmsg =
-    static_cast<const DebuggerResponseMessage&>(aMsg);
-  MOZ_RELEASE_ASSERT(gResponseBuffer && gResponseBuffer->empty());
-  gResponseBuffer->append(nmsg.Buffer(), nmsg.BufferSize());
-}
-
-static bool Middleman_SendDebuggerRequest(JSContext* aCx,
-                                          unsigned aArgc, Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
-    return false;
-  }
-
-  RootedObject requestObject(aCx, NonNullObject(aCx, args.get(1)));
+  RootedObject requestObject(aCx, NonNullObject(aCx, args.get(0)));
   if (!requestObject) {
     return false;
   }
 
   CharBuffer requestBuffer;
   if (!ToJSONMaybeSafely(aCx, requestObject, FillCharBufferCallback,
                          &requestBuffer)) {
     return false;
   }
 
   CharBuffer responseBuffer;
-
-  MOZ_RELEASE_ASSERT(!gResponseBuffer);
-  gResponseBuffer = &responseBuffer;
-
-  DebuggerRequestMessage* msg =
-    DebuggerRequestMessage::New(requestBuffer.begin(), requestBuffer.length());
-  child->SendMessage(*msg);
-  free(msg);
-
-  // Wait for the child to respond to the query.
-  child->WaitUntilPaused();
-  MOZ_RELEASE_ASSERT(gResponseBuffer == &responseBuffer);
-  MOZ_RELEASE_ASSERT(gResponseBuffer->length() != 0);
-  gResponseBuffer = nullptr;
+  parent::SendRequest(requestBuffer, &responseBuffer);
 
   return JS_ParseJSON(aCx, responseBuffer.begin(), responseBuffer.length(),
                       args.rval());
 }
 
-static bool Middleman_SendAddBreakpoint(JSContext* aCx,
-                                        unsigned aArgc, Value* aVp) {
+static bool Middleman_AddBreakpoint(JSContext* aCx, unsigned aArgc,
+                                    Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
-    return false;
-  }
-
-  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(1)));
+  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(0)));
   if (!positionObject) {
     return false;
   }
 
   BreakpointPosition position;
   if (!position.Decode(aCx, positionObject)) {
     return false;
   }
 
-  child->SendMessage(AddBreakpointMessage(position));
+  parent::AddBreakpoint(position);
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_SendClearBreakpoints(JSContext* aCx,
-                                           unsigned aArgc, Value* aVp) {
+/* static */ bool Middleman_ClearBreakpoints(JSContext* aCx, unsigned aArgc,
+                                             Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
-  if (!child) {
-    return false;
-  }
+  parent::ClearBreakpoints();
+
+  args.rval().setUndefined();
+  return true;
+}
 
-  child->SendMessage(ClearBreakpointsMessage());
+static bool Middleman_MaybeSwitchToReplayingChild(JSContext* aCx,
+                                                  unsigned aArgc, Value* aVp) {
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::MaybeSwitchToReplayingChild();
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool Middleman_HadRepaint(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
@@ -597,63 +341,40 @@ static bool Middleman_HadRepaintFailure(
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
   parent::UpdateGraphicsInUIProcess(nullptr);
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_InRepaintStressMode(JSContext* aCx,
-                                          unsigned aArgc, Value* aVp) {
+static bool Middleman_ChildIsRecording(JSContext* aCx, unsigned aArgc,
+                                       Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  args.rval().setBoolean(parent::InRepaintStressMode());
+  args.rval().setBoolean(parent::ActiveChildIsRecording());
   return true;
 }
 
-// Recording children can idle indefinitely while waiting for input, without
-// creating a checkpoint. If this might be a problem, this method induces the
-// child to create a new checkpoint and pause.
-static void MaybeCreateCheckpointInChild(parent::ChildProcessInfo* aChild) {
-  if (aChild->IsRecording() && !aChild->IsPaused()) {
-    aChild->SendMessage(CreateCheckpointMessage());
-  }
+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::ChildProcessInfo* child = GetChildById(aCx, args.get(0),
-                                                 /* aAllowUnpaused = */ true);
-  if (!child) {
-    return false;
-  }
-
-  if (ToBoolean(args.get(1))) {
-    MaybeCreateCheckpointInChild(child);
-  }
-
-  Message::UniquePtr msg = child->WaitUntilPaused();
+  parent::WaitUntilActiveChildIsPaused();
 
-  if (!msg) {
-    JS_ReportErrorASCII(aCx, "Child process is already paused");
-  }
-
-  MOZ_RELEASE_ASSERT(msg->mType == MessageType::HitExecutionPoint);
-  const HitExecutionPointMessage& nmsg =
-    static_cast<const HitExecutionPointMessage&>(*msg);
-
-  JSObject* obj = EncodeChannelMessage(aCx, nmsg);
-  if (!obj) {
-    return false;
-  }
-
-  args.rval().setObject(*obj);
+  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)));
@@ -1167,30 +888,28 @@ static bool RecordReplay_Dump(JSContext*
 
 ///////////////////////////////////////////////////////////////////////////////
 // Plumbing
 ///////////////////////////////////////////////////////////////////////////////
 
 static const JSFunctionSpec gMiddlemanMethods[] = {
     JS_FN("registerReplayDebugger", Middleman_RegisterReplayDebugger, 1, 0),
     JS_FN("canRewind", Middleman_CanRewind, 0, 0),
-    JS_FN("spawnReplayingChild", Middleman_SpawnReplayingChild, 0, 0),
-    JS_FN("setActiveChild", Middleman_SetActiveChild, 1, 0),
-    JS_FN("sendSetSaveCheckpoint", Middleman_SendSetSaveCheckpoint, 3, 0),
-    JS_FN("sendFlushRecording", Middleman_SendFlushRecording, 1, 0),
-    JS_FN("sendResume", Middleman_SendResume, 2, 0),
-    JS_FN("sendRestoreCheckpoint", Middleman_SendRestoreCheckpoint, 2, 0),
-    JS_FN("sendRunToPoint", Middleman_SendRunToPoint, 2, 0),
-    JS_FN("sendDebuggerRequest", Middleman_SendDebuggerRequest, 2, 0),
-    JS_FN("sendAddBreakpoint", Middleman_SendAddBreakpoint, 2, 0),
-    JS_FN("sendClearBreakpoints", Middleman_SendClearBreakpoints, 1, 0),
+    JS_FN("resume", Middleman_Resume, 1, 0),
+    JS_FN("timeWarp", Middleman_TimeWarp, 1, 0),
+    JS_FN("sendRequest", Middleman_SendRequest, 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("inRepaintStressMode", Middleman_InRepaintStressMode, 0, 0),
-    JS_FN("waitUntilPaused", Middleman_WaitUntilPaused, 1, 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),
--- a/toolkit/recordreplay/ipc/JSControl.h
+++ b/toolkit/recordreplay/ipc/JSControl.h
@@ -8,24 +8,19 @@
 #define mozilla_recordreplay_JSControl_h
 
 #include "jsapi.h"
 
 #include "InfallibleVector.h"
 #include "ProcessRewind.h"
 
 #include "mozilla/DefineEnum.h"
-#include "nsString.h"
 
 namespace mozilla {
 namespace recordreplay {
-
-struct Message;
-struct HitExecutionPointMessage;
-
 namespace js {
 
 // This file manages interactions between the record/replay infrastructure and
 // JS code. This interaction can occur in two ways:
 //
 // - In the middleman process, devtools server code can use the
 //   RecordReplayControl object to send requests to the recording/replaying
 //   child process and control its behavior.
@@ -135,17 +130,16 @@ struct BreakpointPosition {
     }
     MOZ_CRASH("Bad BreakpointPosition kind");
   }
 
   const char* KindString() const { return StaticKindString(mKind); }
 
   JSObject* Encode(JSContext* aCx) const;
   bool Decode(JSContext* aCx, JS::HandleObject aObject);
-  void ToString(nsCString& aStr) const;
 };
 
 // Identification for a point in the execution of a child process where it may
 // pause and be inspected by the middleman. A particular execution point will
 // be reached exactly once during the execution of the process.
 struct ExecutionPoint {
   // ID of the last normal checkpoint prior to this point.
   size_t mCheckpoint;
@@ -183,40 +177,33 @@ struct ExecutionPoint {
   }
 
   inline bool operator!=(const ExecutionPoint& o) const {
     return !(*this == o);
   }
 
   JSObject* Encode(JSContext* aCx) const;
   bool Decode(JSContext* aCx, JS::HandleObject aObject);
-  void ToString(nsCString& aStr) const;
 };
 
 // Buffer type used for encoding object data.
 typedef InfallibleVector<char16_t> CharBuffer;
 
+// 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 middleman process to call methods defined
-// by the middleman control logic.
-
-// Setup the middleman control state.
-void SetupMiddlemanControl(const Maybe<size_t>& aRecordingChildId);
-
-// Handle an incoming message from a child process.
-void ForwardHitExecutionPointMessage(size_t aId,
-                                     const HitExecutionPointMessage& aMsg);
-
-// Prepare the child processes so that the recording file can be safely copied.
-void BeforeSaveRecording();
-void AfterSaveRecording();
-
 // The following hooks are used in the recording/replaying process to
 // call methods defined by the JS sandbox.
 
 // Handle an incoming request from the middleman.
 void ProcessRequest(const char16_t* aRequest, size_t aRequestLength,
                     CharBuffer* aResponse);
 
 // Ensure there is a handler in place that will call
@@ -229,16 +216,13 @@ void ClearPositionHandlers();
 
 // Clear all state that is kept while execution is paused.
 void ClearPausedState();
 
 // Given an execution position inside a script, get an execution position for
 // the entry point of that script, otherwise return nothing.
 Maybe<BreakpointPosition> GetEntryPosition(const BreakpointPosition& aPosition);
 
-// Called after receiving a DebuggerResponse from a child.
-void OnDebuggerResponse(const Message& aMsg);
-
 }  // namespace js
 }  // namespace recordreplay
 }  // namespace mozilla
 
 #endif  // mozilla_recordreplay_JSControl_h
--- a/toolkit/recordreplay/ipc/ParentForwarding.cpp
+++ b/toolkit/recordreplay/ipc/ParentForwarding.cpp
@@ -13,21 +13,16 @@
 #include "mozilla/dom/PBrowserChild.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/layers/CompositorBridgeChild.h"
 
 namespace mozilla {
 namespace recordreplay {
 namespace parent {
 
-static bool ActiveChildIsRecording() {
-  ChildProcessInfo* child = GetActiveChild();
-  return child && child->IsRecording();
-}
-
 static bool HandleMessageInMiddleman(ipc::Side aSide,
                                      const IPC::Message& aMessage) {
   IPC::Message::msgid_t type = aMessage.type();
 
   if (aSide == ipc::ParentSide) {
     return false;
   }
 
@@ -73,20 +68,19 @@ static bool HandleMessageInMiddleman(ipc
       if (!found) {
         return false;
       }
     }
 
     ipc::IProtocol::Result r =
         contentChild->PContentChild::OnMessageReceived(aMessage);
     MOZ_RELEASE_ASSERT(r == ipc::IProtocol::MsgProcessed);
-    if (type == dom::PContent::Msg_RegisterChrome__ID) {
-      // After the RegisterChrome message we can load chrome JS and finish
-      // initialization.
-      ChromeRegistered();
+    if (type == dom::PContent::Msg_SetXPCOMProcessAttributes__ID) {
+      // Preferences are initialized via the SetXPCOMProcessAttributes message.
+      PreferencesLoaded();
     }
     return false;
   }
 
   // Handle messages that should only be sent to the middleman.
   if (  // Initialization that should only happen in the middleman.
       type == dom::PContent::Msg_InitRendering__ID ||
       // Record/replay specific messages.
@@ -251,20 +245,17 @@ class MiddlemanProtocol : public ipc::IT
     Message* nMessage = new Message();
     nMessage->CopyFrom(aMessage);
     mOppositeMessageLoop->PostTask(
         NewRunnableFunction("ForwardMessageSync", ForwardMessageSync, mOpposite,
                             nMessage, &aReply));
 
     if (mSide == ipc::ChildSide) {
       AutoMarkMainThreadWaitingForIPDLReply blocked;
-      while (!aReply) {
-        GetActiveChild()->WaitUntilPaused();
-        GetActiveChild()->SendMessage(ResumeMessage(/* aForward = */ true));
-      }
+      ActiveRecordingChild()->WaitUntil([&]() { return !!aReply; });
     } else {
       MonitorAutoLock lock(*gMonitor);
       while (!aReply) {
         gMonitor->Wait();
       }
     }
 
     PrintSpew("SyncMsgDone\n");
@@ -295,20 +286,17 @@ class MiddlemanProtocol : public ipc::IT
     Message* nMessage = new Message();
     nMessage->CopyFrom(aMessage);
     mOppositeMessageLoop->PostTask(
         NewRunnableFunction("ForwardCallMessage", ForwardCallMessage, mOpposite,
                             nMessage, &aReply));
 
     if (mSide == ipc::ChildSide) {
       AutoMarkMainThreadWaitingForIPDLReply blocked;
-      while (!aReply) {
-        GetActiveChild()->WaitUntilPaused();
-        GetActiveChild()->SendMessage(ResumeMessage(/* aForward = */ true));
-      }
+      ActiveRecordingChild()->WaitUntil([&]() { return !!aReply; });
     } else {
       MonitorAutoLock lock(*gMonitor);
       while (!aReply) {
         gMonitor->Wait();
       }
     }
 
     PrintSpew("SyncCallDone\n");
--- a/toolkit/recordreplay/ipc/ParentIPC.cpp
+++ b/toolkit/recordreplay/ipc/ParentIPC.cpp
@@ -42,122 +42,595 @@ void InitializeUIProcess(int aArgc, char
 }
 
 const char* SaveAllRecordingsDirectory() {
   MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
   return gSaveAllRecordingsDirectory;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
-// Child Processes
+// Child Roles
 ///////////////////////////////////////////////////////////////////////////////
 
+static const double FlushSeconds = .5;
+static const double MajorCheckpointSeconds = 2;
+
+// This section describes the strategy used for managing child processes. When
+// recording, there is a single recording process and two replaying processes.
+// When replaying, there are two replaying processes. The main advantage of
+// using two replaying processes is to provide a smooth experience when
+// rewinding.
+//
+// At any time there is one active child: the process which the user is
+// interacting with. This may be any of the two or three children in existence,
+// depending on the user's behavior. The other processes do not interact with
+// the user: inactive recording processes are inert, and sit idle until
+// recording is ready to resume, while inactive replaying processes are on
+// standby, staying close to the active process in the recording's execution
+// space and saving checkpoints in case the user starts rewinding.
+//
+// Below are some scenarios showing the state we attempt to keep the children
+// in, and ways in which the active process switches from one to another.
+// The execution diagrams show the position of each process, with '*' and '-'
+// indicating checkpoints the process reached and, respectively, whether
+// the checkpoint was saved or not.
+//
+// When the recording process is actively recording, flushes are issued to it
+// every FlushSeconds to keep the recording reasonably current and allow the
+// replaying processes to stay behind but close to the position of the
+// recording process. Additionally, one replaying process saves a checkpoint
+// every MajorCheckpointSeconds with the process saving the checkpoint
+// alternating back and forth so that individual processes save checkpoints
+// every MajorCheckpointSeconds*2. These are the major checkpoints for each
+// replaying process.
+//
+// Active  Recording:    -----------------------
+// Standby Replaying #1: *---------*---------*
+// Standby Replaying #2: -----*---------*-----
+//
+// When the recording process is explicitly paused (via the debugger UI) at a
+// checkpoint or breakpoint, it is flushed and the replaying processes will
+// navigate around the recording to ensure all checkpoints going back at least
+// MajorCheckpointSeconds have been saved. These are the intermediate
+// checkpoints. No replaying process needs to rewind past its last major
+// checkpoint, and a given intermediate checkpoint will only ever be saved by
+// the replaying process with the most recent major checkpoint.
+//
+// Active  Recording:    -----------------------
+// Standby Replaying #1: *---------*---------***
+// Standby Replaying #2: -----*---------*****
+//
+// If the user starts rewinding, the replaying process with the most recent
+// major checkpoint (and which has been saving the most recent intermediate
+// checkpoints) becomes the active child.
+//
+// Inert   Recording:    -----------------------
+// Active  Replaying #1: *---------*---------**
+// Standby Replaying #2: -----*---------*****
+//
+// As the user continues rewinding, the replaying process stays active until it
+// goes past its most recent major checkpoint. At that time the other replaying
+// process (which has been saving checkpoints prior to that point) becomes the
+// active child and allows continuous rewinding. The first replaying process
+// rewinds to its last major checkpoint and begins saving older intermediate
+// checkpoints, attempting to maintain the invariant that we have saved (or are
+// saving) all checkpoints going back MajorCheckpointSeconds.
+//
+// Inert   Recording:    -----------------------
+// Standby Replaying #1: *---------*****
+// Active  Replaying #2: -----*---------**
+//
+// Rewinding continues in this manner, alternating back and forth between the
+// replaying processes as the user continues going back in time.
+//
+// Inert   Recording:    -----------------------
+// Active  Replaying #1: *---------**
+// Standby Replaying #2: -----*****
+//
+// If the user starts navigating forward, the replaying processes both run
+// forward and save checkpoints at the same major checkpoints as earlier.
+// Note that this is how all forward execution works when there is no recording
+// process (i.e. we started from a saved recording).
+//
+// Inert   Recording:    -----------------------
+// Active  Replaying #1: *---------**------
+// Standby Replaying #2: -----*****-----*--
+//
+// If the user pauses at a checkpoint or breakpoint in the replay, we again
+// want to fill in all the checkpoints going back MajorCheckpointSeconds to
+// allow smooth rewinding. This cannot be done simultaneously -- as it was when
+// the recording process was active -- since we need to keep one of the
+// replaying processes at an up to date point and be the active one. This falls
+// on the one whose most recent major checkpoint is oldest, as the other is
+// responsible for saving the most recent intermediate checkpoints.
+//
+// Inert   Recording:    -----------------------
+// Active  Replaying #1: *---------**------
+// Standby Replaying #2: -----*****-----***
+//
+// After the recent intermediate checkpoints have been saved the process which
+// took them can become active so the older intermediate checkpoints can be
+// saved.
+//
+// Inert   Recording:    -----------------------
+// Standby Replaying #1: *---------*****
+// Active  Replaying #2: -----*****-----***
+//
+// Finally, if the replay plays forward to the end of the recording (the point
+// where the recording process is situated), the recording process takes over
+// again as the active child and the user can resume interacting with a live
+// process.
+//
+// Active  Recording:    ----------------------------------------
+// Standby Replaying #1: *---------*****-----*---------*-------
+// Standby Replaying #2: -----*****-----***-------*---------*--
+
+// The current active child.
+static ChildProcessInfo* gActiveChild;
+
 // The single recording child process, or null.
 static ChildProcessInfo* gRecordingChild;
 
-// Any replaying child processes that have been spawned.
-static StaticInfallibleVector<UniquePtr<ChildProcessInfo>> gReplayingChildren;
-
-// The currently active child process.
-static ChildProcessInfo* gActiveChild;
+// The two replaying child processes, null if they haven't been spawned yet.
+// When rewinding is disabled, there is only a single replaying child, and zero
+// replaying children if there is a recording child.
+static ChildProcessInfo* gFirstReplayingChild;
+static ChildProcessInfo* gSecondReplayingChild;
 
 void Shutdown() {
   delete gRecordingChild;
-  gReplayingChildren.clear();
+  delete gFirstReplayingChild;
+  delete gSecondReplayingChild;
   _exit(0);
 }
 
 bool IsMiddlemanWithRecordingChild() {
   return IsMiddleman() && gRecordingChild;
 }
 
-ChildProcessInfo* GetActiveChild() {
+static ChildProcessInfo* OtherReplayingChild(ChildProcessInfo* aChild) {
+  MOZ_RELEASE_ASSERT(!aChild->IsRecording() && gFirstReplayingChild &&
+                     gSecondReplayingChild);
+  return aChild == gFirstReplayingChild ? gSecondReplayingChild
+                                        : gFirstReplayingChild;
+}
+
+static void ForEachReplayingChild(
+    const std::function<void(ChildProcessInfo*)>& aCallback) {
+  if (gFirstReplayingChild) {
+    aCallback(gFirstReplayingChild);
+  }
+  if (gSecondReplayingChild) {
+    aCallback(gSecondReplayingChild);
+  }
+}
+
+static void PokeChildren() {
+  ForEachReplayingChild([=](ChildProcessInfo* aChild) {
+    if (aChild->IsPaused()) {
+      aChild->Role()->Poke();
+    }
+  });
+}
+
+static void RecvHitCheckpoint(const HitCheckpointMessage& aMsg);
+static void RecvHitBreakpoint(const HitBreakpointMessage& aMsg);
+static void RecvDebuggerResponse(const DebuggerResponseMessage& aMsg);
+static void RecvRecordingFlushed();
+static void RecvAlwaysMarkMajorCheckpoints();
+static void RecvMiddlemanCallRequest(const MiddlemanCallRequestMessage& aMsg);
+
+// The role taken by the active child.
+class ChildRoleActive final : public ChildRole {
+ public:
+  ChildRoleActive() : ChildRole(Active) {}
+
+  void Initialize() override {
+    gActiveChild = mProcess;
+
+    mProcess->SendMessage(SetIsActiveMessage(true));
+
+    // Always run forward from the primordial checkpoint. Otherwise, the
+    // debugger hooks below determine how the active child changes.
+    if (mProcess->LastCheckpoint() == CheckpointId::Invalid) {
+      mProcess->SendMessage(ResumeMessage(/* aForward = */ true));
+    }
+  }
+
+  void OnIncomingMessage(const Message& aMsg) override {
+    switch (aMsg.mType) {
+      case MessageType::Paint:
+        MaybeUpdateGraphicsAtPaint((const PaintMessage&)aMsg);
+        break;
+      case MessageType::HitCheckpoint:
+        RecvHitCheckpoint((const HitCheckpointMessage&)aMsg);
+        break;
+      case MessageType::HitBreakpoint:
+        RecvHitBreakpoint((const HitBreakpointMessage&)aMsg);
+        break;
+      case MessageType::DebuggerResponse:
+        RecvDebuggerResponse((const DebuggerResponseMessage&)aMsg);
+        break;
+      case MessageType::RecordingFlushed:
+        RecvRecordingFlushed();
+        break;
+      case MessageType::AlwaysMarkMajorCheckpoints:
+        RecvAlwaysMarkMajorCheckpoints();
+        break;
+      case MessageType::MiddlemanCallRequest:
+        RecvMiddlemanCallRequest((const MiddlemanCallRequestMessage&)aMsg);
+        break;
+      case MessageType::ResetMiddlemanCalls:
+        ResetMiddlemanCalls();
+        break;
+      default:
+        MOZ_CRASH("Unexpected message");
+    }
+  }
+};
+
+bool ActiveChildIsRecording() {
+  return gActiveChild && gActiveChild->IsRecording();
+}
+
+ChildProcessInfo* ActiveRecordingChild() {
+  MOZ_RELEASE_ASSERT(ActiveChildIsRecording());
   return gActiveChild;
 }
 
-ChildProcessInfo* GetChildProcess(size_t aId) {
-  if (gRecordingChild && gRecordingChild->GetId() == aId) {
-    return gRecordingChild;
+// The last checkpoint included in the recording.
+static size_t gLastRecordingCheckpoint;
+
+// The role taken by replaying children trying to stay close to the active
+// child and save either major or intermediate checkpoints, depending on
+// whether the active child is paused or rewinding.
+class ChildRoleStandby final : public ChildRole {
+ public:
+  ChildRoleStandby() : ChildRole(Standby) {}
+
+  void Initialize() override {
+    MOZ_RELEASE_ASSERT(mProcess->IsPausedAtCheckpoint());
+    mProcess->SendMessage(SetIsActiveMessage(false));
+    Poke();
+  }
+
+  void OnIncomingMessage(const Message& aMsg) override {
+    MOZ_RELEASE_ASSERT(aMsg.mType == MessageType::HitCheckpoint);
+    Poke();
   }
-  for (const auto& child : gReplayingChildren) {
-    if (child->GetId() == aId) {
-      return child.get();
+
+  void Poke() override;
+};
+
+// The role taken by a recording child while another child is active.
+class ChildRoleInert final : public ChildRole {
+ public:
+  ChildRoleInert() : ChildRole(Inert) {}
+
+  void Initialize() override {
+    MOZ_RELEASE_ASSERT(mProcess->IsRecording() && mProcess->IsPaused());
+  }
+
+  void OnIncomingMessage(const Message& aMsg) override {
+    MOZ_CRASH("Unexpected message from inert recording child");
+  }
+};
+
+// Get the last major checkpoint for a process at or before aId, or
+// CheckpointId::Invalid.
+static size_t LastMajorCheckpointPreceding(ChildProcessInfo* aChild,
+                                           size_t aId) {
+  size_t last = CheckpointId::Invalid;
+  for (size_t majorCheckpoint : aChild->MajorCheckpoints()) {
+    if (majorCheckpoint > aId) {
+      break;
     }
+    last = majorCheckpoint;
   }
-  return nullptr;
+  return last;
 }
 
-size_t SpawnReplayingChild() {
-  ChildProcessInfo* child = new ChildProcessInfo(Nothing());
-  gReplayingChildren.append(child);
-  return child->GetId();
+// Get the replaying process responsible for saving aId when rewinding: the one
+// with the most recent major checkpoint preceding aId.
+static ChildProcessInfo* ReplayingChildResponsibleForSavingCheckpoint(
+    size_t aId) {
+  MOZ_RELEASE_ASSERT(CanRewind() && gFirstReplayingChild &&
+                     gSecondReplayingChild);
+  size_t firstMajor = LastMajorCheckpointPreceding(gFirstReplayingChild, aId);
+  size_t secondMajor = LastMajorCheckpointPreceding(gSecondReplayingChild, aId);
+  return (firstMajor < secondMajor) ? gSecondReplayingChild
+                                    : gFirstReplayingChild;
+}
+
+// Returns a checkpoint if the active child is explicitly paused somewhere,
+// has started rewinding after being explicitly paused, or is attempting to
+// warp to an execution point. The checkpoint returned is the latest one which
+// should be saved, and standby roles must save all intermediate checkpoints
+// they are responsible for, in the range from their most recent major
+// checkpoint up to the returned checkpoint.
+static Maybe<size_t> ActiveChildTargetCheckpoint();
+
+// Ensure that a child will save aCheckpoint iff it is a major checkpoint.
+static void EnsureMajorCheckpointSaved(ChildProcessInfo* aChild,
+                                       size_t aCheckpoint) {
+  // The first checkpoint is always saved, even if not marked as major.
+  bool childShouldSave = aChild->IsMajorCheckpoint(aCheckpoint) ||
+                         aCheckpoint == CheckpointId::First;
+  bool childToldToSave = aChild->ShouldSaveCheckpoint(aCheckpoint);
+
+  if (childShouldSave != childToldToSave) {
+    aChild->SendMessage(SetSaveCheckpointMessage(aCheckpoint, childShouldSave));
+  }
 }
 
-void SetActiveChild(ChildProcessInfo* aChild) {
-  MOZ_RELEASE_ASSERT(aChild->IsPaused());
+void ChildRoleStandby::Poke() {
+  MOZ_RELEASE_ASSERT(mProcess->IsPausedAtCheckpoint());
 
-  if (gActiveChild) {
-    MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
-    gActiveChild->SendMessage(SetIsActiveMessage(false));
+  // Stay paused if we need to while the recording is flushed.
+  if (mProcess->PauseNeeded()) {
+    return;
   }
 
-  aChild->SendMessage(SetIsActiveMessage(true));
-  gActiveChild = aChild;
+  // Check if we need to save a range of intermediate checkpoints.
+  do {
+    // Intermediate checkpoints are only saved when the active child is paused
+    // or rewinding.
+    Maybe<size_t> targetCheckpoint = ActiveChildTargetCheckpoint();
+    if (targetCheckpoint.isNothing()) {
+      break;
+    }
+
+    // The startpoint of the range is the most recent major checkpoint prior to
+    // the target.
+    size_t lastMajorCheckpoint =
+        LastMajorCheckpointPreceding(mProcess, targetCheckpoint.ref());
+
+    // If there is no major checkpoint prior to the target, just idle.
+    if (lastMajorCheckpoint == CheckpointId::Invalid) {
+      return;
+    }
+
+    // If we haven't reached the last major checkpoint, we need to run forward
+    // without saving intermediate checkpoints.
+    if (mProcess->LastCheckpoint() < lastMajorCheckpoint) {
+      EnsureMajorCheckpointSaved(mProcess, mProcess->LastCheckpoint() + 1);
+      mProcess->SendMessage(ResumeMessage(/* aForward = */ true));
+      return;
+    }
+
+    // The endpoint of the range is the checkpoint prior to either the active
+    // child's current position, or the other replaying child's most recent
+    // major checkpoint.
+    size_t otherMajorCheckpoint = LastMajorCheckpointPreceding(
+        OtherReplayingChild(mProcess), targetCheckpoint.ref());
+    if (otherMajorCheckpoint > lastMajorCheckpoint) {
+      MOZ_RELEASE_ASSERT(otherMajorCheckpoint <= targetCheckpoint.ref());
+      targetCheckpoint.ref() = otherMajorCheckpoint - 1;
+    }
+
+    // Find the first checkpoint in the fill range which we have not saved.
+    Maybe<size_t> missingCheckpoint;
+    for (size_t i = lastMajorCheckpoint; i <= targetCheckpoint.ref(); i++) {
+      if (!mProcess->HasSavedCheckpoint(i)) {
+        missingCheckpoint.emplace(i);
+        break;
+      }
+    }
+
+    // If we have already saved everything we need to, we can idle.
+    if (!missingCheckpoint.isSome()) {
+      return;
+    }
+
+    // We must have saved the checkpoint prior to the missing one and can
+    // restore it. missingCheckpoint cannot be lastMajorCheckpoint, because we
+    // always save major checkpoints, and the loop above checked that all
+    // prior checkpoints going back to lastMajorCheckpoint have been saved.
+    size_t restoreTarget = missingCheckpoint.ref() - 1;
+    MOZ_RELEASE_ASSERT(mProcess->HasSavedCheckpoint(restoreTarget));
+
+    // If we need to rewind to the restore target, do so.
+    if (mProcess->LastCheckpoint() != restoreTarget) {
+      mProcess->SendMessage(RestoreCheckpointMessage(restoreTarget));
+      return;
+    }
+
+    // Make sure the process will save the next checkpoint.
+    if (!mProcess->ShouldSaveCheckpoint(missingCheckpoint.ref())) {
+      mProcess->SendMessage(
+          SetSaveCheckpointMessage(missingCheckpoint.ref(), true));
+    }
+
+    // Run forward to the next checkpoint.
+    mProcess->SendMessage(ResumeMessage(/* aForward = */ true));
+    return;
+  } while (false);
+
+  // Run forward until we reach either the active child's position, or the last
+  // checkpoint included in the on-disk recording. Only save major checkpoints.
+  if ((mProcess->LastCheckpoint() < gActiveChild->LastCheckpoint()) &&
+      (!gRecordingChild ||
+       mProcess->LastCheckpoint() < gLastRecordingCheckpoint)) {
+    EnsureMajorCheckpointSaved(mProcess, mProcess->LastCheckpoint() + 1);
+    mProcess->SendMessage(ResumeMessage(/* aForward = */ true));
+  }
 }
 
-void ResumeBeforeWaitingForIPDLReply() {
-  MOZ_RELEASE_ASSERT(gActiveChild->IsRecording());
+///////////////////////////////////////////////////////////////////////////////
+// Major Checkpoints
+///////////////////////////////////////////////////////////////////////////////
+
+// For each checkpoint N, this vector keeps track of the time intervals taken
+// for the active child (excluding idle time) to run from N to N+1.
+static StaticInfallibleVector<TimeDuration> gCheckpointTimes;
+
+// How much time has elapsed (per gCheckpointTimes) since the last flush or
+// major checkpoint was noted.
+static TimeDuration gTimeSinceLastFlush;
+static TimeDuration gTimeSinceLastMajorCheckpoint;
+
+// The replaying process that was given the last major checkpoint.
+static ChildProcessInfo* gLastAssignedMajorCheckpoint;
+
+// For testing, mark new major checkpoints as frequently as possible.
+static bool gAlwaysMarkMajorCheckpoints;
+
+static void RecvAlwaysMarkMajorCheckpoints() {
+  gAlwaysMarkMajorCheckpoints = true;
+}
+
+static void AssignMajorCheckpoint(ChildProcessInfo* aChild, size_t aId) {
+  PrintSpew("AssignMajorCheckpoint: Process %d Checkpoint %d\n",
+            (int)aChild->GetId(), (int)aId);
+  aChild->AddMajorCheckpoint(aId);
+  gLastAssignedMajorCheckpoint = aChild;
+}
+
+static bool MaybeFlushRecording();
+
+static void UpdateCheckpointTimes(const HitCheckpointMessage& aMsg) {
+  if (!CanRewind() || (aMsg.mCheckpointId != gCheckpointTimes.length() + 1)) {
+    return;
+  }
+  gCheckpointTimes.append(
+      TimeDuration::FromMicroseconds(aMsg.mDurationMicroseconds));
+
+  if (gActiveChild->IsRecording()) {
+    gTimeSinceLastFlush += gCheckpointTimes.back();
+
+    // Occasionally flush while recording so replaying processes stay
+    // reasonably current.
+    if (aMsg.mCheckpointId == CheckpointId::First ||
+        gTimeSinceLastFlush >= TimeDuration::FromSeconds(FlushSeconds)) {
+      if (MaybeFlushRecording()) {
+        gTimeSinceLastFlush = 0;
+      }
+    }
+  }
+
+  gTimeSinceLastMajorCheckpoint += gCheckpointTimes.back();
+  if (gTimeSinceLastMajorCheckpoint >=
+          TimeDuration::FromSeconds(MajorCheckpointSeconds) ||
+      gAlwaysMarkMajorCheckpoints) {
+    // Alternate back and forth between assigning major checkpoints to the
+    // two replaying processes.
+    MOZ_RELEASE_ASSERT(gLastAssignedMajorCheckpoint);
+    ChildProcessInfo* child = OtherReplayingChild(gLastAssignedMajorCheckpoint);
+    AssignMajorCheckpoint(child, aMsg.mCheckpointId + 1);
+    gTimeSinceLastMajorCheckpoint = 0;
+  }
+}
 
-  // 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()) {
-    gActiveChild->SendMessage(ResumeMessage(/* aForward = */ true));
+///////////////////////////////////////////////////////////////////////////////
+// Role Management
+///////////////////////////////////////////////////////////////////////////////
+
+static void SpawnRecordingChild(
+    const RecordingProcessData& aRecordingProcessData) {
+  MOZ_RELEASE_ASSERT(!gRecordingChild && !gFirstReplayingChild &&
+                     !gSecondReplayingChild);
+  gRecordingChild = new ChildProcessInfo(MakeUnique<ChildRoleActive>(),
+                                         Some(aRecordingProcessData));
+}
+
+static void SpawnSingleReplayingChild() {
+  MOZ_RELEASE_ASSERT(!gRecordingChild && !gFirstReplayingChild &&
+                     !gSecondReplayingChild);
+  gFirstReplayingChild =
+      new ChildProcessInfo(MakeUnique<ChildRoleActive>(), Nothing());
+}
+
+static void SpawnReplayingChildren() {
+  MOZ_RELEASE_ASSERT(CanRewind() && !gFirstReplayingChild &&
+                     !gSecondReplayingChild);
+  UniquePtr<ChildRole> firstRole;
+  if (gRecordingChild) {
+    firstRole = MakeUnique<ChildRoleStandby>();
+  } else {
+    firstRole = MakeUnique<ChildRoleActive>();
+  }
+  gFirstReplayingChild = new ChildProcessInfo(std::move(firstRole), Nothing());
+  gSecondReplayingChild =
+      new ChildProcessInfo(MakeUnique<ChildRoleStandby>(), Nothing());
+  AssignMajorCheckpoint(gSecondReplayingChild, CheckpointId::First);
+}
+
+// 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 {
+      InfallibleVector<AddBreakpointMessage*> breakpoints;
+      gActiveChild->GetInstalledBreakpoints(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>());
+  }
+
+  // Notify the debugger when switching between recording and replaying
+  // children.
+  if (aChild->IsRecording() != oldActiveChild->IsRecording()) {
+    js::DebuggerOnSwitchChild();
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Preferences
 ///////////////////////////////////////////////////////////////////////////////
 
-static bool gChromeRegistered;
+static bool gPreferencesLoaded;
 static bool gRewindingEnabled;
 
-void ChromeRegistered() {
+void PreferencesLoaded() {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
-  if (gChromeRegistered) {
-    return;
-  }
-  gChromeRegistered = true;
+  MOZ_RELEASE_ASSERT(!gPreferencesLoaded);
+  gPreferencesLoaded = true;
 
   gRewindingEnabled =
       Preferences::GetBool("devtools.recordreplay.enableRewinding");
 
   // Force-disable rewinding and saving checkpoints with an env var for testing.
   if (getenv("NO_REWIND")) {
     gRewindingEnabled = false;
   }
 
-  Maybe<size_t> recordingChildId;
-
   if (gRecordingChild) {
     // Inform the recording child if we will be running devtools server code in
     // this process.
     if (DebuggerRunsInMiddleman()) {
       gRecordingChild->SendMessage(SetDebuggerRunsInMiddlemanMessage());
     }
-    recordingChildId.emplace(gRecordingChild->GetId());
+  } else {
+    // If there is no recording child, we have now initialized enough state
+    // that we can start spawning replaying children.
+    if (CanRewind()) {
+      SpawnReplayingChildren();
+    } else {
+      SpawnSingleReplayingChild();
+    }
   }
-
-  js::SetupMiddlemanControl(recordingChildId);
 }
 
 bool CanRewind() {
-  MOZ_RELEASE_ASSERT(gChromeRegistered);
+  MOZ_RELEASE_ASSERT(gPreferencesLoaded);
   return gRewindingEnabled;
 }
 
 bool DebuggerRunsInMiddleman() {
   if (IsRecordingOrReplaying()) {
     // This can be called in recording/replaying processes as well as the
     // middleman. Fetch the value which the middleman informed us of.
     return child::DebuggerRunsInMiddleman();
@@ -169,53 +642,217 @@ bool DebuggerRunsInMiddleman() {
   MOZ_RELEASE_ASSERT(IsMiddleman());
   return !gRecordingChild || CanRewind();
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Saving Recordings
 ///////////////////////////////////////////////////////////////////////////////
 
+// Synchronously flush the recording to disk.
+static void FlushRecording() {
+  MOZ_RELEASE_ASSERT(NS_IsMainThread());
+  MOZ_RELEASE_ASSERT(gActiveChild->IsRecording() && gActiveChild->IsPaused());
+
+  // All replaying children must be paused while the recording is flushed.
+  ForEachReplayingChild([=](ChildProcessInfo* aChild) {
+    aChild->SetPauseNeeded();
+    aChild->WaitUntilPaused();
+  });
+
+  gActiveChild->SendMessage(FlushRecordingMessage());
+  gActiveChild->WaitUntilPaused();
+
+  gLastRecordingCheckpoint = gActiveChild->LastCheckpoint();
+
+  // We now have a usable recording for replaying children.
+  static bool gHasFlushed = false;
+  if (!gHasFlushed && CanRewind()) {
+    SpawnReplayingChildren();
+  }
+  gHasFlushed = true;
+}
+
+// Get the replaying children to pause, and flush the recording if they already
+// are.
+static bool MaybeFlushRecording() {
+  MOZ_RELEASE_ASSERT(NS_IsMainThread());
+  MOZ_RELEASE_ASSERT(gActiveChild->IsRecording() && gActiveChild->IsPaused());
+
+  bool allPaused = true;
+  ForEachReplayingChild([&](ChildProcessInfo* aChild) {
+    if (!aChild->IsPaused()) {
+      aChild->SetPauseNeeded();
+      allPaused = false;
+    }
+  });
+
+  if (allPaused) {
+    FlushRecording();
+    return true;
+  }
+  return false;
+}
+
+static void RecvRecordingFlushed() {
+  MOZ_RELEASE_ASSERT(NS_IsMainThread());
+  ForEachReplayingChild(
+      [=](ChildProcessInfo* aChild) { aChild->ClearPauseNeeded(); });
+}
+
+// Recording children can idle indefinitely while waiting for input, without
+// creating a checkpoint. If this might be a problem, this method induces the
+// child to create a new checkpoint and pause.
+static void MaybeCreateCheckpointInRecordingChild() {
+  if (gActiveChild->IsRecording() && !gActiveChild->IsPaused()) {
+    gActiveChild->SendMessage(CreateCheckpointMessage());
+  }
+}
+
+// Send a message to the message manager in the UI process. This is consumed by
+// various tests.
+static void SendMessageToUIProcess(const char* aMessage) {
+  AutoSafeJSContext cx;
+  auto* cpmm = dom::ContentProcessMessageManager::Get();
+  ErrorResult err;
+  nsAutoString message;
+  message.Append(NS_ConvertUTF8toUTF16(aMessage));
+  JS::Rooted<JS::Value> undefined(cx);
+  cpmm->SendAsyncMessage(cx, message, undefined, nullptr, nullptr, undefined,
+                         err);
+  MOZ_RELEASE_ASSERT(!err.Failed());
+  err.SuppressException();
+}
+
 // Handle to the recording file opened at startup.
 static FileHandle gRecordingFd;
 
 static void SaveRecordingInternal(const ipc::FileDescriptor& aFile) {
-  // Make sure the recording file is up to date and ready for copying.
-  js::BeforeSaveRecording();
+  MOZ_RELEASE_ASSERT(gRecordingChild);
+
+  if (gRecordingChild == gActiveChild) {
+    // The recording might not be up to date, flush it now.
+    MOZ_RELEASE_ASSERT(gRecordingChild == gActiveChild);
+    MaybeCreateCheckpointInRecordingChild();
+    gRecordingChild->WaitUntilPaused();
+    FlushRecording();
+  }
 
   // Copy the file's contents to the new file.
   DirectSeekFile(gRecordingFd, 0);
   ipc::FileDescriptor::UniquePlatformHandle writefd =
       aFile.ClonePlatformHandle();
   char buf[4096];
   while (true) {
     size_t n = DirectRead(gRecordingFd, buf, sizeof(buf));
     if (!n) {
       break;
     }
     DirectWrite(writefd.get(), buf, n);
   }
 
   PrintSpew("Saved Recording Copy.\n");
-
-  js::AfterSaveRecording();
+  SendMessageToUIProcess("SaveRecordingFinished");
 }
 
 void SaveRecording(const ipc::FileDescriptor& aFile) {
   MOZ_RELEASE_ASSERT(IsMiddleman());
 
   if (NS_IsMainThread()) {
     SaveRecordingInternal(aFile);
   } else {
     MainThreadMessageLoop()->PostTask(NewRunnableFunction(
         "SaveRecordingInternal", SaveRecordingInternal, aFile));
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
+// Explicit Pauses
+///////////////////////////////////////////////////////////////////////////////
+
+// At the last time the active child was explicitly paused, the ID of the
+// checkpoint that needs to be saved for the child to rewind.
+static size_t gLastExplicitPause;
+
+// Any checkpoint we are trying to warp to and pause.
+static Maybe<size_t> gTimeWarpTarget;
+
+static bool HasSavedCheckpointsInRange(ChildProcessInfo* aChild, size_t aStart,
+                                       size_t aEnd) {
+  for (size_t i = aStart; i <= aEnd; i++) {
+    if (!aChild->HasSavedCheckpoint(i)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+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.
+    FlushRecording();
+  } else if (CanRewind()) {
+    // Make sure we have a replaying child that can rewind from this point.
+    // Switch to the other one if (a) this process is responsible for rewinding
+    // from this point, and (b) this process has not saved all intermediate
+    // checkpoints going back to its last major checkpoint.
+    if (gActiveChild ==
+        ReplayingChildResponsibleForSavingCheckpoint(targetCheckpoint)) {
+      size_t lastMajorCheckpoint =
+          LastMajorCheckpointPreceding(gActiveChild, targetCheckpoint);
+      if (!HasSavedCheckpointsInRange(gActiveChild, lastMajorCheckpoint,
+                                      targetCheckpoint)) {
+        SwitchActiveChild(OtherReplayingChild(gActiveChild));
+      }
+    }
+  }
+
+  gLastExplicitPause = targetCheckpoint;
+  PrintSpew("MarkActiveChildExplicitPause %d\n", (int)gLastExplicitPause);
+
+  PokeChildren();
+}
+
+static Maybe<size_t> ActiveChildTargetCheckpoint() {
+  if (gTimeWarpTarget.isSome()) {
+    return gTimeWarpTarget;
+  }
+  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);
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////
 // Initialization
 ///////////////////////////////////////////////////////////////////////////////
 
 // Message loop processed on the main thread.
 static MessageLoop* gMainThreadMessageLoop;
 
 MessageLoop* MainThreadMessageLoop() { return gMainThreadMessageLoop; }
 
@@ -243,25 +880,275 @@ void InitializeMiddleman(int aArgc, char
   InitializeGraphicsMemory();
 
   gMonitor = new Monitor();
 
   gMainThreadMessageLoop = MessageLoop::current();
 
   if (gProcessKind == ProcessKind::MiddlemanRecording) {
     RecordingProcessData data(aPrefsHandle, aPrefMapHandle);
-    gRecordingChild = new ChildProcessInfo(Some(data));
-
-    // Set the active child to the recording child initially, so that message
-    // forwarding works before the middleman control JS has been initialized.
-    gActiveChild = gRecordingChild;
+    SpawnRecordingChild(data);
   }
 
   InitializeForwarding();
 
   // Open a file handle to the recording file we can use for saving recordings
   // later on.
   gRecordingFd = DirectOpenFile(gRecordingFilename, false);
 }
 
+///////////////////////////////////////////////////////////////////////////////
+// Debugger Messages
+///////////////////////////////////////////////////////////////////////////////
+
+// Buffer for receiving the next debugger response.
+static js::CharBuffer* gResponseBuffer;
+
+static void RecvDebuggerResponse(const DebuggerResponseMessage& aMsg) {
+  MOZ_RELEASE_ASSERT(gResponseBuffer && gResponseBuffer->empty());
+  gResponseBuffer->append(aMsg.Buffer(), aMsg.BufferSize());
+}
+
+void SendRequest(const js::CharBuffer& aBuffer, js::CharBuffer* aResponse) {
+  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 AddBreakpoint(const js::BreakpointPosition& aPosition) {
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
+
+  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(AddBreakpointMessage(aPosition));
+  }
+}
+
+void ClearBreakpoints() {
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
+
+  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
+  // the result.
+  if (InRepaintStressMode()) {
+    MaybeSwitchToReplayingChild();
+
+    const char16_t contents[] = u"{\"type\":\"repaint\"}";
+
+    js::CharBuffer request, response;
+    request.append(contents, ArrayLength(contents) - 1);
+    SendRequest(request, &response);
+
+    AutoSafeJSContext cx;
+    JS::RootedValue value(cx);
+    if (JS_ParseJSON(cx, response.begin(), response.length(), &value)) {
+      MOZ_RELEASE_ASSERT(value.isObject());
+      JS::RootedObject obj(cx, &value.toObject());
+      RootedValue width(cx), height(cx);
+      if (JS_GetProperty(cx, obj, "width", &width) && width.isNumber() &&
+          width.toNumber() && JS_GetProperty(cx, obj, "height", &height) &&
+          height.isNumber() && height.toNumber()) {
+        PaintMessage message(CheckpointId::Invalid, width.toNumber(),
+                             height.toNumber());
+        UpdateGraphicsInUIProcess(&message);
+      }
+    }
+  }
+}
+
+void Resume(bool aForward) {
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
+
+  MaybeSendRepaintMessage();
+
+  // 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");
+      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);
+
+    // This process will be the new active child, so make sure it has saved the
+    // checkpoint we need it to.
+    targetChild->WaitUntil([=]() {
+      return targetChild->HasSavedCheckpoint(targetCheckpoint) &&
+             targetChild->IsPaused();
+    });
+
+    SwitchActiveChild(targetChild);
+  }
+
+  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");
+        js::DebuggerOnPause();
+        return;
+      }
+
+      // Switch to the recording child as the active child and continue
+      // execution.
+      SwitchActiveChild(gRecordingChild);
+    }
+
+    EnsureMajorCheckpointSaved(gActiveChild,
+                               gActiveChild->LastCheckpoint() + 1);
+
+    // Idle children might change their behavior as we run forward.
+    PokeChildren();
+  }
+
+  gActiveChild->SendMessage(ResumeMessage(aForward));
+}
+
+// Whether the child is restoring an earlier checkpoint as part of a time warp.
+static bool gTimeWarpInProgress;
+
+void TimeWarp(const js::ExecutionPoint& aTarget) {
+  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();
+
+  if (!gActiveChild->HasSavedCheckpoint(aTarget.mCheckpoint)) {
+    // Find the replaying child responsible for saving the target checkpoint.
+    ChildProcessInfo* targetChild =
+        ReplayingChildResponsibleForSavingCheckpoint(aTarget.mCheckpoint);
+
+    if (targetChild == gActiveChild) {
+      // Switch to the other replaying child while this one saves the necessary
+      // checkpoint.
+      SwitchActiveChild(OtherReplayingChild(gActiveChild));
+    }
+
+    // This process will be the new active child, so make sure it has saved the
+    // checkpoint we need it to.
+    targetChild->WaitUntil([=]() {
+      return targetChild->HasSavedCheckpoint(aTarget.mCheckpoint) &&
+             targetChild->IsPaused();
+    });
+
+    SwitchActiveChild(targetChild, /* aRecoverPosition = */ false);
+  }
+
+  gTimeWarpTarget.reset();
+
+  if (!gActiveChild->IsPausedAtCheckpoint() ||
+      gActiveChild->LastCheckpoint() != aTarget.mCheckpoint) {
+    MOZ_RELEASE_ASSERT(!gTimeWarpInProgress);
+    gTimeWarpInProgress = true;
+
+    gActiveChild->SendMessage(RestoreCheckpointMessage(aTarget.mCheckpoint));
+    gActiveChild->WaitUntilPaused();
+
+    gTimeWarpInProgress = false;
+  }
+
+  gActiveChild->SendMessage(RunToPointMessage(aTarget));
+
+  gActiveChild->WaitUntilPaused();
+  SendMessageToUIProcess("TimeWarpFinished");
+}
+
+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()) {
+    Resume(true);
+  }
+}
+
+static void RecvHitCheckpoint(const HitCheckpointMessage& aMsg) {
+  // Ignore HitCheckpoint messages received while doing a time warp. TimeWarp()
+  // will immediately resume the child and we don't want to tell the debugger
+  // it ever paused.
+  if (gTimeWarpInProgress) {
+    return;
+  }
+
+  UpdateCheckpointTimes(aMsg);
+  MaybeUpdateGraphicsAtCheckpoint(aMsg.mCheckpointId);
+
+  // 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()) {
+    Resume(true);
+  } else if (!js::DebuggerOnPause()) {
+    gMainThreadMessageLoop->PostTask(
+        NewRunnableFunction("RecvHitCheckpointResume", Resume, true));
+  }
+}
+
+static void RecvHitBreakpoint(const HitBreakpointMessage& aMsg) {
+  // 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);
+  free(response);
+}
+
 }  // namespace parent
 }  // namespace recordreplay
 }  // namespace mozilla
--- a/toolkit/recordreplay/ipc/ParentInternal.h
+++ b/toolkit/recordreplay/ipc/ParentInternal.h
@@ -19,34 +19,28 @@ namespace parent {
 // This file has internal declarations for interaction between different
 // components of middleman logic.
 
 class ChildProcessInfo;
 
 // Get the message loop for the main thread.
 MessageLoop* MainThreadMessageLoop();
 
-// Called when chrome JS can start running and initialization can finish.
-void ChromeRegistered();
+// Called after prefs are available to this process.
+void PreferencesLoaded();
 
 // Return whether replaying processes are allowed to save checkpoints and
 // rewind. Can only be called after PreferencesLoaded().
 bool CanRewind();
 
-// Get the current active child process.
-ChildProcessInfo* GetActiveChild();
-
-// Get a child process by its ID.
-ChildProcessInfo* GetChildProcess(size_t aId);
+// Whether the child currently being interacted with is recording.
+bool ActiveChildIsRecording();
 
-// Spawn a new replaying child process, returning its ID.
-size_t SpawnReplayingChild();
-
-// Specify the current active child.
-void SetActiveChild(ChildProcessInfo* aChild);
+// Get the active recording child process.
+ChildProcessInfo* ActiveRecordingChild();
 
 // Return whether the middleman's main thread is blocked waiting on a
 // synchronous IPDL reply from the recording child.
 bool MainThreadIsWaitingForIPDLReply();
 
 // If necessary, resume execution in the child before the main thread begins
 // to block while waiting on an IPDL reply from the child.
 void ResumeBeforeWaitingForIPDLReply();
@@ -57,16 +51,42 @@ void InitializeForwarding();
 
 // Terminate all children and kill this process.
 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);
+
+// 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 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();
@@ -98,16 +118,67 @@ static const size_t GraphicsMemorySize =
 // making sure that repainting can handle all the system interactions that
 // occur while painting the current tab.
 bool InRepaintStressMode();
 
 ///////////////////////////////////////////////////////////////////////////////
 // Child Processes
 ///////////////////////////////////////////////////////////////////////////////
 
+// Information about the role which a child process is fulfilling, and governs
+// how the process responds to incoming messages.
+class ChildRole {
+ public:
+  // See ParentIPC.cpp for the meaning of these role types.
+#define ForEachRoleType(Macro) Macro(Active) Macro(Standby) Macro(Inert)
+
+  enum Type {
+#define DefineType(Name) Name,
+    ForEachRoleType(DefineType)
+#undef DefineType
+  };
+
+  static const char* TypeString(Type aType) {
+    switch (aType) {
+#define GetTypeString(Name) \
+  case Name:                \
+    return #Name;
+      ForEachRoleType(GetTypeString)
+#undef GetTypeString
+          default : MOZ_CRASH("Bad ChildRole type");
+    }
+  }
+
+ protected:
+  ChildProcessInfo* mProcess;
+  Type mType;
+
+  explicit ChildRole(Type aType) : mProcess(nullptr), mType(aType) {}
+
+ public:
+  void SetProcess(ChildProcessInfo* aProcess) {
+    MOZ_RELEASE_ASSERT(!mProcess);
+    mProcess = aProcess;
+  }
+  Type GetType() const { return mType; }
+
+  virtual ~ChildRole() {}
+
+  // The methods below are all called on the main thread.
+
+  virtual void Initialize() {}
+
+  // When the child is paused and potentially sitting idle, notify the role
+  // that state affecting its behavior has changed and may want to become
+  // active again.
+  virtual void Poke() {}
+
+  virtual void OnIncomingMessage(const Message& aMsg) = 0;
+};
+
 // Handle to the underlying recording process, if there is one. Recording
 // processes are directly spawned by the middleman at startup, since they need
 // to receive all the same IPC which the middleman receives from the UI process
 // in order to initialize themselves. Replaying processes are all spawned by
 // the UI process itself, due to sandboxing restrictions.
 extern ipc::GeckoChildProcessHost* gRecordingProcess;
 
 // Any information needed to spawn a recording child process, in addition to
@@ -128,54 +199,198 @@ class ChildProcessInfo {
   Channel* mChannel;
 
   // The last time we sent or received a message from this process.
   TimeStamp mLastMessageTime;
 
   // Whether this process is recording.
   bool mRecording;
 
+  // The current recovery stage of this process.
+  //
+  // Recovery is used when we are shepherding a child to a particular state:
+  // a particular execution position and sets of installed breakpoints and
+  // saved checkpoints. Recovery is used when changing a child's role, and when
+  // spawning a new process to replace a crashed child process.
+  //
+  // When recovering, the child process won't yet be in the exact place
+  // reflected by the state below, but the main thread will wait until it has
+  // finished reaching this state before it is able to send or receive
+  // messages.
+  enum class RecoveryStage {
+    // No recovery is being performed, and the process can be interacted with.
+    None,
+
+    // The process has not yet reached mLastCheckpoint.
+    ReachingCheckpoint,
+
+    // The process has reached mLastCheckpoint, and additional messages are
+    // being sent to change its intra-checkpoint execution position or install
+    // breakpoints.
+    PlayingMessages
+  };
+  RecoveryStage mRecoveryStage;
+
   // Whether the process is currently paused.
   bool mPaused;
 
+  // If the process is paused, or if it is running while handling a message
+  // that won't cause it to change its execution point, the last message which
+  // caused it to pause.
+  Message* mPausedMessage;
+
+  // The last checkpoint which the child process reached. The child is
+  // somewhere between this and either the next or previous checkpoint,
+  // depending on the messages that have been sent to it.
+  size_t mLastCheckpoint;
+
+  // Messages sent to the process which will affect its behavior as it runs
+  // forward or backward from mLastCheckpoint. This includes all messages that
+  // will need to be sent to another process to recover it to the same state as
+  // this process.
+  InfallibleVector<Message*> mMessages;
+
+  // In the PlayingMessages recovery stage, how much of mMessages has been sent
+  // to the process.
+  size_t mNumRecoveredMessages;
+
+  // Current role of this process.
+  UniquePtr<ChildRole> mRole;
+
+  // Unsorted list of the checkpoints the process has been instructed to save.
+  // Those at or before the most recent checkpoint will have been saved.
+  InfallibleVector<size_t> mShouldSaveCheckpoints;
+
+  // Sorted major checkpoints for this process. See ParentIPC.cpp.
+  InfallibleVector<size_t> mMajorCheckpoints;
+
+  // Whether we need this child to pause while the recording is updated.
+  bool mPauseNeeded;
+
   // Flags for whether we have received messages from the child indicating it
   // is crashing.
   bool mHasBegunFatalError;
   bool mHasFatalError;
 
-  void OnIncomingMessage(const Message& aMsg, bool aForwardToControl);
+  void OnIncomingMessage(size_t aChannelId, const Message& aMsg);
+  void OnIncomingRecoveryMessage(const Message& aMsg);
+  void SendNextRecoveryMessage();
+  void SendMessageRaw(const Message& aMsg);
 
   static void MaybeProcessPendingMessageRunnable();
-  void ReceiveChildMessageOnMainThread(Message::UniquePtr aMsg);
+  void ReceiveChildMessageOnMainThread(size_t aChannelId, Message* aMsg);
+
+  // Get the position of this process relative to its last checkpoint.
+  enum Disposition {
+    AtLastCheckpoint,
+    BeforeLastCheckpoint,
+    AfterLastCheckpoint
+  };
+  Disposition GetDisposition();
+
+  void Recover(bool aPaused, Message* aPausedMessage, size_t aLastCheckpoint,
+               Message** aMessages, size_t aNumMessages);
 
   void OnCrash(const char* aWhy);
   void LaunchSubprocess(
       const Maybe<RecordingProcessData>& aRecordingProcessData);
 
  public:
-  explicit ChildProcessInfo(const Maybe<RecordingProcessData>& aRecordingProcessData);
+  ChildProcessInfo(UniquePtr<ChildRole> aRole,
+                   const Maybe<RecordingProcessData>& aRecordingProcessData);
   ~ChildProcessInfo();
 
+  ChildRole* Role() { return mRole.get(); }
   size_t GetId() { return mChannel->GetId(); }
   bool IsRecording() { return mRecording; }
+  size_t LastCheckpoint() { return mLastCheckpoint; }
+  bool IsRecovering() { return mRecoveryStage != RecoveryStage::None; }
+  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(
+      InfallibleVector<AddBreakpointMessage*>& aBreakpoints);
+
+  typedef std::function<bool(js::BreakpointPosition::Kind)> BreakpointFilter;
+
+  // 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.
+  size_t RewindTargetCheckpoint() {
+    switch (GetDisposition()) {
+      case BeforeLastCheckpoint:
+      case AtLastCheckpoint:
+        // This will return CheckpointId::Invalid if we are the beginning of the
+        // recording.
+        return LastCheckpoint() - 1;
+      case AfterLastCheckpoint:
+        return LastCheckpoint();
+    }
+  }
+
+  bool ShouldSaveCheckpoint(size_t aId) {
+    return VectorContains(mShouldSaveCheckpoints, aId);
+  }
+
+  bool IsMajorCheckpoint(size_t aId) {
+    return VectorContains(mMajorCheckpoints, aId);
+  }
+
+  bool HasSavedCheckpoint(size_t aId) {
+    return (aId <= MostRecentCheckpoint()) && ShouldSaveCheckpoint(aId);
+  }
+
+  size_t MostRecentSavedCheckpoint() {
+    size_t id = MostRecentCheckpoint();
+    while (!ShouldSaveCheckpoint(id)) {
+      id--;
+    }
+    return id;
+  }
+
+  void SetPauseNeeded() { mPauseNeeded = true; }
+
+  void ClearPauseNeeded() {
+    MOZ_RELEASE_ASSERT(IsPaused());
+    mPauseNeeded = false;
+    mRole->Poke();
+  }
+
+  void AddMajorCheckpoint(size_t aId);
+  void SetRole(UniquePtr<ChildRole> aRole);
   void SendMessage(const Message& aMessage);
 
   // Recover to the same state as another process.
   void Recover(ChildProcessInfo* aTargetProcess);
 
   // Recover to be paused at a checkpoint with no breakpoints set.
   void RecoverToCheckpoint(size_t aCheckpoint);
 
-  // Handle incoming messages from this process (and no others) until it pauses.
-  // The return value is null if it is already paused, otherwise the message
-  // which caused it to pause. In the latter case, OnIncomingMessage will *not*
-  // be called with the message.
-  Message::UniquePtr WaitUntilPaused();
+  // Handle incoming messages from this process (and no others) until the
+  // callback succeeds.
+  void WaitUntil(const std::function<bool()>& aCallback);
+
+  void WaitUntilPaused() {
+    WaitUntil([=]() { return IsPaused(); });
+  }
+
+  static bool MaybeProcessPendingMessage(ChildProcessInfo* aProcess);
 
   static void SetIntroductionMessage(IntroductionMessage* aMessage);
 };
 
 }  // namespace parent
 }  // namespace recordreplay
 }  // namespace mozilla