Bug 1470795 Part 7 - Add RecordReplayControl JS interface, r=mccr8.
authorBrian Hackett <bhackett1024@gmail.com>
Sun, 22 Jul 2018 11:59:00 +0000
changeset 427742 1fede6c8a76a69c35aa93b234e551c1937a1f891
parent 427741 301403057435baf88a758f9176a3c7cf4844ea48
child 427743 dd58259f140ade2a5f38b41978c5e8f0bdb4ca28
push id34314
push usercsabou@mozilla.com
push dateMon, 23 Jul 2018 09:31:12 +0000
treeherdermozilla-central@143984185dce [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmccr8
bugs1470795
milestone63.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 1470795 Part 7 - Add RecordReplayControl JS interface, r=mccr8.
toolkit/recordreplay/ipc/ChildIPC.cpp
toolkit/recordreplay/ipc/JSControl.cpp
toolkit/recordreplay/ipc/JSControl.h
toolkit/recordreplay/moz.build
--- a/toolkit/recordreplay/ipc/ChildIPC.cpp
+++ b/toolkit/recordreplay/ipc/ChildIPC.cpp
@@ -16,17 +16,16 @@
 #include "ipc/Channel.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/layers/ImageDataSerializer.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/VsyncDispatcher.h"
 
 #include "InfallibleVector.h"
 #include "MemorySnapshot.h"
-#include "Monitor.h"
 #include "ParentInternal.h"
 #include "ProcessRecordReplay.h"
 #include "ProcessRedirect.h"
 #include "ProcessRewind.h"
 #include "Thread.h"
 #include "Units.h"
 
 #include <algorithm>
@@ -37,17 +36,17 @@ namespace mozilla {
 namespace recordreplay {
 namespace child {
 
 ///////////////////////////////////////////////////////////////////////////////
 // Record/Replay IPC
 ///////////////////////////////////////////////////////////////////////////////
 
 // Monitor used for various synchronization tasks.
-static Monitor* gMonitor;
+Monitor* gMonitor;
 
 // The singleton channel for communicating with the middleman.
 Channel* gChannel;
 
 static base::ProcessId gMiddlemanPid;
 static base::ProcessId gParentPid;
 static StaticInfallibleVector<char*> gParentArgv;
 
new file mode 100644
--- /dev/null
+++ b/toolkit/recordreplay/ipc/JSControl.cpp
@@ -0,0 +1,726 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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 "JSControl.h"
+
+#include "js/Conversions.h"
+#include "ChildInternal.h"
+#include "ParentInternal.h"
+#include "xpcprivate.h"
+
+using namespace JS;
+
+namespace mozilla {
+namespace recordreplay {
+namespace js {
+
+// Callback for filling CharBuffers when converting objects to JSON.
+static bool
+FillCharBufferCallback(const char16_t* buf, uint32_t len, void* data)
+{
+  CharBuffer* buffer = (CharBuffer*) data;
+  MOZ_RELEASE_ASSERT(buffer->length() == 0);
+  buffer->append(buf, len);
+  return true;
+}
+
+static JSObject*
+NonNullObject(JSContext* aCx, HandleValue aValue)
+{
+  if (!aValue.isObject()) {
+    JS_ReportErrorASCII(aCx, "Expected object");
+    return nullptr;
+  }
+  return &aValue.toObject();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// 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";
+static const char gOffsetProperty[] = "offset";
+static const char gFrameIndexProperty[] = "frameIndex";
+
+JSObject*
+BreakpointPosition::Encode(JSContext* aCx) const
+{
+  RootedString kindString(aCx, JS_NewStringCopyZ(aCx, KindString()));
+  RootedObject obj(aCx, JS_NewObject(aCx, nullptr));
+  if (!kindString || !obj ||
+      !JS_DefineProperty(aCx, obj, gKindProperty, kindString, JSPROP_ENUMERATE) ||
+      (mScript != BreakpointPosition::EMPTY_SCRIPT &&
+       !JS_DefineProperty(aCx, obj, gScriptProperty, mScript, JSPROP_ENUMERATE)) ||
+      (mOffset != BreakpointPosition::EMPTY_OFFSET &&
+       !JS_DefineProperty(aCx, obj, gOffsetProperty, mOffset, JSPROP_ENUMERATE)) ||
+      (mFrameIndex != BreakpointPosition::EMPTY_FRAME_INDEX &&
+       !JS_DefineProperty(aCx, obj, gFrameIndexProperty, mFrameIndex, JSPROP_ENUMERATE)))
+  {
+    return nullptr;
+  }
+  return obj;
+}
+
+static bool
+MaybeGetNumberProperty(JSContext* aCx, HandleObject aObject, const char* aProperty, uint32_t* aResult)
+{
+  RootedValue v(aCx);
+  if (!JS_GetProperty(aCx, aObject, aProperty, &v)) {
+    return false;
+  }
+  if (v.isNumber()) {
+    *aResult = (size_t) v.toNumber();
+  }
+  return true;
+}
+
+bool
+BreakpointPosition::Decode(JSContext* aCx, HandleObject aObject)
+{
+  RootedValue v(aCx);
+  if (!JS_GetProperty(aCx, aObject, gKindProperty, &v)) {
+    return false;
+  }
+
+  RootedString str(aCx, ToString(aCx, v));
+  for (size_t i = BreakpointPosition::Invalid + 1; i < BreakpointPosition::sKindCount; i++) {
+    BreakpointPosition::Kind kind = (BreakpointPosition::Kind) i;
+    bool match;
+    if (!JS_StringEqualsAscii(aCx, str, BreakpointPosition::StaticKindString(kind), &match))
+      return false;
+    if (match) {
+      mKind = kind;
+      break;
+    }
+  }
+  if (mKind == BreakpointPosition::Invalid) {
+    JS_ReportErrorASCII(aCx, "Could not decode breakpoint position kind");
+    return false;
+  }
+
+  if (!MaybeGetNumberProperty(aCx, aObject, gScriptProperty, &mScript) ||
+      !MaybeGetNumberProperty(aCx, aObject, gOffsetProperty, &mOffset) ||
+      !MaybeGetNumberProperty(aCx, aObject, gFrameIndexProperty, &mFrameIndex))
+  {
+    return false;
+  }
+
+  return true;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Middleman Methods
+///////////////////////////////////////////////////////////////////////////////
+
+// Keep track of all replay debuggers in existence, so that they can all be
+// invalidated when the process is unpaused.
+static StaticInfallibleVector<PersistentRootedObject*> gReplayDebuggers;
+
+static bool
+Middleman_RegisterReplayDebugger(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  RootedObject obj(aCx, NonNullObject(aCx, args.get(0)));
+  if (!obj) {
+    return false;
+  }
+
+  PersistentRootedObject* root = new PersistentRootedObject(aCx);
+  *root = obj;
+  gReplayDebuggers.append(root);
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+InvalidateReplayDebuggersAfterUnpause(JSContext* aCx)
+{
+  RootedValue rval(aCx);
+  for (auto root : gReplayDebuggers) {
+    JSAutoRealm ac(aCx, *root);
+    if (!JS_CallFunctionName(aCx, *root, "invalidateAfterUnpause",
+                             HandleValueArray::empty(), &rval))
+    {
+      return false;
+    }
+  }
+  return true;
+}
+
+static bool
+Middleman_CanRewind(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  args.rval().setBoolean(parent::CanRewind());
+  return true;
+}
+
+static bool
+Middleman_Resume(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  bool forward = ToBoolean(args.get(0));
+
+  if (!InvalidateReplayDebuggersAfterUnpause(aCx)) {
+    return false;
+  }
+
+  parent::Resume(forward);
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+Middleman_Pause(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  parent::Pause();
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+Middleman_SendRequest(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  RootedObject requestObject(aCx, NonNullObject(aCx, args.get(0)));
+  if (!requestObject) {
+    return false;
+  }
+
+  CharBuffer requestBuffer;
+  if (!ToJSONMaybeSafely(aCx, requestObject, FillCharBufferCallback, &requestBuffer)) {
+    return false;
+  }
+
+  CharBuffer responseBuffer;
+  parent::SendRequest(requestBuffer, &responseBuffer);
+
+  return JS_ParseJSON(aCx, responseBuffer.begin(), responseBuffer.length(), args.rval());
+}
+
+struct InstalledBreakpoint
+{
+  PersistentRootedObject mHandler;
+  BreakpointPosition mPosition;
+
+  InstalledBreakpoint(JSContext* aCx, JSObject* aHandler, const BreakpointPosition& aPosition)
+    : mHandler(aCx, aHandler), mPosition(aPosition)
+  {}
+};
+static StaticInfallibleVector<InstalledBreakpoint*> gBreakpoints;
+
+static bool
+Middleman_SetBreakpoint(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+
+  RootedObject handler(aCx, NonNullObject(aCx, args.get(0)));
+  RootedObject positionObject(aCx, NonNullObject(aCx, args.get(1)));
+  if (!handler || !positionObject) {
+    return false;
+  }
+
+  BreakpointPosition position;
+  if (!position.Decode(aCx, positionObject)) {
+    return false;
+  }
+
+  size_t breakpointId;
+  for (breakpointId = 0; breakpointId < gBreakpoints.length(); breakpointId++) {
+    if (!gBreakpoints[breakpointId]) {
+      break;
+    }
+  }
+  if (breakpointId == gBreakpoints.length()) {
+    gBreakpoints.append(nullptr);
+  }
+
+  gBreakpoints[breakpointId] = new InstalledBreakpoint(aCx, handler, position);
+
+  parent::SetBreakpoint(breakpointId, position);
+
+  args.rval().setInt32(breakpointId);
+  return true;
+}
+
+bool
+HitBreakpoint(JSContext* aCx, size_t aId)
+{
+  InstalledBreakpoint* breakpoint = gBreakpoints[aId];
+  MOZ_RELEASE_ASSERT(breakpoint);
+
+  JSAutoRealm ac(aCx, breakpoint->mHandler);
+
+  RootedValue handlerValue(aCx, ObjectValue(*breakpoint->mHandler));
+  RootedValue rval(aCx);
+  return JS_CallFunctionValue(aCx, nullptr, handlerValue,
+                              HandleValueArray::empty(), &rval)
+      // The replaying process will resume after this hook returns, if it
+      // hasn't already been explicitly resumed.
+      && InvalidateReplayDebuggersAfterUnpause(aCx);
+}
+
+/* static */ bool
+Middleman_ClearBreakpoint(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  if (!args.get(0).isNumber()) {
+    JS_ReportErrorASCII(aCx, "Bad breakpoint ID");
+    return false;
+  }
+
+  size_t breakpointId = (size_t) args.get(0).toNumber();
+  if (breakpointId >= gBreakpoints.length() || !gBreakpoints[breakpointId]) {
+    JS_ReportErrorASCII(aCx, "Bad breakpoint ID");
+    return false;
+  }
+
+  delete gBreakpoints[breakpointId];
+  gBreakpoints[breakpointId] = nullptr;
+
+  parent::SetBreakpoint(breakpointId, BreakpointPosition());
+
+  args.rval().setUndefined();
+  return true;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Devtools Sandbox
+///////////////////////////////////////////////////////////////////////////////
+
+static PersistentRootedObject* gDevtoolsSandbox;
+
+// URL of the root script that runs when recording/replaying.
+#define ReplayScriptURL "resource://devtools/server/actors/replay/replay.js"
+
+void
+SetupDevtoolsSandbox()
+{
+  MOZ_RELEASE_ASSERT(!gDevtoolsSandbox);
+
+  dom::AutoJSAPI jsapi;
+  if (!jsapi.Init(xpc::PrivilegedJunkScope())) {
+    MOZ_CRASH("SetupDevtoolsSandbox");
+  }
+
+  JSContext* cx = jsapi.cx();
+
+  xpc::SandboxOptions options;
+  options.sandboxName.AssignLiteral("Record/Replay Devtools Sandbox");
+  options.invisibleToDebugger = true;
+  RootedValue v(cx);
+  nsresult rv = CreateSandboxObject(cx, &v, nsXPConnect::SystemPrincipal(), options);
+  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+
+  gDevtoolsSandbox = new PersistentRootedObject(cx);
+  *gDevtoolsSandbox = ::js::UncheckedUnwrap(&v.toObject());
+
+  JSAutoRealm ac(cx, *gDevtoolsSandbox);
+
+  ErrorResult er;
+  dom::GlobalObject global(cx, *gDevtoolsSandbox);
+  RootedObject obj(cx);
+  dom::ChromeUtils::Import(global, NS_LITERAL_STRING(ReplayScriptURL),
+                           dom::Optional<HandleObject>(), &obj, er);
+  MOZ_RELEASE_ASSERT(!er.Failed());
+}
+
+extern "C" {
+
+MOZ_EXPORT bool
+RecordReplayInterface_IsInternalScript(const char* aURL)
+{
+  return !strcmp(aURL, ReplayScriptURL);
+}
+
+} // extern "C"
+
+#undef ReplayScriptURL
+
+void
+ProcessRequest(const char16_t* aRequest, size_t aRequestLength, CharBuffer* aResponse)
+{
+  AutoDisallowThreadEvents disallow;
+  AutoSafeJSContext cx;
+  JSAutoRealm ac(cx, *gDevtoolsSandbox);
+
+  RootedValue requestValue(cx);
+  if (!JS_ParseJSON(cx, aRequest, aRequestLength, &requestValue)) {
+    MOZ_CRASH("ProcessRequest: ParseJSON failed");
+  }
+
+  RootedValue responseValue(cx);
+  if (!JS_CallFunctionName(cx, *gDevtoolsSandbox, "ProcessRequest",
+                           HandleValueArray(requestValue), &responseValue)) {
+    MOZ_CRASH("ProcessRequest: Handler failed");
+  }
+
+  // Processing the request may have called into MaybeDivergeFromRecording.
+  // Now that we've finished processing it, don't tolerate future events that
+  // would otherwise cause us to rewind to the last checkpoint.
+  DisallowUnhandledDivergeFromRecording();
+
+  if (!responseValue.isObject()) {
+    MOZ_CRASH("ProcessRequest: Response must be an object");
+  }
+
+  RootedObject responseObject(cx, &responseValue.toObject());
+  if (!ToJSONMaybeSafely(cx, responseObject, FillCharBufferCallback, aResponse)) {
+    MOZ_CRASH("ProcessRequest: ToJSONMaybeSafely failed");
+  }
+}
+
+void
+EnsurePositionHandler(const BreakpointPosition& aPosition)
+{
+  AutoDisallowThreadEvents disallow;
+  AutoSafeJSContext cx;
+  JSAutoRealm ac(cx, *gDevtoolsSandbox);
+
+  RootedObject obj(cx, aPosition.Encode(cx));
+  if (!obj) {
+    MOZ_CRASH("EnsurePositionHandler");
+  }
+
+  RootedValue objValue(cx, ObjectValue(*obj));
+  RootedValue rval(cx);
+  if (!JS_CallFunctionName(cx, *gDevtoolsSandbox, "EnsurePositionHandler",
+                           HandleValueArray(objValue), &rval)) {
+    MOZ_CRASH("EnsurePositionHandler");
+  }
+}
+
+void
+ClearPositionHandlers()
+{
+  AutoDisallowThreadEvents disallow;
+  AutoSafeJSContext cx;
+  JSAutoRealm ac(cx, *gDevtoolsSandbox);
+
+  RootedValue rval(cx);
+  if (!JS_CallFunctionName(cx, *gDevtoolsSandbox, "ClearPositionHandlers",
+                           HandleValueArray::empty(), &rval)) {
+    MOZ_CRASH("ClearPositionHandlers");
+  }
+}
+
+void
+ClearPausedState()
+{
+  AutoDisallowThreadEvents disallow;
+  AutoSafeJSContext cx;
+  JSAutoRealm ac(cx, *gDevtoolsSandbox);
+
+  RootedValue rval(cx);
+  if (!JS_CallFunctionName(cx, *gDevtoolsSandbox, "ClearPausedState",
+                           HandleValueArray::empty(), &rval)) {
+    MOZ_CRASH("ClearPausedState");
+  }
+}
+
+Maybe<BreakpointPosition>
+GetEntryPosition(const BreakpointPosition& aPosition)
+{
+  AutoDisallowThreadEvents disallow;
+  AutoSafeJSContext cx;
+  JSAutoRealm ac(cx, *gDevtoolsSandbox);
+
+  RootedObject positionObject(cx, aPosition.Encode(cx));
+  if (!positionObject) {
+    MOZ_CRASH("GetEntryPosition");
+  }
+
+  RootedValue rval(cx);
+  RootedValue positionValue(cx, ObjectValue(*positionObject));
+  if (!JS_CallFunctionName(cx, *gDevtoolsSandbox, "GetEntryPosition",
+                           HandleValueArray(positionValue), &rval)) {
+    MOZ_CRASH("GetEntryPosition");
+  }
+
+  if (!rval.isObject()) {
+    return Nothing();
+  }
+
+  RootedObject rvalObject(cx, &rval.toObject());
+  BreakpointPosition entryPosition;
+  if (!entryPosition.Decode(cx, rvalObject)) {
+    MOZ_CRASH("GetEntryPosition");
+  }
+
+  return Some(entryPosition);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Replaying process content
+///////////////////////////////////////////////////////////////////////////////
+
+struct ContentInfo
+{
+  const void* mToken;
+  char* mURL;
+  char* mContentType;
+  InfallibleVector<char16_t> mContent;
+
+  ContentInfo(const void* aToken, const char* aURL, const char* aContentType)
+    : mToken(aToken),
+      mURL(strdup(aURL)),
+      mContentType(strdup(aContentType))
+  {}
+
+  ContentInfo(ContentInfo&& aOther)
+    : mToken(aOther.mToken),
+      mURL(aOther.mURL),
+      mContentType(aOther.mContentType),
+      mContent(std::move(aOther.mContent))
+  {
+    aOther.mURL = nullptr;
+    aOther.mContentType = nullptr;
+  }
+
+  ~ContentInfo()
+  {
+    free(mURL);
+    free(mContentType);
+  }
+};
+
+// All content that has been parsed so far. Protected by child::gMonitor.
+static StaticInfallibleVector<ContentInfo> gContent;
+
+extern "C" {
+
+MOZ_EXPORT void
+RecordReplayInterface_BeginContentParse(const void* aToken,
+                                        const char* aURL, const char* aContentType)
+{
+  MOZ_RELEASE_ASSERT(IsRecordingOrReplaying());
+  MOZ_RELEASE_ASSERT(aToken);
+
+  RecordReplayAssert("BeginContentParse %s", aURL);
+
+  MonitorAutoLock lock(*child::gMonitor);
+  for (ContentInfo& info : gContent) {
+    MOZ_RELEASE_ASSERT(info.mToken != aToken);
+  }
+  gContent.emplaceBack(aToken, aURL, aContentType);
+}
+
+MOZ_EXPORT void
+RecordReplayInterface_AddContentParseData(const void* aToken,
+                                          const char16_t* aBuffer, size_t aLength)
+{
+  MOZ_RELEASE_ASSERT(IsRecordingOrReplaying());
+  MOZ_RELEASE_ASSERT(aToken);
+
+  RecordReplayAssert("AddContentParseDataForRecordReplay %d", (int) aLength);
+
+  MonitorAutoLock lock(*child::gMonitor);
+  for (ContentInfo& info : gContent) {
+    if (info.mToken == aToken) {
+      info.mContent.append(aBuffer, aLength);
+      return;
+    }
+  }
+  MOZ_CRASH("Unknown content parse token");
+}
+
+MOZ_EXPORT void
+RecordReplayInterface_EndContentParse(const void* aToken)
+{
+  MOZ_RELEASE_ASSERT(IsRecordingOrReplaying());
+  MOZ_RELEASE_ASSERT(aToken);
+
+  MonitorAutoLock lock(*child::gMonitor);
+  for (ContentInfo& info : gContent) {
+    if (info.mToken == aToken) {
+      info.mToken = nullptr;
+      return;
+    }
+  }
+  MOZ_CRASH("Unknown content parse token");
+}
+
+} // extern "C"
+
+static bool
+FetchContent(JSContext* aCx, HandleString aURL,
+             MutableHandleString aContentType, MutableHandleString aContent)
+{
+  MonitorAutoLock lock(*child::gMonitor);
+  for (ContentInfo& info : gContent) {
+    if (JS_FlatStringEqualsAscii(JS_ASSERT_STRING_IS_FLAT(aURL), info.mURL)) {
+      aContentType.set(JS_NewStringCopyZ(aCx, info.mContentType));
+      aContent.set(JS_NewUCStringCopyN(aCx, (const char16_t*) info.mContent.begin(),
+                                       info.mContent.length()));
+      return aContentType && aContent;
+    }
+  }
+  aContentType.set(JS_NewStringCopyZ(aCx, "text/plain"));
+  aContent.set(JS_NewStringCopyZ(aCx, "Could not find record/replay content"));
+  return aContentType && aContent;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Recording/Replaying Methods
+///////////////////////////////////////////////////////////////////////////////
+
+static bool
+RecordReplay_AreThreadEventsDisallowed(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  args.rval().setBoolean(AreThreadEventsDisallowed());
+  return true;
+}
+
+static bool
+RecordReplay_MaybeDivergeFromRecording(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  args.rval().setBoolean(navigation::MaybeDivergeFromRecording());
+  return true;
+}
+
+static bool
+RecordReplay_AdvanceProgressCounter(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  AdvanceExecutionProgressCounter();
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+RecordReplay_PositionHit(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  RootedObject obj(aCx, NonNullObject(aCx, args.get(0)));
+  if (!obj) {
+    return false;
+  }
+
+  BreakpointPosition position;
+  if (!position.Decode(aCx, obj)) {
+    return false;
+  }
+
+  navigation::PositionHit(position);
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
+RecordReplay_GetContent(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  RootedString url(aCx, ToString(aCx, args.get(0)));
+
+  RootedString contentType(aCx), content(aCx);
+  if (!FetchContent(aCx, url, &contentType, &content)) {
+    return false;
+  }
+
+  RootedObject obj(aCx, JS_NewObject(aCx, nullptr));
+  if (!obj ||
+      !JS_DefineProperty(aCx, obj, "contentType", contentType, JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(aCx, obj, "content", content, JSPROP_ENUMERATE))
+  {
+    return false;
+  }
+
+  args.rval().setObject(*obj);
+  return true;
+}
+
+static bool
+RecordReplay_Dump(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  // This method is an alternative to dump() that can be used in places where
+  // thread events are disallowed.
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  for (size_t i = 0; i < args.length(); i++) {
+    RootedString str(aCx, ToString(aCx, args[i]));
+    if (!str) {
+      return false;
+    }
+    char* cstr = JS_EncodeString(aCx, str);
+    if (!cstr) {
+      return false;
+    }
+    Print("%s", cstr);
+    JS_free(aCx, cstr);
+  }
+
+  args.rval().setUndefined();
+  return true;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// 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("pause", Middleman_Pause, 0, 0),
+  JS_FN("sendRequest", Middleman_SendRequest, 1, 0),
+  JS_FN("setBreakpoint", Middleman_SetBreakpoint, 2, 0),
+  JS_FN("clearBreakpoint", Middleman_ClearBreakpoint, 1, 0),
+  JS_FS_END
+};
+
+static const JSFunctionSpec gRecordReplayMethods[] = {
+  JS_FN("areThreadEventsDisallowed", RecordReplay_AreThreadEventsDisallowed, 0, 0),
+  JS_FN("maybeDivergeFromRecording", RecordReplay_MaybeDivergeFromRecording, 0, 0),
+  JS_FN("advanceProgressCounter", RecordReplay_AdvanceProgressCounter, 0, 0),
+  JS_FN("positionHit", RecordReplay_PositionHit, 1, 0),
+  JS_FN("getContent", RecordReplay_GetContent, 1, 0),
+  JS_FN("dump", RecordReplay_Dump, 1, 0),
+  JS_FS_END
+};
+
+extern "C" {
+
+MOZ_EXPORT bool
+RecordReplayInterface_DefineRecordReplayControlObject(JSContext* aCx, JSObject* aObjectArg)
+{
+  RootedObject object(aCx, aObjectArg);
+
+  RootedObject staticObject(aCx, JS_NewObject(aCx, nullptr));
+  if (!staticObject || !JS_DefineProperty(aCx, object, "RecordReplayControl", staticObject, 0)) {
+    return false;
+  }
+
+  // FIXME Bug 1475901 Define this interface via WebIDL instead of raw JSAPI.
+  if (IsMiddleman()) {
+    if (!JS_DefineFunctions(aCx, staticObject, gMiddlemanMethods)) {
+      return false;
+    }
+  } else if (IsRecordingOrReplaying()) {
+    if (!JS_DefineFunctions(aCx, staticObject, gRecordReplayMethods)) {
+      return false;
+    }
+  } else {
+    // Leave RecordReplayControl as an empty object. We still define the object
+    // to avoid reference errors in scripts that run in normal processes.
+  }
+
+  return true;
+}
+
+} // extern "C"
+
+} // namespace js
+} // namespace recordreplay
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/recordreplay/ipc/JSControl.h
@@ -0,0 +1,208 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+#ifndef mozilla_recordreplay_JSControl_h
+#define mozilla_recordreplay_JSControl_h
+
+#include "jsapi.h"
+
+#include "InfallibleVector.h"
+#include "ProcessRewind.h"
+
+#include "mozilla/DefineEnum.h"
+
+namespace mozilla {
+namespace recordreplay {
+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.
+//
+// - In the recording/replaying process, a JS sandbox is created before the
+//   first checkpoint is reached, which responds to the middleman's requests.
+//   The RecordReplayControl object is also provided here, but has a different
+//   interface which allows the JS to query the current process.
+
+// Identification for a position where a breakpoint can be installed in a child
+// process. Breakpoint positions describe all places between checkpoints where
+// the child process can pause and be inspected by the middleman. A particular
+// BreakpointPosition can be reached any number of times during execution of
+// the process.
+struct BreakpointPosition
+{
+  MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(Kind, (
+    Invalid,
+
+    // Break at a script offset. Requires script/offset.
+    Break,
+
+    // Break for an on-step handler within a frame.
+    // Requires script/offset/frameIndex.
+    OnStep,
+
+    // Break either when any frame is popped, or when a specific frame is
+    // popped. Requires script/frameIndex in the latter case.
+    OnPop,
+
+    // Break when entering any frame.
+    EnterFrame,
+
+    // Break when a new top-level script is created.
+    NewScript
+  ));
+
+  Kind mKind;
+
+  // Optional information associated with the breakpoint.
+  uint32_t mScript;
+  uint32_t mOffset;
+  uint32_t mFrameIndex;
+
+  static const uint32_t EMPTY_SCRIPT = (uint32_t) -1;
+  static const uint32_t EMPTY_OFFSET = (uint32_t) -1;
+  static const uint32_t EMPTY_FRAME_INDEX = (uint32_t) -1;
+
+  BreakpointPosition()
+    : mKind(Invalid), mScript(EMPTY_SCRIPT), mOffset(EMPTY_OFFSET), mFrameIndex(EMPTY_FRAME_INDEX)
+  {}
+
+  explicit BreakpointPosition(Kind aKind,
+                              uint32_t aScript = EMPTY_SCRIPT,
+                              uint32_t aOffset = EMPTY_OFFSET,
+                              uint32_t aFrameIndex = EMPTY_FRAME_INDEX)
+    : mKind(aKind), mScript(aScript), mOffset(aOffset), mFrameIndex(aFrameIndex)
+  {}
+
+  bool IsValid() const { return mKind != Invalid; }
+
+  inline bool operator==(const BreakpointPosition& o) const {
+    return mKind == o.mKind
+        && mScript == o.mScript
+        && mOffset == o.mOffset
+        && mFrameIndex == o.mFrameIndex;
+  }
+
+  inline bool operator!=(const BreakpointPosition& o) const { return !(*this == o); }
+
+  // Return whether an execution point matching |o| also matches this.
+  inline bool Subsumes(const BreakpointPosition& o) const {
+    return (*this == o)
+        || (mKind == OnPop && o.mKind == OnPop && mScript == EMPTY_SCRIPT)
+        || (mKind == Break && o.mKind == OnStep && mScript == o.mScript && mOffset == o.mOffset);
+  }
+
+  static const char* StaticKindString(Kind aKind) {
+    switch (aKind) {
+    case Invalid: return "Invalid";
+    case Break: return "Break";
+    case OnStep: return "OnStep";
+    case OnPop: return "OnPop";
+    case EnterFrame: return "EnterFrame";
+    case NewScript: return "NewScript";
+    }
+    MOZ_CRASH("Bad BreakpointPosition kind");
+  }
+
+  const char* KindString() const {
+    return StaticKindString(mKind);
+  }
+
+  JSObject* Encode(JSContext* aCx) const;
+  bool Decode(JSContext* aCx, JS::HandleObject aObject);
+};
+
+// Identification for a point in the execution of a child process where it may
+// pause and be inspected by the middleman. A particular execution point will
+// be reached exactly once during the execution of the process.
+struct ExecutionPoint
+{
+  // ID of the last normal checkpoint prior to this point.
+  size_t mCheckpoint;
+
+  // How much progress execution has made prior to reaching the point,
+  // or zero if the execution point refers to the checkpoint itself.
+  //
+  // A given BreakpointPosition may not be reached twice without an intervening
+  // increment of the global progress counter.
+  ProgressCounter mProgress;
+
+  // The position reached after making the specified amount of progress,
+  // invalid if the execution point refers to the checkpoint itself.
+  BreakpointPosition mPosition;
+
+  ExecutionPoint()
+    : mCheckpoint(CheckpointId::Invalid)
+    , mProgress(0)
+  {}
+
+  explicit ExecutionPoint(size_t aCheckpoint)
+    : mCheckpoint(aCheckpoint)
+    , mProgress(0)
+  {}
+
+  ExecutionPoint(size_t aCheckpoint, ProgressCounter aProgress,
+                 const BreakpointPosition& aPosition)
+    : mCheckpoint(aCheckpoint), mProgress(aProgress), mPosition(aPosition)
+  {
+    // ExecutionPoint positions must be as precise as possible, and cannot
+    // subsume other positions.
+    MOZ_RELEASE_ASSERT(aPosition.IsValid());
+    MOZ_RELEASE_ASSERT(aPosition.mKind != BreakpointPosition::OnPop ||
+                       aPosition.mScript != BreakpointPosition::EMPTY_SCRIPT);
+    MOZ_RELEASE_ASSERT(aPosition.mKind != BreakpointPosition::Break);
+  }
+
+  bool HasPosition() const { return mPosition.IsValid(); }
+
+  inline bool operator==(const ExecutionPoint& o) const {
+    return mCheckpoint == o.mCheckpoint
+        && mProgress == o.mProgress
+        && mPosition == o.mPosition;
+  }
+
+  inline bool operator!=(const ExecutionPoint& o) const { return !(*this == o); }
+};
+
+// Buffer type used for encoding object data.
+typedef InfallibleVector<char16_t> CharBuffer;
+
+// Called in the middleman when a breakpoint with the specified id has been hit.
+bool HitBreakpoint(JSContext* aCx, size_t id);
+
+// Set up the JS sandbox in the current recording/replaying process and load
+// its target script.
+void SetupDevtoolsSandbox();
+
+// The following hooks are used in the recording/replaying process to
+// call methods defined by the JS sandbox.
+
+// Handle an incoming request from the middleman.
+void ProcessRequest(const char16_t* aRequest, size_t aRequestLength,
+                    CharBuffer* aResponse);
+
+// Ensure there is a handler in place that will call RecordReplayControl.positionHit
+// whenever the specified execution position is reached.
+void EnsurePositionHandler(const BreakpointPosition& aPosition);
+
+// Clear all installed position handlers.
+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);
+
+} // namespace js
+} // namespace recordreplay
+} // namespace mozilla
+
+#endif // mozilla_recordreplay_JSControl_h
--- a/toolkit/recordreplay/moz.build
+++ b/toolkit/recordreplay/moz.build
@@ -13,17 +13,19 @@ if CONFIG['OS_ARCH'] == 'Darwin' and CON
     UNIFIED_SOURCES += [
         'Assembler.cpp',
         'Callback.cpp',
         'DirtyMemoryHandler.cpp',
         'File.cpp',
         'HashTable.cpp',
         'ipc/Channel.cpp',
         'ipc/ChildIPC.cpp',
+        'ipc/ChildNavigation.cpp',
         'ipc/ChildProcess.cpp',
+        'ipc/JSControl.cpp',
         'ipc/ParentForwarding.cpp',
         'ipc/ParentGraphics.cpp',
         'ipc/ParentIPC.cpp',
         'Lock.cpp',
         'MemorySnapshot.cpp',
         'ProcessRecordReplay.cpp',
         'ProcessRedirectDarwin.cpp',
         'ProcessRewind.cpp',
@@ -41,11 +43,12 @@ if CONFIG['OS_ARCH'] == 'Darwin' and CON
 else:
     UNIFIED_SOURCES += [
         'ipc/DisabledIPC.cpp',
     ]
 
 LOCAL_INCLUDES += [
     '!/ipc/ipdl/_ipdlheaders',
     '/ipc/chromium/src',
+    '/js/xpconnect/src',
 ]
 
 FINAL_LIBRARY = 'xul'