Bug 1507359 Part 2 - Bindings and internal changes to allow ReplayDebugger to control child pausing/resuming, r=mccr8.
authorBrian Hackett <bhackett1024@gmail.com>
Wed, 14 Nov 2018 16:09:58 -1000
changeset 446931 1c7fc8389e01
parent 446930 70a8eb10c67f
child 446932 3a3c453432c1
push id35059
push usercbrindusan@mozilla.com
push dateSun, 18 Nov 2018 11:17:46 +0000
treeherdermozilla-central@b3ceae83e290 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmccr8
bugs1507359
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1507359 Part 2 - Bindings and internal changes to allow ReplayDebugger to control child pausing/resuming, r=mccr8.
toolkit/recordreplay/ipc/JSControl.cpp
toolkit/recordreplay/ipc/JSControl.h
toolkit/recordreplay/ipc/ParentIPC.cpp
toolkit/recordreplay/ipc/ParentInternal.h
--- a/toolkit/recordreplay/ipc/JSControl.cpp
+++ b/toolkit/recordreplay/ipc/JSControl.cpp
@@ -172,76 +172,91 @@ ExecutionPoint::Decode(JSContext* aCx, H
       && GetNumberProperty(aCx, aObject, gCheckpointProperty, &mCheckpoint)
       && GetNumberProperty(aCx, aObject, gProgressProperty, &mProgress);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Middleman Methods
 ///////////////////////////////////////////////////////////////////////////////
 
-// Keep track of all replay debuggers in existence, so that they can all be
-// invalidated when the process is unpaused.
-static StaticInfallibleVector<PersistentRootedObject*> gReplayDebuggers;
+// There can be at most one replay debugger in existence.
+static PersistentRootedObject* gReplayDebugger;
 
 static bool
 Middleman_RegisterReplayDebugger(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  if (gReplayDebugger) {
+    args.rval().setObject(**gReplayDebugger);
+    return true;
+  }
+
   RootedObject obj(aCx, NonNullObject(aCx, args.get(0)));
   if (!obj) {
     return false;
   }
 
   obj = ::js::CheckedUnwrap(obj);
   if (!obj) {
     ::js::ReportAccessDenied(aCx);
     return false;
   }
 
-  PersistentRootedObject* root = new PersistentRootedObject(aCx);
-  *root = obj;
-  gReplayDebuggers.append(root);
+  gReplayDebugger = new PersistentRootedObject(aCx);
+  *gReplayDebugger = obj;
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool
-InvalidateReplayDebuggersAfterUnpause(JSContext* aCx)
+CallReplayDebuggerHook(const char* aMethod)
 {
-  RootedValue rval(aCx);
-  for (auto root : gReplayDebuggers) {
-    JSAutoRealm ar(aCx, *root);
-    if (!JS_CallFunctionName(aCx, *root, "invalidateAfterUnpause",
-                             HandleValueArray::empty(), &rval))
-    {
-      return false;
-    }
+  if (!gReplayDebugger) {
+    return false;
+  }
+
+  AutoSafeJSContext cx;
+  JSAutoRealm ar(cx, *gReplayDebugger);
+  RootedValue rval(cx);
+  if (!JS_CallFunctionName(cx, *gReplayDebugger, aMethod,
+                           HandleValueArray::empty(), &rval))
+  {
+    Print("Warning: ReplayDebugger hook %s threw an exception\n", aMethod);
   }
   return true;
 }
 
+bool
+DebuggerOnPause()
+{
+  return CallReplayDebuggerHook("_onPause");
+}
+
+void
+DebuggerOnSwitchChild()
+{
+  CallReplayDebuggerHook("_onSwitchChild");
+}
+
 static bool
 Middleman_CanRewind(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   args.rval().setBoolean(parent::CanRewind());
   return true;
 }
 
 static bool
 Middleman_Resume(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   bool forward = ToBoolean(args.get(0));
 
-  if (!InvalidateReplayDebuggersAfterUnpause(aCx)) {
-    return false;
-  }
-
   parent::Resume(forward);
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool
 Middleman_TimeWarp(JSContext* aCx, unsigned aArgc, Value* aVp)
@@ -252,38 +267,23 @@ Middleman_TimeWarp(JSContext* aCx, unsig
     return false;
   }
 
   ExecutionPoint target;
   if (!target.Decode(aCx, targetObject)) {
     return false;
   }
 
-  if (!InvalidateReplayDebuggersAfterUnpause(aCx)) {
-    return false;
-  }
-
   parent::TimeWarp(target);
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool
-Middleman_Pause(JSContext* aCx, unsigned aArgc, Value* aVp)
-{
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::Pause();
-
-  args.rval().setUndefined();
-  return true;
-}
-
-static bool
 Middleman_SendRequest(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   RootedObject requestObject(aCx, NonNullObject(aCx, args.get(0)));
   if (!requestObject) {
     return false;
   }
 
@@ -375,16 +375,67 @@ Middleman_HadRepaintFailure(JSContext* a
 static bool
 Middleman_ChildIsRecording(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   args.rval().setBoolean(parent::ActiveChildIsRecording());
   return true;
 }
 
+static bool
+Middleman_MarkExplicitPause(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::MarkActiveChildExplicitPause();
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+Middleman_WaitUntilPaused(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::WaitUntilActiveChildIsPaused();
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+Middleman_PositionSubsumes(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  RootedObject firstPositionObject(aCx, NonNullObject(aCx, args.get(0)));
+  if (!firstPositionObject) {
+    return false;
+  }
+
+  BreakpointPosition firstPosition;
+  if (!firstPosition.Decode(aCx, firstPositionObject)) {
+    return false;
+  }
+
+  RootedObject secondPositionObject(aCx, NonNullObject(aCx, args.get(1)));
+  if (!secondPositionObject) {
+    return false;
+  }
+
+  BreakpointPosition secondPosition;
+  if (!secondPosition.Decode(aCx, secondPositionObject)) {
+    return false;
+  }
+
+  args.rval().setBoolean(firstPosition.Subsumes(secondPosition));
+  return true;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Devtools Sandbox
 ///////////////////////////////////////////////////////////////////////////////
 
 static PersistentRootedObject* gDevtoolsSandbox;
 
 // URL of the root script that runs when recording/replaying.
 #define ReplayScriptURL "resource://devtools/server/actors/replay/replay.js"
@@ -925,24 +976,26 @@ RecordReplay_Dump(JSContext* aCx, unsign
 // Plumbing
 ///////////////////////////////////////////////////////////////////////////////
 
 static const JSFunctionSpec gMiddlemanMethods[] = {
   JS_FN("registerReplayDebugger", Middleman_RegisterReplayDebugger, 1, 0),
   JS_FN("canRewind", Middleman_CanRewind, 0, 0),
   JS_FN("resume", Middleman_Resume, 1, 0),
   JS_FN("timeWarp", Middleman_TimeWarp, 1, 0),
-  JS_FN("pause", Middleman_Pause, 0, 0),
   JS_FN("sendRequest", Middleman_SendRequest, 1, 0),
   JS_FN("addBreakpoint", Middleman_AddBreakpoint, 1, 0),
   JS_FN("clearBreakpoints", Middleman_ClearBreakpoints, 0, 0),
   JS_FN("maybeSwitchToReplayingChild", Middleman_MaybeSwitchToReplayingChild, 0, 0),
   JS_FN("hadRepaint", Middleman_HadRepaint, 2, 0),
   JS_FN("hadRepaintFailure", Middleman_HadRepaintFailure, 0, 0),
   JS_FN("childIsRecording", Middleman_ChildIsRecording, 0, 0),
+  JS_FN("markExplicitPause", Middleman_MarkExplicitPause, 0, 0),
+  JS_FN("waitUntilPaused", Middleman_WaitUntilPaused, 0, 0),
+  JS_FN("positionSubsumes", Middleman_PositionSubsumes, 2, 0),
   JS_FS_END
 };
 
 static const JSFunctionSpec gRecordReplayMethods[] = {
   JS_FN("areThreadEventsDisallowed", RecordReplay_AreThreadEventsDisallowed, 0, 0),
   JS_FN("maybeDivergeFromRecording", RecordReplay_MaybeDivergeFromRecording, 0, 0),
   JS_FN("advanceProgressCounter", RecordReplay_AdvanceProgressCounter, 0, 0),
   JS_FN("shouldUpdateProgressCounter", RecordReplay_ShouldUpdateProgressCounter, 1, 0),
--- a/toolkit/recordreplay/ipc/JSControl.h
+++ b/toolkit/recordreplay/ipc/JSControl.h
@@ -56,26 +56,17 @@ struct BreakpointPosition
 
     // Break when a new top-level script is created.
     NewScript,
 
     // Break when a message is logged to the web console.
     ConsoleMessage,
 
     // Break when NewTimeWarpTarget() is called.
-    WarpTarget,
-
-    // Break when the debugger should pause even if no breakpoint has been
-    // set: the beginning or end of the replay has been reached, or a time
-    // warp has reached its destination.
-    ForcedPause,
-
-    // Break when the child process reaches a checkpoint or we switch between
-    // recording and replaying child processes.
-    PositionChange
+    WarpTarget
   ));
 
   Kind mKind;
 
   // Optional information associated with the breakpoint.
   uint32_t mScript;
   uint32_t mOffset;
   uint32_t mFrameIndex;
@@ -118,18 +109,16 @@ struct BreakpointPosition
     case Invalid: return "Invalid";
     case Break: return "Break";
     case OnStep: return "OnStep";
     case OnPop: return "OnPop";
     case EnterFrame: return "EnterFrame";
     case NewScript: return "NewScript";
     case ConsoleMessage: return "ConsoleMessage";
     case WarpTarget: return "WarpTarget";
-    case ForcedPause: return "ForcedPause";
-    case PositionChange: return "PositionChange";
     }
     MOZ_CRASH("Bad BreakpointPosition kind");
   }
 
   const char* KindString() const {
     return StaticKindString(mKind);
   }
 
@@ -188,16 +177,24 @@ struct ExecutionPoint
 
   JSObject* Encode(JSContext* aCx) const;
   bool Decode(JSContext* aCx, JS::HandleObject aObject);
 };
 
 // Buffer type used for encoding object data.
 typedef InfallibleVector<char16_t> CharBuffer;
 
+// Called in the middleman when the child has hit a checkpoint or breakpoint.
+// The return value is whether there is a ReplayDebugger available which the
+// notification was sent to.
+bool DebuggerOnPause();
+
+// Called in the middleman when the child has changed.
+void DebuggerOnSwitchChild();
+
 // Set up the JS sandbox in the current recording/replaying process and load
 // its target script.
 void SetupDevtoolsSandbox();
 
 // The following hooks are used in the recording/replaying process to
 // call methods defined by the JS sandbox.
 
 // Handle an incoming request from the middleman.
--- a/toolkit/recordreplay/ipc/ParentIPC.cpp
+++ b/toolkit/recordreplay/ipc/ParentIPC.cpp
@@ -579,19 +579,16 @@ SpawnReplayingChildren()
   }
   gFirstReplayingChild =
     new ChildProcessInfo(std::move(firstRole), Nothing());
   gSecondReplayingChild =
     new ChildProcessInfo(MakeUnique<ChildRoleStandby>(), Nothing());
   AssignMajorCheckpoint(gSecondReplayingChild, CheckpointId::First);
 }
 
-// Hit any installed breakpoints with the specified kind.
-static void HitBreakpointsWithKind(js::BreakpointPosition::Kind aKind);
-
 // Change the current active child, and select a new role for the old one.
 static void
 SwitchActiveChild(ChildProcessInfo* aChild, bool aRecoverPosition = true)
 {
   MOZ_RELEASE_ASSERT(aChild != gActiveChild);
   ChildProcessInfo* oldActiveChild = gActiveChild;
   aChild->WaitUntilPaused();
   if (!aChild->IsRecording()) {
@@ -608,20 +605,19 @@ SwitchActiveChild(ChildProcessInfo* aChi
   aChild->SetRole(MakeUnique<ChildRoleActive>());
   if (oldActiveChild->IsRecording()) {
     oldActiveChild->SetRole(MakeUnique<ChildRoleInert>());
   } else {
     oldActiveChild->RecoverToCheckpoint(oldActiveChild->MostRecentSavedCheckpoint());
     oldActiveChild->SetRole(MakeUnique<ChildRoleStandby>());
   }
 
-  // Position state is affected when we switch between recording and
-  // replaying children.
+  // Notify the debugger when switching between recording and replaying children.
   if (aChild->IsRecording() != oldActiveChild->IsRecording()) {
-    HitBreakpointsWithKind(js::BreakpointPosition::Kind::PositionChange);
+    js::DebuggerOnSwitchChild();
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Preferences
 ///////////////////////////////////////////////////////////////////////////////
 
 static bool gPreferencesLoaded;
@@ -830,17 +826,17 @@ HasSavedCheckpointsInRange(ChildProcessI
   for (size_t i = aStart; i <= aEnd; i++) {
     if (!aChild->HasSavedCheckpoint(i)) {
       return false;
     }
   }
   return true;
 }
 
-static void
+void
 MarkActiveChildExplicitPause()
 {
   MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
   size_t targetCheckpoint = gActiveChild->RewindTargetCheckpoint();
 
   if (gActiveChild->IsRecording()) {
     // Make sure any replaying children can play forward to the same point as
     // the recording.
@@ -872,16 +868,30 @@ ActiveChildTargetCheckpoint()
   }
   if (gActiveChild->RewindTargetCheckpoint() <= gLastExplicitPause) {
     return Some(gActiveChild->RewindTargetCheckpoint());
   }
   return Nothing();
 }
 
 void
+WaitUntilActiveChildIsPaused()
+{
+  if (gActiveChild->IsPaused()) {
+    // The debugger expects an OnPause notification after calling this, even if
+    // it is already paused. This should only happen when attaching the
+    // debugger to a paused child process.
+    js::DebuggerOnPause();
+  } else {
+    MaybeCreateCheckpointInRecordingChild();
+    gActiveChild->WaitUntilPaused();
+  }
+}
+
+void
 MaybeSwitchToReplayingChild()
 {
   if (gActiveChild->IsRecording() && CanRewind()) {
     FlushRecording();
     size_t checkpoint = gActiveChild->RewindTargetCheckpoint();
     ChildProcessInfo* child =
       OtherReplayingChild(ReplayingChildResponsibleForSavingCheckpoint(checkpoint));
     SwitchActiveChild(child);
@@ -958,18 +968,17 @@ RecvDebuggerResponse(const DebuggerRespo
 {
   MOZ_RELEASE_ASSERT(gResponseBuffer && gResponseBuffer->empty());
   gResponseBuffer->append(aMsg.Buffer(), aMsg.BufferSize());
 }
 
 void
 SendRequest(const js::CharBuffer& aBuffer, js::CharBuffer* aResponse)
 {
-  MaybeCreateCheckpointInRecordingChild();
-  gActiveChild->WaitUntilPaused();
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
 
   MOZ_RELEASE_ASSERT(!gResponseBuffer);
   gResponseBuffer = aResponse;
 
   DebuggerRequestMessage* msg = DebuggerRequestMessage::New(aBuffer.begin(), aBuffer.length());
   gActiveChild->SendMessage(*msg);
   free(msg);
 
@@ -1003,25 +1012,16 @@ ClearBreakpoints()
   gActiveChild->SendMessage(ClearBreakpointsMessage());
 
   // Clear breakpoints in the recording child, as for AddBreakpoint().
   if (!gActiveChild->IsRecording() && gRecordingChild) {
     gRecordingChild->SendMessage(ClearBreakpointsMessage());
   }
 }
 
-// Flags for the preferred direction of travel when execution unpauses,
-// according to the last direction we were explicitly given.
-static bool gChildExecuteForward = true;
-static bool gChildExecuteBackward = false;
-
-// Whether there is a ResumeForwardOrBackward task which should execute on the
-// main thread. This will continue execution in the preferred direction.
-static bool gResumeForwardOrBackward = false;
-
 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.
@@ -1047,34 +1047,29 @@ MaybeSendRepaintMessage()
       }
     }
   }
 }
 
 void
 Resume(bool aForward)
 {
-  gActiveChild->WaitUntilPaused();
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
 
   MaybeSendRepaintMessage();
 
-  // Set the preferred direction of travel.
-  gResumeForwardOrBackward = false;
-  gChildExecuteForward = aForward;
-  gChildExecuteBackward = !aForward;
-
   // When rewinding, make sure the active child can rewind to the previous
   // checkpoint.
   if (!aForward && !gActiveChild->HasSavedCheckpoint(gActiveChild->RewindTargetCheckpoint())) {
     size_t targetCheckpoint = gActiveChild->RewindTargetCheckpoint();
 
     // Don't rewind if we are at the beginning of the recording.
     if (targetCheckpoint == CheckpointId::Invalid) {
       SendMessageToUIProcess("HitRecordingBeginning");
-      HitBreakpointsWithKind(js::BreakpointPosition::Kind::ForcedPause);
+      js::DebuggerOnPause();
       return;
     }
 
     // Find the replaying child responsible for saving the target checkpoint.
     // We should have explicitly paused before rewinding and given fill roles
     // to the replaying children.
     ChildProcessInfo* targetChild = ReplayingChildResponsibleForSavingCheckpoint(targetCheckpoint);
     MOZ_RELEASE_ASSERT(targetChild != gActiveChild);
@@ -1091,17 +1086,17 @@ Resume(bool aForward)
 
   if (aForward) {
     // Don't send a replaying process past the recording endpoint.
     if (gActiveChild->IsPausedAtRecordingEndpoint()) {
       // Look for a recording child we can transition into.
       MOZ_RELEASE_ASSERT(!gActiveChild->IsRecording());
       if (!gRecordingChild) {
         SendMessageToUIProcess("HitRecordingEndpoint");
-        HitBreakpointsWithKind(js::BreakpointPosition::Kind::ForcedPause);
+        js::DebuggerOnPause();
         return;
       }
 
       // Switch to the recording child as the active child and continue execution.
       SwitchActiveChild(gRecordingChild);
     }
 
     EnsureMajorCheckpointSaved(gActiveChild, gActiveChild->LastCheckpoint() + 1);
@@ -1111,22 +1106,17 @@ Resume(bool aForward)
   }
 
   gActiveChild->SendMessage(ResumeMessage(aForward));
 }
 
 void
 TimeWarp(const js::ExecutionPoint& aTarget)
 {
-  gActiveChild->WaitUntilPaused();
-
-  // There is no preferred direction of travel after warping.
-  gResumeForwardOrBackward = false;
-  gChildExecuteForward = false;
-  gChildExecuteBackward = false;
+  MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
 
   // Make sure the active child can rewind to the checkpoint prior to the
   // warp target.
   MOZ_RELEASE_ASSERT(gTimeWarpTarget.isNothing());
   gTimeWarpTarget.emplace(aTarget.mCheckpoint);
 
   PokeChildren();
 
@@ -1156,149 +1146,60 @@ TimeWarp(const js::ExecutionPoint& aTarg
     gActiveChild->SendMessage(RestoreCheckpointMessage(aTarget.mCheckpoint));
     gActiveChild->WaitUntilPaused();
   }
 
   gActiveChild->SendMessage(RunToPointMessage(aTarget));
 
   gActiveChild->WaitUntilPaused();
   SendMessageToUIProcess("TimeWarpFinished");
-  HitBreakpointsWithKind(js::BreakpointPosition::Kind::ForcedPause);
-}
-
-void
-Pause()
-{
-  MaybeCreateCheckpointInRecordingChild();
-  gActiveChild->WaitUntilPaused();
-
-  // If the debugger has explicitly paused then there is no preferred direction
-  // of travel.
-  gChildExecuteForward = false;
-  gChildExecuteBackward = false;
-
-  MarkActiveChildExplicitPause();
-}
-
-static void
-ResumeForwardOrBackward()
-{
-  MOZ_RELEASE_ASSERT(!gChildExecuteForward || !gChildExecuteBackward);
-
-  if (gResumeForwardOrBackward && (gChildExecuteForward || gChildExecuteBackward)) {
-    Resume(gChildExecuteForward);
-  }
 }
 
 void
 ResumeBeforeWaitingForIPDLReply()
 {
   MOZ_RELEASE_ASSERT(gActiveChild->IsRecording());
 
   // The main thread is about to block while it waits for a sync reply from the
   // recording child process. If the child is paused, resume it immediately so
   // that we don't deadlock.
   if (gActiveChild->IsPaused()) {
-    MOZ_RELEASE_ASSERT(gChildExecuteForward);
     Resume(true);
   }
 }
 
 static void
 RecvHitCheckpoint(const HitCheckpointMessage& aMsg)
 {
   UpdateCheckpointTimes(aMsg);
   MaybeUpdateGraphicsAtCheckpoint(aMsg.mCheckpointId);
 
-  // Position state is affected when new checkpoints are reached.
-  HitBreakpointsWithKind(js::BreakpointPosition::Kind::PositionChange);
-
-  // Resume either forwards or backwards. Break the resume off into a separate
-  // runnable, to avoid starving any code already on the stack and waiting for
-  // the process to pause. Immediately resume if the main thread is blocked.
+  // Immediately resume if the main thread is blocked. If there is no
+  // debugger attached a resume is needed as well, but post a runnable so that
+  // callers waiting for the child to pause (e.g. SaveRecording) don't starve.
   if (MainThreadIsWaitingForIPDLReply()) {
-    MOZ_RELEASE_ASSERT(gChildExecuteForward);
     Resume(true);
-  } else if (!gResumeForwardOrBackward) {
-    gResumeForwardOrBackward = true;
-    gMainThreadMessageLoop->PostTask(NewRunnableFunction("ResumeForwardOrBackward",
-                                                         ResumeForwardOrBackward));
-  }
-}
-
-static void
-HitBreakpoint(uint32_t* aBreakpoints, size_t aNumBreakpoints,
-              js::BreakpointPosition::Kind aSharedKind)
-{
-  if (!gActiveChild->IsPaused()) {
-    delete[] aBreakpoints;
-    return;
+  } else if (!js::DebuggerOnPause()) {
+    gMainThreadMessageLoop->PostTask(NewRunnableFunction("RecvHitCheckpointResume", Resume, true));
   }
-
-  switch (aSharedKind) {
-  case js::BreakpointPosition::ForcedPause:
-    MarkActiveChildExplicitPause();
-    MOZ_FALLTHROUGH;
-  case js::BreakpointPosition::PositionChange:
-    // Call all breakpoint handlers.
-    for (size_t i = 0; i < aNumBreakpoints; i++) {
-      AutoSafeJSContext cx;
-      if (!js::HitBreakpoint(cx, aBreakpoints[i])) {
-        Print("Warning: hitBreakpoint hook threw an exception.\n");
-      }
-    }
-    break;
-  default:
-    gResumeForwardOrBackward = true;
-
-    MarkActiveChildExplicitPause();
-
-    // Call breakpoint handlers until one of them explicitly resumes forward or
-    // backward travel.
-    for (size_t i = 0; i < aNumBreakpoints && gResumeForwardOrBackward; i++) {
-      AutoSafeJSContext cx;
-      if (!js::HitBreakpoint(cx, aBreakpoints[i])) {
-        Print("Warning: hitBreakpoint hook threw an exception.\n");
-      }
-    }
-
-    // If the child was not explicitly resumed by any breakpoint handler,
-    // resume travel in whichever direction we were going previously.
-    if (gResumeForwardOrBackward) {
-      ResumeForwardOrBackward();
-    }
-    break;
-  }
-
-  delete[] aBreakpoints;
 }
 
 static void
 RecvHitBreakpoint(const HitBreakpointMessage& aMsg)
 {
-  uint32_t* breakpoints = new uint32_t[aMsg.NumBreakpoints()];
-  PodCopy(breakpoints, aMsg.Breakpoints(), aMsg.NumBreakpoints());
-  gMainThreadMessageLoop->PostTask(NewRunnableFunction("HitBreakpoint", HitBreakpoint,
-                                                       breakpoints, aMsg.NumBreakpoints(),
-                                                       js::BreakpointPosition::Invalid));
-}
-
-static void
-HitBreakpointsWithKind(js::BreakpointPosition::Kind aKind)
-{
-  Vector<uint32_t> breakpoints;
-  gActiveChild->GetMatchingInstalledBreakpoints([=](js::BreakpointPosition::Kind aInstalled) {
-      return aInstalled == aKind;
-    }, breakpoints);
-  if (!breakpoints.empty()) {
-    uint32_t* newBreakpoints = new uint32_t[breakpoints.length()];
-    PodCopy(newBreakpoints, breakpoints.begin(), breakpoints.length());
-    gMainThreadMessageLoop->PostTask(NewRunnableFunction("HitBreakpoint", HitBreakpoint,
-                                                         newBreakpoints, breakpoints.length(),
-                                                         aKind));
+  // HitBreakpoint messages will be sent both when hitting user breakpoints and
+  // when hitting the endpoint of the recording, if it is at a breakpoint
+  // position. Don't send an OnPause notification in the latter case: if the
+  // user installed a breakpoint here we will have already gotten a
+  // HitBreakpoint message *without* mRecordingEndpoint set, and we don't want
+  // to pause twice at the same point.
+  if (aMsg.mRecordingEndpoint) {
+    Resume(true);
+  } else if (!js::DebuggerOnPause()) {
+    gMainThreadMessageLoop->PostTask(NewRunnableFunction("RecvHitBreakpointResume", Resume, true));
   }
 }
 
 static void
 RecvMiddlemanCallRequest(const MiddlemanCallRequestMessage& aMsg)
 {
   MiddlemanCallResponseMessage* response = ProcessMiddlemanCallMessage(aMsg);
   gActiveChild->SendMessage(*response);
--- a/toolkit/recordreplay/ipc/ParentInternal.h
+++ b/toolkit/recordreplay/ipc/ParentInternal.h
@@ -53,35 +53,39 @@ void InitializeForwarding();
 void Shutdown();
 
 // Monitor used for synchronizing between the main and channel or message loop threads.
 static Monitor* gMonitor;
 
 // Allow the child process to resume execution.
 void Resume(bool aForward);
 
-// Pause the child process at the next opportunity.
-void Pause();
-
 // Direct the child process to warp to a specific point.
 void TimeWarp(const js::ExecutionPoint& target);
 
 // Send a JSON request to the child process, and synchronously wait for a
 // response.
 void SendRequest(const js::CharBuffer& aBuffer, js::CharBuffer* aResponse);
 
 // Set 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();