Bug 1516578 Part 5 - Remove logic for coordinating child processes from C++, r=mccr8.
☠☠ backed out by 9e3564442734 ☠ ☠
authorBrian Hackett <bhackett1024@gmail.com>
Thu, 27 Dec 2018 13:32:36 -1000
changeset 453656 1701613c165d52565cc8dcf48b5e8a8bfebf7a23
parent 453655 9ddc5bc1e961af52dbd772f66559f3fe4199e572
child 453657 91d5c6ff3ee769f779668b19f09b76cba00b8142
push id35365
push userdvarga@mozilla.com
push dateSun, 13 Jan 2019 10:05:55 +0000
treeherdermozilla-central@1218e374fbc7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmccr8
bugs1516578
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
Bug 1516578 Part 5 - Remove logic for coordinating child processes from C++, r=mccr8.
toolkit/recordreplay/ipc/ChildProcess.cpp
toolkit/recordreplay/ipc/ParentForwarding.cpp
toolkit/recordreplay/ipc/ParentIPC.cpp
toolkit/recordreplay/ipc/ParentInternal.h
--- 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,
@@ -457,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));
@@ -498,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);
@@ -557,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);
@@ -578,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/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