Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
authorCiure Andrei <aciure@mozilla.com>
Mon, 14 Jan 2019 05:55:46 +0200
changeset 453695 d23f6e4d9ba2
parent 453693 c3179fe251cc (diff)
parent 453694 edca8877b050 (current diff)
child 453703 c879dc493add
push id111127
push useraciure@mozilla.com
push dateMon, 14 Jan 2019 03:56:19 +0000
treeherdermozilla-inbound@d23f6e4d9ba2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/control.js
@@ -0,0 +1,1025 @@
+/* -*- 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();
+    }
+  }
+
+  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,
+};
+
+// eslint-disable-next-line no-unused-vars
+function ConnectDebugger(dbg) {
+  gDebugger = dbg;
+  dbg._control = gControl;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Utilities
+///////////////////////////////////////////////////////////////////////////////
+
+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 RecordReplayControl
-// interface.
+// recording/replaying process, inspecting them via the interface provided by
+// control.js.
 
 "use strict";
 
 const RecordReplayControl = !isWorker && require("RecordReplayControl");
 const Services = require("Services");
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebugger
@@ -35,18 +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;
   }
 
-  // Whether the process is currently paused.
-  this._paused = false;
+  // We should have been connected to control.js by the call above.
+  assert(this._control);
 
   // 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
@@ -92,53 +92,54 @@ ReplayDebugger.prototype = {
   // General methods
   /////////////////////////////////////////////////////////
 
   replaying: true,
 
   canRewind: RecordReplayControl.canRewind,
 
   replayCurrentExecutionPoint() {
-    return this._sendRequest({ type: "currentExecutionPoint" });
+    assert(this._paused);
+    return this._control.pausePoint();
   },
 
   replayRecordingEndpoint() {
     return this._sendRequest({ type: "recordingEndpoint" });
   },
 
-  replayIsRecording: RecordReplayControl.childIsRecording,
+  replayIsRecording() {
+    return this._control.childIsRecording();
+  },
 
   addDebuggee() {},
   removeAllDebuggees() {},
 
   replayingContent(url) {
     this._ensurePaused();
     return this._sendRequest({ type: "getContent", url });
   },
 
   // Send a request object to the child process, and synchronously wait for it
   // to respond.
   _sendRequest(request) {
-    assert(this._paused);
-    const data = RecordReplayControl.sendRequest(request);
+    const data = this._control.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) {
-    assert(this._paused);
-    RecordReplayControl.maybeSwitchToReplayingChild();
+    this._control.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" });
@@ -168,41 +169,43 @@ 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);
-      RecordReplayControl.resume(forward);
+      this._control.resume(forward);
       if (this._paused) {
         // If we resume and immediately pause, we are at an endpoint of the
         // recording. Force the thread to pause.
         this.replayingOnForcedPause(this.getNewestFrame());
       }
     });
   },
 
   replayTimeWarp(target) {
     this._ensurePaused();
     this._setResume(() => {
-      this._paused = false;
       this._direction = Direction.NONE;
       dumpv("Warping " + JSON.stringify(target));
-      RecordReplayControl.timeWarp(target);
+      this._control.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());
     });
   },
 
@@ -210,27 +213,25 @@ ReplayDebugger.prototype = {
     this._ensurePaused();
 
     // Cancel any pending resume.
     this._resumeCallback = null;
   },
 
   _ensurePaused() {
     if (!this._paused) {
-      RecordReplayControl.waitUntilPaused();
+      this._control.waitUntilPaused();
       assert(this._paused);
     }
   },
 
   // This hook is called whenever the child has paused, which can happen
-  // within a RecordReplayControl method (resume, timeWarp, waitUntilPaused) or
-  // or be delivered via the event loop.
+  // within a control method (resume, timeWarp, waitUntilPaused) 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;
@@ -243,17 +244,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.kind == "Invalid") {
+    if (!point.position) {
       // 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) {
@@ -275,32 +276,28 @@ 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.
-      RecordReplayControl.markExplicitPause();
+      this._control.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
@@ -353,29 +350,29 @@ ReplayDebugger.prototype = {
 
   /////////////////////////////////////////////////////////
   // Breakpoint management
   /////////////////////////////////////////////////////////
 
   _setBreakpoint(handler, position, data) {
     this._ensurePaused();
     dumpv("AddBreakpoint " + JSON.stringify(position));
-    RecordReplayControl.addBreakpoint(position);
+    this._control.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");
-      RecordReplayControl.clearBreakpoints();
+      this._control.clearBreakpoints();
       for (const { position } of newBreakpoints) {
         dumpv("AddBreakpoint " + JSON.stringify(position));
-        RecordReplayControl.addBreakpoint(position);
+        this._control.addBreakpoint(position);
       }
     }
     this._breakpoints = newBreakpoints;
   },
 
   _searchBreakpoints(callback) {
     for (const breakpoint of this._breakpoints) {
       const v = callback(breakpoint);
@@ -440,16 +437,17 @@ 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() {
@@ -656,44 +654,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;
     });
   },
 
   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,18 +1,20 @@
 # -*- 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,16 +514,17 @@ 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,
@@ -733,16 +734,17 @@ 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);
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/rrIControl.idl
@@ -0,0 +1,17 @@
+/* -*- 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,19 +1,18 @@
 /* -*- 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/gfx/wr/webrender/res/ps_text_run.glsl
+++ b/gfx/wr/webrender/res/ps_text_run.glsl
@@ -58,33 +58,29 @@ struct TextRun {
 
 TextRun fetch_text_run(int address) {
     vec4 data[3] = fetch_from_gpu_cache_3(address);
     return TextRun(data[0], data[1], data[2].xy);
 }
 
 VertexInfo write_text_vertex(RectWithSize local_clip_rect,
                              float z,
+                             bool should_snap,
                              Transform transform,
                              PictureTask task,
                              vec2 text_offset,
                              vec2 glyph_offset,
                              RectWithSize glyph_rect,
                              vec2 snap_bias) {
     // The offset to snap the glyph rect to a device pixel
     vec2 snap_offset = vec2(0.0);
     mat2 local_transform;
 
-#ifdef WR_FEATURE_GLYPH_TRANSFORM
-    bool remove_subpx_offset = true;
-#else
-    bool remove_subpx_offset = transform.is_axis_aligned;
-#endif
     // Compute the snapping offset only if the scroll node transform is axis-aligned.
-    if (remove_subpx_offset) {
+    if (should_snap) {
         // Transform from local space to device space.
         float device_scale = task.common_data.device_pixel_scale / transform.m[3].w;
         mat2 device_transform = mat2(transform.m) * device_scale;
 
         // Ensure the transformed text offset does not contain a subpixel translation
         // such that glyph snapping is stable for equivalent glyph subpixel positions.
         vec2 device_text_pos = device_transform * text_offset + transform.m[3].xy * device_scale;
         snap_offset = floor(device_text_pos + 0.5) - device_text_pos;
@@ -173,23 +169,28 @@ void main(void) {
 #ifdef WR_FEATURE_GLYPH_TRANSFORM
     // Transform from local space to glyph space.
     mat2 glyph_transform = mat2(transform.m) * task.common_data.device_pixel_scale;
 
     // Compute the glyph rect in glyph space.
     RectWithSize glyph_rect = RectWithSize(res.offset + glyph_transform * (text.offset + glyph.offset),
                                            res.uv_rect.zw - res.uv_rect.xy);
 
+    // Since the glyph is pre-transformed, snapping is both forced and does not depend on the transform.
+    bool should_snap = true;
 #else
     // Scale from glyph space to local space.
     float scale = res.scale / task.common_data.device_pixel_scale;
 
     // Compute the glyph rect in local space.
     RectWithSize glyph_rect = RectWithSize(scale * res.offset + text.offset + glyph.offset,
                                            scale * (res.uv_rect.zw - res.uv_rect.xy));
+
+    // Check if the primitive is actually safe to snap.
+    bool should_snap = ph.user_data.x != 0;
 #endif
 
     vec2 snap_bias;
     // In subpixel mode, the subpixel offset has already been
     // accounted for while rasterizing the glyph. However, we
     // must still round with a subpixel bias rather than rounding
     // to the nearest whole pixel, depending on subpixel direciton.
     switch (subpx_dir) {
@@ -209,16 +210,17 @@ void main(void) {
             break;
         case SUBPX_DIR_MIXED:
             snap_bias = vec2(0.125);
             break;
     }
 
     VertexInfo vi = write_text_vertex(ph.local_clip_rect,
                                       ph.z,
+                                      should_snap,
                                       transform,
                                       task,
                                       text.offset,
                                       glyph.offset,
                                       glyph_rect,
                                       snap_bias);
     glyph_rect.p0 += vi.snap_offset;
 
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -824,17 +824,17 @@ impl AlphaBatchBuilder {
                             GlyphFormat::ColorBitmap => {
                                 (
                                     BlendMode::PremultipliedAlpha,
                                     ShaderColorMode::ColorBitmap,
                                 )
                             }
                         };
 
-                        let prim_header_index = prim_headers.push(&prim_header, z_id, [0; 3]);
+                        let prim_header_index = prim_headers.push(&prim_header, z_id, [run.should_snap as i32, 0, 0]);
                         let key = BatchKey::new(kind, blend_mode, textures);
                         let base_instance = GlyphInstance::new(
                             prim_header_index,
                         );
                         let batch = alpha_batch_list.set_params_and_get_batch(
                             key,
                             bounding_rect,
                             z_id,
--- a/gfx/wr/webrender/src/prim_store/text_run.rs
+++ b/gfx/wr/webrender/src/prim_store/text_run.rs
@@ -60,16 +60,17 @@ impl AsInstanceKind<TextRunDataHandle> f
         &self,
         data_handle: TextRunDataHandle,
         prim_store: &mut PrimitiveStore,
     ) -> PrimitiveInstanceKind {
         let run_index = prim_store.text_runs.push(TextRunPrimitive {
             used_font: self.font.clone(),
             glyph_keys_range: storage::Range::empty(),
             shadow: self.shadow,
+            should_snap: true,
         });
 
         PrimitiveInstanceKind::TextRun{ data_handle, run_index }
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -222,16 +223,17 @@ impl IsVisible for TextRun {
     }
 }
 
 #[derive(Debug)]
 pub struct TextRunPrimitive {
     pub used_font: FontInstance,
     pub glyph_keys_range: storage::Range<GlyphKey>,
     pub shadow: bool,
+    pub should_snap: bool,
 }
 
 impl TextRunPrimitive {
     pub fn update_font_instance(
         &mut self,
         specified_font: &FontInstance,
         device_pixel_scale: DevicePixelScale,
         transform: &LayoutToWorldTransform,
@@ -257,16 +259,21 @@ impl TextRunPrimitive {
         // Get the font transform matrix (skew / scale) from the complete transform.
         let font_transform = if transform_glyphs {
             // Quantize the transform to minimize thrashing of the glyph cache.
             FontTransform::from(transform).quantize()
         } else {
             FontTransform::identity()
         };
 
+        // We can snap only if the transform is axis-aligned and in screen-space.
+        self.should_snap =
+            transform.preserves_2d_axis_alignment() &&
+            raster_space == RasterSpace::Screen;
+
         // If the transform or device size is different, then the caller of
         // this method needs to know to rebuild the glyphs.
         let cache_dirty =
             self.used_font.transform != font_transform ||
             self.used_font.size != device_font_size;
 
         // Construct used font instance from the specified font instance
         self.used_font = FontInstance {
index 895d42eaf5ce4f7ff138f2fdd88893553a658ec6..e9b2cab05f4a13ce21acd745e41c417805d920ec
GIT binary patch
literal 19485
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
Jy8mqQ{{pP|NhkmS
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/raster-space-snap-ref.yaml
@@ -0,0 +1,10 @@
+root:
+  items:
+    - type: stacking-context
+      bounds: [0, 0, 480, 80]
+      raster-space: screen
+      items:
+      - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+        origin: 20.5 50
+        size: 20
+        font: "FreeSans.ttf"
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/raster-space-snap.yaml
@@ -0,0 +1,10 @@
+root:
+  items:
+    - type: stacking-context
+      bounds: [0, 0, 480, 80]
+      raster-space: local(1.0)
+      items:
+      - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+        origin: 20.5 50
+        size: 20
+        font: "FreeSans.ttf"
--- a/gfx/wr/wrench/reftests/text/reftest.list
+++ b/gfx/wr/wrench/reftests/text/reftest.list
@@ -63,8 +63,9 @@ fuzzy(1,113) platform(linux) == raster-s
 != allow-subpixel.yaml allow-subpixel-ref.yaml
 == bg-color.yaml bg-color-ref.yaml
 != large-glyphs.yaml blank.yaml
 == snap-text-offset.yaml snap-text-offset-ref.yaml
 == shadow-border.yaml shadow-solid-ref.yaml
 == shadow-image.yaml shadow-solid-ref.yaml
 options(disable-aa) == snap-clip.yaml snap-clip-ref.yaml
 platform(linux) == perspective-clip.yaml perspective-clip.png
+options(disable-subpixel) != raster-space-snap.yaml raster-space-snap-ref.yaml
index 1dae24cb84d0722edcab962971b69819ab5be985..a3cb79b38f1d600e9ad6df328c2c601135e5c1af
GIT binary patch
literal 11509
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
--- a/servo/components/style/gecko/regen_atoms.py
+++ b/servo/components/style/gecko/regen_atoms.py
@@ -125,16 +125,17 @@ PRELUDE = '''
 RULE_TEMPLATE = '''
     ("{atom}") => {{{{
         #[allow(unsafe_code)] #[allow(unused_unsafe)]
         unsafe {{ $crate::string_cache::Atom::from_index({index}) }}
     }}}};
 '''[1:]
 
 MACRO_TEMPLATE = '''
+/// Returns a static atom by passing the literal string it represents.
 #[macro_export]
 macro_rules! atom {{
 {body}\
 }}
 '''
 
 def write_atom_macro(atoms, file_name):
     with FileAvoidWrite(file_name) as f:
--- a/servo/components/style/gecko_string_cache/namespace.rs
+++ b/servo/components/style/gecko_string_cache/namespace.rs
@@ -6,16 +6,18 @@
 
 use crate::gecko_bindings::structs::nsAtom;
 use crate::string_cache::{Atom, WeakAtom};
 use precomputed_hash::PrecomputedHash;
 use std::borrow::Borrow;
 use std::fmt;
 use std::ops::Deref;
 
+/// In Gecko namespaces are just regular atoms, so this is a simple macro to
+/// forward one macro to the other.
 #[macro_export]
 macro_rules! ns {
     () => {
         $crate::string_cache::Namespace(atom!(""))
     };
     ($s:tt) => {
         $crate::string_cache::Namespace(atom!($s))
     };
--- a/servo/components/style/properties/properties.mako.rs
+++ b/servo/components/style/properties/properties.mako.rs
@@ -3816,17 +3816,24 @@ impl AliasId {
             AliasedPropertyId::Shorthand(ShorthandId::${alias.original.camel_case}),
             % endif
         % endfor
         ];
         MAP[self as usize]
     }
 }
 
-// NOTE(emilio): Callers are responsible to deal with prefs.
+/// Call the given macro with tokens like this for each longhand and shorthand properties
+/// that is enabled in content:
+///
+/// ```
+/// [CamelCaseName, SetCamelCaseName, PropertyId::Longhand(LonghandId::CamelCaseName)],
+/// ```
+///
+/// NOTE(emilio): Callers are responsible to deal with prefs.
 #[macro_export]
 macro_rules! css_properties_accessors {
     ($macro_name: ident) => {
         $macro_name! {
             % for kind, props in [("Longhand", data.longhands), ("Shorthand", data.shorthands)]:
                 % for property in props:
                     % if property.enabled_in_content():
                         % for prop in [property] + property.alias:
@@ -3839,16 +3846,24 @@ macro_rules! css_properties_accessors {
                         % endfor
                     % endif
                 % endfor
             % endfor
         }
     }
 }
 
+/// Call the given macro with tokens like this for each longhand properties:
+///
+/// ```
+/// { snake_case_ident, true }
+/// ```
+///
+/// … where the boolean indicates whether the property value type
+/// is wrapped in a `Box<_>` in the corresponding `PropertyDeclaration` variant.
 #[macro_export]
 macro_rules! longhand_properties_idents {
     ($macro_name: ident) => {
         $macro_name! {
             % for property in data.longhands:
                 { ${property.ident}, ${"true" if property.boxed else "false"} }
             % endfor
         }
--- a/servo/components/style/values/animated/grid.rs
+++ b/servo/components/style/values/animated/grid.rs
@@ -6,58 +6,62 @@
 
 // Note: we can implement Animate on their generic types directly, but in this case we need to
 // make sure two trait bounds, L: Clone and I: PartialEq, are satisfied on almost all the
 // grid-related types and their other trait implementations because Animate needs them. So in
 // order to avoid adding these two trait bounds (or maybe more..) everywhere, we implement
 // Animate for the computed types, instead of the generic types.
 
 use super::{Animate, Procedure, ToAnimatedZero};
-use crate::values::computed::{GridTemplateComponent, TrackList, TrackSize};
 use crate::values::computed::Integer;
 use crate::values::computed::LengthPercentage;
+use crate::values::computed::{GridTemplateComponent, TrackList, TrackSize};
 use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
 use crate::values::generics::grid as generics;
 
 fn discrete<T: Clone>(from: &T, to: &T, procedure: Procedure) -> Result<T, ()> {
     if let Procedure::Interpolate { progress } = procedure {
-        Ok(if progress < 0.5 { from.clone() } else { to.clone() })
+        Ok(if progress < 0.5 {
+            from.clone()
+        } else {
+            to.clone()
+        })
     } else {
         Err(())
     }
 }
 
 fn animate_with_discrete_fallback<T: Animate + Clone>(
     from: &T,
     to: &T,
-    procedure: Procedure
+    procedure: Procedure,
 ) -> Result<T, ()> {
-    from.animate(to, procedure).or_else(|_| discrete(from, to, procedure))
+    from.animate(to, procedure)
+        .or_else(|_| discrete(from, to, procedure))
 }
 
 impl Animate for TrackSize {
     fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
         match (self, other) {
-            (&generics::TrackSize::Breadth(ref from),
-             &generics::TrackSize::Breadth(ref to)) => {
+            (&generics::TrackSize::Breadth(ref from), &generics::TrackSize::Breadth(ref to)) => {
                 animate_with_discrete_fallback(from, to, procedure)
                     .map(generics::TrackSize::Breadth)
             },
-            (&generics::TrackSize::Minmax(ref from_min, ref from_max),
-             &generics::TrackSize::Minmax(ref to_min, ref to_max)) => {
-                Ok(generics::TrackSize::Minmax(
-                    animate_with_discrete_fallback(from_min, to_min, procedure)?,
-                    animate_with_discrete_fallback(from_max, to_max, procedure)?,
-                ))
-            },
-            (&generics::TrackSize::FitContent(ref from),
-             &generics::TrackSize::FitContent(ref to)) => {
-                animate_with_discrete_fallback(from, to, procedure)
-                    .map(generics::TrackSize::FitContent)
-            },
+            (
+                &generics::TrackSize::Minmax(ref from_min, ref from_max),
+                &generics::TrackSize::Minmax(ref to_min, ref to_max),
+            ) => Ok(generics::TrackSize::Minmax(
+                animate_with_discrete_fallback(from_min, to_min, procedure)?,
+                animate_with_discrete_fallback(from_max, to_max, procedure)?,
+            )),
+            (
+                &generics::TrackSize::FitContent(ref from),
+                &generics::TrackSize::FitContent(ref to),
+            ) => animate_with_discrete_fallback(from, to, procedure)
+                .map(generics::TrackSize::FitContent),
             (_, _) => discrete(self, other, procedure),
         }
     }
 }
 
 impl Animate for generics::TrackRepeat<LengthPercentage, Integer>
 where
     generics::RepeatCount<Integer>: PartialEq,
@@ -67,38 +71,46 @@ where
         // number of tracks. For both auto-fit/fill, the number of columns isn't
         // known until you do layout since it depends on the container size, item
         // placement and other factors, so we cannot do the correct interpolation
         // by computed values. Therefore, return Err(()) if it's keywords. If it
         // is Number, we support animation only if the count is the same and the
         // length of track_sizes is the same.
         // https://github.com/w3c/csswg-drafts/issues/3503
         match (&self.count, &other.count) {
-            (&generics::RepeatCount::Number(from),
-             &generics::RepeatCount::Number(to)) if from == to => (),
+            (&generics::RepeatCount::Number(from), &generics::RepeatCount::Number(to))
+                if from == to =>
+            {
+                ()
+            },
             (_, _) => return Err(()),
         }
 
         // The length of track_sizes should be matched.
         if self.track_sizes.len() != other.track_sizes.len() {
             return Err(());
         }
 
         let count = self.count;
-        let track_sizes = self.track_sizes
+        let track_sizes = self
+            .track_sizes
             .iter()
             .zip(other.track_sizes.iter())
             .map(|(a, b)| a.animate(b, procedure))
             .collect::<Result<Vec<_>, _>>()?;
 
         // The length of |line_names| is always 0 or N+1, where N is the length
         // of |track_sizes|. Besides, <line-names> is always discrete.
         let line_names = discrete(&self.line_names, &other.line_names, procedure)?;
 
-        Ok(generics::TrackRepeat { count, line_names, track_sizes })
+        Ok(generics::TrackRepeat {
+            count,
+            line_names,
+            track_sizes,
+        })
     }
 }
 
 impl Animate for TrackList {
     // Based on https://github.com/w3c/csswg-drafts/issues/3201:
     // 1. Check interpolation type per track, so we need to handle discrete animations
     //    in TrackSize, so any Err(()) returned from TrackSize doesn't make all TrackSize
     //    fallback to discrete animation.
@@ -117,26 +129,32 @@ impl Animate for TrackList {
         // traversing |values| in <auto-track-list>. This may be updated in the future.
         // https://github.com/w3c/csswg-drafts/issues/3503
         if let generics::TrackListType::Auto(_) = self.list_type {
             return Err(());
         }
 
         let list_type = self.list_type;
         let auto_repeat = self.auto_repeat.animate(&other.auto_repeat, procedure)?;
-        let values = self.values
+        let values = self
+            .values
             .iter()
             .zip(other.values.iter())
             .map(|(a, b)| a.animate(b, procedure))
             .collect::<Result<Vec<_>, _>>()?;
         // The length of |line_names| is always 0 or N+1, where N is the length
         // of |track_sizes|. Besides, <line-names> is always discrete.
         let line_names = discrete(&self.line_names, &other.line_names, procedure)?;
 
-        Ok(TrackList { list_type, values, line_names, auto_repeat })
+        Ok(TrackList {
+            list_type,
+            values,
+            line_names,
+            auto_repeat,
+        })
     }
 }
 
 impl ComputeSquaredDistance for GridTemplateComponent {
     #[inline]
     fn compute_squared_distance(&self, _other: &Self) -> Result<SquaredDistance, ()> {
         // TODO: Bug 1518585, we should implement ComputeSquaredDistance.
         Err(())
--- a/servo/components/style/values/computed/box.rs
+++ b/servo/components/style/values/computed/box.rs
@@ -8,17 +8,18 @@ use crate::values::computed::length::{Le
 use crate::values::computed::{Context, Number, ToComputedValue};
 use crate::values::generics::box_::AnimationIterationCount as GenericAnimationIterationCount;
 use crate::values::generics::box_::Perspective as GenericPerspective;
 use crate::values::generics::box_::VerticalAlign as GenericVerticalAlign;
 use crate::values::specified::box_ as specified;
 
 pub use crate::values::specified::box_::{AnimationName, Appearance, BreakBetween, BreakWithin};
 pub use crate::values::specified::box_::{Clear as SpecifiedClear, Float as SpecifiedFloat};
-pub use crate::values::specified::box_::{Contain, Display, Overflow, OverflowAnchor, OverflowClipBox};
+pub use crate::values::specified::box_::{Contain, Display, Overflow};
+pub use crate::values::specified::box_::{OverflowAnchor, OverflowClipBox};
 pub use crate::values::specified::box_::{OverscrollBehavior, ScrollSnapType};
 pub use crate::values::specified::box_::{TouchAction, TransitionProperty, WillChange};
 
 /// A computed value for the `vertical-align` property.
 pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>;
 
 /// A computed value for the `animation-iteration-count` property.
 pub type AnimationIterationCount = GenericAnimationIterationCount<Number>;
--- a/servo/components/style/values/generics/grid.rs
+++ b/servo/components/style/values/generics/grid.rs
@@ -169,24 +169,17 @@ pub enum TrackKeyword {
     MinContent,
 }
 
 /// A track breadth for explicit grid track sizing. It's generic solely to
 /// avoid re-implementing it for the computed type.
 ///
 /// <https://drafts.csswg.org/css-grid/#typedef-track-breadth>
 #[derive(
-    Animate,
-    Clone,
-    Debug,
-    MallocSizeOf,
-    PartialEq,
-    SpecifiedValueInfo,
-    ToComputedValue,
-    ToCss,
+    Animate, Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss,
 )]
 pub enum TrackBreadth<L> {
     /// The generic type is almost always a non-negative `<length-percentage>`
     Breadth(L),
     /// A flex fraction specified in `fr` units.
     #[css(dimension)]
     Fr(CSSFloat),
     /// One of the track-sizing keywords (`auto`, `min-content`, `max-content`)
@@ -487,24 +480,17 @@ impl<L: Clone> TrackRepeat<L, specified:
                 line_names: self.line_names.clone(),
             }
         }
     }
 }
 
 /// Track list values. Can be <track-size> or <track-repeat>
 #[derive(
-    Animate,
-    Clone,
-    Debug,
-    MallocSizeOf,
-    PartialEq,
-    SpecifiedValueInfo,
-    ToComputedValue,
-    ToCss
+    Animate, Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss,
 )]
 pub enum TrackListValue<LengthPercentage, Integer> {
     /// A <track-size> value.
     TrackSize(#[animation(field_bound)] TrackSize<LengthPercentage>),
     /// A <track-repeat> value.
     TrackRepeat(#[animation(field_bound)] TrackRepeat<LengthPercentage, Integer>),
 }
 
@@ -707,30 +693,27 @@ impl ToCss for LineNameList {
         Ok(())
     }
 }
 
 /// Variants for `<grid-template-rows> | <grid-template-columns>`
 /// Subgrid deferred to Level 2 spec due to lack of implementation.
 /// But it's implemented in gecko, so we have to as well.
 #[derive(
-    Animate,
-    Clone,
-    Debug,
-    MallocSizeOf,
-    PartialEq,
-    SpecifiedValueInfo,
-    ToComputedValue,
-    ToCss
+    Animate, Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss,
 )]
 pub enum GridTemplateComponent<L, I> {
     /// `none` value.
     None,
     /// The grid `<track-list>`
-    TrackList(#[animation(field_bound)] #[compute(field_bound)] TrackList<L, I>),
+    TrackList(
+        #[animation(field_bound)]
+        #[compute(field_bound)]
+        TrackList<L, I>,
+    ),
     /// A `subgrid <line-name-list>?`
     /// TODO: Support animations for this after subgrid is addressed in [grid-2] spec.
     #[animation(error)]
     Subgrid(LineNameList),
 }
 
 impl<L, I> GridTemplateComponent<L, I> {
     /// Returns length of the <track-list>s <track-size>
--- a/servo/components/style/values/specified/mod.rs
+++ b/servo/components/style/values/specified/mod.rs
@@ -31,17 +31,18 @@ pub use self::align::{AlignContent, Alig
 pub use self::align::{JustifyContent, JustifyItems, JustifySelf, SelfAlignment};
 pub use self::angle::Angle;
 pub use self::background::{BackgroundRepeat, BackgroundSize};
 pub use self::basic_shape::FillRule;
 pub use self::border::{BorderCornerRadius, BorderImageSlice, BorderImageWidth};
 pub use self::border::{BorderImageRepeat, BorderImageSideWidth};
 pub use self::border::{BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle};
 pub use self::box_::{AnimationIterationCount, AnimationName, Contain, Display};
-pub use self::box_::{Appearance, BreakBetween, BreakWithin, Clear, Float, Overflow, OverflowAnchor};
+pub use self::box_::{Appearance, BreakBetween, BreakWithin};
+pub use self::box_::{Clear, Float, Overflow, OverflowAnchor};
 pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize};
 pub use self::box_::{ScrollSnapType, TouchAction, TransitionProperty, VerticalAlign, WillChange};
 pub use self::color::{Color, ColorPropertyValue, RGBAColor};
 pub use self::column::ColumnCount;
 pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset};
 pub use self::easing::TimingFunction;
 pub use self::effects::{BoxShadow, Filter, SimpleShadow};
 pub use self::flex::FlexBasis;
--- a/servo/components/style_derive/animate.rs
+++ b/servo/components/style_derive/animate.rs
@@ -83,17 +83,20 @@ fn derive_variant_arm(
     let (other_pattern, other_info) = cg::ref_pattern(&variant, "other");
     let (result_value, result_info) = cg::value(&variant, "result");
     let mut computations = quote!();
     let iter = result_info.iter().zip(this_info.iter().zip(&other_info));
     computations.append_all(iter.map(|(result, (this, other))| {
         let field_attrs = cg::parse_field_attrs::<AnimationFieldAttrs>(&result.ast());
         if field_attrs.field_bound {
             let ty = &this.ast().ty;
-            cg::add_predicate(where_clause, parse_quote!(#ty: crate::values::animated::Animate));
+            cg::add_predicate(
+                where_clause,
+                parse_quote!(#ty: crate::values::animated::Animate),
+            );
         }
         if field_attrs.constant {
             quote! {
                 if #this != #other {
                     return Err(());
                 }
                 let #result = std::clone::Clone::clone(#this);
             }
--- a/servo/components/style_traits/values.rs
+++ b/servo/components/style_traits/values.rs
@@ -153,34 +153,16 @@ where
             if !prefix.is_empty() {
                 self.inner.write_str(prefix)?;
             }
         }
         self.inner.write_char(c)
     }
 }
 
-#[macro_export]
-macro_rules! serialize_function {
-    ($dest: expr, $name: ident($( $arg: expr, )+)) => {
-        serialize_function!($dest, $name($($arg),+))
-    };
-    ($dest: expr, $name: ident($first_arg: expr $( , $arg: expr )*)) => {
-        {
-            $dest.write_str(concat!(stringify!($name), "("))?;
-            $first_arg.to_css($dest)?;
-            $(
-                $dest.write_str(", ")?;
-                $arg.to_css($dest)?;
-            )*
-            $dest.write_char(')')
-        }
-    }
-}
-
 /// Convenience wrapper to serialise CSS values separated by a given string.
 pub struct SequenceWriter<'a, 'b: 'a, W: 'b> {
     inner: &'a mut CssWriter<'b, W>,
     separator: &'static str,
 }
 
 impl<'a, 'b, W> SequenceWriter<'a, 'b, W>
 where
@@ -445,17 +427,17 @@ impl_to_css_for_predefined_type!(i8);
 impl_to_css_for_predefined_type!(i32);
 impl_to_css_for_predefined_type!(u16);
 impl_to_css_for_predefined_type!(u32);
 impl_to_css_for_predefined_type!(::cssparser::Token<'a>);
 impl_to_css_for_predefined_type!(::cssparser::RGBA);
 impl_to_css_for_predefined_type!(::cssparser::Color);
 impl_to_css_for_predefined_type!(::cssparser::UnicodeRange);
 
-#[macro_export]
+/// Define an enum type with unit variants that each correspond to a CSS keyword.
 macro_rules! define_css_keyword_enum {
     (pub enum $name:ident { $($variant:ident = $css:expr,)+ }) => {
         #[allow(missing_docs)]
         #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
         #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
         pub enum $name {
             $($variant),+
         }
--- 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* msg = channel->WaitForMessage();
+    Message::UniquePtr msg = channel->WaitForMessage();
     if (!msg) {
       break;
     }
-    channel->mHandler(msg);
+    channel->mHandler(std::move(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* Channel::WaitForMessage() {
+Message::UniquePtr 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* Channel::WaitForMessage() {
       }
       PrintSpew("Channel disconnected, exiting...\n");
       _exit(0);
     }
 
     mMessageBytes += nbytes;
   }
 
-  Message* res = ((Message*)mMessageBuffer->begin())->Clone();
+  Message::UniquePtr 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,28 +232,25 @@ Message* Channel::WaitForMessage() {
 
 void Channel::PrintMessage(const char* aPrefix, const Message& aMsg) {
   if (!SpewEnabled()) {
     return;
   }
   AutoEnsurePassThroughThreadEvents pt;
   nsCString data;
   switch (aMsg.mType) {
-    case MessageType::HitCheckpoint: {
-      const HitCheckpointMessage& nmsg = (const HitCheckpointMessage&)aMsg;
-      data.AppendPrintf("Id %d Endpoint %d Duration %.2f ms",
-                        (int)nmsg.mCheckpointId, nmsg.mRecordingEndpoint,
+    case MessageType::HitExecutionPoint: {
+      const HitExecutionPointMessage& nmsg =
+        (const HitExecutionPointMessage&)aMsg;
+      nmsg.mPoint.ToString(data);
+      data.AppendPrintf(" Endpoint %d Duration %.2f ms",
+                        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,16 +6,17 @@
 
 #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 {
@@ -71,18 +72,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 HitBreakpoint */ \
-  /* message. */                                               \
+  /* a set of positions at which the child process should pause and send a */ \
+  /* HitExecutionPoint 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. */                                   \
@@ -121,20 +122,18 @@ 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 a checkpoint or breakpoint was hit. */ \
-  /* The child will pause after sending these messages. */     \
-  _Macro(HitCheckpoint)                                        \
-  _Macro(HitBreakpoint)                                        \
+  /* Notify the middleman that the child has hit an execution point and paused. */ \
+  _Macro(HitExecutionPoint)                                    \
                                                                \
   /* 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)                                 \
                                                                \
@@ -157,20 +156,23 @@ struct Message {
   uint32_t mSize;
 
  protected:
   Message(MessageType aType, uint32_t aSize) : mType(aType), mSize(aSize) {
     MOZ_RELEASE_ASSERT(mSize >= sizeof(*this));
   }
 
  public:
-  Message* Clone() const {
-    char* res = (char*)malloc(mSize);
+  struct FreePolicy { void operator()(Message* msg) { /*free(msg);*/ } };
+  typedef UniquePtr<Message, FreePolicy> UniquePtr;
+
+  UniquePtr Clone() const {
+    Message* res = static_cast<Message*>(malloc(mSize));
     memcpy(res, this, mSize);
-    return (Message*)res;
+    return UniquePtr(res);
   }
 
   const char* TypeString() const {
     switch (mType) {
 #define EnumToString(Kind) \
   case MessageType::Kind:  \
     return #Kind;
       ForEachMessageType(EnumToString)
@@ -376,40 +378,36 @@ 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 HitCheckpointMessage : public Message {
-  uint32_t mCheckpointId;
+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.
   bool mRecordingEndpoint;
 
-  // When recording, the amount of non-idle time taken to get to this
-  // checkpoint from the previous one.
+  // The amount of non-idle time taken to get to this pause from the last time
+  // the child paused.
   double mDurationMicroseconds;
 
-  HitCheckpointMessage(uint32_t aCheckpointId, bool aRecordingEndpoint,
-                       double aDurationMicroseconds)
-      : Message(MessageType::HitCheckpoint, sizeof(*this)),
-        mCheckpointId(aCheckpointId),
+  HitExecutionPointMessage(const js::ExecutionPoint& aPoint,
+                           bool aRecordingEndpoint,
+                           double aDurationMicroseconds)
+      : Message(MessageType::HitExecutionPoint, sizeof(*this)),
+        mPoint(aPoint),
         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>(); }
@@ -440,17 +438,17 @@ static inline MiddlemanCallResponseMessa
   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*)> MessageHandler;
+  typedef std::function<void(Message::UniquePtr)> MessageHandler;
 
  private:
   // ID for this channel, unique for the middleman.
   size_t mId;
 
   // Callback to invoke off thread on incoming messages.
   MessageHandler mHandler;
 
@@ -474,17 +472,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* WaitForMessage();
+  Message::UniquePtr 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,36 +53,38 @@ 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 IntroductionMessage* gIntroductionMessage;
+static UniquePtr<IntroductionMessage, Message::FreePolicy> gIntroductionMessage;
 
 // When recording, whether developer tools server code runs in the middleman.
 static bool gDebuggerRunsInMiddleman;
 
 // Any response received to the last MiddlemanCallRequest message.
-static MiddlemanCallResponseMessage* gCallResponseMessage;
+static UniquePtr<MiddlemanCallResponseMessage, Message::FreePolicy>
+  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* aMsg) {
+static void ChannelMessageHandler(Message::UniquePtr aMsg) {
   MOZ_RELEASE_ASSERT(MainThreadShouldPause() || aMsg->CanBeSentWhileUnpaused());
 
   switch (aMsg->mType) {
     case MessageType::Introduction: {
       MOZ_RELEASE_ASSERT(!gIntroductionMessage);
-      gIntroductionMessage = (IntroductionMessage*)aMsg->Clone();
+      gIntroductionMessage.reset(
+          static_cast<IntroductionMessage*>(aMsg.release()));
       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()) {
@@ -172,26 +174,24 @@ 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 = (MiddlemanCallResponseMessage*)aMsg;
-      aMsg = nullptr;  // Avoid freeing the message below.
+      gCallResponseMessage.reset(
+          static_cast<MiddlemanCallResponseMessage*>(aMsg.release()));
       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.
-  HitCheckpoint(CheckpointId::Invalid, /* aRecordingEndpoint = */ false);
+  HitExecutionPoint(js::ExecutionPoint(), /* 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,17 +317,16 @@ 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);
 
@@ -629,69 +628,63 @@ void Repaint(size_t* aWidth, size_t* aHe
 bool CurrentRepaintCannotFail() {
   return gRepainting && !gAllowRepaintFailures;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Checkpoint Messages
 ///////////////////////////////////////////////////////////////////////////////
 
-// The time when the last HitCheckpoint message was sent.
-static double gLastCheckpointTime;
+// The time when the last HitExecutionPoint message was sent.
+static double gLastPauseTime;
 
 // 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.
-  gLastCheckpointTime += CurrentTime() - gIdleTimeStart;
+  gLastPauseTime += CurrentTime() - gIdleTimeStart;
   gIdleTimeStart = 0;
 }
 
-void HitCheckpoint(size_t aId, bool aRecordingEndpoint) {
+void HitExecutionPoint(const js::ExecutionPoint& aPoint,
+                       bool aRecordingEndpoint) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
   double time = CurrentTime();
   PauseMainThreadAndInvokeCallback([=]() {
     double duration = 0;
-    if (aId > CheckpointId::First) {
-      duration = time - gLastCheckpointTime;
+    if (gLastPauseTime) {
+      duration = time - gLastPauseTime;
       MOZ_RELEASE_ASSERT(duration > 0);
     }
     gChannel->SendMessage(
-        HitCheckpointMessage(aId, aRecordingEndpoint, duration));
+        HitExecutionPointMessage(aPoint, aRecordingEndpoint, duration));
   });
-  gLastCheckpointTime = time;
+  gLastPauseTime = 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();
   }
@@ -704,17 +697,16 @@ 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,18 +82,17 @@ 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 HitCheckpoint(size_t aId, bool aRecordingEndpoint);
-void HitBreakpoint(bool aRecordingEndpoint);
+void HitExecutionPoint(const js::ExecutionPoint& aPoint, 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,33 +10,16 @@
 
 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.
 //
@@ -227,20 +210,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: ");
-    ExecutionPointToString(mPoint, aStr);
+    mPoint.ToString(aStr);
     if (mTemporaryCheckpoint.isSome()) {
       aStr.AppendPrintf(" TemporaryCheckpoint: ");
-      ExecutionPointToString(mTemporaryCheckpoint.ref(), aStr);
+      mTemporaryCheckpoint.ref().ToString(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
@@ -489,30 +472,26 @@ void PausedPhase::Enter(const ExecutionP
   gNavigation->SetPhase(this);
 
   if (aRewind) {
     MOZ_RELEASE_ASSERT(!aPoint.HasPosition());
     RestoreCheckpointAndResume(CheckpointId(aPoint.mCheckpoint));
     Unreachable();
   }
 
-  if (aPoint.HasPosition()) {
-    child::HitBreakpoint(aRecordingEndpoint);
-  } else {
-    child::HitCheckpoint(aPoint.mCheckpoint, aRecordingEndpoint);
-  }
+  child::HitExecutionPoint(aPoint, 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::HitCheckpoint(mPoint.mCheckpoint, mRecordingEndpoint);
+    child::HitExecutionPoint(mPoint, mRecordingEndpoint);
   } else {
     // We just saved or restored the temporary checkpoint taken while
     // processing debugger requests here.
     MOZ_RELEASE_ASSERT(ThisProcessCanRewind());
     MOZ_RELEASE_ASSERT(mSavedTemporaryCheckpoint);
   }
 }
 
@@ -780,17 +759,17 @@ void ForwardPhase::PositionHit(const Exe
 
   if (hitBreakpoint) {
     gNavigation->mPausedPhase.Enter(aPoint);
   }
 }
 
 void ForwardPhase::HitRecordingEndpoint(const ExecutionPoint& aPoint) {
   nsAutoCString str;
-  ExecutionPointToString(aPoint, str);
+  aPoint.ToString(str);
 
   gNavigation->mPausedPhase.Enter(aPoint, /* aRewind = */ false,
                                   /* aRecordingEndpoint = */ true);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReachBreakpointPhase
 ///////////////////////////////////////////////////////////////////////////////
--- a/toolkit/recordreplay/ipc/ChildProcess.cpp
+++ b/toolkit/recordreplay/ipc/ChildProcess.cpp
@@ -24,377 +24,114 @@ 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());
   }
 }
 
-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) {
+void ChildProcessInfo::OnIncomingMessage(const Message& aMsg,
+                                         bool aForwardToControl) {
   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::HitCheckpoint:
-    case MessageType::HitBreakpoint:
-      MOZ_RELEASE_ASSERT(!mPausedMessage);
-      mPausedMessage = aMsg.Clone();
-      MOZ_FALLTHROUGH;
+    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);
+      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:
-      MOZ_RELEASE_ASSERT(mPausedMessage);
       mPaused = true;
       break;
+    case MessageType::MiddlemanCallRequest: {
+      const MiddlemanCallRequestMessage& nmsg =
+        static_cast<const MiddlemanCallRequestMessage&>(aMsg);
+      Message::UniquePtr response(ProcessMiddlemanCallMessage(nmsg));
+      SendMessage(*response);
+      break;
+    }
+    case MessageType::ResetMiddlemanCalls:
+      ResetMiddlemanCalls();
+      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,
@@ -421,19 +158,20 @@ 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* aMsg) {
-    ReceiveChildMessageOnMainThread(channelId, aMsg);
-  });
+  mChannel = new Channel(channelId, IsRecording(),
+                         [=](Message::UniquePtr aMsg) {
+                           ReceiveChildMessageOnMainThread(std::move(aMsg));
+                         });
 
   MOZ_RELEASE_ASSERT(IsRecording() == aRecordingProcessData.isSome());
   if (IsRecording()) {
     std::vector<std::string> extraArgs;
     GetArgumentsForChildProcess(base::GetCurrentProcId(), channelId,
                                 gRecordingFilename, /* aRecording = */ true,
                                 extraArgs);
 
@@ -456,21 +194,30 @@ void ChildProcessInfo::LaunchSubprocess(
   } else {
     dom::ContentChild::GetSingleton()->SendCreateReplayingProcess(channelId);
   }
 
   mLastMessageTime = TimeStamp::Now();
 
   SendGraphicsMemoryToChild();
 
-  // The child should send us a HitCheckpoint with an invalid ID to pause.
+  // The child should send us a HitExecutionPoint message with an invalid point
+  // 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));
@@ -497,58 +244,77 @@ 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;
-  size_t mChannelId;
-  Message* mMsg;
+  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);
+  }
 };
 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;
 
-void ChildProcessInfo::WaitUntil(const std::function<bool()>& aCallback) {
+Message::UniquePtr ChildProcessInfo::WaitUntilPaused() {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
+  if (IsPaused()) {
+    return nullptr;
+  }
+
   bool sentTerminateMessage = false;
-  while (!aCallback()) {
+  while (true) {
     MonitorAutoLock lock(*gMonitor);
-    if (!MaybeProcessPendingMessage(this)) {
+
+    // 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 (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);
@@ -556,17 +322,17 @@ void ChildProcessInfo::WaitUntil(const s
           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);
-            SendMessageRaw(TerminateMessage());
+            SendMessage(TerminateMessage());
             sentTerminateMessage = true;
           } else {
             // The child is still non-responsive after sending the terminate
             // message.
             OnCrash("Child process non-responsive");
           }
         }
         gMonitor->WaitUntil(deadline);
@@ -577,36 +343,43 @@ void ChildProcessInfo::WaitUntil(const s
 
 // 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 (MaybeProcessPendingMessage(nullptr)) {
+  while (true) {
+    ChildProcessInfo* process = nullptr;
+    Message::UniquePtr msg = ExtractChildMessage(&process);
+
+    if (msg) {
+      MonitorAutoUnlock unlock(*gMonitor);
+      process->OnIncomingMessage(*msg, /* aForwardToControl = */ true);
+    } else {
+      break;
+    }
   }
 }
 
 // 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(size_t aChannelId,
-                                                       Message* aMsg) {
+void ChildProcessInfo::ReceiveChildMessageOnMainThread(Message::UniquePtr aMsg) {
   MOZ_RELEASE_ASSERT(!NS_IsMainThread());
 
   MonitorAutoLock lock(*gMonitor);
 
   PendingMessage pending;
   pending.mProcess = this;
-  pending.mChannelId = aChannelId;
-  pending.mMsg = aMsg;
-  gPendingMessages.append(pending);
+  pending.mMsg = std::move(aMsg);
+  gPendingMessages.append(std::move(pending));
 
-  // Notify the main thread, if it is waiting in WaitUntil.
+  // Notify the main thread, if it is waiting in WaitUntilPaused.
   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,16 +10,17 @@
 #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 {
@@ -64,16 +65,31 @@ 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";
@@ -101,17 +117,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) {
@@ -129,195 +145,435 @@ 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));
-  RootedObject position(aCx, mPosition.Encode(aCx));
-  if (!obj || !position ||
+  if (!obj ||
       !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;
   }
 
-  RootedObject positionObject(aCx, NonNullObject(aCx, v));
-  return positionObject && mPosition.Decode(aCx, positionObject) &&
-         GetNumberProperty(aCx, aObject, gCheckpointProperty, &mCheckpoint) &&
+  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) &&
          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 true;
+    return JS_WrapValue(aCx, args.rval());
   }
 
   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_Resume(JSContext* aCx, unsigned aArgc, Value* aVp) {
+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) {
+  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));
 
-  parent::Resume(forward);
+  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();
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_TimeWarp(JSContext* aCx, unsigned aArgc, Value* aVp) {
+static bool Middleman_SendResume(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-  RootedObject targetObject(aCx, NonNullObject(aCx, args.get(0)));
-  if (!targetObject) {
+
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
     return false;
   }
 
-  ExecutionPoint target;
-  if (!target.Decode(aCx, targetObject)) {
+  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) {
     return false;
   }
 
-  parent::TimeWarp(target);
+  double checkpoint;
+  if (!ToNumber(aCx, args.get(1), &checkpoint)) {
+    return false;
+  }
+
+  child->SendMessage(RestoreCheckpointMessage(checkpoint));
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_SendRequest(JSContext* aCx, unsigned aArgc, Value* aVp) {
+static bool Middleman_SendRunToPoint(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-  RootedObject requestObject(aCx, NonNullObject(aCx, args.get(0)));
+
+  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)));
   if (!requestObject) {
     return false;
   }
 
   CharBuffer requestBuffer;
   if (!ToJSONMaybeSafely(aCx, requestObject, FillCharBufferCallback,
                          &requestBuffer)) {
     return false;
   }
 
   CharBuffer responseBuffer;
-  parent::SendRequest(requestBuffer, &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;
 
   return JS_ParseJSON(aCx, responseBuffer.begin(), responseBuffer.length(),
                       args.rval());
 }
 
-static bool Middleman_AddBreakpoint(JSContext* aCx, unsigned aArgc,
-                                    Value* aVp) {
+static bool Middleman_SendAddBreakpoint(JSContext* aCx,
+                                        unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(0)));
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
+
+  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(1)));
   if (!positionObject) {
     return false;
   }
 
   BreakpointPosition position;
   if (!position.Decode(aCx, positionObject)) {
     return false;
   }
 
-  parent::AddBreakpoint(position);
+  child->SendMessage(AddBreakpointMessage(position));
 
   args.rval().setUndefined();
   return true;
 }
 
-/* static */ bool Middleman_ClearBreakpoints(JSContext* aCx, unsigned aArgc,
-                                             Value* aVp) {
+static bool Middleman_SendClearBreakpoints(JSContext* aCx,
+                                           unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  parent::ClearBreakpoints();
-
-  args.rval().setUndefined();
-  return true;
-}
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
 
-static bool Middleman_MaybeSwitchToReplayingChild(JSContext* aCx,
-                                                  unsigned aArgc, Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::MaybeSwitchToReplayingChild();
+  child->SendMessage(ClearBreakpointsMessage());
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool Middleman_HadRepaint(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
@@ -341,40 +597,63 @@ static bool Middleman_HadRepaintFailure(
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
   parent::UpdateGraphicsInUIProcess(nullptr);
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_ChildIsRecording(JSContext* aCx, unsigned aArgc,
-                                       Value* aVp) {
+static bool Middleman_InRepaintStressMode(JSContext* aCx,
+                                          unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-  args.rval().setBoolean(parent::ActiveChildIsRecording());
+
+  args.rval().setBoolean(parent::InRepaintStressMode());
   return true;
 }
 
-static bool Middleman_MarkExplicitPause(JSContext* aCx, unsigned aArgc,
-                                        Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::MarkActiveChildExplicitPause();
-
-  args.rval().setUndefined();
-  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_WaitUntilPaused(JSContext* aCx, unsigned aArgc,
                                       Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  parent::WaitUntilActiveChildIsPaused();
+  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();
 
-  args.rval().setUndefined();
+  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);
   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)));
@@ -888,28 +1167,30 @@ 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("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("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("hadRepaint", Middleman_HadRepaint, 2, 0),
     JS_FN("hadRepaintFailure", Middleman_HadRepaintFailure, 0, 0),
-    JS_FN("childIsRecording", Middleman_ChildIsRecording, 0, 0),
-    JS_FN("markExplicitPause", Middleman_MarkExplicitPause, 0, 0),
-    JS_FN("waitUntilPaused", Middleman_WaitUntilPaused, 0, 0),
+    JS_FN("inRepaintStressMode", Middleman_InRepaintStressMode, 0, 0),
+    JS_FN("waitUntilPaused", Middleman_WaitUntilPaused, 1, 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,19 +8,24 @@
 #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.
@@ -130,16 +135,17 @@ 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;
@@ -177,33 +183,40 @@ 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
@@ -216,13 +229,16 @@ 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,16 +13,21 @@
 #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;
   }
 
@@ -68,19 +73,20 @@ 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_SetXPCOMProcessAttributes__ID) {
-      // Preferences are initialized via the SetXPCOMProcessAttributes message.
-      PreferencesLoaded();
+    if (type == dom::PContent::Msg_RegisterChrome__ID) {
+      // After the RegisterChrome message we can load chrome JS and finish
+      // initialization.
+      ChromeRegistered();
     }
     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.
@@ -245,17 +251,20 @@ 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;
-      ActiveRecordingChild()->WaitUntil([&]() { return !!aReply; });
+      while (!aReply) {
+        GetActiveChild()->WaitUntilPaused();
+        GetActiveChild()->SendMessage(ResumeMessage(/* aForward = */ true));
+      }
     } else {
       MonitorAutoLock lock(*gMonitor);
       while (!aReply) {
         gMonitor->Wait();
       }
     }
 
     PrintSpew("SyncMsgDone\n");
@@ -286,17 +295,20 @@ 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;
-      ActiveRecordingChild()->WaitUntil([&]() { return !!aReply; });
+      while (!aReply) {
+        GetActiveChild()->WaitUntilPaused();
+        GetActiveChild()->SendMessage(ResumeMessage(/* aForward = */ true));
+      }
     } else {
       MonitorAutoLock lock(*gMonitor);
       while (!aReply) {
         gMonitor->Wait();
       }
     }
 
     PrintSpew("SyncCallDone\n");
--- a/toolkit/recordreplay/ipc/ParentIPC.cpp
+++ b/toolkit/recordreplay/ipc/ParentIPC.cpp
@@ -42,595 +42,122 @@ void InitializeUIProcess(int aArgc, char
 }
 
 const char* SaveAllRecordingsDirectory() {
   MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
   return gSaveAllRecordingsDirectory;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
-// Child Roles
+// Child Processes
 ///////////////////////////////////////////////////////////////////////////////
 
-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;
 
-// 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;
+// Any replaying child processes that have been spawned.
+static StaticInfallibleVector<UniquePtr<ChildProcessInfo>> gReplayingChildren;
+
+// The currently active child process.
+static ChildProcessInfo* gActiveChild;
 
 void Shutdown() {
   delete gRecordingChild;
-  delete gFirstReplayingChild;
-  delete gSecondReplayingChild;
+  gReplayingChildren.clear();
   _exit(0);
 }
 
 bool IsMiddlemanWithRecordingChild() {
   return IsMiddleman() && gRecordingChild;
 }
 
-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());
+ChildProcessInfo* GetActiveChild() {
   return gActiveChild;
 }
 
-// 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();
+ChildProcessInfo* GetChildProcess(size_t aId) {
+  if (gRecordingChild && gRecordingChild->GetId() == aId) {
+    return gRecordingChild;
   }
-
-  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());
+  for (const auto& child : gReplayingChildren) {
+    if (child->GetId() == aId) {
+      return child.get();
+    }
   }
-
-  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 last;
+  return nullptr;
 }
 
-// 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;
+size_t SpawnReplayingChild() {
+  ChildProcessInfo* child = new ChildProcessInfo(Nothing());
+  gReplayingChildren.append(child);
+  return child->GetId();
 }
 
-// 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();
+void SetActiveChild(ChildProcessInfo* aChild) {
+  MOZ_RELEASE_ASSERT(aChild->IsPaused());
 
-// 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 ChildRoleStandby::Poke() {
-  MOZ_RELEASE_ASSERT(mProcess->IsPausedAtCheckpoint());
-
-  // Stay paused if we need to while the recording is flushed.
-  if (mProcess->PauseNeeded()) {
-    return;
+  if (gActiveChild) {
+    MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
+    gActiveChild->SendMessage(SetIsActiveMessage(false));
   }
 
-  // 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));
-  }
+  aChild->SendMessage(SetIsActiveMessage(true));
+  gActiveChild = aChild;
 }
 
-///////////////////////////////////////////////////////////////////////////////
-// 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;
-  }
-}
+void ResumeBeforeWaitingForIPDLReply() {
+  MOZ_RELEASE_ASSERT(gActiveChild->IsRecording());
 
-///////////////////////////////////////////////////////////////////////////////
-// 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();
+  // 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));
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Preferences
 ///////////////////////////////////////////////////////////////////////////////
 
-static bool gPreferencesLoaded;
+static bool gChromeRegistered;
 static bool gRewindingEnabled;
 
-void PreferencesLoaded() {
+void ChromeRegistered() {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
 
-  MOZ_RELEASE_ASSERT(!gPreferencesLoaded);
-  gPreferencesLoaded = true;
+  if (gChromeRegistered) {
+    return;
+  }
+  gChromeRegistered = 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());
     }
-  } 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();
-    }
+    recordingChildId.emplace(gRecordingChild->GetId());
   }
+
+  js::SetupMiddlemanControl(recordingChildId);
 }
 
 bool CanRewind() {
-  MOZ_RELEASE_ASSERT(gPreferencesLoaded);
+  MOZ_RELEASE_ASSERT(gChromeRegistered);
   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();
@@ -642,217 +169,53 @@ 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) {
-  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();
-  }
+  // Make sure the recording file is up to date and ready for copying.
+  js::BeforeSaveRecording();
 
   // 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");
-  SendMessageToUIProcess("SaveRecordingFinished");
+
+  js::AfterSaveRecording();
 }
 
 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; }
 
@@ -880,275 +243,25 @@ void InitializeMiddleman(int aArgc, char
   InitializeGraphicsMemory();
 
   gMonitor = new Monitor();
 
   gMainThreadMessageLoop = MessageLoop::current();
 
   if (gProcessKind == ProcessKind::MiddlemanRecording) {
     RecordingProcessData data(aPrefsHandle, aPrefMapHandle);
-    SpawnRecordingChild(data);
+    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;
   }
 
   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,28 +19,34 @@ 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 after prefs are available to this process.
-void PreferencesLoaded();
+// Called when chrome JS can start running and initialization can finish.
+void ChromeRegistered();
 
 // Return whether replaying processes are allowed to save checkpoints and
 // rewind. Can only be called after PreferencesLoaded().
 bool CanRewind();
 
-// Whether the child currently being interacted with is recording.
-bool ActiveChildIsRecording();
+// Get the current active child process.
+ChildProcessInfo* GetActiveChild();
+
+// Get a child process by its ID.
+ChildProcessInfo* GetChildProcess(size_t aId);
 
-// Get the active recording child process.
-ChildProcessInfo* ActiveRecordingChild();
+// Spawn a new replaying child process, returning its ID.
+size_t SpawnReplayingChild();
+
+// Specify the current active child.
+void SetActiveChild(ChildProcessInfo* aChild);
 
 // 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();
@@ -51,42 +57,16 @@ 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();
@@ -118,67 +98,16 @@ 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
@@ -199,198 +128,54 @@ 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(size_t aChannelId, const Message& aMsg);
-  void OnIncomingRecoveryMessage(const Message& aMsg);
-  void SendNextRecoveryMessage();
-  void SendMessageRaw(const Message& aMsg);
+  void OnIncomingMessage(const Message& aMsg, bool aForwardToControl);
 
   static void MaybeProcessPendingMessageRunnable();
-  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 ReceiveChildMessageOnMainThread(Message::UniquePtr aMsg);
 
   void OnCrash(const char* aWhy);
   void LaunchSubprocess(
       const Maybe<RecordingProcessData>& aRecordingProcessData);
 
  public:
-  ChildProcessInfo(UniquePtr<ChildRole> aRole,
-                   const Maybe<RecordingProcessData>& aRecordingProcessData);
+  explicit ChildProcessInfo(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 the
-  // callback succeeds.
-  void WaitUntil(const std::function<bool()>& aCallback);
-
-  void WaitUntilPaused() {
-    WaitUntil([=]() { return IsPaused(); });
-  }
-
-  static bool MaybeProcessPendingMessage(ChildProcessInfo* aProcess);
+  // 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();
 
   static void SetIntroductionMessage(IntroductionMessage* aMessage);
 };
 
 }  // namespace parent
 }  // namespace recordreplay
 }  // namespace mozilla