Bug 1516578 Part 6 - IDL and binding changes for coordinating child processes from JS, r=lsmyth.
☠☠ backed out by 9e3564442734 ☠ ☠
authorBrian Hackett <bhackett1024@gmail.com>
Thu, 27 Dec 2018 13:37:22 -1000
changeset 510760 91d5c6ff3ee769f779668b19f09b76cba00b8142
parent 510759 1701613c165d52565cc8dcf48b5e8a8bfebf7a23
child 510761 6689773a4e1caafd89480b2c6182c5a41fc081ce
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsmyth
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 6 - IDL and binding changes for coordinating child processes from JS, r=lsmyth.
devtools/server/actors/replay/moz.build
devtools/server/actors/replay/rrIControl.idl
devtools/server/actors/replay/rrIReplay.idl
toolkit/recordreplay/ipc/JSControl.cpp
toolkit/recordreplay/ipc/JSControl.h
--- a/devtools/server/actors/replay/moz.build
+++ b/devtools/server/actors/replay/moz.build
@@ -1,18 +1,20 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'control.js',
     'debugger.js',
     'graphics.js',
     'replay.js',
 )
 
 XPIDL_MODULE = 'devtools_rr'
 
 XPIDL_SOURCES = [
+    'rrIControl.idl',
     'rrIGraphics.idl',
     'rrIReplay.idl',
 ]
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/rrIControl.idl
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+// This interface defines the methods used for calling into control.js in a
+// middleman process.
+[scriptable, uuid(c296e7c3-8a27-4fd0-94c2-b6e5126909ba)]
+interface rrIControl : nsISupports {
+  void Initialize(in jsval recordingChildId);
+  void ConnectDebugger(in jsval replayDebugger);
+  void HitExecutionPoint(in long childId, in jsval msg);
+  void BeforeSaveRecording();
+  void AfterSaveRecording();
+};
--- a/devtools/server/actors/replay/rrIReplay.idl
+++ b/devtools/server/actors/replay/rrIReplay.idl
@@ -1,19 +1,18 @@
 /* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
+// This interface defines the methods used for calling into replay.js in a
+// recording/replaying process. See JSControl.h for the documentation of these
+// methods.
 [scriptable, uuid(8b86b71f-8471-472e-9997-c5f21f9d0598)]
 interface rrIReplay : nsISupports {
   jsval ProcessRequest(in jsval request);
-
   void EnsurePositionHandler(in jsval position);
-
   void ClearPositionHandlers();
-
   void ClearPausedState();
-
   jsval GetEntryPosition(in jsval position);
 };
--- a/toolkit/recordreplay/ipc/JSControl.cpp
+++ b/toolkit/recordreplay/ipc/JSControl.cpp
@@ -10,16 +10,17 @@
 #include "mozilla/StaticPtr.h"
 #include "js/CharacterEncoding.h"
 #include "js/Conversions.h"
 #include "js/JSON.h"
 #include "js/PropertySpec.h"
 #include "ChildInternal.h"
 #include "ParentInternal.h"
 #include "nsImportModule.h"
+#include "rrIControl.h"
 #include "rrIReplay.h"
 #include "xpcprivate.h"
 
 using namespace JS;
 
 namespace mozilla {
 namespace recordreplay {
 namespace js {
@@ -64,16 +65,31 @@ static bool GetNumberProperty(JSContext*
   if (!v.isNumber()) {
     JS_ReportErrorASCII(aCx, "Object missing required property");
     return false;
   }
   *aResult = v.toNumber();
   return true;
 }
 
+static parent::ChildProcessInfo* GetChildById(JSContext* aCx,
+                                              const Value& aValue,
+                                              bool aAllowUnpaused = false) {
+  if (!aValue.isNumber()) {
+    JS_ReportErrorASCII(aCx, "Expected child ID");
+    return nullptr;
+  }
+  parent::ChildProcessInfo* child = parent::GetChildProcess(aValue.toNumber());
+  if (!child || (!aAllowUnpaused && !child->IsPaused())) {
+    JS_ReportErrorASCII(aCx, "Unpaused or bad child ID");
+    return nullptr;
+  }
+  return child;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // BreakpointPosition Conversion
 ///////////////////////////////////////////////////////////////////////////////
 
 // Names of properties which JS code uses to specify the contents of a
 // BreakpointPosition.
 static const char gKindProperty[] = "kind";
 static const char gScriptProperty[] = "script";
@@ -196,157 +212,368 @@ ExecutionPoint::ToString(nsCString& aStr
   if (HasPosition()) {
     aStr.AppendPrintf(" Progress %llu Position ", mProgress);
     mPosition.ToString(aStr);
   }
   aStr.AppendPrintf(" }");
 }
 
 ///////////////////////////////////////////////////////////////////////////////
+// Message Conversion
+///////////////////////////////////////////////////////////////////////////////
+
+static JSObject* EncodeChannelMessage(JSContext* aCx,
+                                      const HitExecutionPointMessage& aMsg) {
+  RootedObject obj(aCx, JS_NewObject(aCx, nullptr));
+  if (!obj) {
+    return nullptr;
+  }
+
+  RootedObject pointObject(aCx, aMsg.mPoint.Encode(aCx));
+  if (!pointObject ||
+      !JS_DefineProperty(aCx, obj, "point", pointObject,
+                         JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(aCx, obj, "recordingEndpoint",
+                         aMsg.mRecordingEndpoint, JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(aCx, obj, "duration",
+                         aMsg.mDurationMicroseconds / 1000.0,
+                         JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+
+  return obj;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Middleman Control
+///////////////////////////////////////////////////////////////////////////////
+
+static StaticRefPtr<rrIControl> gControl;
+
+void SetupMiddlemanControl(const Maybe<size_t>& aRecordingChildId) {
+  MOZ_RELEASE_ASSERT(!gControl);
+
+  nsCOMPtr<rrIControl> control =
+    do_ImportModule("resource://devtools/server/actors/replay/control.js");
+  gControl = control.forget();
+  ClearOnShutdown(&gControl);
+
+  MOZ_RELEASE_ASSERT(gControl);
+
+  AutoSafeJSContext cx;
+  JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
+
+  RootedValue recordingChildValue(cx);
+  if (aRecordingChildId.isSome()) {
+    recordingChildValue.setInt32(aRecordingChildId.ref());
+  }
+  if (NS_FAILED(gControl->Initialize(recordingChildValue))) {
+    MOZ_CRASH("SetupMiddlemanControl");
+  }
+}
+
+void ForwardHitExecutionPointMessage(size_t aId, const HitExecutionPointMessage& aMsg) {
+  MOZ_RELEASE_ASSERT(gControl);
+
+  AutoSafeJSContext cx;
+  JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
+
+  JSObject* obj = EncodeChannelMessage(cx, aMsg);
+  MOZ_RELEASE_ASSERT(obj);
+
+  RootedValue value(cx, ObjectValue(*obj));
+  if (NS_FAILED(gControl->HitExecutionPoint(aId, value))) {
+    MOZ_CRASH("ForwardMessageToMiddlemanControl");
+  }
+}
+
+void BeforeSaveRecording() {
+  MOZ_RELEASE_ASSERT(gControl);
+
+  AutoSafeJSContext cx;
+  JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
+
+  if (NS_FAILED(gControl->BeforeSaveRecording())) {
+    MOZ_CRASH("BeforeSaveRecording");
+  }
+}
+
+void AfterSaveRecording() {
+  MOZ_RELEASE_ASSERT(gControl);
+
+  AutoSafeJSContext cx;
+  JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
+
+  if (NS_FAILED(gControl->AfterSaveRecording())) {
+    MOZ_CRASH("AfterSaveRecording");
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////
 // Middleman Methods
 ///////////////////////////////////////////////////////////////////////////////
 
 // There can be at most one replay debugger in existence.
 static PersistentRootedObject* gReplayDebugger;
 
 static bool Middleman_RegisterReplayDebugger(JSContext* aCx, unsigned aArgc,
                                              Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
   if (gReplayDebugger) {
     args.rval().setObject(**gReplayDebugger);
-    return true;
+    return JS_WrapValue(aCx, args.rval());
   }
 
   RootedObject obj(aCx, NonNullObject(aCx, args.get(0)));
   if (!obj) {
     return false;
   }
 
+  {
+    JSAutoRealm ar(aCx, xpc::PrivilegedJunkScope());
+
+    RootedValue debuggerValue(aCx, ObjectValue(*obj));
+    if (!JS_WrapValue(aCx, &debuggerValue)) {
+      return false;
+    }
+
+    if (NS_FAILED(gControl->ConnectDebugger(debuggerValue))) {
+      JS_ReportErrorASCII(aCx, "ConnectDebugger failed\n");
+      return false;
+    }
+  }
+
   obj = ::js::CheckedUnwrap(obj);
   if (!obj) {
     ::js::ReportAccessDenied(aCx);
     return false;
   }
 
   gReplayDebugger = new PersistentRootedObject(aCx);
   *gReplayDebugger = obj;
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool CallReplayDebuggerHook(const char* aMethod) {
-  if (!gReplayDebugger) {
-    return false;
-  }
-
-  AutoSafeJSContext cx;
-  JSAutoRealm ar(cx, *gReplayDebugger);
-  RootedValue rval(cx);
-  if (!JS_CallFunctionName(cx, *gReplayDebugger, aMethod,
-                           HandleValueArray::empty(), &rval)) {
-    Print("Warning: ReplayDebugger hook %s threw an exception\n", aMethod);
-  }
-  return true;
-}
-
-bool DebuggerOnPause() { return CallReplayDebuggerHook("_onPause"); }
-
-void DebuggerOnSwitchChild() { CallReplayDebuggerHook("_onSwitchChild"); }
-
 static bool Middleman_CanRewind(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   args.rval().setBoolean(parent::CanRewind());
   return true;
 }
 
-static bool Middleman_Resume(JSContext* aCx, unsigned aArgc, Value* aVp) {
+static bool Middleman_SpawnReplayingChild(JSContext* aCx,
+                                          unsigned aArgc, Value* aVp) {
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  size_t id = parent::SpawnReplayingChild();
+  args.rval().setInt32(id);
+  return true;
+}
+
+static bool Middleman_SetActiveChild(JSContext* aCx,
+                                     unsigned aArgc, Value* aVp) {
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
+
+  parent::SetActiveChild(child);
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool Middleman_SendSetSaveCheckpoint(JSContext* aCx,
+                                            unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-  bool forward = ToBoolean(args.get(0));
+
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
+
+  double checkpoint;
+  if (!ToNumber(aCx, args.get(1), &checkpoint)) {
+    return false;
+  }
+
+  bool shouldSave = ToBoolean(args.get(2));
+
+  child->SendMessage(SetSaveCheckpointMessage(checkpoint, shouldSave));
 
-  parent::Resume(forward);
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool Middleman_SendFlushRecording(JSContext* aCx,
+                                         unsigned aArgc, Value* aVp) {
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
+
+  child->SendMessage(FlushRecordingMessage());
+
+  // The child will unpause until the flush finishes.
+  child->WaitUntilPaused();
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_TimeWarp(JSContext* aCx, unsigned aArgc, Value* aVp) {
+static bool Middleman_SendResume(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-  RootedObject targetObject(aCx, NonNullObject(aCx, args.get(0)));
-  if (!targetObject) {
+
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
     return false;
   }
 
-  ExecutionPoint target;
-  if (!target.Decode(aCx, targetObject)) {
+  bool forward = ToBoolean(args.get(1));
+
+  child->SendMessage(ResumeMessage(forward));
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool Middleman_SendRestoreCheckpoint(JSContext* aCx, unsigned aArgc, Value* aVp) {
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
     return false;
   }
 
-  parent::TimeWarp(target);
+  double checkpoint;
+  if (!ToNumber(aCx, args.get(1), &checkpoint)) {
+    return false;
+  }
+
+  child->SendMessage(RestoreCheckpointMessage(checkpoint));
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_SendRequest(JSContext* aCx, unsigned aArgc, Value* aVp) {
+static bool Middleman_SendRunToPoint(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-  RootedObject requestObject(aCx, NonNullObject(aCx, args.get(0)));
+
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
+
+  RootedObject pointObject(aCx, NonNullObject(aCx, args.get(1)));
+  if (!pointObject) {
+    return false;
+  }
+
+  ExecutionPoint point;
+  if (!point.Decode(aCx, pointObject)) {
+    return false;
+  }
+
+  child->SendMessage(RunToPointMessage(point));
+
+  args.rval().setUndefined();
+  return true;
+}
+
+// Buffer for receiving the next debugger response.
+static js::CharBuffer* gResponseBuffer;
+
+void OnDebuggerResponse(const Message& aMsg) {
+  const DebuggerResponseMessage& nmsg =
+    static_cast<const DebuggerResponseMessage&>(aMsg);
+  MOZ_RELEASE_ASSERT(gResponseBuffer && gResponseBuffer->empty());
+  gResponseBuffer->append(nmsg.Buffer(), nmsg.BufferSize());
+}
+
+static bool Middleman_SendDebuggerRequest(JSContext* aCx,
+                                          unsigned aArgc, Value* aVp) {
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
+
+  RootedObject requestObject(aCx, NonNullObject(aCx, args.get(1)));
   if (!requestObject) {
     return false;
   }
 
   CharBuffer requestBuffer;
   if (!ToJSONMaybeSafely(aCx, requestObject, FillCharBufferCallback,
                          &requestBuffer)) {
     return false;
   }
 
   CharBuffer responseBuffer;
-  parent::SendRequest(requestBuffer, &responseBuffer);
+
+  MOZ_RELEASE_ASSERT(!gResponseBuffer);
+  gResponseBuffer = &responseBuffer;
+
+  DebuggerRequestMessage* msg =
+    DebuggerRequestMessage::New(requestBuffer.begin(), requestBuffer.length());
+  child->SendMessage(*msg);
+  free(msg);
+
+  // Wait for the child to respond to the query.
+  child->WaitUntilPaused();
+  MOZ_RELEASE_ASSERT(gResponseBuffer == &responseBuffer);
+  MOZ_RELEASE_ASSERT(gResponseBuffer->length() != 0);
+  gResponseBuffer = nullptr;
 
   return JS_ParseJSON(aCx, responseBuffer.begin(), responseBuffer.length(),
                       args.rval());
 }
 
-static bool Middleman_AddBreakpoint(JSContext* aCx, unsigned aArgc,
-                                    Value* aVp) {
+static bool Middleman_SendAddBreakpoint(JSContext* aCx,
+                                        unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(0)));
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
+
+  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(1)));
   if (!positionObject) {
     return false;
   }
 
   BreakpointPosition position;
   if (!position.Decode(aCx, positionObject)) {
     return false;
   }
 
-  parent::AddBreakpoint(position);
+  child->SendMessage(AddBreakpointMessage(position));
 
   args.rval().setUndefined();
   return true;
 }
 
-/* static */ bool Middleman_ClearBreakpoints(JSContext* aCx, unsigned aArgc,
-                                             Value* aVp) {
+static bool Middleman_SendClearBreakpoints(JSContext* aCx,
+                                           unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  parent::ClearBreakpoints();
-
-  args.rval().setUndefined();
-  return true;
-}
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0));
+  if (!child) {
+    return false;
+  }
 
-static bool Middleman_MaybeSwitchToReplayingChild(JSContext* aCx,
-                                                  unsigned aArgc, Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::MaybeSwitchToReplayingChild();
+  child->SendMessage(ClearBreakpointsMessage());
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool Middleman_HadRepaint(JSContext* aCx, unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
@@ -370,40 +597,63 @@ static bool Middleman_HadRepaintFailure(
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
   parent::UpdateGraphicsInUIProcess(nullptr);
 
   args.rval().setUndefined();
   return true;
 }
 
-static bool Middleman_ChildIsRecording(JSContext* aCx, unsigned aArgc,
-                                       Value* aVp) {
+static bool Middleman_InRepaintStressMode(JSContext* aCx,
+                                          unsigned aArgc, Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
-  args.rval().setBoolean(parent::ActiveChildIsRecording());
+
+  args.rval().setBoolean(parent::InRepaintStressMode());
   return true;
 }
 
-static bool Middleman_MarkExplicitPause(JSContext* aCx, unsigned aArgc,
-                                        Value* aVp) {
-  CallArgs args = CallArgsFromVp(aArgc, aVp);
-
-  parent::MarkActiveChildExplicitPause();
-
-  args.rval().setUndefined();
-  return true;
+// Recording children can idle indefinitely while waiting for input, without
+// creating a checkpoint. If this might be a problem, this method induces the
+// child to create a new checkpoint and pause.
+static void MaybeCreateCheckpointInChild(parent::ChildProcessInfo* aChild) {
+  if (aChild->IsRecording() && !aChild->IsPaused()) {
+    aChild->SendMessage(CreateCheckpointMessage());
+  }
 }
 
 static bool Middleman_WaitUntilPaused(JSContext* aCx, unsigned aArgc,
                                       Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
-  parent::WaitUntilActiveChildIsPaused();
+  parent::ChildProcessInfo* child = GetChildById(aCx, args.get(0),
+                                                 /* aAllowUnpaused = */ true);
+  if (!child) {
+    return false;
+  }
+
+  if (ToBoolean(args.get(1))) {
+    MaybeCreateCheckpointInChild(child);
+  }
+
+  Message::UniquePtr msg = child->WaitUntilPaused();
 
-  args.rval().setUndefined();
+  if (!msg) {
+    JS_ReportErrorASCII(aCx, "Child process is already paused");
+  }
+
+  MOZ_RELEASE_ASSERT(msg->mType == MessageType::HitExecutionPoint);
+  const HitExecutionPointMessage& nmsg =
+    static_cast<const HitExecutionPointMessage&>(*msg);
+
+  JSObject* obj = EncodeChannelMessage(aCx, nmsg);
+  if (!obj) {
+    return false;
+  }
+
+  args.rval().setObject(*obj);
   return true;
 }
 
 static bool Middleman_PositionSubsumes(JSContext* aCx, unsigned aArgc,
                                        Value* aVp) {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
   RootedObject firstPositionObject(aCx, NonNullObject(aCx, args.get(0)));
@@ -917,28 +1167,30 @@ static bool RecordReplay_Dump(JSContext*
 
 ///////////////////////////////////////////////////////////////////////////////
 // Plumbing
 ///////////////////////////////////////////////////////////////////////////////
 
 static const JSFunctionSpec gMiddlemanMethods[] = {
     JS_FN("registerReplayDebugger", Middleman_RegisterReplayDebugger, 1, 0),
     JS_FN("canRewind", Middleman_CanRewind, 0, 0),
-    JS_FN("resume", Middleman_Resume, 1, 0),
-    JS_FN("timeWarp", Middleman_TimeWarp, 1, 0),
-    JS_FN("sendRequest", Middleman_SendRequest, 1, 0),
-    JS_FN("addBreakpoint", Middleman_AddBreakpoint, 1, 0),
-    JS_FN("clearBreakpoints", Middleman_ClearBreakpoints, 0, 0),
-    JS_FN("maybeSwitchToReplayingChild", Middleman_MaybeSwitchToReplayingChild,
-          0, 0),
+    JS_FN("spawnReplayingChild", Middleman_SpawnReplayingChild, 0, 0),
+    JS_FN("setActiveChild", Middleman_SetActiveChild, 1, 0),
+    JS_FN("sendSetSaveCheckpoint", Middleman_SendSetSaveCheckpoint, 3, 0),
+    JS_FN("sendFlushRecording", Middleman_SendFlushRecording, 1, 0),
+    JS_FN("sendResume", Middleman_SendResume, 2, 0),
+    JS_FN("sendRestoreCheckpoint", Middleman_SendRestoreCheckpoint, 2, 0),
+    JS_FN("sendRunToPoint", Middleman_SendRunToPoint, 2, 0),
+    JS_FN("sendDebuggerRequest", Middleman_SendDebuggerRequest, 2, 0),
+    JS_FN("sendAddBreakpoint", Middleman_SendAddBreakpoint, 2, 0),
+    JS_FN("sendClearBreakpoints", Middleman_SendClearBreakpoints, 1, 0),
     JS_FN("hadRepaint", Middleman_HadRepaint, 2, 0),
     JS_FN("hadRepaintFailure", Middleman_HadRepaintFailure, 0, 0),
-    JS_FN("childIsRecording", Middleman_ChildIsRecording, 0, 0),
-    JS_FN("markExplicitPause", Middleman_MarkExplicitPause, 0, 0),
-    JS_FN("waitUntilPaused", Middleman_WaitUntilPaused, 0, 0),
+    JS_FN("inRepaintStressMode", Middleman_InRepaintStressMode, 0, 0),
+    JS_FN("waitUntilPaused", Middleman_WaitUntilPaused, 1, 0),
     JS_FN("positionSubsumes", Middleman_PositionSubsumes, 2, 0),
     JS_FS_END};
 
 static const JSFunctionSpec gRecordReplayMethods[] = {
     JS_FN("areThreadEventsDisallowed", RecordReplay_AreThreadEventsDisallowed,
           0, 0),
     JS_FN("maybeDivergeFromRecording", RecordReplay_MaybeDivergeFromRecording,
           0, 0),
--- a/toolkit/recordreplay/ipc/JSControl.h
+++ b/toolkit/recordreplay/ipc/JSControl.h
@@ -12,16 +12,20 @@
 #include "InfallibleVector.h"
 #include "ProcessRewind.h"
 
 #include "mozilla/DefineEnum.h"
 #include "nsString.h"
 
 namespace mozilla {
 namespace recordreplay {
+
+struct Message;
+struct HitExecutionPointMessage;
+
 namespace js {
 
 // This file manages interactions between the record/replay infrastructure and
 // JS code. This interaction can occur in two ways:
 //
 // - In the middleman process, devtools server code can use the
 //   RecordReplayControl object to send requests to the recording/replaying
 //   child process and control its behavior.
@@ -185,28 +189,34 @@ struct ExecutionPoint {
   JSObject* Encode(JSContext* aCx) const;
   bool Decode(JSContext* aCx, JS::HandleObject aObject);
   void ToString(nsCString& aStr) const;
 };
 
 // Buffer type used for encoding object data.
 typedef InfallibleVector<char16_t> CharBuffer;
 
-// Called in the middleman when the child has hit a checkpoint or breakpoint.
-// The return value is whether there is a ReplayDebugger available which the
-// notification was sent to.
-bool DebuggerOnPause();
-
-// Called in the middleman when the child has changed.
-void DebuggerOnSwitchChild();
-
 // Set up the JS sandbox in the current recording/replaying process and load
 // its target script.
 void SetupDevtoolsSandbox();
 
+// The following hooks are used in the middleman process to call methods defined
+// by the middleman control logic.
+
+// Setup the middleman control state.
+void SetupMiddlemanControl(const Maybe<size_t>& aRecordingChildId);
+
+// Handle an incoming message from a child process.
+void ForwardHitExecutionPointMessage(size_t aId,
+                                     const HitExecutionPointMessage& aMsg);
+
+// Prepare the child processes so that the recording file can be safely copied.
+void BeforeSaveRecording();
+void AfterSaveRecording();
+
 // The following hooks are used in the recording/replaying process to
 // call methods defined by the JS sandbox.
 
 // Handle an incoming request from the middleman.
 void ProcessRequest(const char16_t* aRequest, size_t aRequestLength,
                     CharBuffer* aResponse);
 
 // Ensure there is a handler in place that will call
@@ -219,13 +229,16 @@ void ClearPositionHandlers();
 
 // Clear all state that is kept while execution is paused.
 void ClearPausedState();
 
 // Given an execution position inside a script, get an execution position for
 // the entry point of that script, otherwise return nothing.
 Maybe<BreakpointPosition> GetEntryPosition(const BreakpointPosition& aPosition);
 
+// Called after receiving a DebuggerResponse from a child.
+void OnDebuggerResponse(const Message& aMsg);
+
 }  // namespace js
 }  // namespace recordreplay
 }  // namespace mozilla
 
 #endif  // mozilla_recordreplay_JSControl_h