Bug 1524127 Part 2 - Keep track of minor checkpoints in control logic, r=lsmyth.
authorBrian Hackett <bhackett1024@gmail.com>
Wed, 30 Jan 2019 15:28:28 -1000
changeset 457578 4e98200ee53017fe8cd037abf948ebaabd33f229
parent 457577 51e195638ef6c836b528df200fde56dfc994539d
child 457579 87ad4b91e29fb9025d90ceb89a62b5a4eb7d584a
child 458148 81aac3b38db3c93bf3fef050ecdf389d9747bab6
push id111759
push userbhackett@mozilla.com
push dateThu, 07 Feb 2019 19:16:17 +0000
treeherdermozilla-inbound@4e98200ee530 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsmyth
bugs1524127
milestone67.0a1
first release with
nightly linux32
4e98200ee530 / 67.0a1 / 20190207214423 / files
nightly linux64
4e98200ee530 / 67.0a1 / 20190207214423 / files
nightly mac
4e98200ee530 / 67.0a1 / 20190207214423 / files
nightly win32
4e98200ee530 / 67.0a1 / 20190207214423 / files
nightly win64
4e98200ee530 / 67.0a1 / 20190207214423 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1524127 Part 2 - Keep track of minor checkpoints in control logic, r=lsmyth.
devtools/server/actors/replay/control.js
--- a/devtools/server/actors/replay/control.js
+++ b/devtools/server/actors/replay/control.js
@@ -49,16 +49,17 @@ function ChildProcess(id, recording, rol
   // All currently installed breakpoints
   this.breakpoints = [];
 
   // Any debugger requests sent while paused at the current point.
   this.debuggerRequests = [];
 
   this._willSaveCheckpoints = [];
   this._majorCheckpoints = [];
+  this._minorCheckpoints = new Set();
 
   // 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 });
@@ -80,16 +81,20 @@ ChildProcess.prototype = {
     this.role = role;
     this.role.initialize(this, { startup: false });
   },
 
   addMajorCheckpoint(checkpointId) {
     this._majorCheckpoints.push(checkpointId);
   },
 
+  addMinorCheckpoint(checkpointId) {
+    this._minorCheckpoints.add(checkpointId);
+  },
+
   _unpause() {
     this.paused = false;
     this.debuggerRequests.length = 0;
   },
 
   sendResume({ forward }) {
     assert(this.paused);
     this._unpause();
@@ -144,16 +149,20 @@ ChildProcess.prototype = {
     }
     return last;
   },
 
   isMajorCheckpoint(id) {
     return this._majorCheckpoints.some(major => major == id);
   },
 
+  isMinorCheckpoint(id) {
+    return this._minorCheckpoints.has(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) {
@@ -170,19 +179,23 @@ ChildProcess.prototype = {
     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 whether this child has saved all minor checkpoints between the last
+  // major checkpoint preceding to id and id itself. This is required in order
+  // for the child to rewind through this span of checkpoints.
+  canRewindFrom(id) {
+    const lastMajorCheckpoint = this.lastMajorCheckpointPreceding(id);
+    for (let i = lastMajorCheckpoint + 1; i <= id; i++) {
+      if (this.isMinorCheckpoint(i) && !this.hasSavedCheckpoint(i)) {
         return false;
       }
     }
     return true;
   },
 
   lastSavedCheckpointPriorTo(id) {
     while (!this.hasSavedCheckpoint(id)) {
@@ -207,16 +220,17 @@ ChildProcess.prototype = {
     assert(this.paused);
     this.debuggerRequests.push(request);
     return RecordReplayControl.sendDebuggerRequest(this.id, request);
   },
 };
 
 const FlushMs = .5 * 1000;
 const MajorCheckpointMs = 2 * 1000;
+const MinorCheckpointMs = .25 * 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
@@ -243,90 +257,91 @@ const MajorCheckpointMs = 2 * 1000;
 // 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.
+// navigate around the recording to save a second set of checkpoints going back
+// at least MajorCheckpointSeconds, with the goal of making sure saved
+// checkpoints are no more than MinorCheckpointSeconds apart. No replaying
+// process needs to rewind past its last major checkpoint, and a given
+// minor checkpoint will only ever be saved by the replaying process with the
+// most recent major checkpoint.
 //
 // Active  Recording:    -----------------------
-// Standby Replaying #1: *---------*---------***
-// Standby Replaying #2: -----*---------*****
+// 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
+// major checkpoint (and which has been saving the most recent minor
 // checkpoints) becomes the active child.
 //
 // Inert   Recording:    -----------------------
-// Active  Replaying #1: *---------*---------**
-// Standby Replaying #2: -----*---------*****
+// 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
+// rewinds to its last major checkpoint and begins saving older minor
 // 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: -----*---------**
+// 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: -----*****
+// 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: -----*****-----*--
+// 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.
+// saving the most recent minor checkpoints.
 //
 // Inert   Recording:    -----------------------
-// Active  Replaying #1: *---------**------
-// Standby Replaying #2: -----*****-----***
+// 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
+// After the recent minor checkpoints have been saved the process which
+// took them can become active so the older minor checkpoints can be
 // saved.
 //
 // Inert   Recording:    -----------------------
-// Standby Replaying #1: *---------*****
-// Active  Replaying #2: -----*****-----***
+// 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: -----*****-----***-------*---------*--
+// 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) {
@@ -393,18 +408,18 @@ ChildRoleActive.prototype = {
 
   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.
+// child and save either major or minor 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) {
@@ -421,21 +436,21 @@ ChildRoleStandby.prototype = {
     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
+    // Minor 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
+      // Minor 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;
@@ -447,17 +462,17 @@ ChildRoleStandby.prototype = {
       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.
+    // without saving minor 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
@@ -465,45 +480,50 @@ ChildRoleStandby.prototype = {
     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.
+    // Find the first minor checkpoint in the fill range which we have not saved.
     let missingCheckpoint;
-    for (let i = lastMajorCheckpoint; i <= targetCheckpoint; i++) {
-      if (!this.child.hasSavedCheckpoint(i)) {
+    for (let i = lastMajorCheckpoint + 1; i <= targetCheckpoint; i++) {
+      if (this.child.isMinorCheckpoint(i) && !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 (this.child.lastCheckpoint() < missingCheckpoint) {
+      // We can run forward to reach the missing checkpoint.
+    } else {
+      // We need to rewind in order to save the missing checkpoint. Find the
+      // last saved checkpoint prior to the missing one. This must be
+      // lastMajorCheckpoint or later, as we always save major checkpoints.
+      let restoreTarget = missingCheckpoint - 1;
+      while (!this.child.hasSavedCheckpoint(restoreTarget)) {
+        restoreTarget--;
+      }
+      assert(restoreTarget >= lastMajorCheckpoint);
 
-    // 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);
+    // Make sure the process will save minor checkpoints as it runs forward.
+    if (missingCheckpoint == this.child.lastCheckpoint() + 1) {
+      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() {}
@@ -598,37 +618,43 @@ function maybeSwitchToReplayingChild() {
     const checkpoint = gActiveChild.rewindTargetCheckpoint();
     const child = otherReplayingChild(
       replayingChildResponsibleForSavingCheckpoint(checkpoint));
     switchActiveChild(child);
   }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
-// Major Checkpoints
+// Major and Minor 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.
+// major/minor checkpoint was noted.
 let gTimeSinceLastFlush;
-let gTimeSinceLastMajorCheckpoint;
+let gTimeSinceLastMajorCheckpoint = 0;
+let gTimeSinceLastMinorCheckpoint = 0;
 
 // 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 assignMinorCheckpoint(child, checkpointId) {
+  dumpv(`AssignMinorCheckpoint: #${child.id} Checkpoint ${checkpointId}`);
+  child.addMinorCheckpoint(checkpointId);
+}
+
 function updateCheckpointTimes(msg) {
   if (msg.point.checkpoint != gCheckpointTimes.length + 1 ||
       msg.point.position) {
     return;
   }
   gCheckpointTimes.push(msg.duration);
 
   if (gActiveChild.recording) {
@@ -640,23 +666,28 @@ function updateCheckpointTimes(msg) {
         gTimeSinceLastFlush >= FlushMs) {
       if (maybeFlushRecording()) {
         gTimeSinceLastFlush = 0;
       }
     }
   }
 
   gTimeSinceLastMajorCheckpoint += msg.duration;
+  gTimeSinceLastMinorCheckpoint += 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;
+  } else if (gTimeSinceLastMinorCheckpoint >= MinorCheckpointMs) {
+    // Assign a minor checkpoint to the process which saved the last major one.
+    assignMinorCheckpoint(gLastAssignedMajorCheckpoint, msg.point.checkpoint + 1);
+    gTimeSinceLastMinorCheckpoint = 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);
@@ -784,54 +815,44 @@ function HitExecutionPoint(id, msg) {
 ///////////////////////////////////////////////////////////////////////////////
 // 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.
+// warp to an execution point. Standby roles will try to save minor checkpoints
+// 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.
+    // Make sure we have a replaying child that has saved the right checkpoints
+    // for rewinding 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 minor checkpoints going back to its last major checkpoint.
     if (gActiveChild ==
         replayingChildResponsibleForSavingCheckpoint(targetCheckpoint)) {
-      const lastMajorCheckpoint =
-        gActiveChild.lastMajorCheckpointPreceding(targetCheckpoint);
-      if (!gActiveChild.hasSavedCheckpointsInRange(lastMajorCheckpoint,
-                                                   targetCheckpoint)) {
+      if (!gActiveChild.canRewindFrom(targetCheckpoint)) {
         switchActiveChild(otherReplayingChild(gActiveChild));
       }
     }
   }
 
   gLastExplicitPause = targetCheckpoint;
   dumpv(`MarkActiveChildExplicitPause ${gLastExplicitPause}`);
 
@@ -869,38 +890,43 @@ function waitUntilChildHasSavedCheckpoin
   }
 }
 
 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())) {
+  if (!forward) {
     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.
+    // Make sure the active child has saved minor checkpoints prior to its
+    // position.
     const targetChild =
       replayingChildResponsibleForSavingCheckpoint(targetCheckpoint);
-    assert(targetChild != gActiveChild);
-
-    waitUntilChildHasSavedCheckpoint(targetChild, targetCheckpoint);
-    switchActiveChild(targetChild);
+    if (targetChild == gActiveChild) {
+      // markExplicitPause() should ensure that we are only active if the child
+      // has saved the appropriate minor checkpoints.
+      assert(gActiveChild.canRewindFrom(targetCheckpoint));
+    } else {
+      let saveTarget = targetCheckpoint;
+      while (!targetChild.isMajorCheckpoint(saveTarget) &&
+             !targetChild.isMinorCheckpoint(saveTarget)) {
+        saveTarget--;
+      }
+      waitUntilChildHasSavedCheckpoint(targetChild, saveTarget);
+      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) {
@@ -926,46 +952,42 @@ function resume(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();
+  // Find the replaying child responsible for saving the target checkpoint.
+  const targetChild =
+    replayingChildResponsibleForSavingCheckpoint(targetCheckpoint);
+  if (targetChild != gActiveChild) {
+    switchActiveChild(otherReplayingChild(gActiveChild));
+  }
 
-  if (!gActiveChild.hasSavedCheckpoint(targetCheckpoint)) {
-    // Find the replaying child responsible for saving the target checkpoint.
-    const targetChild =
-      replayingChildResponsibleForSavingCheckpoint(targetCheckpoint);
+  // Rewind first if the child is past the warp target or if it is not paused
+  // at a checkpoint. RunToPoint can only be used when the child is at a
+  // checkpoint.
+  let restoreTarget;
+  if (gActiveChild.lastCheckpoint() >= targetCheckpoint) {
+    restoreTarget = targetCheckpoint;
+  } else if (gActiveChild.lastPausePoint.position) {
+    restoreTarget = gActiveChild.lastPausePoint.checkpoint;
+  }
 
-    if (targetChild == gActiveChild) {
-      // Switch to the other replaying child while this one saves the necessary
-      // checkpoint.
-      switchActiveChild(otherReplayingChild(gActiveChild));
+  if (restoreTarget) {
+    while (!gActiveChild.hasSavedCheckpoint(restoreTarget)) {
+      restoreTarget--;
     }
 
-    waitUntilChildHasSavedCheckpoint(targetChild, targetCheckpoint);
-    switchActiveChild(targetChild, /* aRecoverPosition = */ false);
-  }
-
-  gTimeWarpTarget = undefined;
-
-  if (gActiveChild.lastPausePoint.position ||
-      gActiveChild.lastCheckpoint() != targetCheckpoint) {
     assert(!gTimeWarpInProgress);
     gTimeWarpInProgress = true;
 
-    gActiveChild.sendRestoreCheckpoint(targetCheckpoint);
+    gActiveChild.sendRestoreCheckpoint(restoreTarget);
     gActiveChild.waitUntilPaused();
 
     gTimeWarpInProgress = false;
   }
 
   gActiveChild.sendRunToPoint(targetPoint);
   gActiveChild.waitUntilPaused();