Bug 1606447 - Initial landing for cloud replay, r=jlast.
authorBrian Hackett <bhackett1024@gmail.com>
Fri, 03 Jan 2020 20:43:08 +0000
changeset 508784 052285013972a76b57e4c12bc29a4d53a3320083
parent 508783 ceab48d53ff1251b22564dce17cada76529f87e3
child 508785 46ec9d57ca2012e8a78c94d7d3191f451cea12df
push id36978
push useraiakab@mozilla.com
push dateSat, 04 Jan 2020 09:46:47 +0000
treeherdermozilla-central@d5402d5ef78b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlast
bugs1606447
milestone73.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 1606447 - Initial landing for cloud replay, r=jlast. Differential Revision: https://phabricator.services.mozilla.com/D58444
devtools/client/webreplay/components/WebReplayPlayer.js
devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-07.js
devtools/client/webreplay/mochitest/examples/doc_debugger_statements.html
devtools/client/webreplay/mochitest/examples/doc_rr_basic.html
devtools/server/actors/replay/connection-worker.js
devtools/server/actors/replay/connection.js
devtools/server/actors/replay/control.js
devtools/server/actors/replay/moz.build
devtools/server/actors/replay/replay.js
devtools/server/actors/replay/rrIConnection.idl
devtools/server/actors/replay/rrIControl.idl
mfbt/RecordReplay.cpp
mfbt/RecordReplay.h
modules/libpref/init/all.js
toolkit/recordreplay/Assembler.cpp
toolkit/recordreplay/Assembler.h
toolkit/recordreplay/BufferStream.h
toolkit/recordreplay/DirtyMemoryHandler.cpp
toolkit/recordreplay/DirtyMemoryHandler.h
toolkit/recordreplay/ExternalCall.cpp
toolkit/recordreplay/ExternalCall.h
toolkit/recordreplay/File.cpp
toolkit/recordreplay/File.h
toolkit/recordreplay/HashTable.cpp
toolkit/recordreplay/Lock.cpp
toolkit/recordreplay/Lock.h
toolkit/recordreplay/MemorySnapshot.cpp
toolkit/recordreplay/MemorySnapshot.h
toolkit/recordreplay/MiddlemanCall.cpp
toolkit/recordreplay/MiddlemanCall.h
toolkit/recordreplay/ProcessRecordReplay.cpp
toolkit/recordreplay/ProcessRecordReplay.h
toolkit/recordreplay/ProcessRedirect.cpp
toolkit/recordreplay/ProcessRedirect.h
toolkit/recordreplay/ProcessRedirectDarwin.cpp
toolkit/recordreplay/ProcessRewind.cpp
toolkit/recordreplay/ProcessRewind.h
toolkit/recordreplay/Recording.cpp
toolkit/recordreplay/Recording.h
toolkit/recordreplay/Thread.cpp
toolkit/recordreplay/Thread.h
toolkit/recordreplay/ThreadSnapshot.cpp
toolkit/recordreplay/ThreadSnapshot.h
toolkit/recordreplay/ipc/Channel.cpp
toolkit/recordreplay/ipc/Channel.h
toolkit/recordreplay/ipc/ChildIPC.cpp
toolkit/recordreplay/ipc/ChildInternal.h
toolkit/recordreplay/ipc/ChildProcess.cpp
toolkit/recordreplay/ipc/DisabledIPC.cpp
toolkit/recordreplay/ipc/JSControl.cpp
toolkit/recordreplay/ipc/JSControl.h
toolkit/recordreplay/ipc/ParentForwarding.cpp
toolkit/recordreplay/ipc/ParentGraphics.cpp
toolkit/recordreplay/ipc/ParentIPC.cpp
toolkit/recordreplay/ipc/ParentIPC.h
toolkit/recordreplay/ipc/ParentInternal.h
toolkit/recordreplay/moz.build
--- a/devtools/client/webreplay/components/WebReplayPlayer.js
+++ b/devtools/client/webreplay/components/WebReplayPlayer.js
@@ -612,21 +612,24 @@ class WebReplayPlayer extends Component 
 
     const isHighlighted = highlightedMessage == message.id;
 
     const uncached = message.executionPoint && !this.isCached(message);
 
     const atPausedLocation =
       pausedMessage && sameLocation(pausedMessage, message);
 
-    const { source, line, column } = message.frame;
-    const filename = source.split("/").pop();
-    let frameLocation = `${filename}:${line}`;
-    if (column > 100) {
-      frameLocation += `:${column}`;
+    let frameLocation = "";
+    if (message.frame) {
+      const { source, line, column } = message.frame;
+      const filename = source.split("/").pop();
+      frameLocation = `${filename}:${line}`;
+      if (column > 100) {
+        frameLocation += `:${column}`;
+      }
     }
 
     return dom.a({
       className: classname("message", {
         overlayed: isOverlayed,
         future: isFuture,
         highlighted: isHighlighted,
         uncached,
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-07.js
+++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-07.js
@@ -6,25 +6,26 @@
 
 "use strict";
 
 // Pausing at a debugger statement on startup confuses the debugger.
 PromiseTestUtils.whitelistRejectionsGlobally(/Unknown source actor/);
 
 // Test interaction of breakpoints with debugger statements.
 add_task(async function() {
-  const dbg = await attachRecordingDebugger("doc_debugger_statements.html", {
-    skipInterrupt: true,
-  });
+  const dbg = await attachRecordingDebugger("doc_debugger_statements.html");
+  await resume(dbg);
+
+  invokeInTab("foo");
 
   await waitForPaused(dbg);
   const pauseLine = getVisibleSelectedFrameLine(dbg);
-  ok(pauseLine == 6, "Paused at first debugger statement");
+  ok(pauseLine == 7, "Paused at first debugger statement");
 
-  await addBreakpoint(dbg, "doc_debugger_statements.html", 7);
-  await resumeToLine(dbg, 7);
+  await addBreakpoint(dbg, "doc_debugger_statements.html", 8);
   await resumeToLine(dbg, 8);
+  await resumeToLine(dbg, 9);
   await dbg.actions.removeAllBreakpoints(getContext(dbg));
-  await rewindToLine(dbg, 6);
-  await resumeToLine(dbg, 8);
+  await rewindToLine(dbg, 7);
+  await resumeToLine(dbg, 9);
 
   await shutdownDebugger(dbg);
 });
--- a/devtools/client/webreplay/mochitest/examples/doc_debugger_statements.html
+++ b/devtools/client/webreplay/mochitest/examples/doc_debugger_statements.html
@@ -1,10 +1,12 @@
 <html lang="en" dir="ltr">
 <body>
 <div id="maindiv" style="padding-top:50px">Hello World!</div>
 </body>
 <script>
-debugger;
-document.getElementById("maindiv").innerHTML = "Foobar!";
-debugger;
+function foo() {
+  debugger;
+  document.getElementById("maindiv").innerHTML = "Foobar!";
+  debugger;
+}
 </script>
 </html>
--- a/devtools/client/webreplay/mochitest/examples/doc_rr_basic.html
+++ b/devtools/client/webreplay/mochitest/examples/doc_rr_basic.html
@@ -9,28 +9,28 @@ function recordingFinished() {
 }
 var number = 0;
 function f() {
   updateNumber();
   if (number >= 10) {
     window.setTimeout(recordingFinished);
     return;
   }
-  window.setTimeout(f, 1);
+  window.setTimeout(f, 100);
 }
 function updateNumber() {
   number++;
   document.getElementById("maindiv").innerHTML = "Number: " + number;
   testStepping();
 }
 function testStepping() {
   var a = 0;
   testStepping2();
   return a;
 }
 function testStepping2() {
   var c = this; // Note: using 'this' causes the script to have a prologue.
   c++;
   c--;
 }
-window.setTimeout(f, 1);
+window.setTimeout(f, 100);
 </script>
 </html>
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/connection-worker.js
@@ -0,0 +1,123 @@
+/* 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/. */
+/* eslint-disable spaced-comment, brace-style, indent-legacy, no-shadow */
+
+"use strict";
+
+// This worker is spawned in the parent process the first time a middleman
+// connects to a cloud based replaying process, and manages the web sockets
+// which are used to connect to those remote processes. This could be done on
+// the main thread, but is handled here because main thread web sockets must
+// be associated with particular windows, which is not the case for the
+// scope which connection.js is created in.
+
+self.addEventListener("message", function({ data }) {
+  const { type, id } = data;
+  switch (type) {
+    case "connect":
+      try {
+        doConnect(id, data.channelId, data.address);
+      } catch (e) {
+        dump(`doConnect error: ${e}\n`);
+      }
+      break;
+    case "send":
+      try {
+        doSend(id, data.buf);
+      } catch (e) {
+        dump(`doSend error: ${e}\n`);
+      }
+      break;
+    default:
+      ThrowError(`Unknown event type ${type}`);
+  }
+});
+
+const gConnections = [];
+
+function doConnect(id, channelId, address) {
+  if (gConnections[id]) {
+    ThrowError(`Duplicate connection ID ${id}`);
+  }
+  const socket = new WebSocket(address);
+  socket.onopen = evt => onOpen(id, evt);
+  socket.onclose = evt => onClose(id, evt);
+  socket.onmessage = evt => onMessage(id, evt);
+  socket.onerror = evt => onError(id, evt);
+
+  const connection = { outgoing: [] };
+  const promise = new Promise(r => (connection.openWaiter = r));
+
+  gConnections[id] = connection;
+
+  (async function sendMessages() {
+    await promise;
+    while (gConnections[id]) {
+      if (connection.outgoing.length) {
+        const buf = connection.outgoing.shift();
+        try {
+          socket.send(buf);
+        } catch (e) {
+          ThrowError(`Send error ${e}`);
+        }
+      } else {
+        await new Promise(resolve => (connection.sendWaiter = resolve));
+      }
+    }
+  })();
+}
+
+function doSend(id, buf) {
+  const connection = gConnections[id];
+  connection.outgoing.push(buf);
+  if (connection.sendWaiter) {
+    connection.sendWaiter();
+    connection.sendWaiter = null;
+  }
+}
+
+function onOpen(id) {
+  // Messages can now be sent to the socket.
+  gConnections[id].openWaiter();
+}
+
+function onClose(id, evt) {
+  gConnections[id] = null;
+}
+
+// Message data must be sent to the main thread in the order it was received.
+// This is a bit tricky with web socket messages as they return data blobs,
+// and blob.arrayBuffer() returns a promise such that multiple promises could
+// be resolved out of order. Make sure this doesn't happen by using a single
+// async frame to resolve the promises and forward them in order.
+const gMessages = [];
+let gMessageWaiter = null;
+(async function processMessages() {
+  while (true) {
+    if (gMessages.length) {
+      const { id, promise } = gMessages.shift();
+      const buf = await promise;
+      postMessage({ id, buf });
+    } else {
+      await new Promise(resolve => (gMessageWaiter = resolve));
+    }
+  }
+})();
+
+function onMessage(id, evt) {
+  gMessages.push({ id, promise: evt.data.arrayBuffer() });
+  if (gMessageWaiter) {
+    gMessageWaiter();
+    gMessageWaiter = null;
+  }
+}
+
+function onError(id, evt) {
+  ThrowError(`Socket error ${evt}`);
+}
+
+function ThrowError(msg) {
+  dump(`Connection Worker Error: ${msg}\n`);
+  throw new Error(msg);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/connection.js
@@ -0,0 +1,39 @@
+/* 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/. */
+/* eslint-disable spaced-comment, brace-style, indent-legacy, no-shadow */
+
+"use strict";
+
+// This file provides an interface for connecting middleman processes with
+// replaying processes living remotely in the cloud.
+
+let gWorker;
+let gCallback;
+let gNextConnectionId = 1;
+
+// eslint-disable-next-line no-unused-vars
+function Initialize(callback) {
+  gWorker = new Worker("connection-worker.js");
+  gWorker.addEventListener("message", onMessage);
+  gCallback = callback;
+}
+
+function onMessage(evt) {
+  gCallback(evt.data.id, evt.data.buf);
+}
+
+// eslint-disable-next-line no-unused-vars
+function Connect(channelId, address, callback) {
+  const id = gNextConnectionId++;
+  gWorker.postMessage({ type: "connect", id, channelId, address });
+  return id;
+}
+
+// eslint-disable-next-line no-unused-vars
+function SendMessage(id, buf) {
+  gWorker.postMessage({ type: "send", id, buf });
+}
+
+// eslint-disable-next-line no-unused-vars
+var EXPORTED_SYMBOLS = ["Initialize", "Connect", "SendMessage"];
--- a/devtools/server/actors/replay/control.js
+++ b/devtools/server/actors/replay/control.js
@@ -25,471 +25,583 @@ Cu.evalInSandbox(
     "addDebuggerToGlobal(this);",
   sandbox
 );
 const {
   RecordReplayControl,
   Services,
   pointEquals,
   pointToString,
+  positionToString,
   findClosestPoint,
   pointArrayIncludes,
   pointPrecedes,
-  positionEquals,
   positionSubsumes,
   setInterval,
 } = sandbox;
 
 const InvalidCheckpointId = 0;
 const FirstCheckpointId = 1;
 
 // Application State Control
 //
 // This section describes the strategy used for managing child processes so that
 // we can be responsive to user interactions. There is at most one recording
-// child process, and one or more replaying child processes.
+// child process, and any number of replaying child processes.
 //
-// The recording child cannot rewind: it only runs forward and adds new data to
-// the recording. If we are paused or trying to pause at a place within the
-// recording, then the recording child is also paused.
+// When the user is not viewing old parts of the recording, they will interact
+// with the recording process. The recording process only runs forward and adds
+// new data to the recording which the replaying processes can consume.
 //
-// To manage the replaying children, we identify a set of checkpoints that will
-// be saved by some replaying child. The duration between saved checkpoints
-// should be roughly equal, and they must be sufficiently closely spaced that
-// any point in the recording can be quickly reached by some replaying child
-// restoring the previous saved checkpoint and then running forward to the
-// target point.
+// Replaying processes can be created in two ways: we can spawn a replaying
+// process at the beginning of the recording, or we can ask an existing
+// replaying process to fork, which creates a new replaying process at the same
+// point in the recording.
+//
+// Replaying processes fit into one of the following categories:
 //
-// As we identify the saved checkpoints, each is assigned to some replaying
-// child, which is responsible for both saving that checkpoint and for scanning
-// the contents of the recording from that checkpoint until the next saved
-// checkpoint.
+// * Root children are spawned at the beginning of the recording. They stop at
+//   the first checkpoint and coordinate communication with other processes that
+//   have been forked from them.
 //
-// When adding new data to the recording, replaying children will scan and save
-// the regions and checkpoints they are responsible for, and will otherwise play
-// forward as far as they can in the recording. We always want to have one or
-// more replaying children that are at far end of the recording and able to
-// start scanning/saving as the recording grows. In order to ensure this,
-// consider the following:
+// * Trunk children are forked from the root child. They run through the
+//   recording and periodically fork off new branches. There is only one trunk
+//   for each root child.
 //
-// - Replaying must be faster than recording. While recording there is idle time
-//   as we wait for user input, timers, etc. that is not present while
-//   replaying. Suppose that replaying speed is F times the recording speed.
-//   F must be less than one.
+// * Branch children are forked from trunk or other branch children. They run
+//   forward to a specific point in the recording, and then sit idle while they
+//   periodically fork off new leaves.
 //
-// - Scanning and saving a section of the recording is slower than recording.
-//   Both of these have a lot of overhead, and suppose that scanning is S times
-//   the recording speed. S will be more than one.
+// * Leaf children are forked from branches. They run through a part of the
+//   recording and can perform actions such as scanning the recording or
+//   performing operations at different points in the recording.
 //
-// - When there is more than one replaying child, each child can divide its time
-//   between scanning/saving and simply replaying. We want its average speed to
-//   match that of the recording. If there are N replaying children, each child
-//   scans 1/N of the recording, so the average speed compared to the recording
-//   is S/N + F*(N-1)/N. We want this term to be one or slightly less.
+// During startup, we will spawn a single trunk child which runs through the
+// recording and forks a branch child at certain saved checkpoints. The saved
+// checkpoints are spaced so that they aren't far apart. Each branch child forks
+// a leaf child which scans the recording from that branch's saved checkpoint up
+// to the next saved checkpoint, and then sits idle and responds to queries
+// about the scanned region. Other leaf children can be forked from the
+// branches, allowing us to quickly perform tasks anywhere in the recording once
+// all the branches are in place.
 //
-// For example, if F = 0.75 and S = 3, then replaying is 33% faster than
-// recording, and scanning is three times slower than recording. If N = 4,
-// the average speed is 3/4 + 0.75*3/4 = 1.31 times that of recording, which
-// will cause the replaying processes to fall further and further behind the
-// recording. If N = 12, the average speed is 3/12 + 0.75*11/12 = 0.94 times
-// that of the recording, which will allow the replaying processes to keep up
-// with the recording.
-//
-// Eventually we'll want to do this analysis dynamically to scale up or throttle
-// back the number of active replaying processes. For now, though, we rely on
-// a fixed number of replaying processes, and hope that it is enough.
+// The number of leaves which can simultaneously be operating has a small
+// limit to avoid overloading the system. Tasks requiring new children are
+// placed in a queue and resolved in priority order.
 
 ////////////////////////////////////////////////////////////////////////////////
 // Child Processes
 ////////////////////////////////////////////////////////////////////////////////
 
+// Get a unique ID for a child process.
+function processId(rootId, forkId) {
+  return forkId ? `#${rootId}:${forkId}` : `#${rootId}`;
+}
+
+// Child which is always at the end of the recording. When there is a recording
+// child this is it, and when we are replaying an old execution this is a
+// replaying child that doesn't fork and is used like a recording child.
+let gMainChild;
+
+// All child processes, indexed by their ID.
+const gChildren = new Map();
+
+// All child processes that are currently unpaused.
+const gUnpausedChildren = new Set();
+
 // Information about a child recording or replaying process.
-function ChildProcess(id, recording) {
-  this.id = id;
+function ChildProcess(rootId, forkId, recording, recordingLength) {
+  // ID for the root recording/replaying process this is associated with.
+  this.rootId = rootId;
+
+  // ID for the particular fork of the root this process represents, or zero if
+  // not forked.
+  this.forkId = forkId;
+
+  // Unique ID for this process.
+  this.id = processId(rootId, forkId);
+  gChildren.set(this.id, this);
 
   // Whether this process is recording.
   this.recording = recording;
 
+  // How much of the recording is available to this process.
+  this.recordingLength = recordingLength;
+
   // Whether this process is paused.
   this.paused = false;
+  gUnpausedChildren.add(this);
 
   // The last point we paused at.
   this.lastPausePoint = null;
 
-  // Last reported memory usage for this child.
-  this.lastMemoryUsage = null;
-
-  // All checkpoints which this process has saved or will save, which is a
-  // subset of all the saved checkpoints.
-  this.savedCheckpoints = new Set(recording ? [] : [FirstCheckpointId]);
+  // Whether this child has terminated and is unusable.
+  this.terminated = false;
 
-  // All saved checkpoints whose region of the recording has been scanned or is
-  // in the process of being scanned by this child.
-  this.scannedCheckpoints = new Set();
+  // The last time a ping message was sent to the process.
+  this.lastPingTime = Date.now();
 
-  // All snapshots which this child has taken.
-  this.snapshots = [];
-
-  // Whether this child has diverged from the recording and cannot run forward.
-  this.divergedFromRecording = false;
-
-  // Whether this child has crashed and is unusable.
-  this.crashed = false;
+  // All pings we have sent since they were reset for this process.
+  this.pings = [];
 
-  // Any manifest which is currently being executed. Child processes initially
-  // have a manifest to run forward to the first checkpoint.
-  this.manifest = {
-    onFinished: ({ point }) => {
-      if (this == gMainChild) {
-        getCheckpointInfo(FirstCheckpointId).point = point;
-        Services.tm.dispatchToMainThread(
-          recording ? maybeResumeRecording : setMainChild
-        );
-      } else {
-        this.snapshots.push(checkpointExecutionPoint(FirstCheckpointId));
-      }
+  // Manifests queued up for this child. If the child is unpaused, the first one
+  // is currently being processed. Child processes initially have a manifest to
+  // run forward to the first checkpoint.
+  this.manifests = [
+    {
+      manifest: {},
+      onFinished: ({ point }) => {
+        if (this == gMainChild) {
+          getCheckpointInfo(FirstCheckpointId).point = point;
+          Services.tm.dispatchToMainThread(
+            recording ? maybeResumeRecording : setMainChild
+          );
+        }
+      },
     },
-  };
-
-  // The time the manifest was sent to the child.
-  this.manifestSendTime = Date.now();
-
-  // Any async manifest which this child has partially processed.
-  this.asyncManifest = null;
+  ];
 }
 
+const PingIntervalSeconds = 2;
+const MaxStalledPings = 10;
+let gNextPingId = 1;
+
 ChildProcess.prototype = {
+  // Stop this child.
+  terminate() {
+    gChildren.delete(this.id);
+    gUnpausedChildren.delete(this);
+    RecordReplayControl.terminate(this.rootId, this.forkId);
+    this.terminated = true;
+  },
+
   // Get the execution point where this child is currently paused.
   pausePoint() {
     assert(this.paused);
     return this.lastPausePoint;
   },
 
   // Get the checkpoint where this child is currently paused.
   pauseCheckpoint() {
     const point = this.pausePoint();
     assert(!point.position);
     return point.checkpoint;
   },
 
   // Send a manifest to paused child to execute. The child unpauses while
-  // executing the manifest, and pauses again when it finishes. Manifests have
-  // the following properties:
-  //
-  // contents: The JSON object to send to the child describing the operation.
-  // onFinished: A callback which is called after the manifest finishes with the
-  //   manifest's result.
-  // destination: An optional destination where the child will end up.
-  // expectedDuration: Optional estimate of the time needed to run the manifest.
-  // mightRewind: Flag that must be set if the child might restore a snapshot
-  //   while processing the manifest.
-  sendManifest(manifest) {
-    assert(!this.crashed);
-    assert(this.paused);
-    this.paused = false;
-    this.manifest = manifest;
-    this.manifestSendTime = Date.now();
+  // executing the manifest, and pauses again when it finishes. This returns a
+  // promise that resolves with the manifest's result when it finishes.
+  // If specified, onFinishedCallback will be invoked with the manifest
+  // result as well.
+  sendManifest(manifest, onFinishedCallback) {
+    assert(!this.terminated);
+    return new Promise(resolve => {
+      this.manifests.push({
+        manifest,
+        onFinished(response) {
+          if (onFinishedCallback) {
+            onFinishedCallback(response);
+          }
+          resolve(response);
+        },
+      });
 
-    const { contents, mightRewind } = manifest;
-
-    dumpv(`SendManifest #${this.id} ${stringify(contents)}`);
-    RecordReplayControl.sendManifest(this.id, contents, mightRewind);
+      if (this.paused) {
+        this._startNextManifest();
+      }
+    });
   },
 
   // Called when the child's current manifest finishes.
   manifestFinished(response) {
+    dumpv(`ManifestFinished ${this.id} ${stringify(response)}`);
+
     assert(!this.paused);
+    this.paused = true;
+    gUnpausedChildren.delete(this);
+    const { onFinished } = this.manifests.shift();
+
     if (response) {
       if (response.point) {
         this.lastPausePoint = response.point;
       }
-      if (response.memoryUsage) {
-        this.lastMemoryUsage = response.memoryUsage;
-      }
       if (response.exception) {
         ThrowError(response.exception);
       }
-      if (response.restoredSnapshot) {
-        assert(this.manifest.mightRewind);
-      }
     }
-    this.paused = true;
-    this.manifest.onFinished(response);
-    this.manifest = null;
-    maybeDumpStatistics();
+    onFinished(response);
+
+    this._startNextManifest();
+  },
+
+  // Send this child the next manifest in its queue, if there is one.
+  _startNextManifest() {
+    assert(this.paused);
 
-    if (this != gMainChild) {
-      pokeChildSoon(this);
+    if (this.manifests.length) {
+      const { manifest } = this.manifests[0];
+      dumpv(`SendManifest ${this.id} ${stringify(manifest)}`);
+      RecordReplayControl.sendManifest(this.rootId, this.forkId, manifest);
+      this.paused = false;
+      gUnpausedChildren.add(this);
+
+      // Reset pings when sending a new manifest.
+      this.pings.length = 0;
+      this.lastPingTime = Date.now();
     }
   },
 
   // Block until this child is paused. If maybeCreateCheckpoint is specified
   // then a checkpoint is created if this child is recording, so that it pauses
   // quickly (otherwise it could sit indefinitely if there is no page activity).
   waitUntilPaused(maybeCreateCheckpoint) {
     if (this.paused) {
       return;
     }
-    RecordReplayControl.waitUntilPaused(this.id, maybeCreateCheckpoint);
-    assert(this.paused || this.crashed);
-  },
-
-  // Add a checkpoint for this child to save.
-  addSavedCheckpoint(checkpoint) {
-    dumpv(`AddSavedCheckpoint #${this.id} ${checkpoint}`);
-    this.savedCheckpoints.add(checkpoint);
-  },
-
-  // Get the checkpoints which the child must save in the range [start, end].
-  savedCheckpointsInRange(start, end) {
-    const rv = [];
-    for (let i = start; i <= end; i++) {
-      if (this.savedCheckpoints.has(i)) {
-        rv.push(i);
-      }
+    dumpv(`WaitUntilPaused ${this.id}`);
+    if (maybeCreateCheckpoint) {
+      assert(this.recording && !this.forkId);
+      RecordReplayControl.createCheckpointInRecording(this.rootId);
     }
-    return rv;
-  },
-
-  // Get the locations of snapshots for saved checkpoints which the child must
-  // save when running forward to endpoint.
-  getSnapshotsForSavedCheckpoints(endpoint) {
-    assert(pointPrecedes(this.pausePoint(), endpoint));
-    return this.savedCheckpointsInRange(
-      this.pausePoint().checkpoint + 1,
-      endpoint.checkpoint
-    ).map(checkpointExecutionPoint);
+    while (!this.paused) {
+      this.maybePing();
+      RecordReplayControl.maybeProcessNextMessage();
+    }
+    dumpv(`WaitUntilPaused Done`);
+    assert(this.paused || this.terminated);
   },
 
-  // Get the point of the last snapshot which was taken.
-  lastSnapshot() {
-    return this.snapshots[this.snapshots.length - 1];
+  // Hang Detection
+  //
+  // Replaying processes will be terminated if no execution progress has been
+  // made for some number of seconds. This generates a crash report for
+  // diagnosis and allows another replaying process to be spawned in its place.
+  // We detect that no progress is being made by periodically sending ping
+  // messages to the replaying process, and comparing the progress values
+  // returned by them. The ping messages are sent at least PingIntervalSeconds
+  // apart, and the process is considered hanged if at any point the last
+  // MaxStalledPings ping messages either did not respond or responded with the
+  // same progress value.
+  //
+  // Dividing our accounting between different ping messages avoids treating
+  // processes as hanged when the computer wakes up after sleeping: no pings
+  // will be sent while the computer is sleeping and processes are suspended,
+  // so the computer will need to be awake for some time before any processes
+  // are marked as hanged (before which they will hopefully be able to make
+  // progress).
+  isHanged() {
+    if (this.pings.length < MaxStalledPings) {
+      return false;
+    }
+
+    const firstIndex = this.pings.length - MaxStalledPings;
+    const firstValue = this.pings[firstIndex].progress;
+    if (!firstValue) {
+      // The child hasn't responded to any of the pings.
+      return true;
+    }
+
+    for (let i = firstIndex; i < this.pings.length; i++) {
+      if (this.pings[i].progress && this.pings[i].progress != firstValue) {
+        return false;
+      }
+    }
+
+    return true;
   },
 
-  // Get an estimate of the amount of time required for this child to reach an
-  // execution point.
-  timeToReachPoint(point) {
-    let startDelay = 0;
-    let startPoint = this.lastPausePoint;
-    if (!startPoint) {
-      startPoint = checkpointExecutionPoint(FirstCheckpointId);
+  maybePing() {
+    assert(!this.paused);
+    if (this.recording) {
+      return;
+    }
+
+    const now = Date.now();
+    if (now < this.lastPingTime + PingIntervalSeconds * 1000) {
+      return;
     }
-    if (!this.paused) {
-      if (this.manifest.expectedDuration) {
-        const elapsed = Date.now() - this.manifestSendTime;
-        if (elapsed < this.manifest.expectedDuration) {
-          startDelay = this.manifest.expectedDuration - elapsed;
-        }
-      }
-      if (this.manifest.destination) {
-        startPoint = this.manifest.destination;
+
+    if (this.isHanged()) {
+      // Try to get the child to crash, so that we can get a minidump.
+      RecordReplayControl.crashHangedChild(this.rootId, this.forkId);
+    } else {
+      const id = gNextPingId++;
+      RecordReplayControl.ping(this.rootId, this.forkId, id);
+      this.pings.push({ id });
+    }
+  },
+
+  pingResponse(id, progress) {
+    for (const entry of this.pings) {
+      if (entry.id == id) {
+        entry.progress = progress;
+        break;
       }
     }
-    let startCheckpoint;
-    if (this.snapshots.length) {
-      let snapshotIndex = this.snapshots.length - 1;
-      while (pointPrecedes(point, this.snapshots[snapshotIndex])) {
-        snapshotIndex--;
-      }
-      startCheckpoint = this.snapshots[snapshotIndex].checkpoint;
-    } else {
-      // Children which just started haven't taken snapshots.
-      startCheckpoint = FirstCheckpointId;
-    }
-    return (
-      startDelay + checkpointRangeDuration(startCheckpoint, point.checkpoint)
+  },
+
+  updateRecording() {
+    RecordReplayControl.updateRecording(
+      this.rootId,
+      this.forkId,
+      this.recordingLength
     );
+    this.recordingLength = RecordReplayControl.recordingLength();
   },
 };
 
-// Child which is always at the end of the recording. When there is a recording
-// child this is it, and when we are replaying an old execution this is a
-// replaying child that is unable to rewind and is used in the same way as the
-// recording child.
-let gMainChild;
-
-// Replaying children available for exploring the interior of the recording,
-// indexed by their ID.
-const gReplayingChildren = [];
-
-function lookupChild(id) {
-  if (id == gMainChild.id) {
-    return gMainChild;
+// eslint-disable-next-line no-unused-vars
+function ManifestFinished(rootId, forkId, response) {
+  try {
+    const child = gChildren.get(processId(rootId, forkId));
+    if (child) {
+      child.manifestFinished(response);
+    }
+  } catch (e) {
+    dump(`ERROR: ManifestFinished threw exception: ${e} ${e.stack}\n`);
   }
-  return gReplayingChildren[id];
 }
 
-function closestChild(point) {
-  let minChild = null,
-    minTime = Infinity;
-  for (const child of gReplayingChildren) {
+// eslint-disable-next-line no-unused-vars
+function PingResponse(rootId, forkId, pingId, progress) {
+  try {
+    const child = gChildren.get(processId(rootId, forkId));
     if (child) {
-      const time = child.timeToReachPoint(point);
-      if (time < minTime) {
-        minChild = child;
-        minTime = time;
-      }
+      child.pingResponse(pingId, progress);
     }
+  } catch (e) {
+    dump(`ERROR: PingResponse threw exception: ${e} ${e.stack}\n`);
   }
-  return minChild;
 }
 
 // The singleton ReplayDebugger, or undefined if it doesn't exist.
 let gDebugger;
 
 ////////////////////////////////////////////////////////////////////////////////
-// Asynchronous Manifests
+// Child Management
 ////////////////////////////////////////////////////////////////////////////////
 
 // Priority levels for asynchronous manifests.
 const Priority = {
   HIGH: 0,
   MEDIUM: 1,
   LOW: 2,
 };
 
-// Asynchronous manifest worklists.
-const gAsyncManifests = [new Set(), new Set(), new Set()];
+// The singleton root child, if any.
+let gRootChild;
+
+// The singleton trunk child, if any.
+let gTrunkChild;
+
+// All branch children available for creating new leaves, in order.
+const gBranchChildren = [];
+
+// ID for the next fork we create.
+let gNextForkId = 1;
+
+// Fork a replaying child, returning the new one.
+function forkChild(child) {
+  const forkId = gNextForkId++;
+  const newChild = new ChildProcess(
+    child.rootId,
+    forkId,
+    false,
+    child.recordingLength
+  );
+
+  dumpv(`Forking ${child.id} -> ${newChild.id}`);
+
+  child.sendManifest({ kind: "fork", id: forkId });
+  return newChild;
+}
+
+// Immediately create a new leaf child and take it to endpoint, by forking the
+// closest branch child to endpoint. onFinished will be called when the child
+// reaches the endpoint.
+function newLeafChild(endpoint, onFinished = () => {}) {
+  assert(
+    !pointPrecedes(checkpointExecutionPoint(gLastSavedCheckpoint), endpoint)
+  );
 
-// Send a manifest asynchronously, returning a promise that resolves when the
-// manifest has been finished. Async manifests have the following properties:
-//
-// shouldSkip: Callback invoked before sending the manifest. Returns true if the
-//   manifest should not be sent, and the promise resolved immediately.
-//
-// contents: Callback invoked with the executing child when it is being sent.
-//   Returns the contents to send to the child.
-//
-// onFinished: Callback invoked with the executing child and manifest response
-//   after the manifest finishes.
-//
-// point: Optional point which the associated child must reach before sending
-//   the manifest.
-//
-// snapshot: Optional point at which the associated child should take a snapshot
-//   while heading to the point.
-//
-// scanCheckpoint: If the manifest relies on scan data, the saved checkpoint
-//   whose range the child must have scanned. Such manifests do not have side
-//   effects in the child, and can be sent to the active child.
-//
-// priority: Optional priority for this manifest. Default is HIGH.
-//
-// destination: An optional destination where the child will end up.
-//
-// expectedDuration: Optional estimate of the time needed to run the manifest.
-//
-// mightRewind: Flag that must be set if the child might restore a snapshot
-//   while processing the manifest.
-function sendAsyncManifest(manifest) {
-  pokeChildrenSoon();
+  let entry;
+  for (let i = gBranchChildren.length - 1; i >= 0; i--) {
+    entry = gBranchChildren[i];
+    if (!pointPrecedes(endpoint, entry.point)) {
+      break;
+    }
+  }
+
+  const child = forkChild(entry.child);
+  if (pointEquals(endpoint, entry.point)) {
+    onFinished(child);
+  } else {
+    child.sendManifest({ kind: "runToPoint", endpoint }, () =>
+      onFinished(child)
+    );
+  }
+  return child;
+}
+
+// How many leaf children we can have running simultaneously.
+const MaxRunningLeafChildren = 4;
+
+// How many leaf children are currently running.
+let gNumRunningLeafChildren = 0;
+
+// Resolve hooks for tasks waiting on a new leaf child, organized by priority.
+const gChildWaiters = [[], [], []];
+
+// Asynchronously create a new leaf child and take it to endpoint, respecting
+// the limits on the maximum number of running leaves and returning the new
+// child when it reaches the endpoint.
+async function ensureLeafChild(endpoint, priority = Priority.HIGH) {
+  if (gNumRunningLeafChildren < MaxRunningLeafChildren) {
+    gNumRunningLeafChildren++;
+  } else {
+    await new Promise(resolve => gChildWaiters[priority].push(resolve));
+  }
+
   return new Promise(resolve => {
-    manifest.resolve = resolve;
-    const priority = manifest.priority || Priority.HIGH;
-    gAsyncManifests[priority].add(manifest);
+    newLeafChild(endpoint, child => resolve(child));
   });
 }
 
-// Pick the best async manifest for a child to process.
-function pickAsyncManifest(child, priority) {
-  const worklist = gAsyncManifests[priority];
+// After a leaf child becomes idle and/or is about to be terminated, mark it ass
+// no longer running and continue any task waiting on a new leaf.
+function stopRunningLeafChild() {
+  gNumRunningLeafChildren--;
 
-  let best = null,
-    bestTime = Infinity;
-  for (const manifest of worklist) {
-    // Prune any manifests that can be skipped.
-    if (manifest.shouldSkip()) {
-      manifest.resolve();
-      worklist.delete(manifest);
-      continue;
-    }
-
-    // Manifests relying on scan data can be handled by any child, at any point.
-    // These are the best ones to pick.
-    if (manifest.scanCheckpoint) {
-      if (child.scannedCheckpoints.has(manifest.scanCheckpoint)) {
-        assert(!manifest.point);
-        best = manifest;
-        break;
-      } else {
-        continue;
+  if (gNumRunningLeafChildren < MaxRunningLeafChildren) {
+    for (const waiters of gChildWaiters) {
+      if (waiters.length) {
+        const resolve = waiters.shift();
+        gNumRunningLeafChildren++;
+        resolve();
       }
     }
+  }
+}
 
-    // The active child cannot process other asynchronous manifests which don't
-    // rely on scan data, as they can move the child or have other side effects.
-    if (child == gActiveChild) {
-      continue;
-    }
+// Terminate a running leaf child that is no longer needed.
+function terminateRunningLeafChild(child) {
+  stopRunningLeafChild();
 
-    // Pick the manifest which requires the least amount of travel time.
-    assert(manifest.point);
-    const time = child.timeToReachPoint(manifest.point);
-    if (time < bestTime) {
-      best = manifest;
-      bestTime = time;
-    }
+  dumpv(`Terminate ${child.id}`);
+  child.terminate();
+}
+
+// The next ID to use for a root replaying process.
+let gNextRootId = 1;
+
+// Spawn the single trunk child.
+function spawnTrunkChild() {
+  if (!RecordReplayControl.canRewind()) {
+    return;
   }
 
-  if (best) {
-    worklist.delete(best);
-  }
+  const id = gNextRootId++;
+  RecordReplayControl.spawnReplayingChild(id);
+  gRootChild = new ChildProcess(
+    id,
+    0,
+    false,
+    RecordReplayControl.recordingLength()
+  );
 
-  return best;
+  gTrunkChild = forkChild(gRootChild);
+
+  // We should be at the beginning of the recording, and don't have enough space
+  // to create any branches yet.
+  assert(gLastSavedCheckpoint == InvalidCheckpointId);
 }
 
-function processAsyncManifest(child) {
-  // If the child has partially processed a manifest, continue with it.
-  let manifest = child.asyncManifest;
-  child.asyncManifest = null;
-
-  if (manifest && child == gActiveChild) {
-    // After a child becomes the active child, it gives up on any in progress
-    // async manifest it was processing.
-    sendAsyncManifest(manifest);
-    manifest = null;
-  }
-
-  if (!manifest) {
-    for (const priority of Object.values(Priority)) {
-      manifest = pickAsyncManifest(child, priority);
-      if (manifest) {
-        break;
-      }
-    }
-    if (!manifest) {
-      return false;
-    }
+function forkBranchChild(lastSavedCheckpoint, nextSavedCheckpoint) {
+  if (!RecordReplayControl.canRewind()) {
+    return;
   }
 
-  child.asyncManifest = manifest;
+  // The recording is flushed and the trunk child updated every time we add
+  // a saved checkpoint, so the child we fork here will have the recording
+  // contents up to the next saved checkpoint. Branch children are only able to
+  // run up to the next saved checkpoint.
+  gTrunkChild.updateRecording();
+
+  const child = forkChild(gTrunkChild);
+  const point = checkpointExecutionPoint(lastSavedCheckpoint);
+  gBranchChildren.push({ child, point });
 
-  if (
-    manifest.point &&
-    maybeReachPoint(child, manifest.point, manifest.snapshot)
-  ) {
-    // The manifest has been partially processed.
-    return true;
-  }
+  const endpoint = checkpointExecutionPoint(nextSavedCheckpoint);
+  gTrunkChild.sendManifest({
+    kind: "runToPoint",
+    endpoint,
+
+    // External calls are flushed in the trunk child so that external calls
+    // from throughout the recording will be cached in the root child.
+    flushExternalCalls: true,
+  });
+}
 
-  child.sendManifest({
-    contents: manifest.contents(child),
-    onFinished: data => {
-      child.asyncManifest = null;
-      manifest.onFinished(child, data);
-      manifest.resolve();
-    },
-    destination: manifest.destination,
-    expectedDuration: manifest.expectedDuration,
-    mightRewind: manifest.mightRewind,
-  });
+// eslint-disable-next-line no-unused-vars
+function Initialize(recordingChildId) {
+  try {
+    if (recordingChildId != undefined) {
+      assert(recordingChildId == 0);
+      gMainChild = new ChildProcess(recordingChildId, 0, true);
+    } else {
+      // If there is no recording child, spawn a replaying child in its place.
+      const id = gNextRootId++;
+      RecordReplayControl.spawnReplayingChild(id);
+      gMainChild = new ChildProcess(
+        id,
+        0,
+        false,
+        RecordReplayControl.recordingLength
+      );
+    }
+    gActiveChild = gMainChild;
+    return gControl;
+  } catch (e) {
+    dump(`ERROR: Initialize threw exception: ${e}\n`);
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// PromiseMap
+////////////////////////////////////////////////////////////////////////////////
 
-  return true;
+// Map from arbitrary keys to promises that resolve when any associated work is
+// complete. This is used to avoid doing redundant work in different async
+// tasks.
+function PromiseMap() {
+  this.map = new Map();
 }
 
+PromiseMap.prototype = {
+  // Returns either { promise } or { promise, resolve }. If resolve is included
+  // then the caller is responsible for invoking it later.
+  get(key) {
+    let promise = this.map.get(key);
+    if (promise) {
+      return { promise };
+    }
+
+    let resolve;
+    promise = new Promise(r => {
+      resolve = r;
+    });
+    this.map.set(key, promise);
+    return { promise, resolve };
+  },
+
+  set(key, value) {
+    this.map.set(key, value);
+  },
+};
+
 ////////////////////////////////////////////////////////////////////////////////
 // Application State
 ////////////////////////////////////////////////////////////////////////////////
 
 // Any child the user is interacting with, which may be paused or not.
 let gActiveChild = null;
 
 // Information about each checkpoint, indexed by the checkpoint's id.
@@ -501,25 +613,16 @@ function CheckpointInfo() {
   this.duration = 0;
 
   // Execution point at the checkpoint.
   this.point = null;
 
   // Whether the checkpoint is saved.
   this.saved = false;
 
-  // If the checkpoint is saved, the time it was assigned to a child.
-  this.assignTime = null;
-
-  // If the checkpoint is saved and scanned, the time it finished being scanned.
-  this.scanTime = null;
-
-  // If the checkpoint is saved and scanned, the duration of the scan.
-  this.scanDuration = null;
-
   // If the checkpoint is saved, any debugger statement hits in its region.
   this.debuggerStatements = [];
 
   // If the checkpoint is saved, any events in its region.
   this.events = [];
 }
 
 function getCheckpointInfo(id) {
@@ -538,123 +641,58 @@ function checkpointRangeDuration(start, 
   return time;
 }
 
 // How much execution time has elapsed since a checkpoint.
 function timeSinceCheckpoint(id) {
   return checkpointRangeDuration(id, gCheckpoints.length);
 }
 
-// How much execution time is captured by a saved checkpoint.
-function timeForSavedCheckpoint(id) {
-  return checkpointRangeDuration(id, nextSavedCheckpoint(id));
-}
-
-// The checkpoint up to which the recording runs.
-let gLastFlushCheckpoint = InvalidCheckpointId;
-
 // How often we want to flush the recording.
 const FlushMs = 0.5 * 1000;
 
-// ID of the last replaying child we picked for saving a checkpoint.
-let gLastPickedChildId = 0;
+// The most recent saved checkpoint. The recording is flushed at every saved
+// checkpoint.
+let gLastSavedCheckpoint = InvalidCheckpointId;
+const gSavedCheckpointWaiters = [];
 
 function addSavedCheckpoint(checkpoint) {
-  getCheckpointInfo(checkpoint).saved = true;
-  getCheckpointInfo(checkpoint).assignTime = Date.now();
+  assert(checkpoint >= gLastSavedCheckpoint);
+  if (checkpoint == gLastSavedCheckpoint) {
+    return;
+  }
+
+  if (gLastSavedCheckpoint != InvalidCheckpointId) {
+    forkBranchChild(gLastSavedCheckpoint, checkpoint);
+  }
 
-  if (RecordReplayControl.canRewind()) {
-    // Use a round robin approach when picking children for saving checkpoints.
-    let child;
-    while (true) {
-      gLastPickedChildId = (gLastPickedChildId + 1) % gReplayingChildren.length;
-      child = gReplayingChildren[gLastPickedChildId];
-      if (child) {
-        break;
-      }
-    }
+  getCheckpointInfo(checkpoint).saved = true;
+  gLastSavedCheckpoint = checkpoint;
 
-    child.addSavedCheckpoint(checkpoint);
-  }
+  gSavedCheckpointWaiters.forEach(resolve => resolve());
+  gSavedCheckpointWaiters.length = 0;
+}
+
+function waitForSavedCheckpoint() {
+  return new Promise(resolve => gSavedCheckpointWaiters.push(resolve));
 }
 
 function addCheckpoint(checkpoint, duration) {
   assert(!getCheckpointInfo(checkpoint).duration);
   getCheckpointInfo(checkpoint).duration = duration;
 }
 
-// Bring a child to the specified execution point, sending it one or more
-// manifests if necessary. Returns true if the child has not reached the point
-// yet but some progress was made, or false if the child is at the point.
-// Snapshot specifies any point at which a snapshot should be taken.
-function maybeReachPoint(child, endpoint, snapshot) {
-  if (
-    pointEquals(child.pausePoint(), endpoint) &&
-    !child.divergedFromRecording
-  ) {
-    return false;
-  }
-
-  if (pointPrecedes(endpoint, child.pausePoint())) {
-    restoreSnapshotPriorTo(endpoint);
-    return true;
-  }
-
-  if (child.divergedFromRecording) {
-    restoreSnapshotPriorTo(child.pausePoint());
-    return true;
-  }
-
-  const snapshotPoints = child.getSnapshotsForSavedCheckpoints(endpoint);
-  if (
-    snapshot &&
-    pointPrecedes(child.pausePoint(), snapshot) &&
-    !pointArrayIncludes(snapshotPoints, snapshot)
-  ) {
-    snapshotPoints.push(snapshot);
+function nextSavedCheckpoint(checkpoint) {
+  assert(checkpoint == InvalidCheckpointId || gCheckpoints[checkpoint].saved);
+  while (++checkpoint < gCheckpoints.length) {
+    if (gCheckpoints[checkpoint].saved) {
+      return checkpoint;
+    }
   }
-
-  child.sendManifest({
-    contents: { kind: "runToPoint", endpoint, snapshotPoints },
-    onFinished() {
-      child.snapshots.push(...snapshotPoints);
-    },
-    destination: endpoint,
-    expectedDuration: checkpointRangeDuration(
-      child.pausePoint().checkpoint,
-      endpoint.checkpoint
-    ),
-  });
-
-  return true;
-
-  // Send the child to its most recent saved checkpoint at or before target.
-  function restoreSnapshotPriorTo(target) {
-    let numSnapshots = 0;
-    while (pointPrecedes(target, child.lastSnapshot())) {
-      numSnapshots++;
-      child.snapshots.pop();
-    }
-    child.sendManifest({
-      contents: { kind: "restoreSnapshot", numSnapshots },
-      onFinished({ restoredSnapshot }) {
-        assert(restoredSnapshot);
-        child.divergedFromRecording = false;
-      },
-      destination: child.lastSnapshot(),
-      mightRewind: true,
-    });
-  }
-}
-
-function nextSavedCheckpoint(checkpoint) {
-  assert(gCheckpoints[checkpoint].saved);
-  // eslint-disable-next-line no-empty
-  while (!gCheckpoints[++checkpoint].saved) {}
-  return checkpoint;
+  return undefined;
 }
 
 function forSavedCheckpointsInRange(start, end, callback) {
   if (start == FirstCheckpointId && !gCheckpoints[start].saved) {
     return;
   }
   assert(gCheckpoints[start].saved);
   for (
@@ -662,145 +700,82 @@ function forSavedCheckpointsInRange(star
     checkpoint < end;
     checkpoint = nextSavedCheckpoint(checkpoint)
   ) {
     callback(checkpoint);
   }
 }
 
 function forAllSavedCheckpoints(callback) {
-  forSavedCheckpointsInRange(FirstCheckpointId, gLastFlushCheckpoint, callback);
+  forSavedCheckpointsInRange(FirstCheckpointId, gLastSavedCheckpoint, callback);
 }
 
 function getSavedCheckpoint(checkpoint) {
   while (!gCheckpoints[checkpoint].saved) {
     checkpoint--;
   }
   return checkpoint;
 }
 
 // Get the execution point to use for a checkpoint.
 function checkpointExecutionPoint(checkpoint) {
   return gCheckpoints[checkpoint].point;
 }
 
-// Check to see if an idle replaying child can make any progress.
-function pokeChild(child) {
-  assert(child != gMainChild);
-
-  if (!child.paused) {
-    return;
-  }
-
-  if (processAsyncManifest(child)) {
-    return;
-  }
-
-  if (child == gActiveChild) {
-    sendActiveChildToPausePoint();
-    return;
-  }
-
-  // If there is nothing to do, run forward to the end of the recording.
-  if (gLastFlushCheckpoint) {
-    maybeReachPoint(child, checkpointExecutionPoint(gLastFlushCheckpoint));
-  }
-}
-
-function pokeChildSoon(child) {
-  Services.tm.dispatchToMainThread(() => pokeChild(child));
-}
-
-let gPendingPokeChildren = false;
-
-function pokeChildren() {
-  gPendingPokeChildren = false;
-  for (const child of gReplayingChildren) {
-    if (child) {
-      pokeChild(child);
-    }
-  }
-}
-
-function pokeChildrenSoon() {
-  if (!gPendingPokeChildren) {
-    Services.tm.dispatchToMainThread(() => pokeChildren());
-    gPendingPokeChildren = true;
-  }
-}
-
 ////////////////////////////////////////////////////////////////////////////////
 // Search State
 ////////////////////////////////////////////////////////////////////////////////
 
 // All currently installed breakpoints.
 const gBreakpoints = [];
 
 // Recording Scanning
 //
 // Scanning a section of the recording between two neighboring saved checkpoints
 // allows the execution points for each script breakpoint position to be queried
-// by sending a manifest to the child which performed the scan.
+// by sending a manifest to the child which performed the scan. Scanning is done
+// using leaf children. When the child has finished scanning, it is marked as no
+// longer running and remains idle remains idle while it responds to queries on
+// the scanned data.
+
+// Map from saved checkpoints to the leaf child responsible for scanning that saved
+// checkpoint's region.
+const gSavedCheckpointChildren = new PromiseMap();
+
+// Set of saved checkpoints which have finished scanning.
+const gScannedSavedCheckpoints = new Set();
 
-function findScanChild(checkpoint, requireComplete) {
-  for (const child of gReplayingChildren) {
-    if (child && child.scannedCheckpoints.has(checkpoint)) {
-      if (
-        requireComplete &&
-        !child.paused &&
-        child.manifest.contents.kind == "scanRecording" &&
-        child.lastPausePoint.checkpoint == checkpoint
-      ) {
-        continue;
-      }
-      return child;
-    }
+// Ensure the region for a saved checkpoint has been scanned by some child, and
+// return that child.
+async function scanRecording(checkpoint) {
+  assert(gCheckpoints[checkpoint].saved);
+  if (checkpoint == gLastSavedCheckpoint) {
+    await waitForSavedCheckpoint();
   }
-  return null;
-}
 
-// Ensure the region for a saved checkpoint has been scanned by some child.
-async function scanRecording(checkpoint) {
-  assert(checkpoint < gLastFlushCheckpoint);
-
-  const child = findScanChild(checkpoint);
-  if (child) {
-    return;
+  const { promise, resolve } = gSavedCheckpointChildren.get(checkpoint);
+  if (!resolve) {
+    return promise;
   }
 
   const endpoint = checkpointExecutionPoint(nextSavedCheckpoint(checkpoint));
-  let snapshotPoints = null;
-  await sendAsyncManifest({
-    shouldSkip: () => !!findScanChild(checkpoint),
-    contents(child) {
-      child.scannedCheckpoints.add(checkpoint);
-      snapshotPoints = child.getSnapshotsForSavedCheckpoints(endpoint);
-      return {
-        kind: "scanRecording",
-        endpoint,
-        snapshotPoints,
-      };
-    },
-    onFinished(child, { duration }) {
-      child.snapshots.push(...snapshotPoints);
-      const info = getCheckpointInfo(checkpoint);
-      if (!info.scanTime) {
-        info.scanTime = Date.now();
-        info.scanDuration = duration;
-      }
-      if (gDebugger) {
-        gDebugger._callOnPositionChange();
-      }
-    },
-    point: checkpointExecutionPoint(checkpoint),
-    destination: endpoint,
-    expectedDuration: checkpointRangeDuration(checkpoint, endpoint) * 5,
-  });
+  const child = await ensureLeafChild(checkpointExecutionPoint(checkpoint));
+  await child.sendManifest({ kind: "scanRecording", endpoint });
+
+  stopRunningLeafChild();
+
+  gScannedSavedCheckpoints.add(checkpoint);
 
-  assert(findScanChild(checkpoint));
+  // Update the unscanned regions in the UI.
+  if (gDebugger) {
+    gDebugger._callOnPositionChange();
+  }
+
+  resolve(child);
+  return child;
 }
 
 function unscannedRegions() {
   const result = [];
 
   function addRegion(startCheckpoint, endCheckpoint) {
     const start = checkpointExecutionPoint(startCheckpoint).progress;
     const end = checkpointExecutionPoint(endCheckpoint).progress;
@@ -808,94 +783,63 @@ function unscannedRegions() {
     if (result.length && result[result.length - 1].end == start) {
       result[result.length - 1].end = end;
     } else {
       result.push({ start, end });
     }
   }
 
   forAllSavedCheckpoints(checkpoint => {
-    if (!findScanChild(checkpoint, /* requireComplete */ true)) {
+    if (!gScannedSavedCheckpoints.has(checkpoint)) {
       addRegion(checkpoint, nextSavedCheckpoint(checkpoint));
     }
   });
 
-  const lastFlush = gLastFlushCheckpoint || FirstCheckpointId;
+  const lastFlush = gLastSavedCheckpoint || FirstCheckpointId;
   if (lastFlush != gRecordingEndpoint) {
     addRegion(lastFlush, gMainChild.lastPausePoint.checkpoint);
   }
 
   return result;
 }
 
-// Map from saved checkpoints to information about breakpoint hits within the
-// range of that checkpoint.
-const gHitSearches = new Map();
+// Map from saved checkpoints and positions to the breakpoint hits for that
+// position within the range of the checkpoint.
+const gHitSearches = new PromiseMap();
 
 // Only hits on script locations (Break and OnStep positions) can be found by
 // scanning the recording.
 function canFindHits(position) {
   return position.kind == "Break" || position.kind == "OnStep";
 }
 
 // Find all hits on the specified position between a saved checkpoint and the
 // following saved checkpoint, using data from scanning the recording. This
 // returns a promise that resolves with the resulting hits.
 async function findHits(checkpoint, position) {
   assert(canFindHits(position));
   assert(gCheckpoints[checkpoint].saved);
 
-  if (!gHitSearches.has(checkpoint)) {
-    gHitSearches.set(checkpoint, []);
-  }
-
-  // Check if we already have the hits.
-  let hits = findExisting();
-  if (hits) {
-    return hits;
+  const key = `${checkpoint}:${positionToString(position)}`;
+  const { promise, resolve } = gHitSearches.get(key);
+  if (!resolve) {
+    return promise;
   }
 
-  await scanRecording(checkpoint);
   const endpoint = nextSavedCheckpoint(checkpoint);
-  await sendAsyncManifest({
-    shouldSkip: () => !!findExisting(),
-    contents() {
-      return {
-        kind: "findHits",
-        position,
-        startpoint: checkpoint,
-        endpoint,
-      };
-    },
-    onFinished(_, hits) {
-      if (!gHitSearches.has(checkpoint)) {
-        gHitSearches.set(checkpoint, []);
-      }
-      const checkpointHits = gHitSearches.get(checkpoint);
-      checkpointHits.push({ position, hits });
-    },
-    scanCheckpoint: checkpoint,
+  const child = await scanRecording(checkpoint);
+  const hits = await child.sendManifest({
+    kind: "findHits",
+    position,
+    startpoint: checkpoint,
+    endpoint,
   });
 
-  hits = findExisting();
-  assert(hits);
+  resolve(hits);
   return hits;
-
-  function findExisting() {
-    const checkpointHits = gHitSearches.get(checkpoint);
-    if (!checkpointHits) {
-      return null;
-    }
-    const entry = checkpointHits.find(
-      ({ position: existingPosition, hits }) => {
-        return positionEquals(position, existingPosition);
-      }
-    );
-    return entry ? entry.hits : null;
-  }
 }
 
 // Asynchronously find all hits on a breakpoint's position.
 async function findBreakpointHits(checkpoint, position) {
   if (position.kind == "Break") {
     findHits(checkpoint, position);
   }
 }
@@ -908,149 +852,111 @@ async function findBreakpointHits(checkp
 // stepping in, and will be used in the future to improve stepping performance.
 //
 // The steps for a frame are the list of execution points for breakpoint
 // positions traversed when executing a particular script frame, from the
 // initial EnterFrame to the final OnPop. The steps also include the EnterFrame
 // execution points for any direct callees of the frame.
 
 // Map from point strings to the steps which contain them.
-const gFrameSteps = new Map();
+const gFrameSteps = new PromiseMap();
 
 // Map from frame entry point strings to the parent frame's entry point.
-const gParentFrames = new Map();
+const gParentFrames = new PromiseMap();
 
 // Find all the steps in the frame which point is part of. This returns a
 // promise that resolves with the steps that were found.
 async function findFrameSteps(point) {
   if (!point.position) {
     return null;
   }
 
   assert(
     point.position.kind == "EnterFrame" ||
       point.position.kind == "OnStep" ||
       point.position.kind == "OnPop"
   );
 
-  let steps = findExisting();
-  if (steps) {
-    return steps;
+  const { promise, resolve } = gFrameSteps.get(pointToString(point));
+  if (!resolve) {
+    return promise;
   }
 
   // Gather information which the child which did the scan can use to figure out
   // the different frame steps.
   const info = gControl.sendRequestMainChild({
     type: "frameStepsInfo",
     script: point.position.script,
   });
 
   const checkpoint = getSavedCheckpoint(point.checkpoint);
-  await scanRecording(checkpoint);
-  await sendAsyncManifest({
-    shouldSkip: () => !!findExisting(),
-    contents: () => ({ kind: "findFrameSteps", targetPoint: point, ...info }),
-    onFinished: (_, steps) => {
-      for (const p of steps) {
-        if (p.position.frameIndex == point.position.frameIndex) {
-          gFrameSteps.set(pointToString(p), steps);
-        } else {
-          assert(p.position.kind == "EnterFrame");
-          gParentFrames.set(pointToString(p), steps[0]);
-        }
-      }
-    },
-    scanCheckpoint: checkpoint,
+  const child = await scanRecording(checkpoint);
+  const steps = await child.sendManifest({
+    kind: "findFrameSteps",
+    targetPoint: point,
+    ...info,
   });
 
-  steps = findExisting();
-  assert(steps);
-  return steps;
+  for (const p of steps) {
+    if (p.position.frameIndex == point.position.frameIndex) {
+      gFrameSteps.set(pointToString(p), steps);
+    } else {
+      assert(p.position.kind == "EnterFrame");
+      gParentFrames.set(pointToString(p), steps[0]);
+    }
+  }
 
-  function findExisting() {
-    return gFrameSteps.get(pointToString(point));
-  }
+  resolve(steps);
+  return steps;
 }
 
 async function findParentFrameEntryPoint(point) {
   assert(point.position.kind == "EnterFrame");
   assert(point.position.frameIndex > 0);
 
-  let parentPoint = findExisting();
-  if (parentPoint) {
-    return parentPoint;
+  const { promise, resolve } = gParentFrames.get(pointToString(point));
+  if (!resolve) {
+    return promise;
   }
 
   const checkpoint = getSavedCheckpoint(point.checkpoint);
-  await scanRecording(checkpoint);
-  await sendAsyncManifest({
-    shouldSkip: () => !!findExisting(),
-    contents: () => ({ kind: "findParentFrameEntryPoint", point }),
-    onFinished: (_, { parentPoint }) => {
-      gParentFrames.set(pointToString(point), parentPoint);
-    },
-    scanCheckpoint: checkpoint,
+  const child = await scanRecording(checkpoint);
+  const { parentPoint } = await child.sendManifest({
+    kind: "findParentFrameEntryPoint",
+    point,
   });
 
-  parentPoint = findExisting();
-  assert(parentPoint);
+  resolve(parentPoint);
   return parentPoint;
-
-  function findExisting() {
-    return gParentFrames.get(pointToString(point));
-  }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Pause Data
 ////////////////////////////////////////////////////////////////////////////////
 
 const gPauseData = new Map();
 const gQueuedPauseData = new Set();
 
 // Cached points indicate messages where we have gathered pause data. These are
 // shown differently in the UI.
 const gCachedPoints = new Map();
 
-async function queuePauseData({
-  point,
-  snapshot,
-  trackCached,
-  shouldSkip: shouldSkipCallback,
-}) {
+async function queuePauseData({ point, trackCached }) {
   if (gQueuedPauseData.has(pointToString(point))) {
     return;
   }
   gQueuedPauseData.add(pointToString(point));
 
   await waitForFlushed(point.checkpoint);
 
-  await sendAsyncManifest({
-    shouldSkip() {
-      if (maybeGetPauseData(point)) {
-        return true;
-      }
+  const child = await ensureLeafChild(point, Priority.LOW);
+  const data = await child.sendManifest({ kind: "getPauseData" });
 
-      return shouldSkipCallback && shouldSkipCallback();
-    },
-    contents() {
-      return { kind: "getPauseData" };
-    },
-    onFinished(child, data) {
-      if (!data.restoredSnapshot) {
-        addPauseData(point, data, trackCached);
-        child.divergedFromRecording = true;
-      }
-    },
-    point,
-    snapshot,
-    expectedDuration: 250,
-    priority: Priority.LOW,
-    mightRewind: true,
-  });
+  addPauseData(point, data, trackCached);
+  terminateRunningLeafChild(child);
 }
 
 function addPauseData(point, data, trackCached) {
   if (data.paintData) {
     // Atomize paint data strings to ensure that we don't store redundant
     // strings for execution points with the same paint data.
     data.paintData = RecordReplayControl.atomize(data.paintData);
   }
@@ -1077,132 +983,80 @@ function cachedPoints() {
 ////////////////////////////////////////////////////////////////////////////////
 
 // The pause mode classifies the current state of the debugger.
 const PauseModes = {
   // The main child is the active child, and is either paused or actively
   // recording. gPausePoint is the last point the main child reached.
   RUNNING: "RUNNING",
 
-  // gActiveChild is a replaying child paused at gPausePoint.
+  // gActiveChild is a replaying child paused or being taken to gPausePoint.
   PAUSED: "PAUSED",
 
-  // gActiveChild is a replaying child being taken to gPausePoint. The debugger
-  // is considered to be paused, but interacting with the child must wait until
-  // it arrives.
-  ARRIVING: "ARRIVING",
-
   // gActiveChild is null, and we are looking for the last breakpoint hit prior
   // to or following gPausePoint, at which we will pause.
   RESUMING_BACKWARD: "RESUMING_BACKWARD",
   RESUMING_FORWARD: "RESUMING_FORWARD",
 };
 
 // Current pause mode.
 let gPauseMode = PauseModes.RUNNING;
 
-// In PAUSED or ARRIVING modes, the point we are paused at or sending the active
-// child to.
+// In PAUSED mode, the point we are paused at or sending the active child to.
 let gPausePoint = null;
 
 // In PAUSED mode, any debugger requests that have been sent to the child.
-// In ARRIVING mode, the requests must be sent once the child arrives.
 const gDebuggerRequests = [];
 
 function addDebuggerRequest(request) {
   gDebuggerRequests.push({
     request,
     stack: Error().stack,
   });
 }
 
 function setPauseState(mode, point, child) {
   assert(mode);
-  const idString = child ? ` #${child.id}` : "";
+  const idString = child ? ` ${child.id}` : "";
   dumpv(`SetPauseState ${mode} ${JSON.stringify(point)}${idString}`);
 
+  if (gActiveChild && gActiveChild != gMainChild && gActiveChild != child) {
+    terminateRunningLeafChild(gActiveChild);
+  }
+
   gPauseMode = mode;
   gPausePoint = point;
   gActiveChild = child;
 
-  if (mode == PauseModes.ARRIVING) {
+  if (mode == PauseModes.PAUSED) {
     simulateNearbyNavigation();
   }
-
-  pokeChildrenSoon();
 }
 
 // Mark the debugger as paused, and asynchronously send a child to the pause
 // point.
 function setReplayingPauseTarget(point) {
   assert(!gDebuggerRequests.length);
-  setPauseState(PauseModes.ARRIVING, point, closestChild(point.checkpoint));
+
+  const child = newLeafChild(point);
+  setPauseState(PauseModes.PAUSED, point, child);
 
   gDebugger._onPause();
-
   findFrameSteps(point);
 }
 
-function sendActiveChildToPausePoint() {
-  assert(gActiveChild.paused);
-
-  switch (gPauseMode) {
-    case PauseModes.PAUSED:
-      assert(pointEquals(gActiveChild.pausePoint(), gPausePoint));
-      return;
-
-    case PauseModes.ARRIVING:
-      if (pointEquals(gActiveChild.pausePoint(), gPausePoint)) {
-        setPauseState(PauseModes.PAUSED, gPausePoint, gActiveChild);
+// Synchronously bring a new leaf child to the current pause point.
+function bringNewReplayingChildToPausePoint() {
+  const child = newLeafChild(gPausePoint);
+  setPauseState(PauseModes.PAUSED, gPausePoint, child);
 
-        // Send any debugger requests the child is considered to have received.
-        if (gDebuggerRequests.length) {
-          const child = gActiveChild;
-          child.sendManifest({
-            contents: {
-              kind: "batchDebuggerRequest",
-              requests: gDebuggerRequests.map(r => r.request),
-            },
-            onFinished(finishData) {
-              if (finishData.divergedFromRecording) {
-                child.divergedFromRecording = true;
-              }
-            },
-          });
-        }
-      } else {
-        maybeReachPoint(gActiveChild, gPausePoint);
-      }
-      return;
-
-    default:
-      ThrowError(`Unexpected pause mode: ${gPauseMode}`);
-  }
-}
-
-function waitUntilPauseFinishes() {
-  assert(gActiveChild);
-
-  if (gActiveChild == gMainChild) {
-    gActiveChild.waitUntilPaused(true);
-    return;
-  }
-
-  while (gPauseMode != PauseModes.PAUSED) {
-    gActiveChild.waitUntilPaused();
-    pokeChild(gActiveChild);
-  }
-
-  gActiveChild.waitUntilPaused();
-}
-
-// Synchronously send a child to the specific point and pause.
-function pauseReplayingChild(child, point) {
-  setPauseState(PauseModes.ARRIVING, point, child);
-  waitUntilPauseFinishes();
+  child.sendManifest({
+    kind: "batchDebuggerRequest",
+    requests: gDebuggerRequests.map(r => r.request),
+  });
 }
 
 // Find the point where the debugger should pause when running forward or
 // backward from a point and using a given set of breakpoints. Returns null if
 // there is no point to pause at before hitting the beginning or end of the
 // recording.
 async function resumeTarget(point, forward, breakpoints) {
   let startCheckpoint = point.checkpoint;
@@ -1211,17 +1065,17 @@ async function resumeTarget(point, forwa
     if (startCheckpoint == InvalidCheckpointId) {
       return null;
     }
   }
   startCheckpoint = getSavedCheckpoint(startCheckpoint);
 
   let checkpoint = startCheckpoint;
   for (; ; forward ? checkpoint++ : checkpoint--) {
-    if ([InvalidCheckpointId, gLastFlushCheckpoint].includes(checkpoint)) {
+    if ([InvalidCheckpointId, gLastSavedCheckpoint].includes(checkpoint)) {
       return null;
     }
 
     if (!gCheckpoints[checkpoint].saved) {
       continue;
     }
 
     const hits = [];
@@ -1310,17 +1164,16 @@ function resume(forward) {
     return;
   }
   setPauseState(
     forward ? PauseModes.RESUMING_FORWARD : PauseModes.RESUMING_BACKWARD,
     gPausePoint,
     null
   );
   finishResume();
-  pokeChildren();
 }
 
 // Synchronously bring the active child to the specified execution point.
 function timeWarp(point) {
   gDebuggerRequests.length = 0;
   setReplayingPauseTarget(point);
   Services.cpmm.sendAsyncMessage("TimeWarpFinished");
 }
@@ -1331,71 +1184,47 @@ function timeWarp(point) {
 
 // The maximum number of crashes which we can recover from.
 const MaxCrashes = 4;
 
 // How many child processes have crashed.
 let gNumCrashes = 0;
 
 // eslint-disable-next-line no-unused-vars
-function ChildCrashed(id) {
+function ChildCrashed(rootId, forkId) {
+  const id = processId(rootId, forkId);
   dumpv(`Child Crashed: ${id}`);
 
   // In principle we can recover when any replaying child process crashes.
   // For simplicity, there are some cases where we don't yet try to recover if
   // a replaying process crashes.
   //
   // - It is the main child, and running forward through the recording. While it
   //   could crash here just as easily as any other replaying process, any crash
   //   will happen early on and won't interrupt a long-running debugger session.
   //
   // - It is the active child, and is paused at gPausePoint. It must have
   //   crashed while processing a debugger request, which is unlikely.
-  const child = gReplayingChildren[id];
+  const child = gChildren.get(id);
   if (
     !child ||
     !child.manifest ||
     (child == gActiveChild && gPauseMode == PauseModes.PAUSED)
   ) {
     ThrowError("Cannot recover from crashed child");
   }
 
   if (++gNumCrashes > MaxCrashes) {
     ThrowError("Too many crashes");
   }
 
-  delete gReplayingChildren[id];
-  child.crashed = true;
-
-  // Spawn a new child to replace the one which just crashed.
-  const newChild = spawnReplayingChild();
-  pokeChildSoon(newChild);
-
-  // The new child should save the same checkpoints as the old one.
-  for (const checkpoint of child.savedCheckpoints) {
-    newChild.addSavedCheckpoint(checkpoint);
-  }
+  child.terminate();
 
-  // Any regions the old child scanned need to be rescanned.
-  for (const checkpoint of child.scannedCheckpoints) {
-    scanRecording(checkpoint);
-  }
-
-  // Requeue any async manifest the child was processing.
-  if (child.asyncManifest) {
-    sendAsyncManifest(child.asyncManifest);
-  }
-
-  // If the active child crashed while heading to the pause point, pick another
-  // child to head to the pause point.
-  if (child == gActiveChild) {
-    assert(gPauseMode == PauseModes.ARRIVING);
-    gActiveChild = closestChild(gPausePoint.checkpoint);
-    pokeChildSoon(gActiveChild);
-  }
+  // Crash recovery does not yet deal with fork based rewinding.
+  ThrowError("Fork based crash recovery NYI");
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Simulated Navigation
 ////////////////////////////////////////////////////////////////////////////////
 
 // When the user is paused somewhere in the recording, we want to obtain pause
 // data for points which they can get to via the UI. This includes all messages
@@ -1433,16 +1262,17 @@ async function findFrameEntryPoint(point
   // when a frame was pushed on the stack. Instead, find the first point in the
   // frame which is a breakpoint site.
   assert(point.position.kind == "EnterFrame");
   const steps = await findFrameSteps(point);
   assert(pointEquals(steps[0], point));
   return steps[1];
 }
 
+// eslint-disable-next-line complexity
 async function simulateSteppingNavigation(point, count, frameCount, last) {
   if (!count || !point.position) {
     return;
   }
   const { script } = point.position;
   const dbgScript = gDebugger._getScript(script);
 
   const steps = await findFrameSteps(point);
@@ -1479,17 +1309,19 @@ async function simulateSteppingNavigatio
       if (p.position.script != script) {
         // Currently, the debugger will stop at the EnterFrame site and then run
         // forward to the first breakpoint site before pausing. We need pause
         // data from both points, unfortunately.
         queuePauseData({ point: p, snapshot: steps[0] });
 
         const np = await findFrameEntryPoint(p);
         queuePauseData({ point: np, snapshot: steps[0] });
-        findHits(getSavedCheckpoint(point.checkpoint), np.position);
+        if (canFindHits(np.position)) {
+          findHits(getSavedCheckpoint(point.checkpoint), np.position);
+        }
         simulateSteppingNavigation(np, count - 1, frameCount - 1, "stepIn");
         break;
       }
     }
   }
 
   if (
     frameCount &&
@@ -1504,17 +1336,19 @@ async function simulateSteppingNavigatio
     const parentEntryPoint = await findParentFrameEntryPoint(steps[0]);
     const parentSteps = await findFrameSteps(parentEntryPoint);
     for (let i = 0; i < parentSteps.length; i++) {
       const p = parentSteps[i];
       if (pointPrecedes(point, p)) {
         // When stepping out we will stop at the next breakpoint site,
         // and do not need a point that is a stepping target.
         queuePauseData({ point: p, snapshot: parentSteps[0] });
-        findHits(getSavedCheckpoint(point.checkpoint), p.position);
+        if (canFindHits(p.position)) {
+          findHits(getSavedCheckpoint(point.checkpoint), p.position);
+        }
         simulateSteppingNavigation(p, count - 1, frameCount - 1, "stepOut");
         break;
       }
     }
   }
 
   function isStepOverTarget(p) {
     const { kind, offset } = p.position;
@@ -1555,39 +1389,29 @@ async function evaluateLogpoint({
   point,
   text,
   condition,
   callback,
   snapshot,
   fast,
 }) {
   assert(point);
-  await sendAsyncManifest({
-    shouldSkip: () => false,
-    contents() {
-      return { kind: "hitLogpoint", text, condition, fast };
-    },
-    onFinished(child, { result, resultData, restoredSnapshot }) {
-      if (restoredSnapshot) {
-        callback(point, ["Recording divergence evaluating logpoint"]);
-      } else {
-        if (result) {
-          callback(point, result, resultData);
-        }
-        if (!fast) {
-          child.divergedFromRecording = true;
-        }
-      }
-    },
-    point,
-    snapshot,
-    expectedDuration: 250,
-    priority: Priority.MEDIUM,
-    mightRewind: true,
+
+  const child = await ensureLeafChild(point, Priority.MEDIUM);
+  const { result, resultData } = await child.sendManifest({
+    kind: "hitLogpoint",
+    text,
+    condition,
+    fast,
   });
+  terminateRunningLeafChild(child);
+
+  if (result) {
+    callback(point, result, resultData);
+  }
 }
 
 // Asynchronously invoke a logpoint's callback with all results from hitting
 // the logpoint in the range of the recording covered by checkpoint.
 async function findLogpointHits(
   checkpoint,
   { position, text, condition, messageCallback, validCallback }
 ) {
@@ -1649,38 +1473,35 @@ async function findLogpointHits(
 ////////////////////////////////////////////////////////////////////////////////
 // Event Breakpoints
 ////////////////////////////////////////////////////////////////////////////////
 
 // Event kinds which will be logged. For now this set can only grow, as we don't
 // have a way to remove old event logpoints from the client.
 const gLoggedEvents = [];
 
-const gEventFrameEntryPoints = new Map();
+const gEventFrameEntryPoints = new PromiseMap();
 
 async function findEventFrameEntry(checkpoint, progress) {
-  if (gEventFrameEntryPoints.has(progress)) {
-    return gEventFrameEntryPoints.get(progress);
+  const { promise, resolve } = gEventFrameEntryPoints.get(progress);
+  if (!resolve) {
+    return promise;
   }
 
   const savedCheckpoint = getSavedCheckpoint(checkpoint);
-  await scanRecording(savedCheckpoint);
-  await sendAsyncManifest({
-    shouldSkip: () => gEventFrameEntryPoints.has(progress),
-    contents: () => ({ kind: "findEventFrameEntry", checkpoint, progress }),
-    onFinished: (_, { rv }) => gEventFrameEntryPoints.set(progress, rv),
-    scanCheckpoint: savedCheckpoint,
+  const child = await scanRecording(savedCheckpoint);
+  const { rv } = await child.sendManifest({
+    kind: "findEventFrameEntry",
+    checkpoint,
+    progress,
   });
 
-  const point = gEventFrameEntryPoints.get(progress);
-  if (!point) {
-    return null;
-  }
-
-  return findFrameEntryPoint(point);
+  const point = await findFrameEntryPoint(rv);
+  resolve(point);
+  return point;
 }
 
 async function findEventLogpointHits(checkpoint, event, callback) {
   for (const info of getCheckpointInfo(checkpoint).events) {
     if (info.event == event) {
       const point = await findEventFrameEntry(info.checkpoint, info.progress);
       if (point) {
         callback(point, ["Loading..."]);
@@ -1763,100 +1584,96 @@ function handleResumeManifestResponse({
 
 // If necessary, continue executing in the main child.
 function maybeResumeRecording() {
   if (gActiveChild != gMainChild) {
     return;
   }
 
   if (
-    !gLastFlushCheckpoint ||
-    timeSinceCheckpoint(gLastFlushCheckpoint) >= FlushMs
+    !gLastSavedCheckpoint ||
+    timeSinceCheckpoint(gLastSavedCheckpoint) >= FlushMs
   ) {
     ensureFlushed();
   }
 
   const checkpoint = gMainChild.pausePoint().checkpoint;
   if (!gMainChild.recording && checkpoint == gRecordingEndpoint) {
     ensureFlushed();
     Services.cpmm.sendAsyncMessage("HitRecordingEndpoint");
     if (gDebugger) {
       gDebugger._hitRecordingBoundary();
     }
     return;
   }
-  gMainChild.sendManifest({
-    contents: {
+  gMainChild.sendManifest(
+    {
       kind: "resume",
       breakpoints: gBreakpoints,
-      pauseOnDebuggerStatement: true,
+      pauseOnDebuggerStatement: !!gDebugger,
     },
-    onFinished(response) {
+    response => {
       handleResumeManifestResponse(response);
 
       gPausePoint = gMainChild.pausePoint();
       if (gDebugger) {
         gDebugger._onPause();
       } else {
         Services.tm.dispatchToMainThread(maybeResumeRecording);
       }
-    },
-  });
+    }
+  );
 }
 
 // Resolve callbacks for any promises waiting on the recording to be flushed.
 const gFlushWaiters = [];
 
 function waitForFlushed(checkpoint) {
-  if (checkpoint < gLastFlushCheckpoint) {
+  if (checkpoint < gLastSavedCheckpoint) {
     return undefined;
   }
   return new Promise(resolve => {
     gFlushWaiters.push(resolve);
   });
 }
 
 let gLastFlushTime = Date.now();
 
 // If necessary, synchronously flush the recording to disk.
 function ensureFlushed() {
   gMainChild.waitUntilPaused(true);
 
   gLastFlushTime = Date.now();
 
-  if (gLastFlushCheckpoint == gMainChild.pauseCheckpoint()) {
+  if (gLastSavedCheckpoint == gMainChild.pauseCheckpoint()) {
     return;
   }
 
   if (gMainChild.recording) {
-    gMainChild.sendManifest({
-      contents: { kind: "flushRecording" },
-      onFinished() {},
-    });
+    gMainChild.sendManifest({ kind: "flushRecording" });
     gMainChild.waitUntilPaused();
   }
 
-  const oldFlushCheckpoint = gLastFlushCheckpoint || FirstCheckpointId;
-  gLastFlushCheckpoint = gMainChild.pauseCheckpoint();
-
   // We now have a usable recording for replaying children, so spawn them if
   // necessary.
-  if (gReplayingChildren.length == 0) {
-    spawnReplayingChildren();
+  if (!gTrunkChild) {
+    spawnTrunkChild();
   }
 
+  const oldSavedCheckpoint = gLastSavedCheckpoint || FirstCheckpointId;
+  addSavedCheckpoint(gMainChild.pauseCheckpoint());
+
   // Checkpoints where the recording was flushed to disk are saved. This allows
   // the recording to be scanned as soon as it has been flushed.
-  addSavedCheckpoint(gLastFlushCheckpoint);
 
   // Flushing creates a new region of the recording for replaying children
   // to scan.
   forSavedCheckpointsInRange(
-    oldFlushCheckpoint,
-    gLastFlushCheckpoint,
+    oldSavedCheckpoint,
+    gLastSavedCheckpoint,
     checkpoint => {
       scanRecording(checkpoint);
 
       // Scan for breakpoint and logpoint hits in this new region.
       gBreakpoints.forEach(position =>
         findBreakpointHits(checkpoint, position)
       );
       gLogpoints.forEach(logpoint => findLogpointHits(checkpoint, logpoint));
@@ -1865,38 +1682,36 @@ function ensureFlushed() {
       }
     }
   );
 
   for (const waiter of gFlushWaiters) {
     waiter();
   }
   gFlushWaiters.length = 0;
-
-  pokeChildren();
 }
 
 const CheckFlushMs = 1000;
 
 setInterval(() => {
   // Periodically make sure the recording is flushed. If the tab is sitting
   // idle we still want to keep the recording up to date.
   const elapsed = Date.now() - gLastFlushTime;
   if (
     elapsed > CheckFlushMs &&
     gMainChild.lastPausePoint &&
-    gMainChild.lastPausePoint.checkpoint != gLastFlushCheckpoint
+    gMainChild.lastPausePoint.checkpoint != gLastSavedCheckpoint
   ) {
     ensureFlushed();
   }
 
   // Ping children that are executing manifests to ensure they haven't hanged.
-  for (const child of gReplayingChildren) {
-    if (child) {
-      RecordReplayControl.maybePing(child.id);
+  for (const child of gUnpausedChildren) {
+    if (!child.recording) {
+      child.maybePing();
     }
   }
 }, 1000);
 
 // eslint-disable-next-line no-unused-vars
 function BeforeSaveRecording() {
   if (gActiveChild == gMainChild) {
     // The recording might not be up to date, ensure it flushes after pausing.
@@ -1906,100 +1721,39 @@ function BeforeSaveRecording() {
 
 // eslint-disable-next-line no-unused-vars
 function AfterSaveRecording() {
   Services.cpmm.sendAsyncMessage("SaveRecordingFinished");
 }
 
 let gRecordingEndpoint;
 
-function setMainChild() {
+async function setMainChild() {
   assert(!gMainChild.recording);
 
-  gMainChild.sendManifest({
-    contents: { kind: "setMainChild" },
-    onFinished({ endpoint }) {
-      gRecordingEndpoint = endpoint;
-      Services.tm.dispatchToMainThread(maybeResumeRecording);
-    },
-  });
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Child Management
-////////////////////////////////////////////////////////////////////////////////
-
-function spawnReplayingChild() {
-  const id = RecordReplayControl.spawnReplayingChild();
-  const child = new ChildProcess(id, false);
-  gReplayingChildren[id] = child;
-  return child;
-}
-
-// How many replaying children to spawn. This should be a pref instead...
-const NumReplayingChildren = 4;
-
-function spawnReplayingChildren() {
-  if (RecordReplayControl.canRewind()) {
-    for (let i = 0; i < NumReplayingChildren; i++) {
-      spawnReplayingChild();
-    }
-  }
-  addSavedCheckpoint(FirstCheckpointId);
-}
-
-// eslint-disable-next-line no-unused-vars
-function Initialize(recordingChildId) {
-  try {
-    if (recordingChildId != undefined) {
-      gMainChild = new ChildProcess(recordingChildId, true);
-    } else {
-      // If there is no recording child, we have now initialized enough state
-      // that we can start spawning replaying children.
-      const id = RecordReplayControl.spawnReplayingChild();
-      gMainChild = new ChildProcess(id, false);
-      spawnReplayingChildren();
-    }
-    gActiveChild = gMainChild;
-    return gControl;
-  } catch (e) {
-    dump(`ERROR: Initialize threw exception: ${e}\n`);
-  }
-}
-
-// eslint-disable-next-line no-unused-vars
-function ManifestFinished(id, response) {
-  try {
-    dumpv(`ManifestFinished #${id} ${stringify(response)}`);
-    const child = lookupChild(id);
-    if (child) {
-      child.manifestFinished(response);
-    } else {
-      // Ignore messages from child processes that we have marked as crashed.
-    }
-  } catch (e) {
-    dump(`ERROR: ManifestFinished threw exception: ${e} ${e.stack}\n`);
-  }
+  const { endpoint } = await gMainChild.sendManifest({ kind: "setMainChild" });
+  gRecordingEndpoint = endpoint;
+  Services.tm.dispatchToMainThread(maybeResumeRecording);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Debugger Operations
 ////////////////////////////////////////////////////////////////////////////////
 
 // From the debugger's perspective, there is a single target to interact with,
 // represented by gActiveChild. The details of the various children the control
 // system is managing are hidden away. This object describes the interface which
 // the debugger uses to access the control system.
 const gControl = {
   // Get the current point where the active child is paused, or null.
   pausePoint() {
     if (gActiveChild && gActiveChild == gMainChild) {
       return gActiveChild.paused ? gActiveChild.pausePoint() : null;
     }
-    if (gPauseMode == PauseModes.PAUSED || gPauseMode == PauseModes.ARRIVING) {
+    if (gPauseMode == PauseModes.PAUSED) {
       return gPausePoint;
     }
     return null;
   },
 
   lastPausePoint() {
     return gPausePoint;
   },
@@ -2075,79 +1829,66 @@ const gControl = {
   maybeSwitchToReplayingChild() {
     assert(gControl.pausePoint());
     if (gActiveChild == gMainChild && RecordReplayControl.canRewind()) {
       const point = gActiveChild.pausePoint();
 
       if (point.position) {
         // We can only flush the recording at checkpoints, so we need to send the
         // main child forward and pause/flush ASAP.
-        gMainChild.sendManifest({
-          contents: {
+        gMainChild.sendManifest(
+          {
             kind: "resume",
             breakpoints: [],
             pauseOnDebuggerStatement: false,
           },
-          onFinished(response) {
-            handleResumeManifestResponse(response);
-          },
-        });
+          handleResumeManifestResponse
+        );
         gMainChild.waitUntilPaused(true);
       }
 
       ensureFlushed();
-      const child = closestChild(point);
-      pauseReplayingChild(child, point);
+      bringNewReplayingChildToPausePoint();
     }
   },
 
   // Synchronously send a debugger request to a paused active child, returning
   // the response.
   sendRequest(request) {
-    waitUntilPauseFinishes();
-
     let data;
-    gActiveChild.sendManifest({
-      contents: { kind: "debuggerRequest", request },
-      onFinished(finishData) {
+    gActiveChild.sendManifest(
+      { kind: "debuggerRequest", request },
+      finishData => {
         data = finishData;
-      },
-      mightRewind: true,
-    });
-    gActiveChild.waitUntilPaused();
-
-    if (data.restoredSnapshot) {
-      // The child had an unhandled recording diverge and restored an earlier
-      // checkpoint. Restore the child to the point it should be paused at and
-      // fill its paused state back in by resending earlier debugger requests.
-      pauseReplayingChild(gActiveChild, gPausePoint);
-      return { unhandledDivergence: true };
+      }
+    );
+    while (!data) {
+      gActiveChild.waitUntilPaused();
     }
 
-    if (data.divergedFromRecording) {
-      // Remember whether the child diverged from the recording.
-      gActiveChild.divergedFromRecording = true;
+    if (data.unhandledDivergence) {
+      bringNewReplayingChildToPausePoint();
+    } else {
+      addDebuggerRequest(request);
     }
-
-    addDebuggerRequest(request);
     return data.response;
   },
 
   // Synchronously send a debugger request to the main child, which will always
   // be at the end of the recording and can receive requests even when the
   // active child is not currently paused.
   sendRequestMainChild(request) {
     gMainChild.waitUntilPaused(true);
     let data;
-    gMainChild.sendManifest({
-      contents: { kind: "debuggerRequest", request },
-      onFinished(finishData) {
+    gMainChild.sendManifest(
+      { kind: "debuggerRequest", request },
+      finishData => {
         data = finishData;
-      },
-    });
+      }
+    );
     gMainChild.waitUntilPaused();
     assert(!data.divergedFromRecording);
     return data.response;
   },
 
   resume,
   timeWarp,
 
@@ -2164,31 +1905,35 @@ const gControl = {
   unscannedRegions,
   cachedPoints,
 
   debuggerRequests() {
     return gDebuggerRequests;
   },
 
   getPauseDataAndRepaint() {
-    // If the child has not arrived at the pause point yet, see if there is
-    // cached pause data for this point already which we can immediately return.
-    if (gPauseMode == PauseModes.ARRIVING && !gDebuggerRequests.length) {
+    // Use cached pause data if possible, which we can immediately return
+    // without waiting for the child to arrive at the pause point.
+    if (!gDebuggerRequests.length) {
       const data = maybeGetPauseData(gPausePoint);
       if (data) {
         // After the child pauses, it will need to generate the pause data so
         // that any referenced objects will be instantiated.
+        gActiveChild.sendManifest({
+          kind: "debuggerRequest",
+          request: { type: "pauseData" },
+        });
         addDebuggerRequest({ type: "pauseData" });
         RecordReplayControl.hadRepaint(data.paintData);
         return data;
       }
     }
     gControl.maybeSwitchToReplayingChild();
     const data = gControl.sendRequest({ type: "pauseData" });
-    if (data.unhandledDivergence) {
+    if (!data) {
       RecordReplayControl.clearGraphics();
     } else {
       addPauseData(gPausePoint, data, /* trackCached */ true);
       if (data.paintData) {
         RecordReplayControl.hadRepaint(data.paintData);
       }
     }
     return data;
@@ -2208,81 +1953,16 @@ const gControl = {
       const { debuggerStatements } = getCheckpointInfo(checkpoint);
       return pointArrayIncludes(debuggerStatements, point);
     }
     return false;
   },
 };
 
 ///////////////////////////////////////////////////////////////////////////////
-// Statistics
-///////////////////////////////////////////////////////////////////////////////
-
-let lastDumpTime = Date.now();
-
-function maybeDumpStatistics() {
-  const now = Date.now();
-  if (now - lastDumpTime < 5000) {
-    return;
-  }
-  lastDumpTime = now;
-
-  let delayTotal = 0;
-  let unscannedTotal = 0;
-  let timeTotal = 0;
-  let scanDurationTotal = 0;
-
-  forAllSavedCheckpoints(checkpoint => {
-    const checkpointTime = timeForSavedCheckpoint(checkpoint);
-    const info = getCheckpointInfo(checkpoint);
-
-    timeTotal += checkpointTime;
-    if (info.scanTime) {
-      delayTotal += checkpointTime * (info.scanTime - info.assignTime);
-      scanDurationTotal += info.scanDuration;
-    } else {
-      unscannedTotal += checkpointTime;
-    }
-  });
-
-  const memoryUsage = [];
-  let totalSaved = 0;
-
-  for (const child of gReplayingChildren) {
-    if (!child) {
-      continue;
-    }
-    totalSaved += child.savedCheckpoints.size;
-    if (!child.lastMemoryUsage) {
-      continue;
-    }
-    for (const [name, value] of Object.entries(child.lastMemoryUsage)) {
-      if (!memoryUsage[name]) {
-        memoryUsage[name] = 0;
-      }
-      memoryUsage[name] += value;
-    }
-  }
-
-  const delay = delayTotal / timeTotal;
-  const overhead = scanDurationTotal / (timeTotal - unscannedTotal);
-
-  dumpv(`Statistics:`);
-  dumpv(`Total recording time: ${timeTotal}`);
-  dumpv(`Unscanned fraction: ${unscannedTotal / timeTotal}`);
-  dumpv(`Average scan delay: ${delay}`);
-  dumpv(`Average scanning overhead: ${overhead}`);
-
-  dumpv(`Saved checkpoints: ${totalSaved}`);
-  for (const [name, value] of Object.entries(memoryUsage)) {
-    dumpv(`Memory ${name}: ${value}`);
-  }
-}
-
-///////////////////////////////////////////////////////////////////////////////
 // Utilities
 ///////////////////////////////////////////////////////////////////////////////
 
 function getPreference(name) {
   return Services.prefs.getBoolPref(`devtools.recordreplay.${name}`);
 }
 
 const loggingFullEnabled = getPreference("loggingFull");
@@ -2330,9 +2010,10 @@ function stringify(object) {
 // eslint-disable-next-line no-unused-vars
 var EXPORTED_SYMBOLS = [
   "Initialize",
   "ConnectDebugger",
   "ManifestFinished",
   "BeforeSaveRecording",
   "AfterSaveRecording",
   "ChildCrashed",
+  "PingResponse",
 ];
--- a/devtools/server/actors/replay/moz.build
+++ b/devtools/server/actors/replay/moz.build
@@ -4,22 +4,25 @@
 # 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/.
 
 DIRS += [
     'utils'
 ]
 
 DevToolsModules(
+    'connection-worker.js',
+    'connection.js',
     'control.js',
     'debugger.js',
     'graphics.js',
     'inspector.js',
     'replay.js',
 )
 
 XPIDL_MODULE = 'devtools_rr'
 
 XPIDL_SOURCES = [
+    'rrIConnection.idl',
     'rrIControl.idl',
     'rrIGraphics.idl',
     'rrIReplay.idl',
 ]
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -3,21 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* eslint-disable spaced-comment, brace-style, indent-legacy, consistent-return */
 
 // This file defines the logic that runs in the record/replay devtools sandbox.
 // This code is loaded into all recording/replaying processes, and responds to
 // requests and other instructions from the middleman via the exported symbols
 // defined at the end of this file.
 //
-// Like all other JavaScript in the recording/replaying process, this code's
-// state is included in memory snapshots and reset when checkpoints are
-// restored. In the process of handling the middleman's requests, however, its
-// state may vary between recording and replaying, or between different
-// replays. As a result, we have to be very careful about performing operations
+// In the process of handling the middleman's requests, state in this file may
+// vary between recording and replaying, or between different replays.
+// As a result, we have to be very careful about performing operations
 // that might interact with the recording --- any time we enter the debuggee
 // and evaluate code or perform other operations.
 // The divergeFromRecording function should be used at any point where such
 // interactions might occur.
 // eslint-disable spaced-comment
 
 "use strict";
 
@@ -40,17 +38,16 @@ Cu.evalInSandbox(
 const {
   Debugger,
   RecordReplayControl,
   Services,
   InspectorUtils,
   CSSRule,
   pointPrecedes,
   pointEquals,
-  pointArrayIncludes,
   findClosestPoint,
 } = sandbox;
 
 const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
 const jsmScope = require("resource://devtools/shared/Loader.jsm");
 const { DebuggerNotificationObserver } = Cu.getGlobalForObject(jsmScope);
 
 const {
@@ -168,46 +165,26 @@ function scriptFrameForIndex(index) {
   }
   return frame;
 }
 
 function isNonNullObject(obj) {
   return obj && (typeof obj == "object" || typeof obj == "function");
 }
 
-function getMemoryUsage() {
-  const memoryKinds = {
-    Generic: [1],
-    Snapshots: [2, 3, 4, 5, 6, 7],
-    ScriptHits: [8],
-  };
-
-  const rv = {};
-  for (const [name, kinds] of Object.entries(memoryKinds)) {
-    let total = 0;
-    kinds.forEach(kind => {
-      total += RecordReplayControl.memoryUsage(kind);
-    });
-    rv[name] = total;
-  }
-  return rv;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Persistent Script State
 ///////////////////////////////////////////////////////////////////////////////
 
 // Association between Debugger.Scripts and their IDs. The indices that this
-// table assigns to scripts are stable across the entire recording, even though
-// this table (like all JS state) is included in snapshots, rolled back when
-// rewinding, and so forth.  In debuggee time, this table only grows (there is
-// no way to remove entries). Scripts created for debugger activity (e.g. eval)
-// are ignored, and off thread compilation is disabled, so this table acquires
-// the same scripts in the same order as we roll back and run forward in the
-// recording.
+// table assigns to scripts are stable across the entire recording. In debuggee
+// time, this table only grows (there is no way to remove entries).
+// Scripts created for debugger activity (e.g. eval) are ignored, and off thread
+// compilation is disabled, so this table acquires the same scripts in the same
+// order as we roll back and run forward in the recording.
 const gScripts = new IdMap();
 
 // Any scripts added since the last checkpoint.
 const gNewScripts = [];
 
 function addScript(script) {
   const id = gScripts.add(script);
   script.setInstrumentationId(id);
@@ -952,42 +929,38 @@ let gManifest = { kind: "primordial" };
 let gManifestStartTime;
 
 // Points of any debugger statements that need to be flushed to the middleman.
 const gNewDebuggerStatements = [];
 
 // Whether to pause on debugger statements when running forward.
 let gPauseOnDebuggerStatement = false;
 
-function ensureRunToPointPositionHandlers({ endpoint, snapshotPoints }) {
+function ensureRunToPointPositionHandlers({ endpoint }) {
   if (gLastCheckpoint == endpoint.checkpoint) {
     assert(endpoint.position);
     ensurePositionHandler(endpoint.position);
   }
-  snapshotPoints.forEach(snapshot => {
-    if (gLastCheckpoint == snapshot.checkpoint && snapshot.position) {
-      ensurePositionHandler(snapshot.position);
-    }
-  });
 }
 
 // Handlers that run when a manifest is first received. This must be specified
 // for all manifests.
 const gManifestStartHandlers = {
   resume({ breakpoints, pauseOnDebuggerStatement }) {
     RecordReplayControl.resumeExecution();
     breakpoints.forEach(ensurePositionHandler);
 
     gPauseOnDebuggerStatement = pauseOnDebuggerStatement;
     dbg.onDebuggerStatement = debuggerStatementHit;
   },
 
-  restoreSnapshot({ numSnapshots }) {
-    RecordReplayControl.restoreSnapshot(numSnapshots);
-    throwError("Unreachable!");
+  fork({ id }) {
+    const point = currentScriptedExecutionPoint() || currentExecutionPoint();
+    RecordReplayControl.fork(id);
+    RecordReplayControl.manifestFinished({ point });
   },
 
   runToPoint(manifest) {
     ensureRunToPointPositionHandlers(manifest);
     RecordReplayControl.resumeExecution();
   },
 
   scanRecording() {
@@ -1112,17 +1085,17 @@ function currentExecutionPoint(position)
   const checkpoint = gLastCheckpoint;
   const progress = RecordReplayControl.progressCounter();
   return { checkpoint, progress, position };
 }
 
 function currentScriptedExecutionPoint() {
   const numFrames = countScriptFrames();
   if (!numFrames) {
-    return null;
+    return undefined;
   }
 
   const index = numFrames - 1;
   const frame = scriptFrameForIndex(index);
   return currentExecutionPoint({
     kind: "OnStep",
     script: gScripts.getId(frame.script),
     offset: frame.offset,
@@ -1147,50 +1120,40 @@ function finishResume(point) {
 
 // Handlers that run after a checkpoint is reached to see if the manifest has
 // finished. This does not need to be specified for all manifests.
 const gManifestFinishedAfterCheckpointHandlers = {
   primordial(_, point) {
     // The primordial manifest runs forward to the first checkpoint, saves it,
     // and then finishes.
     assert(point.checkpoint == FirstCheckpointId);
-    if (!newSnapshot(point)) {
-      return;
-    }
     RecordReplayControl.manifestFinished({ point });
   },
 
   resume(_, point) {
     clearPositionHandlers();
     finishResume(point);
   },
 
-  runToPoint({ endpoint, snapshotPoints }, point) {
+  runToPoint({ endpoint, flushExternalCalls }, point) {
     assert(endpoint.checkpoint >= point.checkpoint);
-    if (pointArrayIncludes(snapshotPoints, point) && !newSnapshot(point)) {
-      return;
-    }
     if (!endpoint.position && point.checkpoint == endpoint.checkpoint) {
+      if (flushExternalCalls) {
+        RecordReplayControl.flushExternalCalls();
+      }
       RecordReplayControl.manifestFinished({ point });
     }
   },
 
-  scanRecording({ endpoint, snapshotPoints }, point) {
+  scanRecording({ endpoint }, point) {
     stopScanningAllScripts();
-    if (pointArrayIncludes(snapshotPoints, point) && !newSnapshot(point)) {
-      return;
-    }
     if (point.checkpoint == endpoint.checkpoint) {
       const duration =
         RecordReplayControl.currentExecutionTime() - gManifestStartTime;
-      RecordReplayControl.manifestFinished({
-        point,
-        duration,
-        memoryUsage: getMemoryUsage(),
-      });
+      RecordReplayControl.manifestFinished({ point, duration });
     }
   },
 };
 
 // Handlers that run after a checkpoint is reached and before execution resumes.
 // This does not need to be specified for all manifests. This is specified
 // separately from gManifestFinishedAfterCheckpointHandlers to ensure that if
 // we finish a manifest after the checkpoint and then start a new one, that new
@@ -1200,17 +1163,17 @@ const gManifestPrepareAfterCheckpointHan
   runToPoint: ensureRunToPointPositionHandlers,
 
   scanRecording({ endpoint }) {
     assert(!endpoint.position);
     startScanningAllScripts();
   },
 };
 
-function processManifestAfterCheckpoint(point, restoredSnapshot) {
+function processManifestAfterCheckpoint(point) {
   if (gManifestFinishedAfterCheckpointHandlers[gManifest.kind]) {
     gManifestFinishedAfterCheckpointHandlers[gManifest.kind](gManifest, point);
   }
 
   if (gManifestPrepareAfterCheckpointHandlers[gManifest.kind]) {
     gManifestPrepareAfterCheckpointHandlers[gManifest.kind](gManifest, point);
   }
 }
@@ -1239,25 +1202,20 @@ function HitCheckpoint(id) {
 // Handlers that run after reaching a position watched by ensurePositionHandler.
 // This must be specified for any manifest that uses ensurePositionHandler.
 const gManifestPositionHandlers = {
   resume(manifest, point) {
     clearPositionHandlers();
     finishResume(point);
   },
 
-  runToPoint({ endpoint, snapshotPoints }, point) {
-    if (pointArrayIncludes(snapshotPoints, point)) {
-      clearPositionHandlers();
-      if (newSnapshot(point)) {
-        ensureRunToPointPositionHandlers({ endpoint, snapshotPoints });
-      }
-    }
+  runToPoint({ endpoint, flushExternalCalls }, point) {
     if (pointEquals(point, endpoint)) {
       clearPositionHandlers();
+      assert(!flushExternalCalls);
       RecordReplayControl.manifestFinished({ point });
     }
   },
 };
 
 function positionHit(position, frame) {
   const point = currentExecutionPoint(position);
 
@@ -1274,28 +1232,16 @@ function debuggerStatementHit() {
   gNewDebuggerStatements.push(point);
 
   if (gPauseOnDebuggerStatement) {
     clearPositionHandlers();
     finishResume(point);
   }
 }
 
-function newSnapshot(point) {
-  if (RecordReplayControl.newSnapshot()) {
-    return true;
-  }
-
-  // After rewinding gManifest won't be correct, so we always mark the current
-  // manifest as finished and rely on the middleman to give us a new one.
-  RecordReplayControl.manifestFinished({ restoredSnapshot: true, point });
-
-  return false;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Handler Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
 function getScriptData(id) {
   const script = gScripts.getObject(id);
   return {
     id,
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/rrIConnection.idl
@@ -0,0 +1,23 @@
+/* -*- 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 when communicating with remote
+// replaying processes over web sockets in the parent process.
+[scriptable, uuid(df6d8e96-4cba-4a1d-893a-1ee19e8d8468)]
+interface rrIConnection : nsISupports {
+  // Supply the callback which will be invoked with the connection ID and an
+  // array buffer when new data is received from a remote process.
+  void Initialize(in jsval callback);
+
+  // Start a new connection with a new remote replaying process, specifying
+  // the channel ID the process will use (unique for the middleman this is
+  // associated with) and returning the globally unique connection ID.
+  long Connect(in long channelId, in AString address);
+
+  // Send message data through a particular connection.
+  void SendMessage(in long connectionId, in jsval buf);
+};
--- a/devtools/server/actors/replay/rrIControl.idl
+++ b/devtools/server/actors/replay/rrIControl.idl
@@ -6,13 +6,15 @@
 #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 ManifestFinished(in long childId, in jsval response);
+  void ManifestFinished(in long rootId, in long forkId, in jsval response);
+  void PingResponse(in long rootId, in long forkId, in long pingId,
+                    in long progress);
   void BeforeSaveRecording();
   void AfterSaveRecording();
-  void ChildCrashed(in long childId);
+  void ChildCrashed(in long rootId, in long forkId);
 };
--- a/mfbt/RecordReplay.cpp
+++ b/mfbt/RecordReplay.cpp
@@ -43,16 +43,18 @@ namespace recordreplay {
   Macro(DefineRecordReplayControlObject, bool, (void* aCx, void* aObj),        \
         (aCx, aObj))
 
 #define FOR_EACH_INTERFACE_VOID(Macro)                                         \
   Macro(InternalBeginOrderedAtomicAccess, (const void* aValue), (aValue))      \
   Macro(InternalEndOrderedAtomicAccess, (), ())                                \
   Macro(InternalBeginPassThroughThreadEvents, (), ())                          \
   Macro(InternalEndPassThroughThreadEvents, (), ())                            \
+  Macro(InternalBeginPassThroughThreadEventsWithLocalReplay, (), ())           \
+  Macro(InternalEndPassThroughThreadEventsWithLocalReplay, (), ())             \
   Macro(InternalBeginDisallowThreadEvents, (), ())                             \
   Macro(InternalEndDisallowThreadEvents, (), ())                               \
   Macro(InternalRecordReplayBytes, (void* aData, size_t aSize),                \
         (aData, aSize))                                                        \
   Macro(InternalInvalidateRecording, (const char* aWhy), (aWhy))               \
   Macro(InternalDestroyPLDHashTableCallbacks, (const PLDHashTableOps* aOps),   \
         (aOps))                                                                \
   Macro(InternalMovePLDHashTableContents,                                      \
@@ -87,17 +89,20 @@ FOR_EACH_INTERFACE(DECLARE_SYMBOL)
 FOR_EACH_INTERFACE_VOID(DECLARE_SYMBOL_VOID)
 
 #undef DECLARE_SYMBOL
 #undef DECLARE_SYMBOL_VOID
 
 static void* LoadSymbol(const char* aName) {
 #ifdef ENABLE_RECORD_REPLAY
   void* rv = dlsym(RTLD_DEFAULT, aName);
-  MOZ_RELEASE_ASSERT(rv);
+  if (!rv) {
+    fprintf(stderr, "Record/Replay LoadSymbol failed: %s\n", aName);
+    MOZ_CRASH("LoadSymbol");
+  }
   return rv;
 #else
   return nullptr;
 #endif
 }
 
 void Initialize(int aArgc, char* aArgv[]) {
   // Only initialize if the right command line option was specified.
--- a/mfbt/RecordReplay.h
+++ b/mfbt/RecordReplay.h
@@ -142,16 +142,38 @@ struct MOZ_RAII AutoEnsurePassThroughThr
   ~AutoEnsurePassThroughThreadEvents() {
     if (!mPassedThrough) EndPassThroughThreadEvents();
   }
 
  private:
   bool mPassedThrough;
 };
 
+// Mark a region where thread events are passed through when locally replaying.
+// Replaying processes can run either on a local machine as a content process
+// associated with a firefox parent process, or on remote machines in the cloud.
+// We want local replaying processes to be able to interact with the system so
+// that they can connect with the parent process and e.g. report crashes.
+// We also want to avoid such interaction when replaying in the cloud, as there
+// won't be a parent process to connect to. Using these methods allows us to
+// handle both of these cases without changing the calling code's control flow.
+static inline void BeginPassThroughThreadEventsWithLocalReplay();
+static inline void EndPassThroughThreadEventsWithLocalReplay();
+
+// RAII class for regions where thread events are passed through when replaying
+// locally.
+struct MOZ_RAII AutoPassThroughThreadEventsWithLocalReplay {
+  AutoPassThroughThreadEventsWithLocalReplay() {
+    BeginPassThroughThreadEventsWithLocalReplay();
+  }
+  ~AutoPassThroughThreadEventsWithLocalReplay() {
+    EndPassThroughThreadEventsWithLocalReplay();
+  }
+};
+
 // Mark a region where thread events are not allowed to occur. The process will
 // crash immediately if an event does happen.
 static inline void BeginDisallowThreadEvents();
 static inline void EndDisallowThreadEvents();
 
 // Whether events in this thread are disallowed.
 static inline bool AreThreadEventsDisallowed();
 
@@ -351,16 +373,20 @@ static inline void NoteContentParse(cons
 
 MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(BeginOrderedAtomicAccess,
                                     (const void* aValue), (aValue))
 MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(EndOrderedAtomicAccess, (), ())
 MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(BeginPassThroughThreadEvents, (), ())
 MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(EndPassThroughThreadEvents, (), ())
 MOZ_MAKE_RECORD_REPLAY_WRAPPER(AreThreadEventsPassedThrough, bool, false, (),
                                ())
+MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(BeginPassThroughThreadEventsWithLocalReplay,
+                                    (), ())
+MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(EndPassThroughThreadEventsWithLocalReplay,
+                                    (), ())
 MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(BeginDisallowThreadEvents, (), ())
 MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(EndDisallowThreadEvents, (), ())
 MOZ_MAKE_RECORD_REPLAY_WRAPPER(AreThreadEventsDisallowed, bool, false, (), ())
 MOZ_MAKE_RECORD_REPLAY_WRAPPER(RecordReplayValue, size_t, aValue,
                                (size_t aValue), (aValue))
 MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(RecordReplayBytes,
                                     (void* aData, size_t aSize), (aData, aSize))
 MOZ_MAKE_RECORD_REPLAY_WRAPPER(HasDivergedFromRecording, bool, false, (), ())
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -848,16 +848,17 @@ pref("toolkit.dump.emit", false);
 #endif
 
 pref("devtools.recordreplay.mvp.enabled", false);
 pref("devtools.recordreplay.allowRepaintFailures", true);
 pref("devtools.recordreplay.includeSystemScripts", false);
 pref("devtools.recordreplay.logging", false);
 pref("devtools.recordreplay.loggingFull", false);
 pref("devtools.recordreplay.fastLogpoints", false);
+pref("devtools.recordreplay.cloudServer", "");
 
 // Preferences for the new performance panel.
 // This pref configures the base URL for the profiler.firefox.com instance to
 // use. This is useful so that a developer can change it while working on
 // profiler.firefox.com, or in tests. This isn't exposed directly to the user.
 pref("devtools.performance.recording.ui-base-url", "https://profiler.firefox.com");
 
 // Profiler buffer size. It is the maximum number of 8-bytes entries in the
--- a/toolkit/recordreplay/Assembler.cpp
+++ b/toolkit/recordreplay/Assembler.cpp
@@ -42,29 +42,27 @@ void Assembler::NoteOriginalInstruction(
   mCopiedInstructions.emplaceBack(aIp, Current());
 }
 
 void Assembler::Advance(size_t aSize) {
   MOZ_RELEASE_ASSERT(aSize <= MaximumAdvance);
   mCursor += aSize;
 }
 
-static const size_t JumpBytes = 17;
-
 uint8_t* Assembler::Current() {
   // Reallocate the buffer if there is not enough space. We need enough for the
   // maximum space used by any of the assembling functions, as well as for a
   // following jump for fallthrough to the next allocated space.
   if (size_t(mCursorEnd - mCursor) <= MaximumAdvance + JumpBytes) {
     MOZ_RELEASE_ASSERT(mCanAllocateStorage);
 
     // Allocate some writable, executable memory.
-    static const size_t BufferSize = PageSize;
-    uint8_t* buffer = new uint8_t[PageSize];
-    UnprotectExecutableMemory(buffer, PageSize);
+    static const size_t BufferSize = PageSize * 32;
+    uint8_t* buffer = new uint8_t[BufferSize];
+    UnprotectExecutableMemory(buffer, BufferSize);
 
     if (mCursor) {
       // Patch a jump for fallthrough from the last allocation.
       MOZ_RELEASE_ASSERT(size_t(mCursorEnd - mCursor) >= JumpBytes);
       PatchJump(mCursor, buffer);
     }
 
     mCursor = buffer;
@@ -76,35 +74,53 @@ uint8_t* Assembler::Current() {
 
 static void Push16(uint8_t** aIp, uint16_t aValue) {
   (*aIp)[0] = 0x66;
   (*aIp)[1] = 0x68;
   *reinterpret_cast<uint16_t*>(*aIp + 2) = aValue;
   (*aIp) += 4;
 }
 
-/* static */
-void Assembler::PatchJump(uint8_t* aIp, void* aTarget) {
+static void PushImmediateAtIp(uint8_t** aIp, void* aValue) {
   // Push the target literal onto the stack, 2 bytes at a time. This is
   // apparently the best way of getting an arbitrary 8 byte literal onto the
   // stack, as 4 byte literals we push will be sign extended to 8 bytes.
-  size_t ntarget = reinterpret_cast<size_t>(aTarget);
-  Push16(&aIp, ntarget >> 48);
-  Push16(&aIp, ntarget >> 32);
-  Push16(&aIp, ntarget >> 16);
-  Push16(&aIp, ntarget);
+  size_t nvalue = reinterpret_cast<size_t>(aValue);
+  Push16(aIp, nvalue >> 48);
+  Push16(aIp, nvalue >> 32);
+  Push16(aIp, nvalue >> 16);
+  Push16(aIp, nvalue);
+}
+
+/* static */
+void Assembler::PatchJump(uint8_t* aIp, void* aTarget) {
+  PushImmediateAtIp(&aIp, aTarget);
   *aIp = 0xC3;  // ret
 }
 
 void Assembler::Jump(void* aTarget) {
   PatchJump(Current(), aTarget);
   mJumps.emplaceBack(Current(), (uint8_t*)aTarget);
   Advance(JumpBytes);
 }
 
+void Assembler::PushImmediate(void* aValue) {
+  uint8_t* ip = Current();
+  PushImmediateAtIp(&ip, aValue);
+  Advance(PushImmediateBytes);
+}
+
+void Assembler::Return() {
+  NewInstruction(0xC3);
+}
+
+void Assembler::Breakpoint() {
+  NewInstruction(0xCC);
+}
+
 static uint8_t OppositeJump(uint8_t aOpcode) {
   // Get the opposite single byte jump opcode for a one or two byte conditional
   // jump. Opposite opcodes are adjacent, e.g. 0x7C -> jl and 0x7D -> jge.
   if (aOpcode >= 0x80 && aOpcode <= 0x8F) {
     aOpcode -= 0x10;
   } else {
     MOZ_RELEASE_ASSERT(aOpcode >= 0x70 && aOpcode <= 0x7F);
   }
@@ -151,20 +167,34 @@ void Assembler::LoadRax(size_t aWidth) {
       MOZ_CRASH();
   }
 }
 
 void Assembler::CompareRaxWithTopOfStack() {
   NewInstruction(0x48, 0x39, 0x04, 0x24);
 }
 
+void Assembler::CompareTopOfStackWithRax() {
+  NewInstruction(0x48, 0x3B, 0x04, 0x24);
+}
+
 void Assembler::PushRbx() { NewInstruction(0x53); }
 
 void Assembler::PopRbx() { NewInstruction(0x5B); }
 
+void Assembler::PopRegister(/*ud_type*/ int aRegister) {
+  MOZ_RELEASE_ASSERT(aRegister == NormalizeRegister(aRegister));
+
+  if (aRegister <= UD_R_RDI) {
+    NewInstruction(0x58 + aRegister - UD_R_RAX);
+  } else {
+    NewInstruction(0x41, 0x58 + aRegister - UD_R_R8);
+  }
+}
+
 void Assembler::StoreRbxToRax(size_t aWidth) {
   switch (aWidth) {
     case 1:
       NewInstruction(0x88, 0x18);
       break;
     case 2:
       NewInstruction(0x66, 0x89, 0x18);
       break;
--- a/toolkit/recordreplay/Assembler.h
+++ b/toolkit/recordreplay/Assembler.h
@@ -39,16 +39,25 @@ class Assembler {
   ///////////////////////////////////////////////////////////////////////////////
   // Routines for assembling instructions in new instruction storage
   ///////////////////////////////////////////////////////////////////////////////
 
   // Jump to aTarget. If aTarget is in the range of instructions being copied,
   // the target will be the copy of aTarget instead.
   void Jump(void* aTarget);
 
+  // Push aValue onto the stack.
+  void PushImmediate(void* aValue);
+
+  // Return to the address at the top of the stack.
+  void Return();
+
+  // For debugging, insert a breakpoint instruction.
+  void Breakpoint();
+
   // Conditionally jump to aTarget, depending on the short jump opcode aCode.
   // If aTarget is in the range of instructions being copied, the target will
   // be the copy of aTarget instead.
   void ConditionalJump(uint8_t aCode, void* aTarget);
 
   // Copy an instruction verbatim from aIp.
   void CopyInstruction(uint8_t* aIp, size_t aSize);
 
@@ -63,20 +72,25 @@ class Assembler {
   void CallRax();
 
   // movq/movl/movb 0(%rax), %rax
   void LoadRax(size_t aWidth);
 
   // cmpq %rax, 0(%rsp)
   void CompareRaxWithTopOfStack();
 
+  // cmpq 0(%rsp), %rax
+  void CompareTopOfStackWithRax();
+
   // push/pop %rbx
   void PushRbx();
   void PopRbx();
 
+  void PopRegister(/*ud_type*/ int aRegister);
+
   // movq/movl/movb %rbx, 0(%rax)
   void StoreRbxToRax(size_t aWidth);
 
   // cmpq/cmpb $literal8, %rax
   void CompareValueWithRax(uint8_t aValue, size_t aWidth);
 
   // movq $value, %rax
   void MoveImmediateToRax(void* aValue);
@@ -169,16 +183,21 @@ class Assembler {
 };
 
 // The number of instruction bytes required for a short jump.
 static const size_t ShortJumpBytes = 2;
 
 // The number of instruction bytes required for a jump that may clobber rax.
 static const size_t JumpBytesClobberRax = 12;
 
+// The number of instruction bytes required for an arbitrary jump.
+static const size_t JumpBytes = 17;
+
+static const size_t PushImmediateBytes = 16;
+
 // The maximum byte length of an x86/x64 instruction.
 static const size_t MaximumInstructionLength = 15;
 
 // Make a region of memory RWX.
 void UnprotectExecutableMemory(uint8_t* aAddress, size_t aSize);
 
 }  // namespace recordreplay
 }  // namespace mozilla
--- a/toolkit/recordreplay/BufferStream.h
+++ b/toolkit/recordreplay/BufferStream.h
@@ -7,19 +7,18 @@
 #ifndef mozilla_recordreplay_BufferStream_h
 #define mozilla_recordreplay_BufferStream_h
 
 #include "InfallibleVector.h"
 
 namespace mozilla {
 namespace recordreplay {
 
-// BufferStream provides similar functionality to Stream in File.h, allowing
-// reading or writing to a stream of data backed by an in memory buffer instead
-// of data stored on disk.
+// BufferStream is a simplified form of Stream from Recording.h, allowing
+// reading or writing to a stream of data backed by an in memory buffer.
 class BufferStream {
   InfallibleVector<char>* mOutput;
 
   const char* mInput;
   size_t mInputSize;
 
  public:
   BufferStream(const char* aInput, size_t aInputSize)
deleted file mode 100644
--- a/toolkit/recordreplay/DirtyMemoryHandler.cpp
+++ /dev/null
@@ -1,125 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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 "DirtyMemoryHandler.h"
-
-#include "ipc/ChildInternal.h"
-#include "mozilla/Sprintf.h"
-#include "MemorySnapshot.h"
-#include "Thread.h"
-
-#include <mach/exc.h>
-#include <mach/mach.h>
-#include <mach/mach_vm.h>
-#include <sys/time.h>
-
-namespace mozilla {
-namespace recordreplay {
-
-static mach_port_t gDirtyMemoryExceptionPort;
-
-// See AsmJSSignalHandlers.cpp.
-static const mach_msg_id_t sExceptionId = 2405;
-
-// This definition was generated by mig (the Mach Interface Generator) for the
-// routine 'exception_raise' (exc.defs). See js/src/wasm/WasmSignalHandlers.cpp.
-#pragma pack(4)
-typedef struct {
-  mach_msg_header_t Head;
-  /* start of the kernel processed data */
-  mach_msg_body_t msgh_body;
-  mach_msg_port_descriptor_t thread;
-  mach_msg_port_descriptor_t task;
-  /* end of the kernel processed data */
-  NDR_record_t NDR;
-  exception_type_t exception;
-  mach_msg_type_number_t codeCnt;
-  int64_t code[2];
-} Request__mach_exception_raise_t;
-#pragma pack()
-
-typedef struct {
-  Request__mach_exception_raise_t body;
-  mach_msg_trailer_t trailer;
-} ExceptionRequest;
-
-static void DirtyMemoryExceptionHandlerThread(void*) {
-  kern_return_t kret;
-
-  while (true) {
-    ExceptionRequest request;
-    kret = mach_msg(&request.body.Head, MACH_RCV_MSG, 0, sizeof(request),
-                    gDirtyMemoryExceptionPort, MACH_MSG_TIMEOUT_NONE,
-                    MACH_PORT_NULL);
-    kern_return_t replyCode = KERN_FAILURE;
-    if (kret == KERN_SUCCESS && request.body.Head.msgh_id == sExceptionId &&
-        request.body.exception == EXC_BAD_ACCESS && request.body.codeCnt == 2) {
-      uint8_t* faultingAddress = (uint8_t*)request.body.code[1];
-      if (HandleDirtyMemoryFault(faultingAddress)) {
-        replyCode = KERN_SUCCESS;
-      } else {
-        child::MinidumpInfo info(request.body.exception, request.body.code[0],
-                                 request.body.code[1],
-                                 request.body.thread.name);
-        child::ReportFatalError(
-            Some(info), "HandleDirtyMemoryFault failed %p %s", faultingAddress,
-            gMozCrashReason ? gMozCrashReason : "");
-      }
-    } else {
-      child::ReportFatalError(Nothing(),
-                              "DirtyMemoryExceptionHandlerThread mach_msg "
-                              "returned unexpected data");
-    }
-
-    __Reply__exception_raise_t reply;
-    reply.Head.msgh_bits =
-        MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(request.body.Head.msgh_bits), 0);
-    reply.Head.msgh_size = sizeof(reply);
-    reply.Head.msgh_remote_port = request.body.Head.msgh_remote_port;
-    reply.Head.msgh_local_port = MACH_PORT_NULL;
-    reply.Head.msgh_id = request.body.Head.msgh_id + 100;
-    reply.NDR = NDR_record;
-    reply.RetCode = replyCode;
-    mach_msg(&reply.Head, MACH_SEND_MSG, sizeof(reply), 0, MACH_PORT_NULL,
-             MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
-  }
-}
-
-void SetupDirtyMemoryHandler() {
-  // Allow repeated calls.
-  static bool hasDirtyMemoryHandler = false;
-  if (hasDirtyMemoryHandler) {
-    return;
-  }
-  hasDirtyMemoryHandler = true;
-
-  MOZ_RELEASE_ASSERT(AreThreadEventsPassedThrough());
-  kern_return_t kret;
-
-  // Get a port which can send and receive data.
-  kret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE,
-                            &gDirtyMemoryExceptionPort);
-  MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS);
-
-  kret = mach_port_insert_right(mach_task_self(), gDirtyMemoryExceptionPort,
-                                gDirtyMemoryExceptionPort,
-                                MACH_MSG_TYPE_MAKE_SEND);
-  MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS);
-
-  // Create a thread to block on reading the port.
-  Thread::SpawnNonRecordedThread(DirtyMemoryExceptionHandlerThread, nullptr);
-
-  // Set exception ports on the entire task. Unfortunately, this clobbers any
-  // other exception ports for the task, and forwarding to those other ports
-  // is not easy to get right.
-  kret = task_set_exception_ports(
-      mach_task_self(), EXC_MASK_BAD_ACCESS, gDirtyMemoryExceptionPort,
-      EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE);
-  MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS);
-}
-
-}  // namespace recordreplay
-}  // namespace mozilla
deleted file mode 100644
--- a/toolkit/recordreplay/DirtyMemoryHandler.h
+++ /dev/null
@@ -1,20 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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_DirtyMemoryHandler_h
-#define mozilla_recordreplay_DirtyMemoryHandler_h
-
-namespace mozilla {
-namespace recordreplay {
-
-// Set up a handler to catch SEGV hardware exceptions and pass them on to
-// HandleDirtyMemoryFault in MemorySnapshot.h for handling.
-void SetupDirtyMemoryHandler();
-
-}  // namespace recordreplay
-}  // namespace mozilla
-
-#endif  // mozilla_recordreplay_DirtyMemoryHandler_h
rename from toolkit/recordreplay/MiddlemanCall.cpp
rename to toolkit/recordreplay/ExternalCall.cpp
--- a/toolkit/recordreplay/MiddlemanCall.cpp
+++ b/toolkit/recordreplay/ExternalCall.cpp
@@ -1,458 +1,480 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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 "MiddlemanCall.h"
+#include "ExternalCall.h"
 
 #include <unordered_map>
 
 namespace mozilla {
 namespace recordreplay {
 
-typedef std::unordered_map<const void*, MiddlemanCall*> MiddlemanCallMap;
+///////////////////////////////////////////////////////////////////////////////
+// Replaying and External Process State
+///////////////////////////////////////////////////////////////////////////////
+
+typedef std::unordered_map<ExternalCallId, ExternalCall*> CallsByIdMap;
+typedef std::unordered_map<const void*, ExternalCallId> CallsByValueMap;
 
-// State used for keeping track of middleman calls in either a replaying
-// process or middleman process.
-struct MiddlemanCallState {
-  // In a replaying or middleman process, all middleman calls that have been
-  // encountered, indexed by their ID.
-  InfallibleVector<MiddlemanCall*> mCalls;
+// State used for keeping track of external calls in either a replaying
+// process or external process.
+struct ExternalCallState {
+  // In a replaying or external process, association between ExternalCallIds
+  // and the associated ExternalCall, for each encountered call.
+  CallsByIdMap mCallsById;
 
-  // In a replaying or middleman process, association between values produced by
-  // a middleman call and the call itself.
-  MiddlemanCallMap mCallMap;
+  // In a replaying or external process, association between values produced by
+  // a external call and the call's ID. This is the inverse of each call's
+  // mValue field, except that if multiple calls have produced the same value
+  // this maps that value to the most recent one.
+  CallsByValueMap mCallsByValue;
 
-  // In a middleman process, any buffers allocated for performed calls.
+  // In an external process, any buffers allocated for performed calls.
   InfallibleVector<void*> mAllocatedBuffers;
 };
 
-// In a replaying process, all middleman call state. In a middleman process,
-// state for the child currently being processed.
-static MiddlemanCallState* gState;
+// In a replaying process, all external call state. In an external process,
+// state for the call currently being processed.
+static ExternalCallState* gState;
 
-// In a middleman process, middleman call state for each child process, indexed
-// by the child ID.
-static StaticInfallibleVector<MiddlemanCallState*> gStatePerChild;
+// In a replaying process, all external calls found in the recording that have
+// not been flushed to the root replaying process.
+static StaticInfallibleVector<ExternalCall*> gUnflushedCalls;
 
-// In a replaying process, lock protecting middleman call state. In the
-// middleman, all accesses occur on the main thread.
+// In a replaying process, lock protecting external call state. In the
+// external process, all accesses occur on the main thread.
 static Monitor* gMonitor;
 
-void InitializeMiddlemanCalls() {
+void InitializeExternalCalls() {
   MOZ_RELEASE_ASSERT(IsRecordingOrReplaying() || IsMiddleman());
 
   if (IsReplaying()) {
-    gState = new MiddlemanCallState();
+    gState = new ExternalCallState();
     gMonitor = new Monitor();
   }
 }
 
-// Apply the ReplayInput phase to aCall and any calls it depends on that have
-// not been sent to the middleman yet, filling aOutgoingCalls with the set of
-// such calls.
-static bool GatherDependentCalls(
-    InfallibleVector<MiddlemanCall*>& aOutgoingCalls, MiddlemanCall* aCall) {
-  MOZ_RELEASE_ASSERT(!aCall->mSent);
-  aCall->mSent = true;
-
-  const Redirection& redirection = GetRedirection(aCall->mCallId);
-
-  CallArguments arguments;
-  aCall->mArguments.CopyTo(&arguments);
-
-  InfallibleVector<MiddlemanCall*> dependentCalls;
+static void SetExternalCallValue(ExternalCall* aCall, const void* aValue) {
+  aCall->mValue.reset();
+  aCall->mValue.emplace(aValue);
 
-  MiddlemanCallContext cx(aCall, &arguments, MiddlemanCallPhase::ReplayInput);
-  cx.mDependentCalls = &dependentCalls;
-  redirection.mMiddlemanCall(cx);
-  if (cx.mFailed) {
-    if (child::CurrentRepaintCannotFail()) {
-      child::ReportFatalError(Nothing(), "Middleman call input failed: %s\n",
-                              redirection.mName);
-    }
-    return false;
-  }
+  gState->mCallsByValue.erase(aValue);
+  gState->mCallsByValue.insert(CallsByValueMap::value_type(aValue, aCall->mId));
+}
 
-  for (MiddlemanCall* dependent : dependentCalls) {
-    if (!dependent->mSent && !GatherDependentCalls(aOutgoingCalls, dependent)) {
-      return false;
+static void GatherDependentCalls(
+    InfallibleVector<ExternalCall*>& aOutgoingCalls, ExternalCall* aCall) {
+  for (ExternalCall* existing : aOutgoingCalls) {
+    if (existing == aCall) {
+      return;
     }
   }
 
   aOutgoingCalls.append(aCall);
-  return true;
+
+  for (ExternalCallId dependentId : aCall->mDependentCalls) {
+    auto iter = gState->mCallsById.find(dependentId);
+    MOZ_RELEASE_ASSERT(iter != gState->mCallsById.end());
+    ExternalCall* dependent = iter->second;
+
+    GatherDependentCalls(aOutgoingCalls, dependent);
+  }
 }
 
-bool SendCallToMiddleman(size_t aCallId, CallArguments* aArguments,
-                         bool aDiverged) {
+bool OnExternalCall(size_t aCallId, CallArguments* aArguments, bool aDiverged) {
   MOZ_RELEASE_ASSERT(IsReplaying());
 
   const Redirection& redirection = GetRedirection(aCallId);
-  MOZ_RELEASE_ASSERT(redirection.mMiddlemanCall);
+  MOZ_RELEASE_ASSERT(redirection.mExternalCall);
+
+  const char* messageName = "";
+  if (!strcmp(redirection.mName, "objc_msgSend")) {
+    messageName = aArguments->Arg<1, const char*>();
+  }
+
+  if (aDiverged) {
+    PrintSpew("OnExternalCall Diverged %s %s\n", redirection.mName, messageName);
+  }
 
   MonitorAutoLock lock(*gMonitor);
 
-  // Allocate and fill in a new MiddlemanCall.
-  size_t id = gState->mCalls.length();
-  MiddlemanCall* newCall = new MiddlemanCall();
-  gState->mCalls.emplaceBack(newCall);
-  newCall->mId = id;
-  newCall->mCallId = aCallId;
-  newCall->mArguments.CopyFrom(aArguments);
+  // Allocate the new ExternalCall.
+  ExternalCall* call = new ExternalCall();
+  call->mCallId = aCallId;
 
-  // Perform the ReplayPreface phase on the new call.
+  // Save all the call's inputs.
   {
-    MiddlemanCallContext cx(newCall, aArguments,
-                            MiddlemanCallPhase::ReplayPreface);
-    redirection.mMiddlemanCall(cx);
+    ExternalCallContext cx(call, aArguments,
+                           ExternalCallPhase::SaveInput);
+    redirection.mExternalCall(cx);
     if (cx.mFailed) {
-      delete newCall;
-      gState->mCalls.popBack();
-      if (child::CurrentRepaintCannotFail()) {
-        child::ReportFatalError(Nothing(),
-                                "Middleman call preface failed: %s\n",
+      delete call;
+      if (child::CurrentRepaintCannotFail() && aDiverged) {
+        child::ReportFatalError("External call input failed: %s\n",
                                 redirection.mName);
       }
       return false;
     }
   }
 
-  // Other phases will not run if we have not diverged from the recording.
-  // Any outputs for the call have been handled by the SaveOutput hook.
+  call->ComputeId();
+
+  bool isNewCall = false;
+  auto iter = gState->mCallsById.find(call->mId);
+  if (iter == gState->mCallsById.end()) {
+    // We haven't seen this call before.
+    isNewCall = true;
+    gState->mCallsById.insert(CallsByIdMap::value_type(call->mId, call));
+  } else {
+    // We've seen this call before, so use the old copy.
+    delete call;
+    call = iter->second;
+
+    // Reuse this call's result if we need to restore the output.
+    if (aDiverged) {
+      ExternalCallContext cx(call, aArguments,
+                             ExternalCallPhase::RestoreOutput);
+      redirection.mExternalCall(cx);
+      return true;
+    }
+  }
+
+  // If we have not diverged from the recording, we already have the outputs
+  // we need. Run the SaveOutput phase to capture these so that we can reuse
+  // them later and associate any system outputs with the call.
   if (!aDiverged) {
+    ExternalCallContext cx(call, aArguments,
+                           ExternalCallPhase::SaveOutput);
+    redirection.mExternalCall(cx);
+    if (isNewCall) {
+      gUnflushedCalls.append(call);
+    }
     return true;
   }
 
-  // Perform the ReplayInput phase on the new call and any others it depends on.
-  InfallibleVector<MiddlemanCall*> outgoingCalls;
-  if (!GatherDependentCalls(outgoingCalls, newCall)) {
-    for (MiddlemanCall* call : outgoingCalls) {
-      call->mSent = false;
-    }
-    return false;
-  }
+  PrintSpew("OnExternalCall Send %s %s\n", redirection.mName, messageName);
 
-  // Encode all calls we are sending to the middleman.
+  // Gather any calls this one transitively depends on.
+  InfallibleVector<ExternalCall*> outgoingCalls;
+  GatherDependentCalls(outgoingCalls, call);
+
+  // Encode all calls that need to be performed, in the order to perform them.
   InfallibleVector<char> inputData;
   BufferStream inputStream(&inputData);
-  for (MiddlemanCall* call : outgoingCalls) {
-    call->EncodeInput(inputStream);
+  for (int i = outgoingCalls.length() - 1; i >= 0; i--) {
+    outgoingCalls[i]->EncodeInput(inputStream);
   }
 
-  // Perform the calls synchronously in the middleman.
+  // Synchronously wait for the call result.
   InfallibleVector<char> outputData;
-  child::SendMiddlemanCallRequest(inputData.begin(), inputData.length(),
-                                  &outputData);
-
-  // Decode outputs for the calls just sent, and perform the ReplayOutput phase
-  // on any older dependent calls we sent.
-  BufferStream outputStream(outputData.begin(), outputData.length());
-  for (MiddlemanCall* call : outgoingCalls) {
-    call->DecodeOutput(outputStream);
+  child::SendExternalCallRequest(call->mId,
+                                 inputData.begin(), inputData.length(),
+                                 &outputData);
 
-    if (call != newCall) {
-      CallArguments oldArguments;
-      call->mArguments.CopyTo(&oldArguments);
-      MiddlemanCallContext cx(call, &oldArguments,
-                              MiddlemanCallPhase::ReplayOutput);
-      cx.mReplayOutputIsOld = true;
-      GetRedirection(call->mCallId).mMiddlemanCall(cx);
-    }
-  }
+  // Decode the external call's output.
+  BufferStream outputStream(outputData.begin(), outputData.length());
+  call->DecodeOutput(outputStream);
 
-  // Perform the ReplayOutput phase to fill in outputs for the current call.
-  newCall->mArguments.CopyTo(aArguments);
-  MiddlemanCallContext cx(newCall, aArguments,
-                          MiddlemanCallPhase::ReplayOutput);
-  redirection.mMiddlemanCall(cx);
+  ExternalCallContext cx(call, aArguments, ExternalCallPhase::RestoreOutput);
+  redirection.mExternalCall(cx);
   return true;
 }
 
-void ProcessMiddlemanCall(size_t aChildId, const char* aInputData,
-                          size_t aInputSize,
-                          InfallibleVector<char>* aOutputData) {
+void ProcessExternalCall(const char* aInputData, size_t aInputSize,
+                         InfallibleVector<char>* aOutputData) {
   MOZ_RELEASE_ASSERT(IsMiddleman());
 
-  while (aChildId >= gStatePerChild.length()) {
-    gStatePerChild.append(nullptr);
-  }
-  if (!gStatePerChild[aChildId]) {
-    gStatePerChild[aChildId] = new MiddlemanCallState();
-  }
-  gState = gStatePerChild[aChildId];
+  gState = new ExternalCallState();
+  auto& calls = gState->mCallsById;
 
   BufferStream inputStream(aInputData, aInputSize);
-  BufferStream outputStream(aOutputData);
+  ExternalCall* lastCall = nullptr;
+
+  ExternalCallContext::ReleaseCallbackVector releaseCallbacks;
 
   while (!inputStream.IsEmpty()) {
-    MiddlemanCall* call = new MiddlemanCall();
+    ExternalCall* call = new ExternalCall();
     call->DecodeInput(inputStream);
 
     const Redirection& redirection = GetRedirection(call->mCallId);
-    MOZ_RELEASE_ASSERT(redirection.mMiddlemanCall);
+    MOZ_RELEASE_ASSERT(redirection.mExternalCall);
+
+    PrintSpew("ProcessExternalCall %lu %s\n", call->mId, redirection.mName);
 
     CallArguments arguments;
-    call->mArguments.CopyTo(&arguments);
 
     bool skipCall;
     {
-      MiddlemanCallContext cx(call, &arguments,
-                              MiddlemanCallPhase::MiddlemanInput);
-      redirection.mMiddlemanCall(cx);
-      skipCall = cx.mSkipCallInMiddleman;
+      ExternalCallContext cx(call, &arguments, ExternalCallPhase::RestoreInput);
+      redirection.mExternalCall(cx);
+      skipCall = cx.mSkipExecuting;
     }
 
     if (!skipCall) {
       RecordReplayInvokeCall(redirection.mBaseFunction, &arguments);
     }
 
     {
-      MiddlemanCallContext cx(call, &arguments,
-                              MiddlemanCallPhase::MiddlemanOutput);
-      redirection.mMiddlemanCall(cx);
+      ExternalCallContext cx(call, &arguments, ExternalCallPhase::SaveOutput);
+      cx.mReleaseCallbacks = &releaseCallbacks;
+      redirection.mExternalCall(cx);
     }
 
-    call->mArguments.CopyFrom(&arguments);
-    call->EncodeOutput(outputStream);
+    lastCall = call;
 
-    while (call->mId >= gState->mCalls.length()) {
-      gState->mCalls.emplaceBack(nullptr);
-    }
-    MOZ_RELEASE_ASSERT(!gState->mCalls[call->mId]);
-    gState->mCalls[call->mId] = call;
+    MOZ_RELEASE_ASSERT(calls.find(call->mId) == calls.end());
+    calls.insert(CallsByIdMap::value_type(call->mId, call));
   }
 
+  BufferStream outputStream(aOutputData);
+  lastCall->EncodeOutput(outputStream);
+
+  for (const auto& callback : releaseCallbacks) {
+    callback();
+  }
+
+  for (auto iter = calls.begin(); iter != calls.end(); ++iter) {
+    delete iter->second;
+  }
+
+  for (auto buffer : gState->mAllocatedBuffers) {
+    free(buffer);
+  }
+
+  delete gState;
   gState = nullptr;
 }
 
-void* MiddlemanCallContext::AllocateBytes(size_t aSize) {
+void* ExternalCallContext::AllocateBytes(size_t aSize) {
   void* rv = malloc(aSize);
 
-  // In a middleman process, any buffers we allocate live until the calls are
-  // reset. In a replaying process, the buffers will either live forever
-  // (if they are allocated in the ReplayPreface phase, to match the lifetime
-  // of the MiddlemanCall itself) or will be recovered when we rewind after we
-  // are done with our divergence from the recording (any other phase).
+  // In an external process, any buffers we allocate live until the calls are
+  // reset. In a replaying process, the buffers will live forever, to match the
+  // lifetime of the ExternalCall itself.
   if (IsMiddleman()) {
     gState->mAllocatedBuffers.append(rv);
   }
 
   return rv;
 }
 
-void ResetMiddlemanCalls(size_t aChildId) {
-  MOZ_RELEASE_ASSERT(IsMiddleman());
+void FlushExternalCalls() {
+  MonitorAutoLock lock(*gMonitor);
 
-  if (aChildId >= gStatePerChild.length()) {
-    return;
-  }
+  for (ExternalCall* call : gUnflushedCalls) {
+    InfallibleVector<char> outputData;
+    BufferStream outputStream(&outputData);
+    call->EncodeOutput(outputStream);
 
-  gState = gStatePerChild[aChildId];
-  if (!gState) {
-    return;
+    child::SendExternalCallOutput(call->mId, outputData.begin(),
+                                  outputData.length());
   }
 
-  for (MiddlemanCall* call : gState->mCalls) {
-    if (call) {
-      CallArguments arguments;
-      call->mArguments.CopyTo(&arguments);
+  gUnflushedCalls.clear();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// External Call Caching
+///////////////////////////////////////////////////////////////////////////////
 
-      MiddlemanCallContext cx(call, &arguments,
-                              MiddlemanCallPhase::MiddlemanRelease);
-      GetRedirection(call->mCallId).mMiddlemanCall(cx);
-    }
+// In root replaying processes, the outputs produced by assorted external calls
+// are cached for fulfilling future external call requests.
+struct ExternalCallOutput {
+  char* mOutput;
+  size_t mOutputSize;
+};
+
+// Protected by gMonitor. Accesses can occur on any thread.
+typedef std::unordered_map<ExternalCallId, ExternalCallOutput> CallOutputMap;
+static CallOutputMap* gCallOutputMap;
+
+void AddExternalCallOutput(ExternalCallId aId, const char* aOutput,
+                           size_t aOutputSize) {
+  MonitorAutoLock lock(*gMonitor);
+
+  if (!gCallOutputMap) {
+    gCallOutputMap = new CallOutputMap();
   }
 
-  // Delete the calls in a second pass. The MiddlemanRelease phase depends on
-  // previous middleman calls still existing.
-  for (MiddlemanCall* call : gState->mCalls) {
-    delete call;
+  ExternalCallOutput output;
+  output.mOutput = new char[aOutputSize];
+  memcpy(output.mOutput, aOutput, aOutputSize);
+  output.mOutputSize = aOutputSize;
+  gCallOutputMap->insert(CallOutputMap::value_type(aId, output));
+}
+
+bool HasExternalCallOutput(ExternalCallId aId,
+                           InfallibleVector<char>* aOutput) {
+  MonitorAutoLock lock(*gMonitor);
+
+  if (!gCallOutputMap) {
+    return false;
   }
 
-  gState->mCalls.clear();
-  for (auto buffer : gState->mAllocatedBuffers) {
-    free(buffer);
+  auto iter = gCallOutputMap->find(aId);
+  if (iter == gCallOutputMap->end()) {
+    return false;
   }
-  gState->mAllocatedBuffers.clear();
-  gState->mCallMap.clear();
 
-  gState = nullptr;
+  aOutput->append(iter->second.mOutput, iter->second.mOutputSize);
+  return true;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // System Values
 ///////////////////////////////////////////////////////////////////////////////
 
-static void AddMiddlemanCallValue(const void* aThing, MiddlemanCall* aCall) {
-  gState->mCallMap.erase(aThing);
-  gState->mCallMap.insert(MiddlemanCallMap::value_type(aThing, aCall));
-}
-
-static MiddlemanCall* LookupMiddlemanCall(const void* aThing) {
-  MiddlemanCallMap::const_iterator iter = gState->mCallMap.find(aThing);
-  if (iter != gState->mCallMap.end()) {
-    return iter->second;
+static ExternalCall* LookupExternalCall(const void* aThing) {
+  CallsByValueMap::const_iterator iter = gState->mCallsByValue.find(aThing);
+  if (iter != gState->mCallsByValue.end()) {
+    CallsByIdMap::const_iterator iter2 = gState->mCallsById.find(iter->second);
+    if (iter2 != gState->mCallsById.end()) {
+      return iter2->second;
+    }
   }
   return nullptr;
 }
 
-static const void* GetMiddlemanCallValue(size_t aId) {
-  MOZ_RELEASE_ASSERT(IsMiddleman());
-  MOZ_RELEASE_ASSERT(aId < gState->mCalls.length() && gState->mCalls[aId] &&
-                     gState->mCalls[aId]->mMiddlemanValue.isSome());
-  return gState->mCalls[aId]->mMiddlemanValue.ref();
+static Maybe<const void*> GetExternalCallValue(ExternalCallId aId) {
+  auto iter = gState->mCallsById.find(aId);
+  if (iter != gState->mCallsById.end()) {
+    return iter->second->mValue;
+  }
+  return Nothing();
 }
 
-bool MM_SystemInput(MiddlemanCallContext& aCx, const void** aThingPtr) {
-  MOZ_RELEASE_ASSERT(aCx.AccessPreface());
+bool EX_SystemInput(ExternalCallContext& aCx, const void** aThingPtr) {
+  MOZ_RELEASE_ASSERT(aCx.AccessInput());
 
-  if (!*aThingPtr) {
-    // Null values are handled by the normal argument copying logic.
+  bool isNull = *aThingPtr == nullptr;
+  aCx.ReadOrWriteInputBytes(&isNull, sizeof(isNull));
+  if (isNull) {
+    *aThingPtr = nullptr;
     return true;
   }
 
-  Maybe<size_t> callId;
-  if (aCx.mPhase == MiddlemanCallPhase::ReplayPreface) {
-    // Determine any middleman call this object came from, before the pointer
-    // has a chance to be clobbered by another call between this and the
-    // ReplayInput phase.
-    MiddlemanCall* call = LookupMiddlemanCall(*aThingPtr);
+  ExternalCallId callId = 0;
+  if (aCx.mPhase == ExternalCallPhase::SaveInput) {
+    ExternalCall* call = LookupExternalCall(*aThingPtr);
     if (call) {
-      callId.emplace(call->mId);
+      callId = call->mId;
+      MOZ_RELEASE_ASSERT(callId);
+      aCx.mCall->mDependentCalls.append(call->mId);
     }
   }
-  aCx.ReadOrWritePrefaceBytes(&callId, sizeof(callId));
+  aCx.ReadOrWriteInputBytes(&callId, sizeof(callId));
 
-  switch (aCx.mPhase) {
-    case MiddlemanCallPhase::ReplayPreface:
-      return true;
-    case MiddlemanCallPhase::ReplayInput:
-      if (callId.isSome()) {
-        aCx.WriteInputScalar(callId.ref());
-        aCx.mDependentCalls->append(gState->mCalls[callId.ref()]);
-        return true;
-      }
-      return false;
-    case MiddlemanCallPhase::MiddlemanInput:
-      if (callId.isSome()) {
-        size_t callIndex = aCx.ReadInputScalar();
-        *aThingPtr = GetMiddlemanCallValue(callIndex);
-        return true;
-      }
-      return false;
-    default:
-      MOZ_CRASH("Bad phase");
+  if (aCx.mPhase == ExternalCallPhase::RestoreInput) {
+    if (callId) {
+      Maybe<const void*> value = GetExternalCallValue(callId);
+      MOZ_RELEASE_ASSERT(value.isSome());
+      *aThingPtr = value.ref();
+    }
   }
+
+  return callId != 0;
 }
 
-// Pointer system values are preserved during the replay so that null tests
-// and equality tests work as expected. We additionally mangle the
-// pointers here by setting one of the two highest bits, depending on whether
-// the pointer came from the recording or from the middleman. This avoids
-// accidentally conflating pointers that happen to have the same value but
-// which originate from different processes.
-static const void* MangleSystemValue(const void* aValue, bool aFromRecording) {
-  return (const void*)((size_t)aValue | (1ULL << (aFromRecording ? 63 : 62)));
+static const void* MangledSystemValue(ExternalCallId aId) {
+  return (const void*)((size_t)aId | (1ULL << 63));
 }
 
-void MM_SystemOutput(MiddlemanCallContext& aCx, const void** aOutput,
+void EX_SystemOutput(ExternalCallContext& aCx, const void** aOutput,
                      bool aUpdating) {
-  if (!*aOutput) {
-    if (aCx.mPhase == MiddlemanCallPhase::MiddlemanOutput) {
-      aCx.mCall->SetMiddlemanValue(*aOutput);
-    }
+  if (!aCx.AccessOutput()) {
     return;
   }
 
-  switch (aCx.mPhase) {
-    case MiddlemanCallPhase::ReplayPreface:
-      if (!HasDivergedFromRecording()) {
-        // If we haven't diverged from the recording, use the output value saved
-        // in the recording.
-        if (!aUpdating) {
-          *aOutput = MangleSystemValue(*aOutput, true);
-        }
-        aCx.mCall->SetRecordingValue(*aOutput);
-        AddMiddlemanCallValue(*aOutput, aCx.mCall);
-      }
-      break;
-    case MiddlemanCallPhase::MiddlemanOutput:
-      aCx.mCall->SetMiddlemanValue(*aOutput);
-      AddMiddlemanCallValue(*aOutput, aCx.mCall);
-      break;
-    case MiddlemanCallPhase::ReplayOutput: {
-      if (!aUpdating) {
-        *aOutput = MangleSystemValue(*aOutput, false);
+  bool isNull = false;
+  Maybe<ExternalCallId> aliasedCall;
+  if (aCx.mPhase == ExternalCallPhase::SaveOutput) {
+    SetExternalCallValue(aCx.mCall, *aOutput);
+
+    isNull = *aOutput == nullptr;
+    if (!isNull) {
+      ExternalCall* call = LookupExternalCall(*aOutput);
+      if (call) {
+        aliasedCall.emplace(call->mId);
       }
-      aCx.mCall->SetMiddlemanValue(*aOutput);
+    }
+  }
+  aCx.ReadOrWriteOutputBytes(&isNull, sizeof(isNull));
+  aCx.ReadOrWriteOutputBytes(&aliasedCall, sizeof(aliasedCall));
 
-      // Associate the value produced by the middleman with this call. If the
-      // call previously went through the ReplayPreface phase when we did not
-      // diverge from the recording, we will associate values from both the
-      // recording and middleman processes with this call. If a call made after
-      // diverging produced the same value as a call made before diverging, use
-      // the value saved in the recording for the first call, so that equality
-      // tests on the value work as expected.
-      MiddlemanCall* previousCall = LookupMiddlemanCall(*aOutput);
-      if (previousCall) {
-        if (previousCall->mRecordingValue.isSome()) {
-          *aOutput = previousCall->mRecordingValue.ref();
+  if (aCx.mPhase == ExternalCallPhase::RestoreOutput) {
+    do {
+      if (isNull) {
+        *aOutput = nullptr;
+        break;
+      }
+      if (aliasedCall.isSome()) {
+        auto iter = gState->mCallsById.find(aliasedCall.ref());
+        if (iter != gState->mCallsById.end()) {
+          *aOutput = iter->second;
+          break;
         }
-      } else {
-        AddMiddlemanCallValue(*aOutput, aCx.mCall);
+        // If we haven't encountered the aliased call, fall through and generate
+        // a new value for it. Aliases might be spurious if they were derived from
+        // the recording and reflect a value that was released and had its memory
+        // reused.
       }
-      break;
-    }
-    default:
-      return;
+      *aOutput = MangledSystemValue(aCx.mCall->mId);
+    } while (false);
+
+    SetExternalCallValue(aCx.mCall, *aOutput);
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
-// MiddlemanCall
+// ExternalCall
 ///////////////////////////////////////////////////////////////////////////////
 
-void MiddlemanCall::EncodeInput(BufferStream& aStream) const {
+void CallReturnRegisters::CopyFrom(CallArguments* aArguments) {
+  rval0 = aArguments->Rval<size_t, 0>();
+  rval1 = aArguments->Rval<size_t, 1>();
+  floatrval0 = aArguments->FloatRval<0>();
+  floatrval1 = aArguments->FloatRval<1>();
+}
+
+void CallReturnRegisters::CopyTo(CallArguments* aArguments) {
+  aArguments->Rval<size_t, 0>() = rval0;
+  aArguments->Rval<size_t, 1>() = rval1;
+  aArguments->FloatRval<0>() = floatrval0;
+  aArguments->FloatRval<1>() = floatrval1;
+}
+
+void ExternalCall::EncodeInput(BufferStream& aStream) const {
   aStream.WriteScalar(mId);
   aStream.WriteScalar(mCallId);
-  aStream.WriteBytes(&mArguments, sizeof(CallRegisterArguments));
-  aStream.WriteScalar(mPreface.length());
-  aStream.WriteBytes(mPreface.begin(), mPreface.length());
+  aStream.WriteScalar(mExcludeInput);
   aStream.WriteScalar(mInput.length());
   aStream.WriteBytes(mInput.begin(), mInput.length());
 }
 
-void MiddlemanCall::DecodeInput(BufferStream& aStream) {
+void ExternalCall::DecodeInput(BufferStream& aStream) {
   mId = aStream.ReadScalar();
   mCallId = aStream.ReadScalar();
-  aStream.ReadBytes(&mArguments, sizeof(CallRegisterArguments));
-  size_t prefaceLength = aStream.ReadScalar();
-  mPreface.appendN(0, prefaceLength);
-  aStream.ReadBytes(mPreface.begin(), prefaceLength);
+  mExcludeInput = aStream.ReadScalar();
   size_t inputLength = aStream.ReadScalar();
   mInput.appendN(0, inputLength);
   aStream.ReadBytes(mInput.begin(), inputLength);
 }
 
-void MiddlemanCall::EncodeOutput(BufferStream& aStream) const {
-  aStream.WriteBytes(&mArguments, sizeof(CallRegisterArguments));
+void ExternalCall::EncodeOutput(BufferStream& aStream) const {
+  aStream.WriteBytes(&mReturnRegisters, sizeof(CallReturnRegisters));
   aStream.WriteScalar(mOutput.length());
   aStream.WriteBytes(mOutput.begin(), mOutput.length());
 }
 
-void MiddlemanCall::DecodeOutput(BufferStream& aStream) {
-  // Only update the return value when decoding arguments, so that we don't
-  // clobber the call's arguments with any changes made in the middleman.
-  CallRegisterArguments newArguments;
-  aStream.ReadBytes(&newArguments, sizeof(CallRegisterArguments));
-  mArguments.CopyRvalFrom(&newArguments);
+void ExternalCall::DecodeOutput(BufferStream& aStream) {
+  aStream.ReadBytes(&mReturnRegisters, sizeof(CallReturnRegisters));
 
   size_t outputLength = aStream.ReadScalar();
   mOutput.appendN(0, outputLength);
   aStream.ReadBytes(mOutput.begin(), outputLength);
 }
 
 }  // namespace recordreplay
 }  // namespace mozilla
rename from toolkit/recordreplay/MiddlemanCall.h
rename to toolkit/recordreplay/ExternalCall.h
--- a/toolkit/recordreplay/MiddlemanCall.h
+++ b/toolkit/recordreplay/ExternalCall.h
@@ -1,458 +1,455 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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_MiddlemanCall_h
-#define mozilla_recordreplay_MiddlemanCall_h
+#ifndef mozilla_recordreplay_ExternalCall_h
+#define mozilla_recordreplay_ExternalCall_h
 
 #include "BufferStream.h"
 #include "ProcessRedirect.h"
 #include "mozilla/Maybe.h"
 
 namespace mozilla {
 namespace recordreplay {
 
-// Middleman Calls Overview
+// External Calls Overview
 //
 // With few exceptions, replaying processes do not interact with the underlying
 // system or call the actual versions of redirected system library functions.
 // This is problematic after diverging from the recording, as then the diverged
 // thread cannot interact with its recording either.
 //
-// Middleman calls are used in a replaying process after diverging from the
-// recording to perform calls in the middleman process instead. Inputs are
-// gathered and serialized in the replaying process, then sent to the middleman
-// process. The middleman calls the function, and its outputs are serialized
-// for reading by the replaying process.
-//
-// Calls that might need to be sent to the middleman are processed in phases,
-// per the MiddlemanCallPhase enum below. The timeline of a middleman call is
-// as follows:
-//
-// - Any redirection with a middleman call hook can potentially be sent to the
-//   middleman. In a replaying process, whenever such a call is encountered,
-//   the hook is invoked in the ReplayPreface phase to capture any input data
-//   that must be examined at the time of the call itself.
+// External calls are used in a replaying process after diverging from the
+// recording to perform calls in another process instead. We call this the
+// external process; currently it is always the middleman, though this will
+// change soon.
 //
-// - If the thread has not diverged from the recording, the call is remembered
-//   but no further action is necessary yet.
-//
-// - If the thread has diverged from the recording, the call needs to go
-//   through the remaining phases. The ReplayInput phase captures any
-//   additional inputs to the call, potentially including values produced by
-//   other middleman calls.
-//
-// - The transitive closure of these call dependencies is produced, and all
-//   calls found go through the ReplayInput phase. The resulting data is sent
-//   to the middleman process, which goes through the MiddlemanInput phase
-//   to decode those inputs.
-//
-// - The middleman performs each of the calls it has been given, and their
-//   outputs are encoded in the MiddlemanOutput phase. These outputs are sent
-//   to the replaying process in a response and decoded in the ReplayOutput
-//   phase, which can then resume execution.
+// External call state is managed so that call results can be reused when
+// possible, to minimize traffic between processes and improve efficiency.
+// Conceptually, the result of performing a system call is entirely determined
+// by its inputs: scalar values and the results of other system calls (and the
+// version of the underlying system, which we ignore for now). This information
+// uniquely identifies the call, and we can form a directed graph out of these
+// calls by connecting them to the other calls they depend on.
 //
-// - The replaying process holds onto information about calls it has sent until
-//   it rewinds to a point before it diverged from the recording. This rewind
-//   will --- without any special action required --- wipe out information on
-//   all calls sent to the middleman, and retain any data gathered in the
-//   ReplayPreface phase for calls that were made prior to the rewind target.
-//
-// - Information about calls and all resources held are retained in the
-//   middleman process are retained until a replaying process asks for them to
-//   be reset, which happens any time the replaying process first diverges from
-//   the recording. The MiddlemanRelease phase is used to release any system
-//   resources held.
-
-// Ways of processing calls that can be sent to the middleman.
-enum class MiddlemanCallPhase {
-  // When replaying, a call is being performed that might need to be sent to
-  // the middleman later.
-  ReplayPreface,
+// Each root replaying process maintains a portion of this graph. As the
+// recording is replayed by other forked processes, nodes are added to the graph
+// by studying the output of calls that appear in the recording itself. When
+// a replaying process wants to make a system call that does not appear in the
+// graph, the inputs for that call and the transitive closure of the calls it
+// depends on is sent to another process running on the target platform. That
+// process executes the call, saves the output and sends it back for adding to
+// the graph. Other ways of updating the graph could be added in the future.
 
-  // A call for which inputs have been gathered is now being sent to the
-  // middleman. This is separate from ReplayPreface because capturing inputs
-  // might need to dereference pointers that could be bogus values originating
-  // from the recording. Waiting to dereference these pointers until we know
-  // the call needs to be sent to the middleman avoids needing to understand
-  // the inputs to all call sites of general purpose redirections such as
-  // CFArrayCreate.
-  ReplayInput,
+// Inputs and outputs to external calls are handled in a reusable fashion by
+// adding a ExternalCall hook to the system call's redirection. This hook is
+// called in one of the following phases.
+enum class ExternalCallPhase {
+  // When replaying, a call which can be put in the external call graph is being
+  // made. This can happen either before or after diverging from the recording,
+  // and saves all inputs to the call which are sufficient to uniquely identify
+  // it in the graph.
+  SaveInput,
 
-  // In the middleman process, a call from the replaying process is being
-  // performed.
-  MiddlemanInput,
-
-  // In the middleman process, a call from the replaying process was just
-  // performed, and its outputs need to be saved.
-  MiddlemanOutput,
+  // In the external process, the inputs saved earlier are being restored in
+  // preparation for executing the call.
+  RestoreInput,
 
-  // Back in the replaying process, the outputs from a call have been received
-  // from the middleman.
-  ReplayOutput,
+  // Save any outputs produced by a call. This can happen either in the
+  // replaying process (using outputs saved in the recording) or in the external
+  // process (using outputs just produced). After saving the outputs to the
+  // call, it can be placed in the external call graph and be used to resolve
+  // external calls which a diverged replaying process is making. Returned
+  // register values are automatically saved.
+  SaveOutput,
 
-  // In the middleman process, release any system resources held after this
-  // call.
-  MiddlemanRelease,
+  // In the replaying process, restore any outputs associated with an external
+  // call. This is only called after diverging from the recording, and allows
+  // diverged execution to continue afterwards.
+  RestoreOutput,
 };
 
-struct MiddlemanCall {
-  // Unique ID for this call.
-  size_t mId;
+// Global identifier for an external call. The result of an external call is
+// determined by its inputs, and its ID is a hash of those inputs. If there are
+// hash collisions between different inputs then two different calls will have
+// the same ID, and things will break (subtly or not). Valid IDs are non-zero.
+typedef uintptr_t ExternalCallId;
+
+// Storage for the returned registers of a call which are automatically saved.
+struct CallReturnRegisters {
+  size_t rval0, rval1;
+  double floatrval0, floatrval1;
+
+  void CopyFrom(CallArguments* aArguments);
+  void CopyTo(CallArguments* aArguments);
+};
+
+struct ExternalCall {
+  // ID for this call.
+  ExternalCallId mId = 0;
 
   // ID of the redirection being invoked.
-  size_t mCallId;
+  size_t mCallId = 0;
 
-  // All register arguments and return values are preserved when sending the
-  // call back and forth between processes.
-  CallRegisterArguments mArguments;
-
-  // Written in ReplayPrefaceInput, read in ReplayInput and MiddlemanInput.
-  InfallibleVector<char> mPreface;
-
-  // Written in ReplayInput, read in MiddlemanInput.
+  // All call inputs. Written in SaveInput, read in RestoreInput.
   InfallibleVector<char> mInput;
 
-  // Written in MiddlemanOutput, read in ReplayOutput.
+  // If non-zero, only the input before this extent is used to characterize this
+  // call and determine if it is the same as another external call.
+  size_t mExcludeInput = 0;
+
+  // Calls which this depends on, written in SaveInput and read in RestoreInput.
+  // Any calls referenced in mInput will also be here.
+  InfallibleVector<ExternalCallId> mDependentCalls;
+
+  // All call outputs. Written in SaveOutput, read in RestoreOutput.
   InfallibleVector<char> mOutput;
 
-  // In a replaying process, whether this call has been sent to the middleman.
-  bool mSent;
+  // Values of any returned registers after the call.
+  CallReturnRegisters mReturnRegisters;
 
-  // In a replaying process, any value associated with this call that was
-  // included in the recording, when the call was made before diverging from
-  // the recording.
-  Maybe<const void*> mRecordingValue;
-
-  // In a replaying or middleman process, any value associated with this call
-  // that was produced by the middleman itself.
-  Maybe<const void*> mMiddlemanValue;
-
-  MiddlemanCall() : mId(0), mCallId(0), mSent(false) {}
+  // Any system value produced by this call. In the external process this is
+  // the actual system value, while in the replaying process this is the value
+  // used during execution to represent the call's result.
+  Maybe<const void*> mValue;
 
   void EncodeInput(BufferStream& aStream) const;
   void DecodeInput(BufferStream& aStream);
 
   void EncodeOutput(BufferStream& aStream) const;
   void DecodeOutput(BufferStream& aStream);
 
-  void SetRecordingValue(const void* aValue) {
-    MOZ_RELEASE_ASSERT(mRecordingValue.isNothing());
-    mRecordingValue.emplace(aValue);
-  }
-
-  void SetMiddlemanValue(const void* aValue) {
-    MOZ_RELEASE_ASSERT(mMiddlemanValue.isNothing());
-    mMiddlemanValue.emplace(aValue);
+  void ComputeId() {
+    MOZ_RELEASE_ASSERT(!mId);
+    size_t extent = mExcludeInput ? mExcludeInput : mInput.length();
+    mId = HashGeneric(mCallId, HashBytes(mInput.begin(), extent));
+    if (!mId) {
+      mId = 1;
+    }
   }
 };
 
-// Information needed to process one of the phases of a middleman call,
-// in either the replaying or middleman process.
-struct MiddlemanCallContext {
+// Information needed to process one of the phases of an external call.
+struct ExternalCallContext {
   // Call being operated on.
-  MiddlemanCall* mCall;
+  ExternalCall* mCall;
 
   // Complete arguments and return value information for the call.
   CallArguments* mArguments;
 
   // Current processing phase.
-  MiddlemanCallPhase mPhase;
-
-  // During the ReplayPreface or ReplayInput phases, whether capturing input
-  // data has failed. In such cases the call cannot be sent to the middleman
-  // and, if the thread has diverged from the recording, an unhandled
-  // divergence and associated rewind will occur.
-  bool mFailed;
+  ExternalCallPhase mPhase;
 
-  // This can be set in the MiddlemanInput phase to avoid performing the call
-  // in the middleman process.
-  bool mSkipCallInMiddleman;
+  // During the SaveInput phase, whether capturing input data has failed.
+  // In such cases the call cannot be placed in the external call graph and,
+  // if the thread has diverged from the recording, an unhandled divergence
+  // will occur.
+  bool mFailed = false;
 
-  // During the ReplayInput phase, this can be used to fill in any middleman
-  // calls whose output the current one depends on.
-  InfallibleVector<MiddlemanCall*>* mDependentCalls;
+  // This can be set in the RestoreInput phase to avoid executing the call
+  // in the external process.
+  bool mSkipExecuting = false;
 
   // Streams of data that can be accessed during the various phases. Streams
   // need to be read or written from at the same points in the phases which use
   // them, so that callbacks operating on these streams can be composed without
   // issues.
 
-  // The preface is written during ReplayPreface, and read during both
-  // ReplayInput and MiddlemanInput.
-  Maybe<BufferStream> mPrefaceStream;
-
-  // Inputs are written during ReplayInput, and read during MiddlemanInput.
+  // Inputs are written during SaveInput, and read during RestoreInput.
   Maybe<BufferStream> mInputStream;
 
-  // Outputs are written during MiddlemanOutput, and read during ReplayOutput.
+  // Outputs are written during SaveOutput, and read during RestoreOutput.
   Maybe<BufferStream> mOutputStream;
 
-  // During the ReplayOutput phase, this is set if the call was made sometime
-  // in the past and pointers referred to in the arguments may no longer be
-  // valid.
-  bool mReplayOutputIsOld;
+  // If we are running the SaveOutput phase in an external process, a list of
+  // callbacks which will release all system resources created by by the call.
+  typedef InfallibleVector<std::function<void()>> ReleaseCallbackVector;
+  ReleaseCallbackVector* mReleaseCallbacks = nullptr;
 
-  MiddlemanCallContext(MiddlemanCall* aCall, CallArguments* aArguments,
-                       MiddlemanCallPhase aPhase)
+  ExternalCallContext(ExternalCall* aCall, CallArguments* aArguments,
+                      ExternalCallPhase aPhase)
       : mCall(aCall),
         mArguments(aArguments),
-        mPhase(aPhase),
-        mFailed(false),
-        mSkipCallInMiddleman(false),
-        mDependentCalls(nullptr),
-        mReplayOutputIsOld(false) {
+        mPhase(aPhase) {
     switch (mPhase) {
-      case MiddlemanCallPhase::ReplayPreface:
-        mPrefaceStream.emplace(&mCall->mPreface);
-        break;
-      case MiddlemanCallPhase::ReplayInput:
-        mPrefaceStream.emplace(mCall->mPreface.begin(),
-                               mCall->mPreface.length());
+      case ExternalCallPhase::SaveInput:
         mInputStream.emplace(&mCall->mInput);
         break;
-      case MiddlemanCallPhase::MiddlemanInput:
-        mPrefaceStream.emplace(mCall->mPreface.begin(),
-                               mCall->mPreface.length());
+      case ExternalCallPhase::RestoreInput:
         mInputStream.emplace(mCall->mInput.begin(), mCall->mInput.length());
         break;
-      case MiddlemanCallPhase::MiddlemanOutput:
+      case ExternalCallPhase::SaveOutput:
+        mCall->mReturnRegisters.CopyFrom(aArguments);
         mOutputStream.emplace(&mCall->mOutput);
         break;
-      case MiddlemanCallPhase::ReplayOutput:
+      case ExternalCallPhase::RestoreOutput:
+        mCall->mReturnRegisters.CopyTo(aArguments);
         mOutputStream.emplace(mCall->mOutput.begin(), mCall->mOutput.length());
         break;
-      case MiddlemanCallPhase::MiddlemanRelease:
-        break;
     }
   }
 
   void MarkAsFailed() {
-    MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::ReplayPreface ||
-                       mPhase == MiddlemanCallPhase::ReplayInput);
+    MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::SaveInput);
     mFailed = true;
   }
 
   void WriteInputBytes(const void* aBuffer, size_t aSize) {
-    MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::ReplayInput);
+    MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::SaveInput);
     mInputStream.ref().WriteBytes(aBuffer, aSize);
   }
 
   void WriteInputScalar(size_t aValue) {
-    MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::ReplayInput);
+    MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::SaveInput);
     mInputStream.ref().WriteScalar(aValue);
   }
 
   void ReadInputBytes(void* aBuffer, size_t aSize) {
-    MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::MiddlemanInput);
+    MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::RestoreInput);
     mInputStream.ref().ReadBytes(aBuffer, aSize);
   }
 
   size_t ReadInputScalar() {
-    MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::MiddlemanInput);
+    MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::RestoreInput);
     return mInputStream.ref().ReadScalar();
   }
 
   bool AccessInput() { return mInputStream.isSome(); }
 
-  void ReadOrWriteInputBytes(void* aBuffer, size_t aSize) {
+  void ReadOrWriteInputBytes(void* aBuffer, size_t aSize, bool aExcludeInput = false) {
     switch (mPhase) {
-      case MiddlemanCallPhase::ReplayInput:
+      case ExternalCallPhase::SaveInput:
+        // Only one buffer can be excluded, and it has to be the last input to
+        // the external call.
+        MOZ_RELEASE_ASSERT(!mCall->mExcludeInput);
+        if (aExcludeInput) {
+          mCall->mExcludeInput = mCall->mInput.length();
+        }
         WriteInputBytes(aBuffer, aSize);
         break;
-      case MiddlemanCallPhase::MiddlemanInput:
+      case ExternalCallPhase::RestoreInput:
         ReadInputBytes(aBuffer, aSize);
         break;
       default:
         MOZ_CRASH();
     }
   }
 
-  bool AccessPreface() { return mPrefaceStream.isSome(); }
-
-  void ReadOrWritePrefaceBytes(void* aBuffer, size_t aSize) {
+  void ReadOrWriteInputBuffer(void** aBufferPtr, size_t aSize,
+                              bool aIncludeContents = true) {
     switch (mPhase) {
-      case MiddlemanCallPhase::ReplayPreface:
-        mPrefaceStream.ref().WriteBytes(aBuffer, aSize);
-        break;
-      case MiddlemanCallPhase::ReplayInput:
-      case MiddlemanCallPhase::MiddlemanInput:
-        mPrefaceStream.ref().ReadBytes(aBuffer, aSize);
+      case ExternalCallPhase::SaveInput:
+        if (aIncludeContents) {
+          WriteInputBytes(*aBufferPtr, aSize);
+        }
         break;
-      default:
-        MOZ_CRASH();
-    }
-  }
-
-  void ReadOrWritePrefaceBuffer(void** aBufferPtr, size_t aSize) {
-    switch (mPhase) {
-      case MiddlemanCallPhase::ReplayPreface:
-        mPrefaceStream.ref().WriteBytes(*aBufferPtr, aSize);
-        break;
-      case MiddlemanCallPhase::ReplayInput:
-      case MiddlemanCallPhase::MiddlemanInput:
+      case ExternalCallPhase::RestoreInput:
         *aBufferPtr = AllocateBytes(aSize);
-        mPrefaceStream.ref().ReadBytes(*aBufferPtr, aSize);
+        if (aIncludeContents) {
+          ReadInputBytes(*aBufferPtr, aSize);
+        }
         break;
       default:
         MOZ_CRASH();
     }
   }
 
   bool AccessOutput() { return mOutputStream.isSome(); }
 
   void ReadOrWriteOutputBytes(void* aBuffer, size_t aSize) {
     switch (mPhase) {
-      case MiddlemanCallPhase::MiddlemanOutput:
+      case ExternalCallPhase::SaveOutput:
         mOutputStream.ref().WriteBytes(aBuffer, aSize);
         break;
-      case MiddlemanCallPhase::ReplayOutput:
+      case ExternalCallPhase::RestoreOutput:
         mOutputStream.ref().ReadBytes(aBuffer, aSize);
         break;
       default:
         MOZ_CRASH();
     }
   }
 
   void ReadOrWriteOutputBuffer(void** aBuffer, size_t aSize) {
-    if (*aBuffer) {
-      if (mPhase == MiddlemanCallPhase::MiddlemanInput || mReplayOutputIsOld) {
+    if (AccessInput()) {
+      bool isNull = *aBuffer == nullptr;
+      ReadOrWriteInputBytes(&isNull, sizeof(isNull));
+      if (isNull) {
+        *aBuffer = nullptr;
+      } else if (mPhase == ExternalCallPhase::RestoreInput) {
         *aBuffer = AllocateBytes(aSize);
       }
+    }
 
-      if (AccessOutput()) {
-        ReadOrWriteOutputBytes(*aBuffer, aSize);
-      }
+    if (AccessOutput() && *aBuffer) {
+      ReadOrWriteOutputBytes(*aBuffer, aSize);
     }
   }
 
   // Allocate some memory associated with the call, which will be released in
-  // the replaying process on a rewind and in the middleman process when the
-  // call state is reset.
+  // the external process after fully processing a call, and will never be
+  // released in the replaying process.
   void* AllocateBytes(size_t aSize);
 };
 
-// Notify the system about a call to a redirection with a middleman call hook.
+// Notify the system about a call to a redirection with an external call hook.
 // aDiverged is set if the current thread has diverged from the recording and
 // any outputs for the call must be filled in; otherwise, they already have
 // been filled in using data from the recording. Returns false if the call was
 // unable to be processed.
-bool SendCallToMiddleman(size_t aCallId, CallArguments* aArguments,
-                         bool aDiverged);
+bool OnExternalCall(size_t aCallId, CallArguments* aArguments,
+                    bool aDiverged);
+
+// In the external process, perform one or more calls encoded in aInputData
+// and encode the output of the final call in aOutputData.
+void ProcessExternalCall(const char* aInputData, size_t aInputSize,
+                         InfallibleVector<char>* aOutputData);
 
-// In the middleman process, perform one or more calls encoded in aInputData
-// and encode their outputs to aOutputData. The calls are associated with the
-// specified child process ID.
-void ProcessMiddlemanCall(size_t aChildId, const char* aInputData,
-                          size_t aInputSize,
-                          InfallibleVector<char>* aOutputData);
+// In a replaying process, flush all new external call found in the recording
+// since the last flush to the root replaying process.
+void FlushExternalCalls();
 
-// In the middleman process, reset all call state for a child process ID.
-void ResetMiddlemanCalls(size_t aChildId);
+// In a root replaying process, remember the output from an external call.
+void AddExternalCallOutput(ExternalCallId aId, const char* aOutput,
+                           size_t aOutputSize);
+
+// In a root replaying process, fetch the output from an external call if known.
+bool HasExternalCallOutput(ExternalCallId aId, InfallibleVector<char>* aOutput);
 
 ///////////////////////////////////////////////////////////////////////////////
-// Middleman Call Helpers
+// External Call Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
-// Capture the contents of an input buffer at BufferArg with element count at
-// CountArg.
-template <size_t BufferArg, size_t CountArg, typename ElemType = char>
-static inline void MM_Buffer(MiddlemanCallContext& aCx) {
-  if (aCx.AccessPreface()) {
-    auto& buffer = aCx.mArguments->Arg<BufferArg, void*>();
-    auto byteSize = aCx.mArguments->Arg<CountArg, size_t>() * sizeof(ElemType);
-    aCx.ReadOrWritePrefaceBuffer(&buffer, byteSize);
+// Capture a scalar argument.
+template <size_t Arg>
+static inline void EX_ScalarArg(ExternalCallContext& aCx) {
+  if (aCx.AccessInput()) {
+    auto& arg = aCx.mArguments->Arg<Arg, size_t>();
+    aCx.ReadOrWriteInputBytes(&arg, sizeof(arg));
   }
 }
 
-// Capture the contents of a fixed size input buffer.
-template <size_t BufferArg, size_t ByteSize>
-static inline void MM_BufferFixedSize(MiddlemanCallContext& aCx) {
-  if (aCx.AccessPreface()) {
+// Capture a floating point argument.
+template <size_t Arg>
+static inline void EX_FloatArg(ExternalCallContext& aCx) {
+  if (aCx.AccessInput()) {
+    auto& arg = aCx.mArguments->FloatArg<Arg>();
+    aCx.ReadOrWriteInputBytes(&arg, sizeof(arg));
+  }
+}
+
+// Capture an input buffer at BufferArg with element count at CountArg.
+// If IncludeContents is not set, the buffer's contents are not captured,
+// but the buffer's pointer will be allocated with the correct size when
+// restoring input.
+template <size_t BufferArg, size_t CountArg, typename ElemType = char,
+          bool IncludeContents = true>
+static inline void EX_Buffer(ExternalCallContext& aCx) {
+  EX_ScalarArg<CountArg>(aCx);
+
+  if (aCx.AccessInput()) {
     auto& buffer = aCx.mArguments->Arg<BufferArg, void*>();
-    if (buffer) {
-      aCx.ReadOrWritePrefaceBuffer(&buffer, ByteSize);
+    auto byteSize = aCx.mArguments->Arg<CountArg, size_t>() * sizeof(ElemType);
+    aCx.ReadOrWriteInputBuffer(&buffer, byteSize, IncludeContents);
+  }
+}
+
+// Capture the contents of an optional input parameter.
+template <size_t BufferArg, typename Type>
+static inline void EX_InParam(ExternalCallContext& aCx) {
+  if (aCx.AccessInput()) {
+    auto& param = aCx.mArguments->Arg<BufferArg, void*>();
+    bool hasParam = !!param;
+    aCx.ReadOrWriteInputBytes(&hasParam, sizeof(hasParam));
+    if (hasParam) {
+      aCx.ReadOrWriteInputBuffer(&param, sizeof(Type));
+    } else {
+      param = nullptr;
     }
   }
 }
 
 // Capture a C string argument.
 template <size_t StringArg>
-static inline void MM_CString(MiddlemanCallContext& aCx) {
-  if (aCx.AccessPreface()) {
+static inline void EX_CString(ExternalCallContext& aCx) {
+  if (aCx.AccessInput()) {
     auto& buffer = aCx.mArguments->Arg<StringArg, char*>();
-    size_t len = (aCx.mPhase == MiddlemanCallPhase::ReplayPreface)
+    size_t len = (aCx.mPhase == ExternalCallPhase::SaveInput)
                      ? strlen(buffer) + 1
                      : 0;
-    aCx.ReadOrWritePrefaceBytes(&len, sizeof(len));
-    aCx.ReadOrWritePrefaceBuffer((void**)&buffer, len);
+    aCx.ReadOrWriteInputBytes(&len, sizeof(len));
+    aCx.ReadOrWriteInputBuffer((void**)&buffer, len);
   }
 }
 
 // Capture the data written to an output buffer at BufferArg with element count
 // at CountArg.
 template <size_t BufferArg, size_t CountArg, typename ElemType>
-static inline void MM_WriteBuffer(MiddlemanCallContext& aCx) {
+static inline void EX_WriteBuffer(ExternalCallContext& aCx) {
+  EX_ScalarArg<CountArg>(aCx);
+
   auto& buffer = aCx.mArguments->Arg<BufferArg, void*>();
   auto count = aCx.mArguments->Arg<CountArg, size_t>();
   aCx.ReadOrWriteOutputBuffer(&buffer, count * sizeof(ElemType));
 }
 
-// Capture the data written to a fixed size output buffer.
-template <size_t BufferArg, size_t ByteSize>
-static inline void MM_WriteBufferFixedSize(MiddlemanCallContext& aCx) {
+// Capture the data written to an out parameter.
+template <size_t BufferArg, typename Type>
+static inline void EX_OutParam(ExternalCallContext& aCx) {
   auto& buffer = aCx.mArguments->Arg<BufferArg, void*>();
-  aCx.ReadOrWriteOutputBuffer(&buffer, ByteSize);
+  aCx.ReadOrWriteOutputBuffer(&buffer, sizeof(Type));
 }
 
 // Capture return values that are too large for register storage.
-template <size_t ByteSize>
-static inline void MM_OversizeRval(MiddlemanCallContext& aCx) {
-  MM_WriteBufferFixedSize<0, ByteSize>(aCx);
+template <typename Type>
+static inline void EX_OversizeRval(ExternalCallContext& aCx) {
+  EX_OutParam<0, Type>(aCx);
 }
 
 // Capture a byte count of stack argument data.
 template <size_t ByteSize>
-static inline void MM_StackArgumentData(MiddlemanCallContext& aCx) {
-  if (aCx.AccessPreface()) {
+static inline void EX_StackArgumentData(ExternalCallContext& aCx) {
+  if (aCx.AccessInput()) {
     auto stack = aCx.mArguments->StackAddress<0>();
-    aCx.ReadOrWritePrefaceBytes(stack, ByteSize);
+    aCx.ReadOrWriteInputBytes(stack, ByteSize);
   }
 }
 
-// Avoid calling a function in the middleman process.
-static inline void MM_SkipInMiddleman(MiddlemanCallContext& aCx) {
-  if (aCx.mPhase == MiddlemanCallPhase::MiddlemanInput) {
-    aCx.mSkipCallInMiddleman = true;
+// Avoid calling a function in the external process.
+static inline void EX_SkipExecuting(ExternalCallContext& aCx) {
+  if (aCx.mPhase == ExternalCallPhase::RestoreInput) {
+    aCx.mSkipExecuting = true;
   }
 }
 
-static inline void MM_NoOp(MiddlemanCallContext& aCx) {}
+static inline void EX_NoOp(ExternalCallContext& aCx) {}
 
-template <MiddlemanCallFn Fn0, MiddlemanCallFn Fn1,
-          MiddlemanCallFn Fn2 = MM_NoOp, MiddlemanCallFn Fn3 = MM_NoOp,
-          MiddlemanCallFn Fn4 = MM_NoOp>
-static inline void MM_Compose(MiddlemanCallContext& aCx) {
+template <ExternalCallFn Fn0, ExternalCallFn Fn1,
+          ExternalCallFn Fn2 = EX_NoOp, ExternalCallFn Fn3 = EX_NoOp,
+          ExternalCallFn Fn4 = EX_NoOp, ExternalCallFn Fn5 = EX_NoOp>
+static inline void EX_Compose(ExternalCallContext& aCx) {
   Fn0(aCx);
   Fn1(aCx);
   Fn2(aCx);
   Fn3(aCx);
   Fn4(aCx);
+  Fn5(aCx);
 }
 
-// Helper for capturing inputs that are produced by other middleman calls.
-// Returns false in the ReplayInput or MiddlemanInput phases if the input
-// system value could not be found.
-bool MM_SystemInput(MiddlemanCallContext& aCx, const void** aThingPtr);
+// Helper for capturing inputs that are produced by other external calls.
+// Returns false in the SaveInput phase if the input system value could not
+// be found.
+bool EX_SystemInput(ExternalCallContext& aCx, const void** aThingPtr);
 
 // Helper for capturing output system values that might be consumed by other
-// middleman calls.
-void MM_SystemOutput(MiddlemanCallContext& aCx, const void** aOutput,
+// external calls.
+void EX_SystemOutput(ExternalCallContext& aCx, const void** aOutput,
                      bool aUpdating = false);
 
+void InitializeExternalCalls();
+
 }  // namespace recordreplay
 }  // namespace mozilla
 
-#endif  // mozilla_recordreplay_MiddlemanCall_h
+#endif  // mozilla_recordreplay_ExternalCall_h
--- a/toolkit/recordreplay/HashTable.cpp
+++ b/toolkit/recordreplay/HashTable.cpp
@@ -108,26 +108,24 @@ class StableHashTableInfo {
       : mLastKey(nullptr),
         mLastNewHash(0),
         mHashGenerator(0),
         mCallbackStorage(nullptr),
         mDestroyed(false),
         mTable(nullptr),
         mCallbackHash(0) {
     // Use AllocateMemory, as the result will have RWX permissions.
-    mCallbackStorage =
-        (uint8_t*)AllocateMemory(CallbackStorageCapacity, MemoryKind::Tracked);
+    mCallbackStorage = (uint8_t*)DirectAllocateMemory(CallbackStorageCapacity);
 
     MarkValid();
   }
 
   ~StableHashTableInfo() {
     MOZ_RELEASE_ASSERT(mHashToKey.empty());
-    DeallocateMemory(mCallbackStorage, CallbackStorageCapacity,
-                     MemoryKind::Tracked);
+    DirectDeallocateMemory(mCallbackStorage, CallbackStorageCapacity);
 
     UnmarkValid();
   }
 
   bool IsDestroyed() { return mDestroyed; }
 
   void MarkDestroyed() {
     MOZ_RELEASE_ASSERT(!IsDestroyed());
--- a/toolkit/recordreplay/Lock.cpp
+++ b/toolkit/recordreplay/Lock.cpp
@@ -32,49 +32,52 @@ struct LockAcquires {
   static const size_t NoNextOwner = 0;
 
   void ReadAndNotifyNextOwner(Thread* aCurrentThread) {
     MOZ_RELEASE_ASSERT(IsReplaying());
     if (mAcquires->AtEnd()) {
       mNextOwner = NoNextOwner;
     } else {
       mNextOwner = mAcquires->ReadScalar();
+      if (!mNextOwner) {
+        Print("CRASH ReadAndNotifyNextOwner ZERO_ID\n");
+      }
       if (mNextOwner != aCurrentThread->Id()) {
         Thread::Notify(mNextOwner);
       }
     }
   }
 };
 
 // Acquires for each lock, indexed by the lock ID.
 static ChunkAllocator<LockAcquires> gLockAcquires;
 
 ///////////////////////////////////////////////////////////////////////////////
 // Locking Interface
 ///////////////////////////////////////////////////////////////////////////////
 
 // Table mapping native lock pointers to the associated Lock structure, for
 // every recorded lock in existence.
-typedef std::unordered_map<void*, Lock*> LockMap;
+typedef std::unordered_map<NativeLock*, Lock*> LockMap;
 static LockMap* gLocks;
 static ReadWriteSpinLock gLocksLock;
 
 static Lock* CreateNewLock(Thread* aThread, size_t aId) {
   LockAcquires* info = gLockAcquires.Create(aId);
-  info->mAcquires = gRecordingFile->OpenStream(StreamName::Lock, aId);
+  info->mAcquires = gRecording->OpenStream(StreamName::Lock, aId);
 
   if (IsReplaying()) {
     info->ReadAndNotifyNextOwner(aThread);
   }
 
   return new Lock(aId);
 }
 
 /* static */
-void Lock::New(void* aNativeLock) {
+void Lock::New(NativeLock* aNativeLock) {
   Thread* thread = Thread::Current();
   RecordingEventSection res(thread);
   if (!res.CanAccessEvents()) {
     Destroy(aNativeLock);  // Clean up any old lock, as below.
     return;
   }
 
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::CreateLock);
@@ -99,33 +102,33 @@ void Lock::New(void* aNativeLock) {
   }
 
   gLocks->insert(LockMap::value_type(aNativeLock, lock));
 
   thread->EndDisallowEvents();
 }
 
 /* static */
-void Lock::Destroy(void* aNativeLock) {
+void Lock::Destroy(NativeLock* aNativeLock) {
   Lock* lock = nullptr;
   {
     AutoWriteSpinLock ex(gLocksLock);
     if (gLocks) {
       LockMap::iterator iter = gLocks->find(aNativeLock);
       if (iter != gLocks->end()) {
         lock = iter->second;
         gLocks->erase(iter);
       }
     }
   }
   delete lock;
 }
 
 /* static */
-Lock* Lock::Find(void* aNativeLock) {
+Lock* Lock::Find(NativeLock* aNativeLock) {
   MOZ_RELEASE_ASSERT(IsRecordingOrReplaying());
 
   AutoReadSpinLock ex(gLocksLock);
 
   if (gLocks) {
     LockMap::iterator iter = gLocks->find(aNativeLock);
     if (iter != gLocks->end()) {
       // Now that we know the lock is recorded, check whether thread events
@@ -141,17 +144,17 @@ Lock* Lock::Find(void* aNativeLock) {
       }
       return lock;
     }
   }
 
   return nullptr;
 }
 
-void Lock::Enter() {
+void Lock::Enter(NativeLock* aNativeLock) {
   Thread* thread = Thread::Current();
 
   RecordingEventSection res(thread);
   if (!res.CanAccessEvents()) {
     return;
   }
 
   // Include an event in each thread's record when a lock acquire begins. This
@@ -166,35 +169,37 @@ void Lock::Enter() {
     acquires->mAcquires->WriteScalar(thread->Id());
   } else {
     // Wait until this thread is next in line to acquire the lock, or until it
     // has been instructed to diverge from the recording.
     while (thread->Id() != acquires->mNextOwner &&
            !thread->MaybeDivergeFromRecording()) {
       Thread::Wait();
     }
-    if (!thread->HasDivergedFromRecording()) {
-      mOwner = thread->Id();
+    if (!thread->HasDivergedFromRecording() && aNativeLock) {
+      thread->AddOwnedLock(aNativeLock);
     }
   }
 }
 
-void Lock::Exit() {
+void Lock::Exit(NativeLock* aNativeLock) {
   Thread* thread = Thread::Current();
   if (IsReplaying() && !thread->HasDivergedFromRecording()) {
-    mOwner = 0;
+    if (aNativeLock) {
+      thread->RemoveOwnedLock(aNativeLock);
+    }
 
     // Notify the next owner before releasing the lock.
     LockAcquires* acquires = gLockAcquires.Get(mId);
     acquires->ReadAndNotifyNextOwner(thread);
   }
 }
 
 /* static */
-void Lock::LockAquiresUpdated(size_t aLockId) {
+void Lock::LockAcquiresUpdated(size_t aLockId) {
   LockAcquires* acquires = gLockAcquires.MaybeGet(aLockId);
   if (acquires && acquires->mAcquires &&
       acquires->mNextOwner == LockAcquires::NoNextOwner) {
     acquires->ReadAndNotifyNextOwner(Thread::Current());
   }
 }
 
 // We use a set of Locks to record and replay the order in which atomic
@@ -253,17 +258,17 @@ MOZ_EXPORT void RecordReplayInterface_In
   // When recording, hold a spin lock so that no other thread can access this
   // same atomic until this access ends. When replaying, we don't need to hold
   // any actual lock, as the atomic access cannot race and the Lock structure
   // ensures that accesses happen in the same order.
   if (IsRecording()) {
     gAtomicLockOwners[atomicId].Lock();
   }
 
-  gAtomicLocks[atomicId]->Enter();
+  gAtomicLocks[atomicId]->Enter(nullptr);
 
   MOZ_RELEASE_ASSERT(thread->AtomicLockId().isNothing());
   thread->AtomicLockId().emplace(atomicId);
 }
 
 MOZ_EXPORT void RecordReplayInterface_InternalEndOrderedAtomicAccess() {
   MOZ_RELEASE_ASSERT(IsRecordingOrReplaying());
 
@@ -276,15 +281,15 @@ MOZ_EXPORT void RecordReplayInterface_In
   MOZ_RELEASE_ASSERT(thread->AtomicLockId().isSome());
   size_t atomicId = thread->AtomicLockId().ref();
   thread->AtomicLockId().reset();
 
   if (IsRecording()) {
     gAtomicLockOwners[atomicId].Unlock();
   }
 
-  gAtomicLocks[atomicId]->Exit();
+  gAtomicLocks[atomicId]->Exit(nullptr);
 }
 
 }  // extern "C"
 
 }  // namespace recordreplay
 }  // namespace mozilla
--- a/toolkit/recordreplay/Lock.h
+++ b/toolkit/recordreplay/Lock.h
@@ -5,17 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_recordreplay_Lock_h
 #define mozilla_recordreplay_Lock_h
 
 #include "mozilla/PodOperations.h"
 #include "mozilla/Types.h"
 
-#include "File.h"
+#include "Recording.h"
 
 namespace mozilla {
 namespace recordreplay {
 
 // Recorded Locks Overview.
 //
 // Each platform has some types used for native locks (e.g. pthread_mutex_t or
 // CRITICAL_SECTION). System APIs which operate on these native locks are
@@ -24,46 +24,43 @@ namespace recordreplay {
 // recorded, and lock acquire orders will be replayed in the same order with
 // which they originally occurred.
 
 // Information about a recorded lock.
 class Lock {
   // Unique ID for this lock.
   size_t mId;
 
-  // When replaying, any thread owning this lock as part of the recording.
-  Atomic<size_t, SequentiallyConsistent, Behavior::DontPreserve> mOwner;
-
  public:
-  explicit Lock(size_t aId) : mId(aId), mOwner(0) { MOZ_ASSERT(aId); }
+  explicit Lock(size_t aId) : mId(aId) { MOZ_ASSERT(aId); }
 
   size_t Id() { return mId; }
 
   // When recording, this is called after the lock has been acquired, and
   // records the acquire in the lock's acquire order stream. When replaying,
   // this is called before the lock has been acquired, and blocks the thread
   // until it is next in line to acquire the lock.
-  void Enter();
+  void Enter(NativeLock* aNativeLock);
 
   // This is called before releasing the lock, allowing the next owner to
   // acquire it while replaying.
-  void Exit();
+  void Exit(NativeLock* aNativeLock);
 
   // Create a new Lock corresponding to a native lock, with a fresh ID.
-  static void New(void* aNativeLock);
+  static void New(NativeLock* aNativeLock);
 
   // Destroy any Lock associated with a native lock.
-  static void Destroy(void* aNativeLock);
+  static void Destroy(NativeLock* aNativeLock);
 
   // Get the recorded Lock for a native lock if there is one, otherwise null.
-  static Lock* Find(void* aNativeLock);
+  static Lock* Find(NativeLock* aNativeLock);
 
   // Initialize locking state.
   static void InitializeLocks();
 
   // Note that new data has been read into a lock's acquires stream.
-  static void LockAquiresUpdated(size_t aLockId);
+  static void LockAcquiresUpdated(size_t aLockId);
 };
 
 }  // namespace recordreplay
 }  // namespace mozilla
 
 #endif  // mozilla_recordreplay_Lock_h
deleted file mode 100644
--- a/toolkit/recordreplay/MemorySnapshot.cpp
+++ /dev/null
@@ -1,1316 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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 "MemorySnapshot.h"
-
-#include "ipc/ChildInternal.h"
-#include "mozilla/Maybe.h"
-#include "DirtyMemoryHandler.h"
-#include "InfallibleVector.h"
-#include "ProcessRecordReplay.h"
-#include "ProcessRewind.h"
-#include "SpinLock.h"
-#include "SplayTree.h"
-#include "Thread.h"
-
-#include <algorithm>
-#include <mach/mach.h>
-#include <mach/mach_vm.h>
-
-// Define to enable the countdown debugging thread. See StartCountdown().
-//#define WANT_COUNTDOWN_THREAD 1
-
-namespace mozilla {
-namespace recordreplay {
-
-///////////////////////////////////////////////////////////////////////////////
-// Memory Snapshots Overview.
-//
-// Snapshots are periodically saved, storing in memory enough information
-// for the process to restore the contents of all tracked memory as it
-// rewinds to the point the snapshot was made. There are two components to a
-// snapshot:
-//
-// - Stack contents for each thread are completely saved for each snapshot.
-//   This is handled by ThreadSnapshot.cpp
-//
-// - Heap and static memory contents (tracked memory) are saved in memory as
-//   the contents of pages modified before either the the next snapshot
-//   or the current execution point (if this is the last snapshot).
-//   This is handled here.
-//
-// Heap memory is only tracked when allocated with TrackedMemoryKind.
-//
-// Snapshots of heap/static memory is modeled on the copy-on-write semantics
-// used by fork. Instead of actually forking, we use write-protected memory and
-// a fault handler to perform the copy-on-write, which both gives more control
-// of the snapshot process and allows snapshots to be taken on platforms
-// without fork (i.e. Windows). The following example shows how snapshots are
-// generated:
-//
-// #1 Save snapshot A. The initial snapshot tabulates all allocated tracked
-//    memory in the process, and write-protects all of it.
-//
-// #2 Write pages P0 and P1. Writing to the pages trips the fault handler. The
-//    handler creates copies of the initial contents of P0 and P1 (P0a and P1a)
-//    and unprotects the pages.
-//
-// #3 Save snapshot B. P0a and P1a, along with any other pages modified
-//    between A and B, become associated with snapshot A. All modified pages
-//    are reprotected.
-//
-// #4 Write pages P1 and P2. Again, writing to the pages trips the fault
-//    handler and copies P1b and P2b are created and the pages are unprotected.
-//
-// #5 Save snapshot C. P1b and P2b become associated with snapshot B, and the
-//    modified pages are reprotected.
-//
-// If we were to then rewind from C to A, we would read and restore P1b/P2b,
-// followed by P0a/P1a. All data associated with snapshots A and later is
-// discarded (we can only rewind; we cannot jump forward in time).
-///////////////////////////////////////////////////////////////////////////////
-
-///////////////////////////////////////////////////////////////////////////////
-// Snapshot Threads Overview.
-//
-// After step #3 above, the main thread has created a diff snapshot with the
-// copies of the original contents of pages modified between two snapshots.
-// These page copies are initially all in memory. It is the responsibility of
-// the snapshot threads to do the following:
-//
-// 1. When rewinding to the last snapshot, snapshot threads are used to
-//    restore the original contents of pages using their in-memory copies.
-//
-// There are a fixed number of snapshot threads that are spawned when the
-// first snapshot is saved, which is always at the first checkpoint. Threads are
-// each responsible for distinct sets of heap memory pages
-// (see AddDirtyPageToWorklist), avoiding synchronization issues between
-// different snapshot threads.
-///////////////////////////////////////////////////////////////////////////////
-
-///////////////////////////////////////////////////////////////////////////////
-// Memory Snapshot Structures
-///////////////////////////////////////////////////////////////////////////////
-
-// A region of allocated memory which should be tracked by MemoryInfo.
-struct AllocatedMemoryRegion {
-  uint8_t* mBase;
-  size_t mSize;
-  bool mExecutable;
-
-  AllocatedMemoryRegion() : mBase(nullptr), mSize(0), mExecutable(false) {}
-
-  AllocatedMemoryRegion(uint8_t* aBase, size_t aSize, bool aExecutable)
-      : mBase(aBase), mSize(aSize), mExecutable(aExecutable) {}
-
-  // For sorting regions by base address.
-  struct AddressSort {
-    typedef void* Lookup;
-    static void* getLookup(const AllocatedMemoryRegion& aRegion) {
-      return aRegion.mBase;
-    }
-    static ssize_t compare(void* aAddress,
-                           const AllocatedMemoryRegion& aRegion) {
-      return (uint8_t*)aAddress - aRegion.mBase;
-    }
-  };
-
-  // For sorting regions by size, from largest to smallest.
-  struct SizeReverseSort {
-    typedef size_t Lookup;
-    static size_t getLookup(const AllocatedMemoryRegion& aRegion) {
-      return aRegion.mSize;
-    }
-    static ssize_t compare(size_t aSize, const AllocatedMemoryRegion& aRegion) {
-      return aRegion.mSize - aSize;
-    }
-  };
-};
-
-// Information about a page which was modified between two snapshots.
-struct DirtyPage {
-  // Base address of the page.
-  uint8_t* mBase;
-
-  // Copy of the page at the first snapshot. Written by the dirty memory
-  // handler via HandleDirtyMemoryFault if this is in the active page set,
-  // otherwise accessed by snapshot threads.
-  uint8_t* mOriginal;
-
-  bool mExecutable;
-
-  DirtyPage(uint8_t* aBase, uint8_t* aOriginal, bool aExecutable)
-      : mBase(aBase), mOriginal(aOriginal), mExecutable(aExecutable) {}
-
-  struct AddressSort {
-    typedef uint8_t* Lookup;
-    static uint8_t* getLookup(const DirtyPage& aPage) { return aPage.mBase; }
-    static ssize_t compare(uint8_t* aBase, const DirtyPage& aPage) {
-      return aBase - aPage.mBase;
-    }
-  };
-};
-
-// A set of dirty pages that can be searched quickly.
-typedef SplayTree<DirtyPage, DirtyPage::AddressSort,
-                  AllocPolicy<MemoryKind::SortedDirtyPageSet>, 4>
-    SortedDirtyPageSet;
-
-// A set of dirty pages associated with some snapshot.
-struct DirtyPageSet {
-  // All dirty pages in the set. Pages may be added or destroyed by the main
-  // thread when all other threads are idle, by the dirty memory handler when
-  // it is active and this is the active page set, and by the snapshot thread
-  // which owns this set.
-  InfallibleVector<DirtyPage, 256, AllocPolicy<MemoryKind::DirtyPageSet>>
-      mPages;
-};
-
-// Worklist used by each snapshot thread.
-struct SnapshotThreadWorklist {
-  // Index into gMemoryInfo->mSnapshotWorklists of the thread.
-  size_t mThreadIndex;
-
-  // Record/replay ID of the thread.
-  size_t mThreadId;
-
-  // Sets of pages in the thread's worklist. Each set is for a different diff,
-  // with the oldest snapshots first.
-  InfallibleVector<DirtyPageSet, 256, AllocPolicy<MemoryKind::Generic>> mSets;
-};
-
-// Structure used to coordinate activity between the main thread and all
-// snapshot threads. The workflow with this structure is as follows:
-//
-// 1. The main thread calls ActivateBegin(), marking the condition as active
-//    and notifying each snapshot thread. The main thread blocks in this call.
-//
-// 2. Each snapshot thread, maybe after waking up, checks the condition, does
-//    any processing it needs to (knowing the main thread is blocked) and
-//    then calls WaitUntilNoLongerActive(), blocking in the call.
-//
-// 3. Once all snapshot threads are blocked in WaitUntilNoLongerActive(), the
-//    main thread is unblocked from ActivateBegin(). It can then do whatever
-//    processing it needs to (knowing all snapshot threads are blocked) and
-//    then calls ActivateEnd(), blocking in the call.
-//
-// 4. Snapshot threads are now unblocked from WaitUntilNoLongerActive(). The
-//    main thread does not unblock from ActivateEnd() until all snapshot
-//    threads have left WaitUntilNoLongerActive().
-//
-// The intent with this class is to ensure that the main thread knows exactly
-// when the snapshot threads are operating and that there is no potential for
-// races between them.
-class SnapshotThreadCondition {
-  Atomic<bool, SequentiallyConsistent, Behavior::DontPreserve> mActive;
-  Atomic<int32_t, SequentiallyConsistent, Behavior::DontPreserve> mCount;
-
- public:
-  void ActivateBegin();
-  void ActivateEnd();
-  bool IsActive();
-  void WaitUntilNoLongerActive();
-};
-
-static const size_t NumSnapshotThreads = 8;
-
-// A set of free regions in the process. There are two of these, for the
-// free regions in tracked and untracked memory.
-class FreeRegionSet {
-  // Kind of memory being managed. This also describes the memory used by the
-  // set itself.
-  MemoryKind mKind;
-
-  // Lock protecting contents of the structure.
-  SpinLock mLock;
-
-  // To avoid reentrancy issues when growing the set, a chunk of pages for
-  // the splay tree is preallocated for use the next time the tree needs to
-  // expand its size.
-  static const size_t ChunkPages = 4;
-  void* mNextChunk;
-
-  // Ensure there is a chunk available for the splay tree.
-  void MaybeRefillNextChunk(AutoSpinLock& aLockHeld);
-
-  // Get the next chunk from the free region set for this memory kind.
-  void* TakeNextChunk();
-
-  struct MyAllocPolicy {
-    FreeRegionSet& mSet;
-
-    template <typename T>
-    void free_(T* aPtr, size_t aSize) {
-      MOZ_CRASH();
-    }
-
-    template <typename T>
-    T* pod_malloc(size_t aNumElems) {
-      MOZ_RELEASE_ASSERT(sizeof(T) * aNumElems <= ChunkPages * PageSize);
-      return (T*)mSet.TakeNextChunk();
-    }
-
-    explicit MyAllocPolicy(FreeRegionSet& aSet) : mSet(aSet) {}
-  };
-
-  // All memory in gMemoryInfo->mTrackedRegions that is not in use at the
-  // current point in execution.
-  typedef SplayTree<AllocatedMemoryRegion,
-                    AllocatedMemoryRegion::SizeReverseSort, MyAllocPolicy,
-                    ChunkPages>
-      Tree;
-  Tree mRegions;
-
-  void InsertLockHeld(void* aAddress, size_t aSize, AutoSpinLock& aLockHeld);
-  void* ExtractLockHeld(size_t aSize, AutoSpinLock& aLockHeld);
-
- public:
-  explicit FreeRegionSet(MemoryKind aKind)
-      : mKind(aKind), mRegions(MyAllocPolicy(*this)) {}
-
-  // Get the single region set for a given memory kind.
-  static FreeRegionSet& Get(MemoryKind aKind);
-
-  // Add a free region to the set.
-  void Insert(void* aAddress, size_t aSize);
-
-  // Remove a free region of the specified size. If aAddress is specified then
-  // this address will be prioritized, but a different pointer may be returned.
-  // The resulting memory will be zeroed.
-  void* Extract(void* aAddress, size_t aSize);
-
-  // Return whether a memory range intersects this set at all.
-  bool Intersects(void* aAddress, size_t aSize);
-};
-
-// Information about the current memory state. The contents of this structure
-// are in untracked memory.
-struct MemoryInfo {
-  // Whether new dirty pages or allocated regions are allowed.
-  bool mMemoryChangesAllowed;
-
-  // Untracked memory regions allocated before the first checkpoint/snapshot.
-  // This is only accessed on the main thread, and is not a vector because of
-  // reentrancy issues.
-  static const size_t MaxInitialUntrackedRegions = 512;
-  AllocatedMemoryRegion mInitialUntrackedRegions[MaxInitialUntrackedRegions];
-  SpinLock mInitialUntrackedRegionsLock;
-
-  // All tracked memory in the process. This may be updated by any thread while
-  // holding mTrackedRegionsLock.
-  SplayTree<AllocatedMemoryRegion, AllocatedMemoryRegion::AddressSort,
-            AllocPolicy<MemoryKind::TrackedRegions>, 4>
-      mTrackedRegions;
-  InfallibleVector<AllocatedMemoryRegion, 512,
-                   AllocPolicy<MemoryKind::TrackedRegions>>
-      mTrackedRegionsByAllocationOrder;
-  SpinLock mTrackedRegionsLock;
-
-  // Pages from |trackedRegions| modified since the last snapshot.
-  // Accessed by any thread (usually the dirty memory handler) when memory
-  // changes are allowed, and by the main thread when memory changes are not
-  // allowed.
-  SortedDirtyPageSet mActiveDirty;
-  SpinLock mActiveDirtyLock;
-
-  // All untracked memory which is available for new allocations.
-  FreeRegionSet mFreeUntrackedRegions;
-
-  // Worklists for each snapshot thread.
-  SnapshotThreadWorklist mSnapshotWorklists[NumSnapshotThreads];
-
-  // Whether snapshot threads should update memory to that when the last saved
-  // diff was started.
-  SnapshotThreadCondition mSnapshotThreadsShouldRestore;
-
-  // Whether snapshot threads should idle.
-  SnapshotThreadCondition mSnapshotThreadsShouldIdle;
-
-  // Counter used by the countdown thread.
-  Atomic<size_t, SequentiallyConsistent, Behavior::DontPreserve> mCountdown;
-
-  // Information for timers.
-  double mStartTime;
-  uint32_t mTimeHits[(size_t)TimerKind::Count];
-  double mTimeTotals[(size_t)TimerKind::Count];
-
-  // Information for memory allocation.
-  Atomic<ssize_t, Relaxed, Behavior::DontPreserve>
-      mMemoryBalance[(size_t)MemoryKind::Count];
-
-  // Recent dirty memory faults.
-  void* mDirtyMemoryFaults[50];
-
-  MemoryInfo()
-      : mMemoryChangesAllowed(true),
-        mFreeUntrackedRegions(MemoryKind::FreeRegions),
-        mStartTime(CurrentTime()) {
-    // The singleton MemoryInfo is allocated with zeroed memory, so other
-    // fields do not need explicit initialization.
-  }
-};
-
-static MemoryInfo* gMemoryInfo = nullptr;
-
-void SetMemoryChangesAllowed(bool aAllowed) {
-  MOZ_RELEASE_ASSERT(gMemoryInfo->mMemoryChangesAllowed == !aAllowed);
-  gMemoryInfo->mMemoryChangesAllowed = aAllowed;
-}
-
-static void EnsureMemoryChangesAllowed() {
-  while (!gMemoryInfo->mMemoryChangesAllowed) {
-    ThreadYield();
-  }
-}
-
-void StartCountdown(size_t aCount) { gMemoryInfo->mCountdown = aCount; }
-
-AutoCountdown::AutoCountdown(size_t aCount) { StartCountdown(aCount); }
-
-AutoCountdown::~AutoCountdown() { StartCountdown(0); }
-
-#ifdef WANT_COUNTDOWN_THREAD
-
-static void CountdownThreadMain(void*) {
-  while (true) {
-    if (gMemoryInfo->mCountdown && --gMemoryInfo->mCountdown == 0) {
-      // When debugging hangs in the child process, we can break here in lldb
-      // to inspect what the process is doing.
-      child::ReportFatalError(Nothing(), "CountdownThread activated");
-    }
-    ThreadYield();
-  }
-}
-
-#endif  // WANT_COUNTDOWN_THREAD
-
-///////////////////////////////////////////////////////////////////////////////
-// Profiling
-///////////////////////////////////////////////////////////////////////////////
-
-AutoTimer::AutoTimer(TimerKind aKind) : mKind(aKind), mStart(CurrentTime()) {}
-
-AutoTimer::~AutoTimer() {
-  if (gMemoryInfo) {
-    gMemoryInfo->mTimeHits[(size_t)mKind]++;
-    gMemoryInfo->mTimeTotals[(size_t)mKind] += CurrentTime() - mStart;
-  }
-}
-
-static const char* gTimerKindNames[] = {
-#define DefineTimerKindName(aKind) #aKind,
-    ForEachTimerKind(DefineTimerKindName)
-#undef DefineTimerKindName
-};
-
-void DumpTimers() {
-  if (!gMemoryInfo) {
-    return;
-  }
-  Print("Times %.2fs\n", (CurrentTime() - gMemoryInfo->mStartTime) / 1000000.0);
-  for (size_t i = 0; i < (size_t)TimerKind::Count; i++) {
-    uint32_t hits = gMemoryInfo->mTimeHits[i];
-    double time = gMemoryInfo->mTimeTotals[i];
-    Print("%s: %d hits, %.2fs\n", gTimerKindNames[i], (int)hits,
-          time / 1000000.0);
-  }
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Snapshot Thread Conditions
-///////////////////////////////////////////////////////////////////////////////
-
-void SnapshotThreadCondition::ActivateBegin() {
-  MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
-  MOZ_RELEASE_ASSERT(!mActive);
-  mActive = true;
-  for (size_t i = 0; i < NumSnapshotThreads; i++) {
-    Thread::Notify(gMemoryInfo->mSnapshotWorklists[i].mThreadId);
-  }
-  while (mCount != NumSnapshotThreads) {
-    Thread::WaitNoIdle();
-  }
-}
-
-void SnapshotThreadCondition::ActivateEnd() {
-  MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
-  MOZ_RELEASE_ASSERT(mActive);
-  mActive = false;
-  for (size_t i = 0; i < NumSnapshotThreads; i++) {
-    Thread::Notify(gMemoryInfo->mSnapshotWorklists[i].mThreadId);
-  }
-  while (mCount) {
-    Thread::WaitNoIdle();
-  }
-}
-
-bool SnapshotThreadCondition::IsActive() {
-  MOZ_RELEASE_ASSERT(!Thread::CurrentIsMainThread());
-  return mActive;
-}
-
-void SnapshotThreadCondition::WaitUntilNoLongerActive() {
-  MOZ_RELEASE_ASSERT(!Thread::CurrentIsMainThread());
-  MOZ_RELEASE_ASSERT(mActive);
-  if (NumSnapshotThreads == ++mCount) {
-    Thread::Notify(MainThreadId);
-  }
-  while (mActive) {
-    Thread::WaitNoIdle();
-  }
-  if (0 == --mCount) {
-    Thread::Notify(MainThreadId);
-  }
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Snapshot Page Allocation
-///////////////////////////////////////////////////////////////////////////////
-
-// Get a page in untracked memory that can be used as a copy of a tracked page.
-static uint8_t* AllocatePageCopy() {
-  return (uint8_t*)AllocateMemory(PageSize, MemoryKind::PageCopy);
-}
-
-// Free a page allocated by AllocatePageCopy.
-static void FreePageCopy(uint8_t* aPage) {
-  DeallocateMemory(aPage, PageSize, MemoryKind::PageCopy);
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Page Fault Handling
-///////////////////////////////////////////////////////////////////////////////
-
-void MemoryMove(void* aDst, const void* aSrc, size_t aSize) {
-  MOZ_RELEASE_ASSERT((size_t)aDst % sizeof(uint32_t) == 0);
-  MOZ_RELEASE_ASSERT((size_t)aSrc % sizeof(uint32_t) == 0);
-  MOZ_RELEASE_ASSERT(aSize % sizeof(uint32_t) == 0);
-  MOZ_RELEASE_ASSERT((size_t)aDst <= (size_t)aSrc ||
-                     (size_t)aDst >= (size_t)aSrc + aSize);
-
-  uint32_t* ndst = (uint32_t*)aDst;
-  const uint32_t* nsrc = (const uint32_t*)aSrc;
-  for (size_t i = 0; i < aSize / sizeof(uint32_t); i++) {
-    ndst[i] = nsrc[i];
-  }
-}
-
-void MemoryZero(void* aDst, size_t aSize) {
-  MOZ_RELEASE_ASSERT((size_t)aDst % sizeof(uint32_t) == 0);
-  MOZ_RELEASE_ASSERT(aSize % sizeof(uint32_t) == 0);
-
-  // Use volatile here to avoid annoying clang optimizations.
-  volatile uint32_t* ndst = (uint32_t*)aDst;
-  for (size_t i = 0; i < aSize / sizeof(uint32_t); i++) {
-    ndst[i] = 0;
-  }
-}
-
-// Return whether an address is in a tracked region. This excludes memory that
-// is in an active new region and is not write protected.
-static bool IsTrackedAddress(void* aAddress, bool* aExecutable) {
-  AutoSpinLock lock(gMemoryInfo->mTrackedRegionsLock);
-  Maybe<AllocatedMemoryRegion> region =
-      gMemoryInfo->mTrackedRegions.lookupClosestLessOrEqual(aAddress);
-  if (region.isSome() &&
-      MemoryContains(region.ref().mBase, region.ref().mSize, aAddress)) {
-    if (aExecutable) {
-      *aExecutable = region.ref().mExecutable;
-    }
-    return true;
-  }
-  return false;
-}
-
-bool HandleDirtyMemoryFault(uint8_t* aAddress) {
-  EnsureMemoryChangesAllowed();
-
-  bool different = false;
-  for (size_t i = ArrayLength(gMemoryInfo->mDirtyMemoryFaults) - 1; i; i--) {
-    gMemoryInfo->mDirtyMemoryFaults[i] = gMemoryInfo->mDirtyMemoryFaults[i - 1];
-    if (gMemoryInfo->mDirtyMemoryFaults[i] != aAddress) {
-      different = true;
-    }
-  }
-  gMemoryInfo->mDirtyMemoryFaults[0] = aAddress;
-  if (!different) {
-    Print("WARNING: Repeated accesses to the same dirty address %p\n",
-          aAddress);
-  }
-
-  // Round down to the base of the page.
-  aAddress = PageBase(aAddress);
-
-  AutoSpinLock lock(gMemoryInfo->mActiveDirtyLock);
-
-  // Check to see if this is already an active dirty page. Once a page has been
-  // marked as dirty it will be accessible until the next snapshot is taken,
-  // but it's possible for multiple threads to access the same protected memory
-  // before we have a chance to unprotect it, in which case we'll end up here
-  // multiple times for the page.
-  if (gMemoryInfo->mActiveDirty.maybeLookup(aAddress)) {
-    return true;
-  }
-
-  // Crash if this address is not in a tracked region.
-  bool executable;
-  if (!IsTrackedAddress(aAddress, &executable)) {
-    return false;
-  }
-
-  // Copy the page's original contents into the active dirty set, and unprotect
-  // it so that execution can proceed.
-  uint8_t* original = AllocatePageCopy();
-  MemoryMove(original, aAddress, PageSize);
-  gMemoryInfo->mActiveDirty.insert(aAddress,
-                                   DirtyPage(aAddress, original, executable));
-  DirectUnprotectMemory(aAddress, PageSize, executable);
-  return true;
-}
-
-bool MemoryRangeIsTracked(void* aAddress, size_t aSize) {
-  for (uint8_t* ptr = PageBase(aAddress); ptr < (uint8_t*)aAddress + aSize;
-       ptr += PageSize) {
-    if (!IsTrackedAddress(ptr, nullptr)) {
-      return false;
-    }
-  }
-  return true;
-}
-
-void UnrecoverableSnapshotFailure() {
-  if (gMemoryInfo) {
-    AutoSpinLock lock(gMemoryInfo->mTrackedRegionsLock);
-    DirectUnprotectMemory(PageBase(&errno), PageSize, false);
-    for (auto region : gMemoryInfo->mTrackedRegionsByAllocationOrder) {
-      DirectUnprotectMemory(region.mBase, region.mSize, region.mExecutable,
-                            /* aIgnoreFailures = */ true);
-    }
-  }
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Initial Memory Region Processing
-///////////////////////////////////////////////////////////////////////////////
-
-void AddInitialUntrackedMemoryRegion(uint8_t* aBase, size_t aSize) {
-  MOZ_RELEASE_ASSERT(!NumSnapshots());
-
-  if (gInitializationFailureMessage) {
-    return;
-  }
-
-  static void* gSkippedRegion;
-  if (!gSkippedRegion) {
-    // We are allocating gMemoryInfo itself, and will directly call this
-    // function again shortly.
-    gSkippedRegion = aBase;
-    return;
-  }
-  MOZ_RELEASE_ASSERT(gSkippedRegion == gMemoryInfo);
-
-  AutoSpinLock lock(gMemoryInfo->mInitialUntrackedRegionsLock);
-
-  for (AllocatedMemoryRegion& region : gMemoryInfo->mInitialUntrackedRegions) {
-    if (!region.mBase) {
-      region.mBase = aBase;
-      region.mSize = aSize;
-      return;
-    }
-  }
-
-  // If we end up here then MaxInitialUntrackedRegions should be larger.
-  MOZ_CRASH();
-}
-
-static void RemoveInitialUntrackedRegion(uint8_t* aBase, size_t aSize) {
-  MOZ_RELEASE_ASSERT(!NumSnapshots());
-  AutoSpinLock lock(gMemoryInfo->mInitialUntrackedRegionsLock);
-
-  for (AllocatedMemoryRegion& region : gMemoryInfo->mInitialUntrackedRegions) {
-    if (region.mBase == aBase) {
-      MOZ_RELEASE_ASSERT(region.mSize == aSize);
-      region.mBase = nullptr;
-      region.mSize = 0;
-      return;
-    }
-  }
-  MOZ_CRASH();
-}
-
-// Get information about the mapped region containing *aAddress, or the next
-// mapped region afterwards if aAddress is not mapped. aAddress is updated to
-// the start of that region, and aSize, aProtection, and aMaxProtection are
-// updated with the size and protection status of the region. Returns false if
-// there are no more mapped regions after *aAddress.
-static bool QueryRegion(uint8_t** aAddress, size_t* aSize,
-                        int* aProtection = nullptr,
-                        int* aMaxProtection = nullptr) {
-  mach_vm_address_t addr = (mach_vm_address_t)*aAddress;
-  mach_vm_size_t nbytes;
-
-  vm_region_basic_info_64 info;
-  mach_msg_type_number_t info_count = sizeof(vm_region_basic_info_64);
-  mach_port_t some_port;
-  kern_return_t rv =
-      mach_vm_region(mach_task_self(), &addr, &nbytes, VM_REGION_BASIC_INFO,
-                     (vm_region_info_t)&info, &info_count, &some_port);
-  if (rv == KERN_INVALID_ADDRESS) {
-    return false;
-  }
-  MOZ_RELEASE_ASSERT(rv == KERN_SUCCESS);
-
-  *aAddress = (uint8_t*)addr;
-  *aSize = nbytes;
-  if (aProtection) {
-    *aProtection = info.protection;
-  }
-  if (aMaxProtection) {
-    *aMaxProtection = info.max_protection;
-  }
-  return true;
-}
-
-static void MarkThreadStacksAsUntracked() {
-  AutoPassThroughThreadEvents pt;
-
-  // Thread stacks are excluded from the tracked regions.
-  for (size_t i = MainThreadId; i <= MaxThreadId; i++) {
-    Thread* thread = Thread::GetById(i);
-    if (!thread->StackBase()) {
-      continue;
-    }
-
-    AddInitialUntrackedMemoryRegion(thread->StackBase(), thread->StackSize());
-
-    // Look for a mapped region with no access permissions immediately after
-    // the thread stack's allocated region, and include this in the untracked
-    // memory if found. This is done to avoid confusing breakpad, which will
-    // scan the allocated memory in this process and will not correctly
-    // determine stack boundaries if we track these trailing regions and end up
-    // marking them as readable.
-
-    // Find the mapped region containing the end of the thread's stack.
-    uint8_t* base = thread->StackBase() + thread->StackSize() - 1;
-    size_t size;
-    if (!QueryRegion(&base, &size)) {
-      MOZ_CRASH("Could not find memory region information for thread stack");
-    }
-
-    // Sanity check the region size. Note that we don't mark this entire region
-    // as untracked, since it may contain TLS data which should be tracked.
-    MOZ_RELEASE_ASSERT(base + size >=
-                       thread->StackBase() + thread->StackSize());
-
-    uint8_t* trailing = base + size;
-    size_t trailingSize;
-    int protection;
-    if (QueryRegion(&trailing, &trailingSize, &protection)) {
-      if (trailing == base + size && protection == 0) {
-        AddInitialUntrackedMemoryRegion(trailing, trailingSize);
-      }
-    }
-  }
-}
-
-// Given an address region [*aAddress, *aAddress + *aSize], return true if
-// there is any intersection with an excluded region
-// [aExclude, aExclude + aExcludeSize], set *aSize to contain the subregion
-// starting at aAddress which which is not excluded, and *aRemaining and
-// *aRemainingSize to any additional subregion which is not excluded.
-static bool MaybeExtractMemoryRegion(uint8_t* aAddress, size_t* aSize,
-                                     uint8_t** aRemaining,
-                                     size_t* aRemainingSize, uint8_t* aExclude,
-                                     size_t aExcludeSize) {
-  uint8_t* addrLimit = aAddress + *aSize;
-
-  // Expand the excluded region out to the containing page boundaries.
-  MOZ_RELEASE_ASSERT((size_t)aExclude % PageSize == 0);
-  aExcludeSize = RoundupSizeToPageBoundary(aExcludeSize);
-
-  uint8_t* excludeLimit = aExclude + aExcludeSize;
-
-  if (excludeLimit <= aAddress || addrLimit <= aExclude) {
-    // No intersection.
-    return false;
-  }
-
-  *aSize = std::max<ssize_t>(aExclude - aAddress, 0);
-  if (aRemaining) {
-    *aRemaining = excludeLimit;
-    *aRemainingSize = std::max<ssize_t>(addrLimit - *aRemaining, 0);
-  }
-  return true;
-}
-
-// Set *aSize to describe the number of bytes starting at aAddress that should
-// be considered tracked memory. *aRemaining and *aRemainingSize are set to any
-// remaining portion of the initial region after the first excluded portion
-// that is found.
-static void ExtractTrackedInitialMemoryRegion(uint8_t* aAddress, size_t* aSize,
-                                              uint8_t** aRemaining,
-                                              size_t* aRemainingSize) {
-  // Look for the earliest untracked region which intersects the given region.
-  const AllocatedMemoryRegion* earliestIntersect = nullptr;
-  for (const AllocatedMemoryRegion& region :
-       gMemoryInfo->mInitialUntrackedRegions) {
-    size_t size = *aSize;
-    if (MaybeExtractMemoryRegion(aAddress, &size, nullptr, nullptr,
-                                 region.mBase, region.mSize)) {
-      // There was an intersection.
-      if (!earliestIntersect || region.mBase < earliestIntersect->mBase) {
-        earliestIntersect = &region;
-      }
-    }
-  }
-
-  if (earliestIntersect) {
-    if (!MaybeExtractMemoryRegion(aAddress, aSize, aRemaining, aRemainingSize,
-                                  earliestIntersect->mBase,
-                                  earliestIntersect->mSize)) {
-      MOZ_CRASH();
-    }
-  } else {
-    // If there is no intersection then the entire region is tracked.
-    *aRemaining = aAddress + *aSize;
-    *aRemainingSize = 0;
-  }
-}
-
-static void AddTrackedRegion(uint8_t* aAddress, size_t aSize,
-                             bool aExecutable) {
-  if (aSize) {
-    AutoSpinLock lock(gMemoryInfo->mTrackedRegionsLock);
-    gMemoryInfo->mTrackedRegions.insert(
-        aAddress, AllocatedMemoryRegion(aAddress, aSize, aExecutable));
-    gMemoryInfo->mTrackedRegionsByAllocationOrder.emplaceBack(aAddress, aSize,
-                                                              aExecutable);
-  }
-}
-
-// Add any tracked subregions of [aAddress, aAddress + aSize].
-void AddInitialTrackedMemoryRegions(uint8_t* aAddress, size_t aSize,
-                                    bool aExecutable) {
-  while (aSize) {
-    uint8_t* remaining;
-    size_t remainingSize;
-    ExtractTrackedInitialMemoryRegion(aAddress, &aSize, &remaining,
-                                      &remainingSize);
-
-    AddTrackedRegion(aAddress, aSize, aExecutable);
-
-    aAddress = remaining;
-    aSize = remainingSize;
-  }
-}
-
-static void UpdateNumTrackedRegionsForSnapshot();
-
-// Fill in the set of tracked memory regions that are currently mapped within
-// this process.
-static void ProcessAllInitialMemoryRegions() {
-  MOZ_ASSERT(!AreThreadEventsPassedThrough());
-
-  {
-    AutoPassThroughThreadEvents pt;
-    for (uint8_t* addr = nullptr;;) {
-      size_t size;
-      int maxProtection;
-      if (!QueryRegion(&addr, &size, nullptr, &maxProtection)) {
-        break;
-      }
-
-      // Consider all memory regions that can possibly be written to, even if
-      // they aren't currently writable.
-      if (maxProtection & VM_PROT_WRITE) {
-        MOZ_RELEASE_ASSERT(maxProtection & VM_PROT_READ);
-        AddInitialTrackedMemoryRegions(addr, size,
-                                       maxProtection & VM_PROT_EXECUTE);
-      }
-
-      addr += size;
-    }
-  }
-
-  UpdateNumTrackedRegionsForSnapshot();
-
-  // Write protect all tracked memory.
-  AutoDisallowMemoryChanges disallow;
-  for (const AllocatedMemoryRegion& region :
-       gMemoryInfo->mTrackedRegionsByAllocationOrder) {
-    DirectWriteProtectMemory(region.mBase, region.mSize, region.mExecutable);
-  }
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Free Region Management
-///////////////////////////////////////////////////////////////////////////////
-
-// All memory in gMemoryInfo->mTrackedRegions that is not in use at the current
-// point in execution.
-static FreeRegionSet gFreeRegions(MemoryKind::Tracked);
-
-// The size of gMemoryInfo->mTrackedRegionsByAllocationOrder we expect to see
-// at the point of the last snapshot.
-static size_t gNumTrackedRegions;
-
-static void UpdateNumTrackedRegionsForSnapshot() {
-  MOZ_ASSERT(Thread::CurrentIsMainThread());
-  gNumTrackedRegions = gMemoryInfo->mTrackedRegionsByAllocationOrder.length();
-}
-
-void FixupFreeRegionsAfterRewind() {
-  // All memory that has been allocated since the associated snapshot was
-  // reached is now free, and may be reused for new allocations.
-  size_t newTrackedRegions =
-      gMemoryInfo->mTrackedRegionsByAllocationOrder.length();
-  for (size_t i = gNumTrackedRegions; i < newTrackedRegions; i++) {
-    const AllocatedMemoryRegion& region =
-        gMemoryInfo->mTrackedRegionsByAllocationOrder[i];
-    gFreeRegions.Insert(region.mBase, region.mSize);
-  }
-  gNumTrackedRegions = newTrackedRegions;
-}
-
-/* static */ FreeRegionSet& FreeRegionSet::Get(MemoryKind aKind) {
-  return (aKind == MemoryKind::Tracked) ? gFreeRegions
-                                        : gMemoryInfo->mFreeUntrackedRegions;
-}
-
-void* FreeRegionSet::TakeNextChunk() {
-  MOZ_RELEASE_ASSERT(mNextChunk);
-  void* res = mNextChunk;
-  mNextChunk = nullptr;
-  return res;
-}
-
-void FreeRegionSet::InsertLockHeld(void* aAddress, size_t aSize,
-                                   AutoSpinLock& aLockHeld) {
-  mRegions.insert(aSize,
-                  AllocatedMemoryRegion((uint8_t*)aAddress, aSize, true));
-}
-
-void FreeRegionSet::MaybeRefillNextChunk(AutoSpinLock& aLockHeld) {
-  if (mNextChunk) {
-    return;
-  }
-
-  // Look for a free region we can take the next chunk from.
-  size_t size = ChunkPages * PageSize;
-  gMemoryInfo->mMemoryBalance[(size_t)mKind] += size;
-
-  mNextChunk = ExtractLockHeld(size, aLockHeld);
-
-  if (!mNextChunk) {
-    // Allocate memory from the system.
-    mNextChunk = DirectAllocateMemory(nullptr, size);
-    RegisterAllocatedMemory(mNextChunk, size, mKind);
-  }
-}
-
-void FreeRegionSet::Insert(void* aAddress, size_t aSize) {
-  MOZ_RELEASE_ASSERT(aAddress && aAddress == PageBase(aAddress));
-  MOZ_RELEASE_ASSERT(aSize && aSize == RoundupSizeToPageBoundary(aSize));
-
-  AutoSpinLock lock(mLock);
-
-  MaybeRefillNextChunk(lock);
-  InsertLockHeld(aAddress, aSize, lock);
-}
-
-void* FreeRegionSet::ExtractLockHeld(size_t aSize, AutoSpinLock& aLockHeld) {
-  Maybe<AllocatedMemoryRegion> best =
-      mRegions.lookupClosestLessOrEqual(aSize, /* aRemove = */ true);
-  if (best.isSome()) {
-    MOZ_RELEASE_ASSERT(best.ref().mSize >= aSize);
-    uint8_t* res = best.ref().mBase;
-    if (best.ref().mSize > aSize) {
-      InsertLockHeld(res + aSize, best.ref().mSize - aSize, aLockHeld);
-    }
-    MemoryZero(res, aSize);
-    return res;
-  }
-  return nullptr;
-}
-
-void* FreeRegionSet::Extract(void* aAddress, size_t aSize) {
-  MOZ_RELEASE_ASSERT(aAddress == PageBase(aAddress));
-  MOZ_RELEASE_ASSERT(aSize && aSize == RoundupSizeToPageBoundary(aSize));
-
-  AutoSpinLock lock(mLock);
-
-  if (aAddress) {
-    MaybeRefillNextChunk(lock);
-
-    // We were given a point at which to try to place the allocation. Look for
-    // a free region which contains [aAddress, aAddress + aSize] entirely.
-    for (typename Tree::Iter iter = mRegions.begin(); !iter.done(); ++iter) {
-      uint8_t* regionBase = iter.ref().mBase;
-      uint8_t* regionExtent = regionBase + iter.ref().mSize;
-      uint8_t* addrBase = (uint8_t*)aAddress;
-      uint8_t* addrExtent = addrBase + aSize;
-      if (regionBase <= addrBase && regionExtent >= addrExtent) {
-        iter.removeEntry();
-        if (regionBase < addrBase) {
-          InsertLockHeld(regionBase, addrBase - regionBase, lock);
-        }
-        if (regionExtent > addrExtent) {
-          InsertLockHeld(addrExtent, regionExtent - addrExtent, lock);
-        }
-        MemoryZero(aAddress, aSize);
-        return aAddress;
-      }
-    }
-    // Fall through and look for a free region at another address.
-  }
-
-  // No address hint, look for the smallest free region which is larger than
-  // the desired allocation size.
-  return ExtractLockHeld(aSize, lock);
-}
-
-bool FreeRegionSet::Intersects(void* aAddress, size_t aSize) {
-  AutoSpinLock lock(mLock);
-  for (typename Tree::Iter iter = mRegions.begin(); !iter.done(); ++iter) {
-    if (MemoryIntersects(iter.ref().mBase, iter.ref().mSize, aAddress, aSize)) {
-      return true;
-    }
-  }
-  return false;
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Memory Management
-///////////////////////////////////////////////////////////////////////////////
-
-void RegisterAllocatedMemory(void* aBaseAddress, size_t aSize,
-                             MemoryKind aKind) {
-  MOZ_RELEASE_ASSERT(aBaseAddress == PageBase(aBaseAddress));
-  MOZ_RELEASE_ASSERT(aSize == RoundupSizeToPageBoundary(aSize));
-
-  uint8_t* aAddress = reinterpret_cast<uint8_t*>(aBaseAddress);
-
-  if (aKind != MemoryKind::Tracked) {
-    if (!NumSnapshots()) {
-      AddInitialUntrackedMemoryRegion(aAddress, aSize);
-    }
-  } else if (NumSnapshots()) {
-    EnsureMemoryChangesAllowed();
-    DirectWriteProtectMemory(aAddress, aSize, true);
-    AddTrackedRegion(aAddress, aSize, true);
-  }
-}
-
-void CheckFixedMemory(void* aAddress, size_t aSize) {
-  MOZ_RELEASE_ASSERT(aAddress == PageBase(aAddress));
-  MOZ_RELEASE_ASSERT(aSize == RoundupSizeToPageBoundary(aSize));
-
-  if (!NumSnapshots()) {
-    return;
-  }
-
-  {
-    // The memory should already be tracked. Check each page in the allocation
-    // because there might be tracked regions adjacent to one another, neither
-    // of which entirely contains this memory.
-    AutoSpinLock lock(gMemoryInfo->mTrackedRegionsLock);
-    for (size_t offset = 0; offset < aSize; offset += PageSize) {
-      uint8_t* page = (uint8_t*)aAddress + offset;
-      Maybe<AllocatedMemoryRegion> region =
-          gMemoryInfo->mTrackedRegions.lookupClosestLessOrEqual(page);
-      if (!region.isSome() ||
-          !MemoryContains(region.ref().mBase, region.ref().mSize, page,
-                          PageSize)) {
-        MOZ_CRASH("Fixed memory is not tracked!");
-      }
-    }
-  }
-
-  // The memory should not be free.
-  if (gFreeRegions.Intersects(aAddress, aSize)) {
-    MOZ_CRASH("Fixed memory is currently free!");
-  }
-}
-
-void RestoreWritableFixedMemory(void* aAddress, size_t aSize) {
-  MOZ_RELEASE_ASSERT(aAddress == PageBase(aAddress));
-  MOZ_RELEASE_ASSERT(aSize == RoundupSizeToPageBoundary(aSize));
-
-  if (!NumSnapshots()) {
-    return;
-  }
-
-  AutoSpinLock lock(gMemoryInfo->mActiveDirtyLock);
-  for (size_t offset = 0; offset < aSize; offset += PageSize) {
-    uint8_t* page = (uint8_t*)aAddress + offset;
-    if (gMemoryInfo->mActiveDirty.maybeLookup(page)) {
-      DirectUnprotectMemory(page, PageSize, true);
-    }
-  }
-}
-
-void* AllocateMemoryTryAddress(void* aAddress, size_t aSize, MemoryKind aKind) {
-  MOZ_RELEASE_ASSERT(aAddress == PageBase(aAddress));
-  aSize = RoundupSizeToPageBoundary(aSize);
-
-  if (gMemoryInfo) {
-    gMemoryInfo->mMemoryBalance[(size_t)aKind] += aSize;
-  }
-
-  if (NumSnapshots()) {
-    if (void* res = FreeRegionSet::Get(aKind).Extract(aAddress, aSize)) {
-      return res;
-    }
-  }
-
-  void* res = DirectAllocateMemory(aAddress, aSize);
-  RegisterAllocatedMemory(res, aSize, aKind);
-  return res;
-}
-
-void* AllocateMemory(size_t aSize, MemoryKind aKind) {
-  if (!IsReplaying()) {
-    return DirectAllocateMemory(nullptr, aSize);
-  }
-  return AllocateMemoryTryAddress(nullptr, aSize, aKind);
-}
-
-void DeallocateMemory(void* aAddress, size_t aSize, MemoryKind aKind) {
-  // Round the supplied region to the containing page boundaries.
-  aSize += (uint8_t*)aAddress - PageBase(aAddress);
-  aAddress = PageBase(aAddress);
-  aSize = RoundupSizeToPageBoundary(aSize);
-
-  if (!aAddress || !aSize) {
-    return;
-  }
-
-  if (gMemoryInfo) {
-    gMemoryInfo->mMemoryBalance[(size_t)aKind] -= aSize;
-  }
-
-  // Memory is returned to the system before reaching the first checkpoint and
-  // saving the first snapshot.
-  if (!NumSnapshots()) {
-    if (IsReplaying() && aKind != MemoryKind::Tracked) {
-      RemoveInitialUntrackedRegion((uint8_t*)aAddress, aSize);
-    }
-    DirectDeallocateMemory(aAddress, aSize);
-    return;
-  }
-
-  if (aKind == MemoryKind::Tracked) {
-    // For simplicity, all free regions must be executable, so ignore
-    // deallocated memory in regions that are not executable.
-    bool executable;
-    if (!IsTrackedAddress(aAddress, &executable) || !executable) {
-      return;
-    }
-  }
-
-  // Mark this region as free, but do not unmap it. It will become usable for
-  // later allocations, but will not need to be remapped if we end up
-  // rewinding to a point where this memory was in use.
-  FreeRegionSet::Get(aKind).Insert(aAddress, aSize);
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Snapshot Threads
-///////////////////////////////////////////////////////////////////////////////
-
-// While on a snapshot thread, restore the contents of all pages belonging to
-// this thread which were modified since the last recorded diff snapshot.
-static void SnapshotThreadRestoreLastDiffSnapshot(
-    SnapshotThreadWorklist* aWorklist) {
-  DirtyPageSet& set = aWorklist->mSets.back();
-
-  // Copy the original contents of all pages.
-  for (size_t index = 0; index < set.mPages.length(); index++) {
-    const DirtyPage& page = set.mPages[index];
-    MOZ_RELEASE_ASSERT(page.mOriginal);
-    DirectUnprotectMemory(page.mBase, PageSize, page.mExecutable);
-    MemoryMove(page.mBase, page.mOriginal, PageSize);
-    DirectWriteProtectMemory(page.mBase, PageSize, page.mExecutable);
-    FreePageCopy(page.mOriginal);
-  }
-
-  // Remove the set from the worklist, if necessary.
-  if (!aWorklist->mSets.empty()) {
-    MOZ_RELEASE_ASSERT(&set == &aWorklist->mSets.back());
-    aWorklist->mSets.popBack();
-  }
-}
-
-// Start routine for a snapshot thread.
-void SnapshotThreadMain(void* aArgument) {
-  size_t threadIndex = (size_t)aArgument;
-  SnapshotThreadWorklist* worklist =
-      &gMemoryInfo->mSnapshotWorklists[threadIndex];
-  worklist->mThreadIndex = threadIndex;
-
-  while (true) {
-    // If the main thread is waiting for us to restore the most recent diff,
-    // then do so and notify the main thread we finished.
-    if (gMemoryInfo->mSnapshotThreadsShouldRestore.IsActive()) {
-      SnapshotThreadRestoreLastDiffSnapshot(worklist);
-      gMemoryInfo->mSnapshotThreadsShouldRestore.WaitUntilNoLongerActive();
-    }
-
-    // Idle if the main thread wants us to.
-    if (gMemoryInfo->mSnapshotThreadsShouldIdle.IsActive()) {
-      gMemoryInfo->mSnapshotThreadsShouldIdle.WaitUntilNoLongerActive();
-    }
-
-    // Idle until notified by the main thread.
-    Thread::WaitNoIdle();
-  }
-}
-
-// An alternative to memcmp that can be called from any place.
-static bool MemoryEquals(void* aDst, void* aSrc, size_t aSize) {
-  MOZ_ASSERT((size_t)aDst % sizeof(size_t) == 0);
-  MOZ_ASSERT((size_t)aSrc % sizeof(size_t) == 0);
-  MOZ_ASSERT(aSize % sizeof(size_t) == 0);
-
-  size_t* ndst = (size_t*)aDst;
-  size_t* nsrc = (size_t*)aSrc;
-  for (size_t i = 0; i < aSize / sizeof(size_t); i++) {
-    if (ndst[i] != nsrc[i]) {
-      return false;
-    }
-  }
-  return true;
-}
-
-// Add a page to the last set in some snapshot thread's worklist. This is
-// called on the main thread while the snapshot thread is idle.
-static void AddDirtyPageToWorklist(uint8_t* aAddress, uint8_t* aOriginal,
-                                   bool aExecutable) {
-  // Distribute pages to snapshot threads using the base address of a page.
-  // This guarantees that the same page will be consistently assigned to the
-  // same thread as different snapshots are taken.
-  MOZ_ASSERT((size_t)aAddress % PageSize == 0);
-  if (MemoryEquals(aAddress, aOriginal, PageSize)) {
-    FreePageCopy(aOriginal);
-  } else {
-    size_t pageIndex = ((size_t)aAddress / PageSize) % NumSnapshotThreads;
-    SnapshotThreadWorklist* worklist =
-        &gMemoryInfo->mSnapshotWorklists[pageIndex];
-    MOZ_RELEASE_ASSERT(!worklist->mSets.empty());
-    DirtyPageSet& set = worklist->mSets.back();
-    set.mPages.emplaceBack(aAddress, aOriginal, aExecutable);
-  }
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// Snapshot Interface
-///////////////////////////////////////////////////////////////////////////////
-
-void InitializeMemorySnapshots() {
-  MOZ_RELEASE_ASSERT(gMemoryInfo == nullptr);
-  void* memory = AllocateMemory(sizeof(MemoryInfo), MemoryKind::Generic);
-  gMemoryInfo = new (memory) MemoryInfo();
-
-  // Mark gMemoryInfo as untracked. See AddInitialUntrackedMemoryRegion.
-  AddInitialUntrackedMemoryRegion(reinterpret_cast<uint8_t*>(memory),
-                                  sizeof(MemoryInfo));
-}
-
-void InitializeCountdownThread() {
-#ifdef WANT_COUNTDOWN_THREAD
-  Thread::SpawnNonRecordedThread(CountdownThreadMain, nullptr);
-#endif
-}
-
-void TakeFirstMemorySnapshot() {
-  MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
-  MOZ_RELEASE_ASSERT(gMemoryInfo->mTrackedRegions.empty());
-
-  // Spawn all snapshot threads.
-  {
-    AutoPassThroughThreadEvents pt;
-
-    for (size_t i = 0; i < NumSnapshotThreads; i++) {
-      Thread* thread =
-          Thread::SpawnNonRecordedThread(SnapshotThreadMain, (void*)i);
-      gMemoryInfo->mSnapshotWorklists[i].mThreadId = thread->Id();
-    }
-  }
-
-  // All threads should have been created by now.
-  MarkThreadStacksAsUntracked();
-
-  // Fill in the tracked regions for the process.
-  ProcessAllInitialMemoryRegions();
-}
-
-void TakeDiffMemorySnapshot() {
-  MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
-
-  UpdateNumTrackedRegionsForSnapshot();
-
-  AutoDisallowMemoryChanges disallow;
-
-  // Stop all snapshot threads while we modify their worklists.
-  gMemoryInfo->mSnapshotThreadsShouldIdle.ActivateBegin();
-
-  // Add a DirtyPageSet to each snapshot thread's worklist for this snapshot.
-  for (size_t i = 0; i < NumSnapshotThreads; i++) {
-    SnapshotThreadWorklist* worklist = &gMemoryInfo->mSnapshotWorklists[i];
-    worklist->mSets.emplaceBack();
-  }
-
-  // Distribute remaining active dirty pages to the snapshot thread worklists.
-  for (SortedDirtyPageSet::Iter iter = gMemoryInfo->mActiveDirty.begin();
-       !iter.done(); ++iter) {
-    AddDirtyPageToWorklist(iter.ref().mBase, iter.ref().mOriginal,
-                           iter.ref().mExecutable);
-    DirectWriteProtectMemory(iter.ref().mBase, PageSize,
-                             iter.ref().mExecutable);
-  }
-
-  gMemoryInfo->mActiveDirty.clear();
-
-  // Allow snapshot threads to resume execution.
-  gMemoryInfo->mSnapshotThreadsShouldIdle.ActivateEnd();
-}
-
-void RestoreMemoryToLastSnapshot() {
-  MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
-  MOZ_RELEASE_ASSERT(!gMemoryInfo->mMemoryChangesAllowed);
-
-  // Restore all dirty regions that have been modified since the last
-  // snapshot was saved/restored.
-  for (SortedDirtyPageSet::Iter iter = gMemoryInfo->mActiveDirty.begin();
-       !iter.done(); ++iter) {
-    MemoryMove(iter.ref().mBase, iter.ref().mOriginal, PageSize);
-    FreePageCopy(iter.ref().mOriginal);
-    DirectWriteProtectMemory(iter.ref().mBase, PageSize,
-                             iter.ref().mExecutable);
-  }
-  gMemoryInfo->mActiveDirty.clear();
-}
-
-void RestoreMemoryToLastDiffSnapshot() {
-  MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
-  MOZ_RELEASE_ASSERT(!gMemoryInfo->mMemoryChangesAllowed);
-  MOZ_RELEASE_ASSERT(gMemoryInfo->mActiveDirty.empty());
-
-  // Wait while the snapshot threads restore all pages modified since the diff
-  // snapshot was recorded.
-  gMemoryInfo->mSnapshotThreadsShouldRestore.ActivateBegin();
-  gMemoryInfo->mSnapshotThreadsShouldRestore.ActivateEnd();
-}
-
-size_t GetMemoryUsage(MemoryKind aKind) {
-  if (!gMemoryInfo) {
-    return 0;
-  }
-  return gMemoryInfo->mMemoryBalance[(size_t)aKind];
-}
-
-}  // namespace recordreplay
-}  // namespace mozilla
deleted file mode 100644
--- a/toolkit/recordreplay/MemorySnapshot.h
+++ /dev/null
@@ -1,129 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=8 sts=2 et sw=2 tw=80: */
-/* 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_MemorySnapshot_h
-#define mozilla_recordreplay_MemorySnapshot_h
-
-#include "mozilla/Types.h"
-#include "ProcessRecordReplay.h"
-
-namespace mozilla {
-namespace recordreplay {
-
-// Memory Snapshots Overview.
-//
-// As described in ProcessRewind.h, periodically snapshots are saved so that
-// their state can be restored later. Memory snapshots are used to save and
-// restore the contents of all heap memory: everything except thread stacks
-// (see ThreadSnapshot.h for saving and restoring these) and untracked memory
-// (which is not saved or restored, see ProcessRecordReplay.h).
-//
-// Each memory snapshot is a diff of the heap memory contents compared to the
-// next one. See MemorySnapshot.cpp for how diffs are represented and computed.
-//
-// Rewinding must restore the exact contents of heap memory that existed when
-// the target snapshot was reached. Because of this, memory that is allocated
-// after a point when a snapshot has been saved will never actually be returned
-// to the system. We instead keep a set of free blocks that are unused at the
-// current point of execution and are available to satisfy new allocations.
-
-// Make sure that a block of memory in a fixed allocation is already allocated.
-void CheckFixedMemory(void* aAddress, size_t aSize);
-
-// After marking a block of memory in a fixed allocation as non-writable,
-// restore writability to any dirty pages in the range.
-void RestoreWritableFixedMemory(void* aAddress, size_t aSize);
-
-// Allocate memory, trying to use a specific address if provided but only if
-// it is free.
-void* AllocateMemoryTryAddress(void* aAddress, size_t aSize, MemoryKind aKind);
-
-// Note a range of memory that was just allocated from the system, and the
-// kind of memory allocation that was performed.
-void RegisterAllocatedMemory(void* aBaseAddress, size_t aSize,
-                             MemoryKind aKind);
-
-// Exclude a region of memory from snapshots, before the first snapshot has
-// been taken.
-void AddInitialUntrackedMemoryRegion(uint8_t* aBase, size_t aSize);
-
-// Return whether a range of memory is in a tracked region. This excludes
-// memory that was allocated after the last snapshot and is not write
-// protected.
-bool MemoryRangeIsTracked(void* aAddress, size_t aSize);
-
-// Initialize the memory snapshots system.
-void InitializeMemorySnapshots();
-
-// Take the first heap memory snapshot.
-void TakeFirstMemorySnapshot();
-
-// Take a differential heap memory snapshot compared to the last one.
-void TakeDiffMemorySnapshot();
-
-// Restore all heap memory to its state when the most recent snapshot was
-// taken.
-void RestoreMemoryToLastSnapshot();
-
-// Restore all heap memory to its state at a snapshot where a complete diff
-// was saved vs. the following snapshot. This requires that no tracked heap
-// memory has been changed since the last snapshot.
-void RestoreMemoryToLastDiffSnapshot();
-
-// Set whether to allow changes to tracked heap memory at this point. If such
-// changes occur when they are not allowed then the process will crash.
-void SetMemoryChangesAllowed(bool aAllowed);
-
-struct MOZ_RAII AutoDisallowMemoryChanges {
-  AutoDisallowMemoryChanges() { SetMemoryChangesAllowed(false); }
-  ~AutoDisallowMemoryChanges() { SetMemoryChangesAllowed(true); }
-};
-
-// After a SEGV on the specified address, check if the violation occurred due
-// to the memory having been write protected by the snapshot mechanism. This
-// function returns whether the fault has been handled and execution may
-// continue.
-bool HandleDirtyMemoryFault(uint8_t* aAddress);
-
-// For debugging, note a point where we hit an unrecoverable failure and try
-// to make things easier for the debugger.
-void UnrecoverableSnapshotFailure();
-
-// After rewinding, mark all memory that has been allocated since the snapshot
-// was taken as free.
-void FixupFreeRegionsAfterRewind();
-
-// When WANT_COUNTDOWN_THREAD is defined (see MemorySnapshot.cpp), set a count
-// that, after a thread consumes it, causes the thread to report a fatal error.
-// This is used for debugging and is a workaround for lldb often being unable
-// to interrupt a running process.
-void StartCountdown(size_t aCount);
-
-// Per StartCountdown, set a countdown and remove it on destruction.
-struct MOZ_RAII AutoCountdown {
-  explicit AutoCountdown(size_t aCount);
-  ~AutoCountdown();
-};
-
-// Initialize the thread consuming the countdown.
-void InitializeCountdownThread();
-
-// This is an alternative to memmove/memcpy that can be called in areas where
-// faults in write protected memory are not allowed. It's hard to avoid dynamic
-// code loading when calling memmove/memcpy directly.
-void MemoryMove(void* aDst, const void* aSrc, size_t aSize);
-
-// Similarly, zero out a range of memory without doing anything weird with
-// dynamic code loading.
-void MemoryZero(void* aDst, size_t aSize);
-
-// Get the amount of allocated memory used by data of the specified kind.
-size_t GetMemoryUsage(MemoryKind aKind);
-
-}  // namespace recordreplay
-}  // namespace mozilla
-
-#endif  // mozilla_recordreplay_MemorySnapshot_h
--- a/toolkit/recordreplay/ProcessRecordReplay.cpp
+++ b/toolkit/recordreplay/ProcessRecordReplay.cpp
@@ -7,43 +7,46 @@
 #include "ProcessRecordReplay.h"
 
 #include "ipc/ChildInternal.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/Compression.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/StaticMutex.h"
-#include "DirtyMemoryHandler.h"
 #include "Lock.h"
-#include "MemorySnapshot.h"
 #include "ProcessRedirect.h"
 #include "ProcessRewind.h"
 #include "ValueIndex.h"
 #include "pratom.h"
 
+#include <dlfcn.h>
 #include <fcntl.h>
 #include <unistd.h>
 
+#include <mach/exc.h>
+#include <mach/mach.h>
+#include <mach/mach_vm.h>
+#include <mach/ndr.h>
+#include <sys/time.h>
+
 namespace mozilla {
 namespace recordreplay {
 
 MOZ_NEVER_INLINE void BusyWait() {
   static volatile int value = 1;
   while (value) {
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Basic interface
 ///////////////////////////////////////////////////////////////////////////////
 
-File* gRecordingFile;
-const char* gSnapshotMemoryPrefix;
-const char* gSnapshotStackPrefix;
+Recording* gRecording;
 
 char* gInitializationFailureMessage;
 
 bool gInitialized;
 ProcessKind gProcessKind;
 char* gRecordingFilename;
 
 // Current process ID.
@@ -53,36 +56,43 @@ static int gPid;
 static int gRecordingPid;
 
 // Whether to spew record/replay messages to stderr.
 static bool gSpewEnabled;
 
 // Whether this is the main child.
 static bool gMainChild;
 
+// Whether we are replaying on a cloud machine.
+static bool gReplayingInCloud;
+
+static void InitializeCrashDetector();
+
 extern "C" {
 
 MOZ_EXPORT void RecordReplayInterface_Initialize(int aArgc, char* aArgv[]) {
   // Parse command line options for the process kind and recording file.
   Maybe<ProcessKind> processKind;
   Maybe<char*> recordingFile;
   for (int i = 0; i < aArgc; i++) {
     if (!strcmp(aArgv[i], gProcessKindOption)) {
       MOZ_RELEASE_ASSERT(processKind.isNothing() && i + 1 < aArgc);
       processKind.emplace((ProcessKind)atoi(aArgv[i + 1]));
     }
     if (!strcmp(aArgv[i], gRecordingFileOption)) {
       MOZ_RELEASE_ASSERT(recordingFile.isNothing() && i + 1 < aArgc);
       recordingFile.emplace(aArgv[i + 1]);
     }
   }
-  MOZ_RELEASE_ASSERT(processKind.isSome() && recordingFile.isSome());
+  MOZ_RELEASE_ASSERT(processKind.isSome());
 
   gProcessKind = processKind.ref();
-  gRecordingFilename = strdup(recordingFile.ref());
+  if (recordingFile.isSome()) {
+    gRecordingFilename = strdup(recordingFile.ref());
+  }
 
   switch (processKind.ref()) {
     case ProcessKind::Recording:
       gIsRecording = gIsRecordingOrReplaying = true;
       fprintf(stderr, "RECORDING %d %s\n", getpid(), recordingFile.ref());
       break;
     case ProcessKind::Replaying:
       gIsReplaying = gIsRecordingOrReplaying = true;
@@ -112,57 +122,58 @@ MOZ_EXPORT void RecordReplayInterface_In
   gPid = getpid();
   if (TestEnv("MOZ_RECORD_REPLAY_SPEW")) {
     gSpewEnabled = true;
   }
 
   EarlyInitializeRedirections();
 
   if (!IsRecordingOrReplaying()) {
-    InitializeMiddlemanCalls();
+    InitializeExternalCalls();
     return;
   }
 
-  gSnapshotMemoryPrefix = mktemp(strdup("/tmp/SnapshotMemoryXXXXXX"));
-  gSnapshotStackPrefix = mktemp(strdup("/tmp/SnapshotStackXXXXXX"));
-
   InitializeCurrentTime();
 
-  gRecordingFile = new File();
-  if (gRecordingFile->Open(recordingFile.ref(),
-                           IsRecording() ? File::WRITE : File::READ)) {
-    InitializeRedirections();
-  } else {
-    gInitializationFailureMessage = strdup("Bad recording file");
-  }
+  gRecording = new Recording();
+
+  InitializeRedirections();
 
   if (gInitializationFailureMessage) {
     fprintf(stderr, "Initialization Failure: %s\n",
             gInitializationFailureMessage);
   }
 
   LateInitializeRedirections();
   Thread::InitializeThreads();
 
   Thread* thread = Thread::GetById(MainThreadId);
   MOZ_ASSERT(thread->Id() == MainThreadId);
 
-  thread->BindToCurrent();
   thread->SetPassThrough(true);
 
-  InitializeMemorySnapshots();
+  // The translation layer we are running under in the cloud will intercept this
+  // and return a non-zero symbol address.
+  gReplayingInCloud = !!dlsym(RTLD_DEFAULT, "RecordReplay_ReplayingInCloud");
+
   Thread::SpawnAllThreads();
-  InitializeCountdownThread();
-  SetupDirtyMemoryHandler();
-  InitializeMiddlemanCalls();
+  InitializeExternalCalls();
+  if (!gReplayingInCloud) {
+    // The crash detector is only useful when we have a local parent process to
+    // report crashes to. Avoid initializing it when running in the cloud
+    // so that we avoid calling mach interfaces with events passed through.
+    InitializeCrashDetector();
+  }
   Lock::InitializeLocks();
 
   // Don't create a stylo thread pool when recording or replaying.
   putenv((char*)"STYLO_THREADS=1");
 
+  child::SetupRecordReplayChannel(aArgc, aArgv);
+
   thread->SetPassThrough(false);
 
   InitializeRewindState();
   gRecordingPid = RecordReplayValue(gPid);
 
   gMainChild = IsRecording();
 
   gInitialized = true;
@@ -192,88 +203,100 @@ MOZ_EXPORT void RecordReplayInterface_In
   thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Bytes);
   thread->Events().CheckInput(aSize);
   thread->Events().RecordOrReplayBytes(aData, aSize);
 }
 
 MOZ_EXPORT void RecordReplayInterface_InternalInvalidateRecording(
     const char* aWhy) {
   if (IsRecording()) {
-    child::ReportFatalError(Nothing(), "Recording invalidated: %s", aWhy);
+    child::ReportFatalError("Recording invalidated: %s", aWhy);
   } else {
-    child::ReportFatalError(Nothing(),
-                            "Recording invalidated while replaying: %s", aWhy);
+    child::ReportFatalError("Recording invalidated while replaying: %s", aWhy);
   }
   Unreachable();
 }
 
+MOZ_EXPORT void RecordReplayInterface_InternalBeginPassThroughThreadEventsWithLocalReplay() {
+  if (IsReplaying() && !gReplayingInCloud) {
+    BeginPassThroughThreadEvents();
+  }
+}
+
+MOZ_EXPORT void RecordReplayInterface_InternalEndPassThroughThreadEventsWithLocalReplay() {
+  // If we are replaying locally we will be skipping over a section of the
+  // recording while events are passed through. Include the current stream
+  // position in the recording so that we will know how much to skip over.
+  MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
+  Stream* localReplayStream = gRecording->OpenStream(StreamName::LocalReplaySkip, 0);
+  Stream& events = Thread::Current()->Events();
+
+  size_t position = IsRecording() ? events.StreamPosition() : 0;
+  localReplayStream->RecordOrReplayScalar(&position);
+
+  if (IsReplaying() && !ReplayingInCloud()) {
+    EndPassThroughThreadEvents();
+    MOZ_RELEASE_ASSERT(events.StreamPosition() <= position);
+    size_t nbytes = position - events.StreamPosition();
+    void* buf = malloc(nbytes);
+    events.ReadBytes(buf, nbytes);
+    free(buf);
+    MOZ_RELEASE_ASSERT(events.StreamPosition() == position);
+  }
+}
+
 }  // extern "C"
 
+// How many bytes have been sent from the recording to the middleman.
+size_t gRecordingDataSentToMiddleman;
+
 void FlushRecording() {
   MOZ_RELEASE_ASSERT(IsRecording());
   MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
 
   // The recording can only be flushed when we are at a checkpoint.
   // Save this endpoint to the recording.
   size_t endpoint = GetLastCheckpoint();
-  Stream* endpointStream = gRecordingFile->OpenStream(StreamName::Main, 0);
+  Stream* endpointStream = gRecording->OpenStream(StreamName::Endpoint, 0);
   endpointStream->WriteScalar(endpoint);
 
-  gRecordingFile->PreventStreamWrites();
-  gRecordingFile->Flush();
-  gRecordingFile->AllowStreamWrites();
-}
-
-// Try to load another recording index, returning whether one was found.
-static bool LoadNextRecordingIndex() {
-  Thread::WaitForIdleThreads();
+  gRecording->PreventStreamWrites();
+  gRecording->Flush();
+  gRecording->AllowStreamWrites();
 
-  InfallibleVector<Stream*> updatedStreams;
-  File::ReadIndexResult result = gRecordingFile->ReadNextIndex(&updatedStreams);
-  if (result == File::ReadIndexResult::InvalidFile) {
-    MOZ_CRASH("Bad recording file");
+  if (gRecording->Size() > gRecordingDataSentToMiddleman) {
+    child::SendRecordingData(gRecordingDataSentToMiddleman,
+                             gRecording->Data() + gRecordingDataSentToMiddleman,
+                             gRecording->Size() - gRecordingDataSentToMiddleman);
+    gRecordingDataSentToMiddleman = gRecording->Size();
   }
-
-  bool found = result == File::ReadIndexResult::FoundIndex;
-  if (found) {
-    for (Stream* stream : updatedStreams) {
-      if (stream->Name() == StreamName::Lock) {
-        Lock::LockAquiresUpdated(stream->NameIndex());
-      }
-    }
-  }
-
-  Thread::ResumeIdleThreads();
-  return found;
 }
 
 void HitEndOfRecording() {
   MOZ_RELEASE_ASSERT(IsReplaying());
   MOZ_RELEASE_ASSERT(!AreThreadEventsPassedThrough());
 
   if (Thread::CurrentIsMainThread()) {
-    // Load more data from the recording. The debugger is not allowed to let us
-    // go past the recording endpoint, so there should be more data.
-    bool found = LoadNextRecordingIndex();
-    MOZ_RELEASE_ASSERT(found);
+    // We should have been provided with all the data needed to run forward in
+    // the replay. Check to see if there is any pending data.
+    child::AddPendingRecordingData();
   } else {
-    // Non-main threads may wait until more recording data is loaded by the
-    // main thread.
+    // Non-main threads may wait until more recording data is added.
     Thread::Wait();
   }
 }
 
 // When replaying, the last endpoint loaded from the recording.
 static size_t gRecordingEndpoint;
 
 size_t RecordingEndpoint() {
   MOZ_RELEASE_ASSERT(IsReplaying());
   MOZ_RELEASE_ASSERT(!AreThreadEventsPassedThrough());
 
-  Stream* endpointStream = gRecordingFile->OpenStream(StreamName::Main, 0);
+  Stream* endpointStream = gRecording->OpenStream(StreamName::Endpoint, 0);
   while (!endpointStream->AtEnd()) {
     gRecordingEndpoint = endpointStream->ReadScalar();
   }
 
   return gRecordingEndpoint;
 }
 
 bool SpewEnabled() { return gSpewEnabled; }
@@ -296,18 +319,21 @@ const char* ThreadEventName(ThreadEvent 
         case ThreadEvent::CallStart : break;
   }
   size_t callId = (size_t)aEvent - (size_t)ThreadEvent::CallStart;
   return GetRedirection(callId).mName;
 }
 
 int GetRecordingPid() { return gRecordingPid; }
 
+void ResetPid() { gPid = getpid(); }
+
 bool IsMainChild() { return gMainChild; }
 void SetMainChild() { gMainChild = true; }
+bool ReplayingInCloud() { return gReplayingInCloud; }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Record/Replay Assertions
 ///////////////////////////////////////////////////////////////////////////////
 
 extern "C" {
 
 MOZ_EXPORT void RecordReplayInterface_InternalRecordReplayAssert(
@@ -319,17 +345,17 @@ MOZ_EXPORT void RecordReplayInterface_In
   }
 
   // Add the asserted string to the recording.
   char text[1024];
   VsprintfLiteral(text, aFormat, aArgs);
 
   // This must be kept in sync with Stream::RecordOrReplayThreadEvent, which
   // peeks at the input string written after the thread event.
-  thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Assert);
+  thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Assert, text);
   thread->Events().CheckInput(text);
 }
 
 MOZ_EXPORT void RecordReplayInterface_InternalRecordReplayAssertBytes(
     const void* aData, size_t aSize) {
   Thread* thread = Thread::Current();
   RecordingEventSection res(thread);
   if (!res.CanAccessEvents()) {
@@ -394,10 +420,101 @@ MOZ_EXPORT void RecordReplayInterface_In
     JSContext* cx = dom::danger::GetJSContext();
     JS::PersistentRootedObject* root = new JS::PersistentRootedObject(cx);
     *root = static_cast<JSObject*>(aJSObj);
   }
 }
 
 }  // extern "C"
 
+static mach_port_t gCrashDetectorExceptionPort;
+
+// See AsmJSSignalHandlers.cpp.
+static const mach_msg_id_t sExceptionId = 2405;
+
+// This definition was generated by mig (the Mach Interface Generator) for the
+// routine 'exception_raise' (exc.defs). See js/src/wasm/WasmSignalHandlers.cpp.
+#pragma pack(4)
+typedef struct {
+  mach_msg_header_t Head;
+  /* start of the kernel processed data */
+  mach_msg_body_t msgh_body;
+  mach_msg_port_descriptor_t thread;
+  mach_msg_port_descriptor_t task;
+  /* end of the kernel processed data */
+  NDR_record_t NDR;
+  exception_type_t exception;
+  mach_msg_type_number_t codeCnt;
+  int64_t code[2];
+} Request__mach_exception_raise_t;
+#pragma pack()
+
+typedef struct {
+  Request__mach_exception_raise_t body;
+  mach_msg_trailer_t trailer;
+} ExceptionRequest;
+
+static void CrashDetectorThread(void*) {
+  kern_return_t kret;
+
+  while (true) {
+    ExceptionRequest request;
+    kret = mach_msg(&request.body.Head, MACH_RCV_MSG, 0, sizeof(request),
+                    gCrashDetectorExceptionPort, MACH_MSG_TIMEOUT_NONE,
+                    MACH_PORT_NULL);
+    Print("Crashing: %s\n", gMozCrashReason);
+
+    kern_return_t replyCode = KERN_FAILURE;
+    if (kret == KERN_SUCCESS && request.body.Head.msgh_id == sExceptionId &&
+        request.body.exception == EXC_BAD_ACCESS && request.body.codeCnt == 2) {
+      uint8_t* faultingAddress = (uint8_t*)request.body.code[1];
+      child::MinidumpInfo info(request.body.exception, request.body.code[0],
+                               request.body.code[1],
+                               request.body.thread.name,
+                               request.body.task.name);
+      child::ReportCrash(info, faultingAddress);
+    } else {
+      child::ReportFatalError("CrashDetectorThread mach_msg "
+                              "returned unexpected data");
+    }
+
+    __Reply__exception_raise_t reply;
+    reply.Head.msgh_bits =
+        MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(request.body.Head.msgh_bits), 0);
+    reply.Head.msgh_size = sizeof(reply);
+    reply.Head.msgh_remote_port = request.body.Head.msgh_remote_port;
+    reply.Head.msgh_local_port = MACH_PORT_NULL;
+    reply.Head.msgh_id = request.body.Head.msgh_id + 100;
+    reply.NDR = NDR_record;
+    reply.RetCode = replyCode;
+    mach_msg(&reply.Head, MACH_SEND_MSG, sizeof(reply), 0, MACH_PORT_NULL,
+             MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
+  }
+}
+
+static void InitializeCrashDetector() {
+  MOZ_RELEASE_ASSERT(AreThreadEventsPassedThrough());
+  kern_return_t kret;
+
+  // Get a port which can send and receive data.
+  kret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE,
+                            &gCrashDetectorExceptionPort);
+  MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS);
+
+  kret = mach_port_insert_right(mach_task_self(), gCrashDetectorExceptionPort,
+                                gCrashDetectorExceptionPort,
+                                MACH_MSG_TYPE_MAKE_SEND);
+  MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS);
+
+  // Create a thread to block on reading the port.
+  Thread::SpawnNonRecordedThread(CrashDetectorThread, nullptr);
+
+  // Set exception ports on the entire task. Unfortunately, this clobbers any
+  // other exception ports for the task, and forwarding to those other ports
+  // is not easy to get right.
+  kret = task_set_exception_ports(
+      mach_task_self(), EXC_MASK_BAD_ACCESS, gCrashDetectorExceptionPort,
+      EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE);
+  MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS);
+}
+
 }  // namespace recordreplay
 }  // namespace mozilla
--- a/toolkit/recordreplay/ProcessRecordReplay.h
+++ b/toolkit/recordreplay/ProcessRecordReplay.h
@@ -67,20 +67,20 @@ enum class ThreadEvent : uint32_t {
   // The start of event IDs for redirected call events. Event IDs after this
   // point are platform specific.
   CallStart
 };
 
 // Get the printable name for a thread event.
 const char* ThreadEventName(ThreadEvent aEvent);
 
-class File;
+class Recording;
 
-// File used during recording and replay.
-extern File* gRecordingFile;
+// Recording being written to or read from.
+extern Recording* gRecording;
 
 // Whether record/replay state has finished initialization.
 extern bool gInitialized;
 
 // If we failed to initialize, any associated message. On an initialization
 // failure, events will be passed through until we have connected with the
 // middleman, reported the failure, and crashed.
 extern char* gInitializationFailureMessage;
@@ -101,16 +101,19 @@ void HitEndOfRecording();
 // Called in a replaying process to load the last checkpoint in the recording.
 size_t RecordingEndpoint();
 
 // Access the flag for whether this is the main child. The main child never
 // rewinds and sends graphics updates to the middleman while running forward.
 bool IsMainChild();
 void SetMainChild();
 
+// Whether we are replaying a recording on a machine in the cloud.
+bool ReplayingInCloud();
+
 // Get the process kind and recording file specified at the command line.
 // These are available in the middleman as well as while recording/replaying.
 extern ProcessKind gProcessKind;
 extern char* gRecordingFilename;
 
 ///////////////////////////////////////////////////////////////////////////////
 // Helper Functions
 ///////////////////////////////////////////////////////////////////////////////
@@ -198,16 +201,19 @@ void InternalPrint(const char* aFormat, 
 MOZ_MakeRecordReplayPrinter(Print, false)
     MOZ_MakeRecordReplayPrinter(PrintSpew, true)
 
 #undef MOZ_MakeRecordReplayPrinter
 
     // Get the ID of the process that produced the recording.
     int GetRecordingPid();
 
+// Update the current pid after a fork.
+void ResetPid();
+
 ///////////////////////////////////////////////////////////////////////////////
 // Profiling
 ///////////////////////////////////////////////////////////////////////////////
 
 void InitializeCurrentTime();
 
 // Get a current timestamp, in microseconds.
 double CurrentTime();
@@ -228,131 +234,33 @@ struct AutoTimer {
  private:
   TimerKind mKind;
   double mStart;
 };
 
 void DumpTimers();
 
 ///////////////////////////////////////////////////////////////////////////////
-// Memory Management
-///////////////////////////////////////////////////////////////////////////////
-
-// In cases where memory is tracked and should be saved/restored with
-// checkoints, malloc and other standard library functions suffice to allocate
-// memory in the record/replay system. The routines below are used for handling
-// redirections for the raw system calls underlying the standard libraries, and
-// for cases where allocated memory should be untracked: the contents are
-// ignored when saving/restoring checkpoints.
-
-// Different kinds of memory used in the system.
-enum class MemoryKind {
-  // Memory whose contents are saved/restored with checkpoints.
-  Tracked,
-
-  // All remaining memory kinds refer to untracked memory.
-
-  // Memory not fitting into one of the categories below.
-  Generic,
-
-  // Memory used for thread snapshots.
-  ThreadSnapshot,
-
-  // Memory used by various parts of the memory snapshot system.
-  TrackedRegions,
-  FreeRegions,
-  DirtyPageSet,
-  SortedDirtyPageSet,
-  PageCopy,
-
-  // Memory used by various parts of JS integration.
-  ScriptHits,
-
-  Count
-};
-
-// Allocate or deallocate a block of memory of a particular kind. Allocated
-// memory is initially zeroed.
-void* AllocateMemory(size_t aSize, MemoryKind aKind);
-void DeallocateMemory(void* aAddress, size_t aSize, MemoryKind aKind);
-
-// Allocation policy for managing memory of a particular kind.
-template <MemoryKind Kind>
-class AllocPolicy {
- public:
-  template <typename T>
-  T* maybe_pod_calloc(size_t aNumElems) {
-    if (aNumElems & tl::MulOverflowMask<sizeof(T)>::value) {
-      MOZ_CRASH();
-    }
-    // Note: AllocateMemory always returns zeroed memory.
-    return static_cast<T*>(AllocateMemory(aNumElems * sizeof(T), Kind));
-  }
-
-  template <typename T>
-  void free_(T* aPtr, size_t aSize) {
-    DeallocateMemory(aPtr, aSize * sizeof(T), Kind);
-  }
-
-  template <typename T>
-  T* maybe_pod_realloc(T* aPtr, size_t aOldSize, size_t aNewSize) {
-    T* res = maybe_pod_calloc<T>(aNewSize);
-    memcpy(res, aPtr, aOldSize * sizeof(T));
-    free_<T>(aPtr, aOldSize);
-    return res;
-  }
-
-  template <typename T>
-  T* maybe_pod_malloc(size_t aNumElems) {
-    return maybe_pod_calloc<T>(aNumElems);
-  }
-
-  template <typename T>
-  T* pod_malloc(size_t aNumElems) {
-    return maybe_pod_malloc<T>(aNumElems);
-  }
-
-  template <typename T>
-  T* pod_calloc(size_t aNumElems) {
-    return maybe_pod_calloc<T>(aNumElems);
-  }
-
-  template <typename T>
-  T* pod_realloc(T* aPtr, size_t aOldSize, size_t aNewSize) {
-    return maybe_pod_realloc<T>(aPtr, aOldSize, aNewSize);
-  }
-
-  void reportAllocOverflow() const {}
-
-  MOZ_MUST_USE bool checkSimulatedOOM() const { return true; }
-};
-
-///////////////////////////////////////////////////////////////////////////////
 // Redirection Bypassing
 ///////////////////////////////////////////////////////////////////////////////
 
 // The functions below bypass any redirections and give access to the system
 // even if events are not passed through in the current thread. These are
 // implemented in the various platform ProcessRedirect*.cpp files, and will
 // crash on errors which can't be handled internally.
 
 // Generic typedef for a system file handle.
 typedef size_t FileHandle;
 
 // Allocate/deallocate a block of memory directly from the system.
-void* DirectAllocateMemory(void* aAddress, size_t aSize);
+void* DirectAllocateMemory(size_t aSize);
 void DirectDeallocateMemory(void* aAddress, size_t aSize);
 
-// Give a block of memory R or RX access.
-void DirectWriteProtectMemory(void* aAddress, size_t aSize, bool aExecutable,
-                              bool aIgnoreFailures = false);
-
-// Give a block of memory RW or RWX access.
-void DirectUnprotectMemory(void* aAddress, size_t aSize, bool aExecutable,
-                           bool aIgnoreFailures = false);
+// Make a memory range inaccessible.
+void DirectMakeInaccessible(void* aAddress, size_t aSize);
 
 // Open an existing file for reading or a new file for writing, clobbering any
 // existing file.
 FileHandle DirectOpenFile(const char* aFilename, bool aWriting);
 
 // Seek to an offset within a file open for reading.
 void DirectSeekFile(FileHandle aFd, uint64_t aOffset);
 
@@ -367,15 +275,26 @@ void DirectWrite(FileHandle aFd, const v
 void DirectPrint(const char* aString);
 
 // Read data from a file, blocking until the read completes.
 size_t DirectRead(FileHandle aFd, void* aData, size_t aSize);
 
 // Create a new pipe.
 void DirectCreatePipe(FileHandle* aWriteFd, FileHandle* aReadFd);
 
+typedef pthread_t NativeThreadId;
+
 // Spawn a new thread.
-void DirectSpawnThread(void (*aFunction)(void*), void* aArgument);
+NativeThreadId DirectSpawnThread(void (*aFunction)(void*), void* aArgument,
+                                 void* aStackBase, size_t aStackSize);
+
+// Get the current thread.
+NativeThreadId DirectCurrentThread();
+
+typedef pthread_mutex_t NativeLock;
+
+void DirectLockMutex(NativeLock* aLock, bool aPassThroughEvents = true);
+void DirectUnlockMutex(NativeLock* aLock, bool aPassThroughEvents = true);
 
 }  // namespace recordreplay
 }  // namespace mozilla
 
 #endif  // mozilla_recordreplay_ProcessRecordReplay_h
--- a/toolkit/recordreplay/ProcessRedirect.cpp
+++ b/toolkit/recordreplay/ProcessRedirect.cpp
@@ -2,17 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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 "ProcessRedirect.h"
 
 #include "InfallibleVector.h"
-#include "MiddlemanCall.h"
+#include "ExternalCall.h"
 #include "ipc/ChildInternal.h"
 #include "ipc/ParentInternal.h"
 #include "mozilla/Sprintf.h"
 
 #include <dlfcn.h>
 #include <string.h>
 
 #if defined(__clang__)
@@ -87,48 +87,46 @@ extern "C" {
       return 1;
     }
 
     MOZ_RELEASE_ASSERT(thread->HasDivergedFromRecording());
 
     // After we have diverged from the recording, we can't access the thread's
     // recording anymore.
 
-    // If the redirection has a middleman preamble hook, call it to see if it
-    // can handle this call. The middleman preamble hook is separate from the
+    // If the redirection has an external preamble hook, call it to see if it
+    // can handle this call. The external preamble hook is separate from the
     // normal preamble hook because entering the RecordingEventSection can
     // cause the current thread to diverge from the recording; testing for
     // HasDivergedFromRecording() does not work reliably in the normal preamble.
-    if (redirection.mMiddlemanPreamble) {
-      if (CallPreambleHook(redirection.mMiddlemanPreamble, aCallId,
+    if (redirection.mExternalPreamble) {
+      if (CallPreambleHook(redirection.mExternalPreamble, aCallId,
                            aArguments)) {
         return 0;
       }
     }
 
-    // If the redirection has a middleman call hook, try to perform the call in
-    // the middleman instead.
-    if (redirection.mMiddlemanCall) {
-      if (SendCallToMiddleman(aCallId, aArguments,
-                              /* aPopulateOutput = */ true)) {
+    // If the redirection has an external call hook, try to get its result
+    // from another process.
+    if (redirection.mExternalCall) {
+      if (OnExternalCall(aCallId, aArguments, /* aPopulateOutput = */ true)) {
         return 0;
       }
     }
 
     if (child::CurrentRepaintCannotFail()) {
       // EnsureNotDivergedFromRecording is going to force us to crash, so fail
       // earlier with a more helpful error message.
-      child::ReportFatalError(Nothing(),
-                              "Could not perform middleman call: %s\n",
+      child::ReportFatalError("Could not perform external call: %s\n",
                               redirection.mName);
     }
 
     // Calling any redirection which performs the standard steps will cause
     // debugger operations that have diverged from the recording to fail.
-    EnsureNotDivergedFromRecording();
+    EnsureNotDivergedFromRecording(Some(aCallId));
     Unreachable();
   }
 
   if (IsRecording()) {
     // Call the original function, passing through events while we do so.
     // Destroy the RecordingEventSection so that we don't prevent the file
     // from being flushed in case we end up blocking.
     res.reset();
@@ -144,39 +142,44 @@ extern "C" {
   // Add an event for the thread.
   thread->Events().RecordOrReplayThreadEvent(CallIdToThreadEvent(aCallId));
 
   // Save any output produced by the call.
   if (redirection.mSaveOutput) {
     redirection.mSaveOutput(thread->Events(), aArguments, &error);
   }
 
-  // Save information about any potential middleman calls encountered if we
-  // haven't diverged from the recording, in case we diverge and later calls
+  // Save information about any external calls encountered if we haven't
+  // diverged from the recording, in case we diverge and later calls
   // access data produced by this one.
-  if (IsReplaying() && redirection.mMiddlemanCall) {
-    (void)SendCallToMiddleman(aCallId, aArguments, /* aDiverged = */ false);
+  if (IsReplaying() && redirection.mExternalCall) {
+    (void)OnExternalCall(aCallId, aArguments, /* aDiverged = */ false);
   }
 
   RestoreError(error);
   return 0;
 }
 
 // Entry point for all redirections. When generated code jumps here, %rax holds
 // the CallEvent being invoked, and all other registers and stack contents are
 // the same as when the call was originally invoked. This fills in a
 // CallArguments structure with information about the call, before invoking
 // RecordReplayInterceptCall.
 extern size_t RecordReplayRedirectCall(...);
 
 __asm(
     "_RecordReplayRedirectCall:"
 
-    // Make space for a CallArguments struct on the stack.
-    "subq $616, %rsp;"
+    // Save rbp for backtraces.
+    "pushq %rbp;"
+    "movq %rsp, %rbp;"
+
+    // Make space for a CallArguments struct on the stack, with a little extra
+    // space for alignment.
+    "subq $624, %rsp;"
 
     // Fill in the structure's contents.
     "movq %rdi, 0(%rsp);"
     "movq %rsi, 8(%rsp);"
     "movq %rdx, 16(%rsp);"
     "movq %rcx, 24(%rsp);"
     "movq %r8, 32(%rsp);"
     "movq %r9, 40(%rsp);"
@@ -189,17 +192,17 @@ extern size_t RecordReplayRedirectCall(.
 
     // Enter the loop below. The compiler might not place this block of code
     // adjacent to the loop, so perform the jump explicitly.
     "jmp _RecordReplayRedirectCall_Loop;"
 
     // Save stack arguments into the structure.
     "_RecordReplayRedirectCall_Loop:"
     "subq $1, %rsi;"
-    "movq 624(%rsp, %rsi, 8), %rdx;"  // Ignore the return ip on the stack.
+    "movq 640(%rsp, %rsi, 8), %rdx;"  // Ignore the rip/rbp saved on stack.
     "movq %rdx, 104(%rsp, %rsi, 8);"
     "testq %rsi, %rsi;"
     "jne _RecordReplayRedirectCall_Loop;"
 
     // Place the CallEvent being made into the first argument register.
     "movq %rax, %rdi;"
 
     // Place the structure's address into the second argument register.
@@ -218,43 +221,50 @@ extern size_t RecordReplayRedirectCall(.
     "movq 16(%rsp), %rdx;"
     "movq 24(%rsp), %rcx;"
     "movq 32(%rsp), %r8;"
     "movq 40(%rsp), %r9;"
     "movsd 48(%rsp), %xmm0;"
     "movsd 56(%rsp), %xmm1;"
     "movsd 64(%rsp), %xmm2;"
     "movq 72(%rsp), %rax;"
-    "addq $616, %rsp;"
+    "addq $624, %rsp;"
+    "popq %rbp;"
     "jmpq *%rax;"
 
     // The message has been recorded/replayed.
     "RecordReplayRedirectCall_done:"
     // Restore scalar and floating point return values.
     "movq 72(%rsp), %rax;"
     "movq 80(%rsp), %rdx;"
     "movsd 88(%rsp), %xmm0;"
     "movsd 96(%rsp), %xmm1;"
 
     // Pop the structure from the stack.
-    "addq $616, %rsp;"
+    "addq $624, %rsp;"
 
     // Return to caller.
+    "popq %rbp;"
     "ret;");
 
 // Call a function address with the specified arguments.
 extern void RecordReplayInvokeCallRaw(CallArguments* aArguments, void* aFnPtr);
 
 __asm(
     "_RecordReplayInvokeCallRaw:"
 
+    // Save rbp for backtraces.
+    "pushq %rbp;"
+    "movq %rsp, %rbp;"
+
     // Save function pointer in rax.
     "movq %rsi, %rax;"
 
-    // Save arguments on the stack. This also aligns the stack.
+    // Save arguments on the stack, with a second copy for alignment.
+    "push %rdi;"
     "push %rdi;"
 
     // Count how many stack arguments we need to copy.
     "movq $64, %rsi;"
 
     // Enter the loop below. The compiler might not place this block of code
     // adjacent to the loop, so perform the jump explicitly.
     "jmp _RecordReplayInvokeCallRaw_Loop;"
@@ -281,21 +291,23 @@ extern void RecordReplayInvokeCallRaw(Ca
     // Call the saved function pointer.
     "callq *%rax;"
 
     // Pop the copied stack arguments.
     "addq $512, %rsp;"
 
     // Save any return values to the arguments.
     "pop %rdi;"
+    "pop %rdi;"
     "movq %rax, 72(%rdi);"
     "movq %rdx, 80(%rdi);"
     "movsd %xmm0, 88(%rdi);"
     "movsd %xmm1, 96(%rdi);"
 
+    "popq %rbp;"
     "ret;");
 
 }  // extern "C"
 
 MOZ_NEVER_INLINE void RecordReplayInvokeCall(void* aFunction,
                                              CallArguments* aArguments) {
   RecordReplayInvokeCallRaw(aArguments, aFunction);
 }
@@ -463,17 +475,19 @@ static uint8_t* MaybeInternalJumpTarget(
          !strstr(startName, "CTRunGetStringIndicesPtr")) ||
         (strstr(startName, "CGColorSpaceCreateDeviceGray") &&
          !strstr(startName, "CGColorSpaceCreateDeviceGray_block_invoke")) ||
         (strstr(startName, "CGColorSpaceCreateDeviceRGB") &&
          !strstr(startName, "CGColorSpaceCreateDeviceRGB_block_invoke")) ||
         // For these functions, there is a syscall near the beginning which
         // other system threads might be inside.
         strstr(startName, "__workq_kernreturn") ||
-        strstr(startName, "kevent64")) {
+        strstr(startName, "kevent64") ||
+        // Workaround suspected udis86 bug when disassembling this function.
+        strstr(startName, "CGAffineTransformMakeScale")) {
       PrintRedirectSpew("Failed [%p]: Vetoed by annotation\n", aIpEnd - 1);
       return aIpEnd - 1;
     }
   }
 
   PrintRedirectSpew("Success!\n");
   return nullptr;
 }
@@ -493,17 +507,18 @@ static void RedirectFailure(const char* 
 }
 
 static void UnknownInstruction(const char* aName, uint8_t* aIp,
                                size_t aNbytes) {
   nsCString byteData;
   for (size_t i = 0; i < aNbytes; i++) {
     byteData.AppendPrintf(" %d", (int)aIp[i]);
   }
-  RedirectFailure("Unknown instruction in %s:%s", aName, byteData.get());
+  RedirectFailure("Unknown instruction in %s [%p]:%s", aName, aIp,
+                  byteData.get());
 }
 
 // Try to emit instructions to |aAssembler| with equivalent behavior to any
 // special jump or ip-dependent instruction at |aIp|, returning true if the
 // instruction was copied.
 static bool CopySpecialInstruction(uint8_t* aIp, ud_t* aUd, size_t aNbytes,
                                    Assembler& aAssembler) {
   aAssembler.NoteOriginalInstruction(aIp);
@@ -536,24 +551,31 @@ static bool CopySpecialInstruction(uint8
         aAssembler.CallRax();
       } else if (mnemonic == UD_Ijmp) {
         aAssembler.Jump(target);
       } else {
         aAssembler.ConditionalJump(aUd->primary_opcode, target);
       }
       return true;
     }
-    if (op->type == UD_OP_MEM && op->base == UD_R_RIP && !op->index &&
-        op->offset == 32) {
-      // jmp *$offset32(%rip)
-      uint8_t* addr = aIp + aNbytes + op->lval.sdword;
-      aAssembler.MoveImmediateToRax(addr);
-      aAssembler.LoadRax(8);
-      aAssembler.JumpToRax();
-      return true;
+    if (op->type == UD_OP_MEM && !op->index) {
+      if (op->base == UD_R_RIP) {
+        if (op->offset == 32) {
+          // jmp *$offset32(%rip)
+          uint8_t* addr = aIp + aNbytes + op->lval.sdword;
+          aAssembler.MoveImmediateToRax(addr);
+          aAssembler.LoadRax(8);
+          aAssembler.JumpToRax();
+          return true;
+        }
+      } else {
+        // Non-IP relative call or jump.
+        aAssembler.CopyInstruction(aIp, aNbytes);
+        return true;
+      }
     }
   }
 
   if (mnemonic == UD_Imov || mnemonic == UD_Ilea) {
     MOZ_RELEASE_ASSERT(!ud_insn_opr(aUd, 2));
     const ud_operand* dst = ud_insn_opr(aUd, 0);
     const ud_operand* src = ud_insn_opr(aUd, 1);
     if (dst->type == UD_OP_REG && src->type == UD_OP_MEM &&
@@ -627,16 +649,34 @@ static bool CopySpecialInstruction(uint8
       aAssembler.PushRax();
       aAssembler.MoveImmediateToRax(addr);
       aAssembler.LoadRax(8);
       aAssembler.CompareRaxWithTopOfStack();
       aAssembler.PopRax();
       aAssembler.PopRax();
       return true;
     }
+    if (dst->type == UD_OP_MEM && src->type == UD_OP_REG &&
+        dst->base == UD_R_RIP && !dst->index && dst->offset == 32) {
+      // cmpq reg, $offset32(%rip)
+      int reg = Assembler::NormalizeRegister(src->base);
+      if (!reg) {
+        return false;
+      }
+      uint8_t* addr = aIp + aNbytes + dst->lval.sdword;
+      aAssembler.PushRax();
+      aAssembler.MoveRegisterToRax(reg);
+      aAssembler.PushRax();
+      aAssembler.MoveImmediateToRax(addr);
+      aAssembler.LoadRax(8);
+      aAssembler.CompareTopOfStackWithRax();
+      aAssembler.PopRax();
+      aAssembler.PopRax();
+      return true;
+    }
   }
 
   if (mnemonic == UD_Ixchg) {
     const ud_operand* dst = ud_insn_opr(aUd, 0);
     const ud_operand* src = ud_insn_opr(aUd, 1);
     if (src->type == UD_OP_REG && src->size == 8 && dst->type == UD_OP_MEM &&
         dst->base == UD_R_RIP && !dst->index && dst->offset == 32) {
       // xchgb reg, $offset32(%rip)
@@ -726,21 +766,47 @@ static uint8_t* CopyInstructions(const c
                                  uint8_t* aIpEnd, Assembler& aAssembler) {
   uint8_t* ip = aIpStart;
   while (ip < aIpEnd) {
     ip += CopyInstruction(aName, ip, aAssembler);
   }
   return ip;
 }
 
+static bool PreserveCallerSaveRegisters(const char* aName) {
+  // LLVM assumes that the call made when getting thread local variables will
+  // preserve registers that are normally caller save. It's not clear what ABI
+  // is actually assumed for this function so preserve all possible registers.
+  return !strcmp(aName, "tlv_get_addr");
+}
+
 // Generate code to set %rax and enter RecordReplayRedirectCall.
 static uint8_t* GenerateRedirectStub(Assembler& aAssembler, size_t aCallId) {
   uint8_t* newFunction = aAssembler.Current();
-  aAssembler.MoveImmediateToRax((void*)aCallId);
-  aAssembler.Jump(BitwiseCast<void*>(RecordReplayRedirectCall));
+  if (PreserveCallerSaveRegisters(GetRedirection(aCallId).mName)) {
+    static int registers[] = {
+      UD_R_RDI, UD_R_RDI /* for alignment */, UD_R_RSI, UD_R_RDX, UD_R_RCX,
+      UD_R_R8, UD_R_R9, UD_R_R10, UD_R_R11,
+    };
+    for (size_t i = 0; i < ArrayLength(registers); i++) {
+      aAssembler.MoveRegisterToRax(registers[i]);
+      aAssembler.PushRax();
+    }
+    aAssembler.MoveImmediateToRax((void*)aCallId);
+    uint8_t* after = aAssembler.Current() + PushImmediateBytes + JumpBytes;
+    aAssembler.PushImmediate(after);
+    aAssembler.Jump(BitwiseCast<void*>(RecordReplayRedirectCall));
+    for (int i = ArrayLength(registers) - 1; i >= 0; i--) {
+      aAssembler.PopRegister(registers[i]);
+    }
+    aAssembler.Return();
+  } else {
+    aAssembler.MoveImmediateToRax((void*)aCallId);
+    aAssembler.Jump(BitwiseCast<void*>(RecordReplayRedirectCall));
+  }
   return newFunction;
 }
 
 // Setup a redirection: overwrite the machine code for its base function, and
 // fill in its original function, to satisfy the function pointer behaviors
 // described in the Redirection structure. aCursor and aCursorEnd are used to
 // allocate executable memory for use in the redirection.
 static void Redirect(size_t aCallId, Redirection& aRedirection,
@@ -750,18 +816,18 @@ static void Redirect(size_t aCallId, Red
   // is doing a best effort sort of thing, and on failure it will crash safely.
   // The main thing we want to avoid is corrupting the code so that it has been
   // redirected but might crash or behave incorrectly when executed.
   uint8_t* functionStart = aRedirection.mBaseFunction;
   uint8_t* ro = functionStart;
 
   if (!functionStart) {
     if (aFirstPass) {
-      PrintSpew("Could not find symbol %s for redirecting.\n",
-                aRedirection.mName);
+      PrintRedirectSpew("Could not find symbol %s for redirecting.\n",
+                        aRedirection.mName);
     }
     return;
   }
 
   if (aRedirection.mOriginalFunction != aRedirection.mBaseFunction) {
     // We already redirected this function.
     MOZ_RELEASE_ASSERT(!aFirstPass);
     return;
--- a/toolkit/recordreplay/ProcessRedirect.h
+++ b/toolkit/recordreplay/ProcessRedirect.h
@@ -68,67 +68,53 @@ namespace recordreplay {
 // functions is incomplete. If a library API is not redirected then it might
 // behave differently between recording and replaying, or it might crash while
 // replaying.
 
 ///////////////////////////////////////////////////////////////////////////////
 // Function Redirections
 ///////////////////////////////////////////////////////////////////////////////
 
-struct CallArguments;
+// Capture the arguments that can be passed to a redirection, and provide
+// storage to specify the redirection's return value. We only need to capture
+// enough argument data here for calls made directly from Gecko code,
+// i.e. where events are not passed through. Calls made while events are passed
+// through are performed with the same stack and register state as when they
+// were initially invoked.
+//
+// Arguments and return value indexes refer to the register contents as passed
+// to the function originally. For functions with complex or floating point
+// arguments and return values, the right index to use might be different than
+// expected, per the requirements of the System V x64 ABI.
+struct CallArguments {
+  // The maximum number of stack arguments that can be captured.
+  static const size_t NumStackArguments = 64;
 
-// All argument and return value data that is stored in registers and whose
-// values are preserved when calling a redirected function.
-struct CallRegisterArguments {
  protected:
   size_t arg0;        // 0
   size_t arg1;        // 8
   size_t arg2;        // 16
   size_t arg3;        // 24
   size_t arg4;        // 32
   size_t arg5;        // 40
   double floatarg0;   // 48
   double floatarg1;   // 56
   double floatarg2;   // 64
   size_t rval0;       // 72
   size_t rval1;       // 80
   double floatrval0;  // 88
   double floatrval1;  // 96
-                      // Size: 104
-
- public:
-  void CopyFrom(const CallRegisterArguments* aArguments);
-  void CopyTo(CallRegisterArguments* aArguments) const;
-  void CopyRvalFrom(const CallRegisterArguments* aArguments);
-};
-
-// Capture the arguments that can be passed to a redirection, and provide
-// storage to specify the redirection's return value. We only need to capture
-// enough argument data here for calls made directly from Gecko code,
-// i.e. where events are not passed through. Calls made while events are passed
-// through are performed with the same stack and register state as when they
-// were initially invoked.
-//
-// Arguments and return value indexes refer to the register contents as passed
-// to the function originally. For functions with complex or floating point
-// arguments and return values, the right index to use might be different than
-// expected, per the requirements of the System V x64 ABI.
-struct CallArguments : public CallRegisterArguments {
-  // The maximum number of stack arguments that can be captured.
-  static const size_t NumStackArguments = 64;
-
- protected:
   size_t stack[NumStackArguments];  // 104
                                     // Size: 616
 
  public:
   template <typename T>
   T& Arg(size_t aIndex) {
     static_assert(sizeof(T) == sizeof(size_t), "Size must match");
-    static_assert(IsFloatingPoint<T>::value == false, "FloatArg NYI");
+    static_assert(IsFloatingPoint<T>::value == false, "Use FloatArg");
     MOZ_RELEASE_ASSERT(aIndex < 70);
     switch (aIndex) {
       case 0:
         return (T&)arg0;
       case 1:
         return (T&)arg1;
       case 2:
         return (T&)arg2;
@@ -143,16 +129,29 @@ struct CallArguments : public CallRegist
     }
   }
 
   template <size_t Index, typename T>
   T& Arg() {
     return Arg<T>(Index);
   }
 
+  template <size_t Index>
+  double& FloatArg() {
+    static_assert(Index < 3, "Bad index");
+    switch (Index) {
+      case 0:
+        return floatarg0;
+      case 1:
+        return floatarg1;
+      case 2:
+        return floatarg2;
+    }
+  }
+
   template <size_t Offset>
   size_t* StackAddress() {
     static_assert(Offset % sizeof(size_t) == 0, "Bad stack offset");
     return &stack[Offset / sizeof(size_t)];
   }
 
   template <typename T, size_t Index = 0>
   T& Rval() {
@@ -174,34 +173,16 @@ struct CallArguments : public CallRegist
       case 0:
         return floatrval0;
       case 1:
         return floatrval1;
     }
   }
 };
 
-inline void CallRegisterArguments::CopyFrom(
-    const CallRegisterArguments* aArguments) {
-  memcpy(this, aArguments, sizeof(CallRegisterArguments));
-}
-
-inline void CallRegisterArguments::CopyTo(
-    CallRegisterArguments* aArguments) const {
-  memcpy(aArguments, this, sizeof(CallRegisterArguments));
-}
-
-inline void CallRegisterArguments::CopyRvalFrom(
-    const CallRegisterArguments* aArguments) {
-  rval0 = aArguments->rval0;
-  rval1 = aArguments->rval1;
-  floatrval0 = aArguments->floatrval0;
-  floatrval1 = aArguments->floatrval1;
-}
-
 // Generic type for a system error code.
 typedef ssize_t ErrorType;
 
 // Signature for a function that saves some output produced by a redirection
 // while recording, and restores that output while replaying. aEvents is the
 // event stream for the current thread, aArguments specifies the arguments to
 // the called function, and aError specifies any system error which the call
 // produces.
@@ -226,20 +207,20 @@ enum class PreambleResult {
   // events were passed through.
   PassThrough,
 };
 
 // Signature for a function that is called on entry to a redirection and can
 // modify its behavior.
 typedef PreambleResult (*PreambleFn)(CallArguments* aArguments);
 
-// Signature for a function that conveys data about a call to or from the
-// middleman process.
-struct MiddlemanCallContext;
-typedef void (*MiddlemanCallFn)(MiddlemanCallContext& aCx);
+// Signature for a function that conveys data about a call to or from an
+// external process.
+struct ExternalCallContext;
+typedef void (*ExternalCallFn)(ExternalCallContext& aCx);
 
 // Information about a system library API function which is being redirected.
 struct Redirection {
   // Name of the function being redirected.
   const char* mName;
 
   // Address of the function which is being redirected. The code for this
   // function is modified so that attempts to call this function will instead
@@ -255,23 +236,23 @@ struct Redirection {
 
   // If specified, will be called at the end of the redirection when events are
   // not being passed through to record/replay any outputs from the call.
   SaveOutputFn mSaveOutput;
 
   // If specified, will be called upon entry to the redirected call.
   PreambleFn mPreamble;
 
-  // If specified, will be called while replaying and diverged from the
-  // recording to perform this call in the middleman process.
-  MiddlemanCallFn mMiddlemanCall;
+  // If specified, allows this call to be made after diverging from the
+  // recording. See ExternalCall.h
+  ExternalCallFn mExternalCall;
 
   // Additional preamble that is only called while replaying and diverged from
   // the recording.
-  PreambleFn mMiddlemanPreamble;
+  PreambleFn mExternalPreamble;
 };
 
 // Platform specific methods describing the set of redirections.
 size_t NumRedirections();
 Redirection& GetRedirection(size_t aCallId);
 
 // Platform specific early initialization of redirections. This is done on both
 // recording/replaying and middleman processes, and allows OriginalCall() to
@@ -394,16 +375,31 @@ static inline void RR_CStringRval(Stream
     // the returned buffer.
     rval = len ? (char*)malloc(len) : nullptr;
   }
   if (len) {
     aEvents.RecordOrReplayBytes(rval, len);
   }
 }
 
+// Record/replay a fixed size rval buffer.
+template <size_t ByteCount>
+static inline void RR_RvalBuffer(Stream& aEvents, CallArguments* aArguments,
+                                 ErrorType* aError) {
+  auto& rval = aArguments->Rval<void*>();
+  bool hasRval = IsRecording() && rval;
+  aEvents.RecordOrReplayValue(&hasRval);
+  if (IsReplaying()) {
+    rval = hasRval ? NewLeakyArray<char>(ByteCount) : nullptr;
+  }
+  if (hasRval) {
+    aEvents.RecordOrReplayBytes(rval, ByteCount);
+  }
+}
+
 // Ensure that the return value matches the specified argument.
 template <size_t Argument>
 static inline void RR_RvalIsArgument(Stream& aEvents, CallArguments* aArguments,
                                      ErrorType* aError) {
   auto& rval = aArguments->Rval<size_t>();
   auto& arg = aArguments->Arg<Argument, size_t>();
   if (IsRecording()) {
     MOZ_RELEASE_ASSERT(rval == arg);
@@ -503,16 +499,24 @@ static inline void RR_WriteOptionalBuffe
                                                    ErrorType* aError) {
   auto& buf = aArguments->Arg<BufferArg, void*>();
   aEvents.CheckInput(!!buf);
   if (buf) {
     aEvents.RecordOrReplayBytes(buf, ByteCount);
   }
 }
 
+// Record/replay an out parameter.
+template <size_t Arg, typename Type>
+static inline void RR_OutParam(Stream& aEvents, CallArguments* aArguments,
+                               ErrorType* aError) {
+  RR_WriteOptionalBufferFixedSize<Arg, sizeof(Type)>(aEvents, aArguments,
+                                                     aError);
+}
+
 // Record/replay the contents of a buffer at argument BufferArg with byte size
 // CountArg, where the call return value plus Offset indicates the amount of
 // data written to the buffer by the call. The return value must already have
 // been recorded/replayed.
 template <size_t BufferArg, size_t CountArg, size_t Offset = 0>
 static inline void RR_WriteBufferViaRval(Stream& aEvents,
                                          CallArguments* aArguments,
                                          ErrorType* aError) {
@@ -583,16 +587,24 @@ static inline PreambleResult Preamble_Pa
 }
 
 static inline PreambleResult Preamble_WaitForever(CallArguments* aArguments) {
   Thread::WaitForever();
   Unreachable();
   return PreambleResult::PassThrough;
 }
 
+static inline PreambleResult Preamble_NYI(CallArguments* aArguments) {
+  if (AreThreadEventsPassedThrough()) {
+    return PreambleResult::PassThrough;
+  }
+  MOZ_CRASH("Redirection NYI");
+  return PreambleResult::Veto;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Other Redirection Interfaces
 ///////////////////////////////////////////////////////////////////////////////
 
 // Given an argument function aFunction, generate code for a new function that
 // takes one fewer argument than aFunction and then calls aFunction with all
 // its arguments and the aArgument value in the last argument position.
 //
--- a/toolkit/recordreplay/ProcessRedirectDarwin.cpp
+++ b/toolkit/recordreplay/ProcessRedirectDarwin.cpp
@@ -5,39 +5,41 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ProcessRedirect.h"
 
 #include "mozilla/Maybe.h"
 
 #include "HashTable.h"
 #include "Lock.h"
-#include "MemorySnapshot.h"
 #include "ProcessRecordReplay.h"
 #include "ProcessRewind.h"
 #include "base/eintr_wrapper.h"
 
+#include <bsm/audit.h>
+#include <bsm/audit_session.h>
+#include <dirent.h>
 #include <dlfcn.h>
 #include <fcntl.h>
-#include <signal.h>
-
-#include <bsm/audit.h>
-#include <bsm/audit_session.h>
 #include <mach/clock.h>
 #include <mach/mach.h>
 #include <mach/mach_time.h>
 #include <mach/mach_vm.h>
 #include <mach/vm_map.h>
+#include <mach-o/loader.h>
+#include <signal.h>
 #include <sys/attr.h>
 #include <sys/event.h>
 #include <sys/mman.h>
 #include <sys/mount.h>
 #include <sys/param.h>
 #include <sys/stat.h>
+#include <sys/syscall.h>
 #include <sys/time.h>
+#include <sys/utsname.h>
 #include <time.h>
 
 #include <Carbon/Carbon.h>
 #include <SystemConfiguration/SystemConfiguration.h>
 #include <objc/objc-runtime.h>
 
 namespace mozilla {
 namespace recordreplay {
@@ -50,32 +52,31 @@ namespace recordreplay {
 // normal non-redirected semantics) is needed.
 #define FOR_EACH_ORIGINAL_FUNCTION(MACRO)   \
   MACRO(__workq_kernreturn)                 \
   MACRO(CFDataGetLength)                    \
   MACRO(close)                              \
   MACRO(lseek)                              \
   MACRO(mach_absolute_time)                 \
   MACRO(mmap)                               \
-  MACRO(mprotect)                           \
-  MACRO(munmap)                             \
   MACRO(objc_msgSend)                       \
   MACRO(open)                               \
   MACRO(OSSpinLockLock)                     \
   MACRO(pipe)                               \
   MACRO(PL_HashTableDestroy)                \
   MACRO(pthread_cond_wait)                  \
   MACRO(pthread_cond_timedwait)             \
   MACRO(pthread_cond_timedwait_relative_np) \
   MACRO(pthread_create)                     \
   MACRO(pthread_mutex_destroy)              \
   MACRO(pthread_mutex_init)                 \
   MACRO(pthread_mutex_lock)                 \
   MACRO(pthread_mutex_trylock)              \
   MACRO(pthread_mutex_unlock)               \
+  MACRO(pthread_self)                       \
   MACRO(read)                               \
   MACRO(start_wqthread)                     \
   MACRO(write)
 
 #define DECLARE_ORIGINAL_FUNCTION(aName) static void* gOriginal_##aName;
 
 FOR_EACH_ORIGINAL_FUNCTION(DECLARE_ORIGINAL_FUNCTION)
 
@@ -172,27 +173,25 @@ static void InitializeStaticClasses() {
 
   gCFConstantStringClass = objc_lookUpClass("__NSCFConstantString");
   MOZ_RELEASE_ASSERT(gCFConstantStringClass);
 }
 
 // Capture an Objective C or CoreFoundation input to a call, which may come
 // either from another middleman call, or from static data in the replaying
 // process.
-static void MM_ObjCInput(MiddlemanCallContext& aCx, id* aThingPtr) {
-  MOZ_RELEASE_ASSERT(aCx.AccessPreface());
-
-  if (MM_SystemInput(aCx, (const void**)aThingPtr)) {
+static void EX_ObjCInput(ExternalCallContext& aCx, id* aThingPtr) {
+  MOZ_RELEASE_ASSERT(aCx.AccessInput());
+
+  if (EX_SystemInput(aCx, (const void**)aThingPtr)) {
     // This value came from a previous middleman call.
     return;
   }
 
-  MOZ_RELEASE_ASSERT(aCx.AccessInput());
-
-  if (aCx.mPhase == MiddlemanCallPhase::ReplayInput) {
+  if (aCx.mPhase == ExternalCallPhase::SaveInput) {
     // Try to determine where this object came from.
 
     // Watch for messages sent to particular classes.
     for (size_t i = 0; i < ArrayLength(gStaticClassNames); i++) {
       if (gStaticClasses[i] == (Class)*aThingPtr) {
         const char* className = gStaticClassNames[i];
         aCx.WriteInputScalar((size_t)ObjCInputKind::StaticClass);
         size_t len = strlen(className) + 1;
@@ -203,26 +202,26 @@ static void MM_ObjCInput(MiddlemanCallCo
     }
 
     // Watch for constant compile time strings baked into the generated code or
     // stored in system libraries. Be careful when accessing the pointer as in
     // the case where a middleman call hook for a function is missing the
     // pointer could have originated from the recording and its address may not
     // be mapped. In this case we would rather gracefully recover and fail to
     // paint, instead of crashing.
-    if (MemoryRangeIsTracked(*aThingPtr, sizeof(CFConstantString))) {
+    Dl_info info;
+    if (dladdr(*aThingPtr, &info) != 0) {
       CFConstantString* str = (CFConstantString*)*aThingPtr;
       if (str->mClass == gCFConstantStringClass &&
           str->mLength <= 4096 &&  // Sanity check.
-          MemoryRangeIsTracked(str->mData, str->mLength)) {
+          dladdr(str->mData, &info) != 0) {
         InfallibleVector<UniChar> buffer;
-        NS_ConvertUTF8toUTF16 converted(str->mData, str->mLength);
         aCx.WriteInputScalar((size_t)ObjCInputKind::ConstantString);
         aCx.WriteInputScalar(str->mLength);
-        aCx.WriteInputBytes(converted.get(), str->mLength * sizeof(UniChar));
+        aCx.WriteInputBytes(str->mData, str->mLength);
         return;
       }
     }
 
     aCx.MarkAsFailed();
     return;
   }
 
@@ -231,120 +230,178 @@ static void MM_ObjCInput(MiddlemanCallCo
       size_t len = aCx.ReadInputScalar();
       UniquePtr<char[]> className(new char[len]);
       aCx.ReadInputBytes(className.get(), len);
       *aThingPtr = (id)objc_lookUpClass(className.get());
       break;
     }
     case ObjCInputKind::ConstantString: {
       size_t len = aCx.ReadInputScalar();
+      UniquePtr<char[]> cstring(new char[len]);
+      aCx.ReadInputBytes(cstring.get(), len);
+
+      // When replaying in the cloud, external string references are generated
+      // on the fly and don't have correct contents. Parse the contents we are
+      // given so we can do a dynamic lookup and generate the right string.
+      static const char cloudPrefix[] = "RECORD_REPLAY_STRING:";
+      if (len >= sizeof(cloudPrefix) &&
+          !memcmp(cstring.get(), cloudPrefix, sizeof(cloudPrefix))) {
+        MOZ_RELEASE_ASSERT(cstring.get()[len - 1] == 0);
+        void* ptr = dlsym(RTLD_DEFAULT, cstring.get() + strlen(cloudPrefix));
+        MOZ_RELEASE_ASSERT(ptr);
+        *aThingPtr = (id)*(CFStringRef*)ptr;
+        break;
+      }
+
+      NS_ConvertUTF8toUTF16 converted(cstring.get(), len);
       UniquePtr<UniChar[]> contents(new UniChar[len]);
-      aCx.ReadInputBytes(contents.get(), len * sizeof(UniChar));
+      memcpy(contents.get(), converted.get(), len * sizeof(UniChar));
       *aThingPtr = (id)CFStringCreateWithCharacters(kCFAllocatorDefault,
                                                     contents.get(), len);
       break;
     }
     default:
       MOZ_CRASH();
   }
 }
 
 template <size_t Argument>
-static void MM_CFTypeArg(MiddlemanCallContext& aCx) {
-  if (aCx.AccessPreface()) {
+static void EX_CFTypeArg(ExternalCallContext& aCx) {
+  if (aCx.AccessInput()) {
     auto& object = aCx.mArguments->Arg<Argument, id>();
-    MM_ObjCInput(aCx, &object);
+    EX_ObjCInput(aCx, &object);
   }
 }
 
-static void MM_CFTypeOutput(MiddlemanCallContext& aCx, CFTypeRef* aOutput,
+static void EX_CFTypeOutput(ExternalCallContext& aCx, CFTypeRef* aOutput,
                             bool aOwnsReference) {
-  MM_SystemOutput(aCx, (const void**)aOutput);
-
-  if (*aOutput) {
-    switch (aCx.mPhase) {
-      case MiddlemanCallPhase::MiddlemanOutput:
-        if (!aOwnsReference) {
-          CFRetain(*aOutput);
-        }
-        break;
-      case MiddlemanCallPhase::MiddlemanRelease:
-        CFRelease(*aOutput);
-        break;
-      default:
-        break;
+  EX_SystemOutput(aCx, (const void**)aOutput);
+
+  const void* value = *aOutput;
+  if (value && aCx.mPhase == ExternalCallPhase::SaveOutput && !IsReplaying()) {
+    if (!aOwnsReference) {
+      CFRetain(value);
     }
+    aCx.mReleaseCallbacks->append([=]() { CFRelease(value); });
   }
 }
 
 // For APIs using the 'Get' rule: no reference is held on the returned value.
-static void MM_CFTypeRval(MiddlemanCallContext& aCx) {
+static void EX_CFTypeRval(ExternalCallContext& aCx) {
   auto& rval = aCx.mArguments->Rval<CFTypeRef>();
-  MM_CFTypeOutput(aCx, &rval, /* aOwnsReference = */ false);
+  EX_CFTypeOutput(aCx, &rval, /* aOwnsReference = */ false);
 }
 
 // For APIs using the 'Create' rule: a reference is held on the returned
 // value which must be released.
-static void MM_CreateCFTypeRval(MiddlemanCallContext& aCx) {
+static void EX_CreateCFTypeRval(ExternalCallContext& aCx) {
   auto& rval = aCx.mArguments->Rval<CFTypeRef>();
-  MM_CFTypeOutput(aCx, &rval, /* aOwnsReference = */ true);
+  EX_CFTypeOutput(aCx, &rval, /* aOwnsReference = */ true);
 }
 
 template <size_t Argument>
-static void MM_CFTypeOutputArg(MiddlemanCallContext& aCx) {
-  MM_WriteBufferFixedSize<Argument, sizeof(const void*)>(aCx);
-
-  auto arg = aCx.mArguments->Arg<Argument, const void**>();
-  MM_CFTypeOutput(aCx, arg, /* aOwnsReference = */ false);
+static void EX_CFTypeOutputArg(ExternalCallContext& aCx) {
+  auto& arg = aCx.mArguments->Arg<Argument, const void**>();
+
+  if (aCx.mPhase == ExternalCallPhase::RestoreInput) {
+    arg = (const void**) aCx.AllocateBytes(sizeof(const void*));
+  }
+
+  EX_CFTypeOutput(aCx, arg, /* aOwnsReference = */ false);
 }
 
 static void SendMessageToObject(const void* aObject, const char* aMessage) {
   CallArguments arguments;
   arguments.Arg<0, const void*>() = aObject;
   arguments.Arg<1, SEL>() = sel_registerName(aMessage);
   RecordReplayInvokeCall(gOriginal_objc_msgSend, &arguments);
 }
 
 // For APIs whose result will be released by the middleman's autorelease pool.
-static void MM_AutoreleaseCFTypeRval(MiddlemanCallContext& aCx) {
-  auto& rval = aCx.mArguments->Rval<const void*>();
-  MM_SystemOutput(aCx, &rval);
-
-  if (rval) {
-    switch (aCx.mPhase) {
-      case MiddlemanCallPhase::MiddlemanOutput:
-        SendMessageToObject(rval, "retain");
-        break;
-      case MiddlemanCallPhase::MiddlemanRelease:
+static void EX_AutoreleaseCFTypeRval(ExternalCallContext& aCx) {
+  auto& rvalReference = aCx.mArguments->Rval<const void*>();
+  EX_SystemOutput(aCx, &rvalReference);
+  const void* rval = rvalReference;
+
+  if (rval && aCx.mPhase == ExternalCallPhase::SaveOutput && !IsReplaying()) {
+    SendMessageToObject(rval, "retain");
+    aCx.mReleaseCallbacks->append([=]() {
         SendMessageToObject(rval, "autorelease");
-        break;
-      default:
-        break;
-    }
+      });
   }
 }
 
 // For functions which have an input CFType value and also have side effects on
 // that value, this associates the call with its own input value so that this
 // will be treated as a dependent for any future calls using the value.
 template <size_t Argument>
-static void MM_UpdateCFTypeArg(MiddlemanCallContext& aCx) {
+static void EX_UpdateCFTypeArg(ExternalCallContext& aCx) {
   auto arg = aCx.mArguments->Arg<Argument, const void*>();
 
-  MM_CFTypeArg<Argument>(aCx);
-  MM_SystemOutput(aCx, &arg, /* aUpdating = */ true);
+  EX_CFTypeArg<Argument>(aCx);
+  EX_SystemOutput(aCx, &arg, /* aUpdating = */ true);
 }
 
 template <int Error = EAGAIN>
 static PreambleResult Preamble_SetError(CallArguments* aArguments) {
   aArguments->Rval<ssize_t>() = -1;
   errno = Error;
   return PreambleResult::Veto;
 }
 
+#define ForEachFixedInputAddress(Macro)                 \
+  Macro(kCFTypeArrayCallBacks)                          \
+  Macro(kCFTypeDictionaryKeyCallBacks)                  \
+  Macro(kCFTypeDictionaryValueCallBacks)
+
+#define ForEachFixedInput(Macro)                \
+  Macro(kCFAllocatorDefault)                    \
+  Macro(kCFAllocatorNull)
+
+enum class FixedInput {
+#define DefineEnum(Name) Name,
+  ForEachFixedInputAddress(DefineEnum)
+  ForEachFixedInput(DefineEnum)
+#undef DefineEnum
+};
+
+static const void* GetFixedInput(FixedInput aWhich) {
+  switch (aWhich) {
+#define FetchEnumAddress(Name) case FixedInput::Name: return &Name;
+    ForEachFixedInputAddress(FetchEnumAddress)
+#undef FetchEnumAddress
+#define FetchEnum(Name) case FixedInput::Name: return Name;
+    ForEachFixedInput(FetchEnum)
+#undef FetchEnum
+  }
+  MOZ_CRASH("Unknown fixed input");
+  return nullptr;
+}
+
+template <size_t Arg, FixedInput Which>
+static void EX_RequireFixed(ExternalCallContext& aCx) {
+  auto& arg = aCx.mArguments->Arg<Arg, const void*>();
+
+  if (aCx.AccessInput()) {
+    const void* value = GetFixedInput(Which);
+    if (aCx.mPhase == ExternalCallPhase::SaveInput) {
+      MOZ_RELEASE_ASSERT(arg == value ||
+                         (Which == FixedInput::kCFAllocatorDefault &&
+                          arg == nullptr));
+    } else {
+      arg = value;
+    }
+  }
+}
+
+template <size_t Arg>
+static void EX_RequireDefaultAllocator(ExternalCallContext& aCx) {
+  EX_RequireFixed<0, FixedInput::kCFAllocatorDefault>(aCx);
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // system call redirections
 ///////////////////////////////////////////////////////////////////////////////
 
 static void RR_recvmsg(Stream& aEvents, CallArguments* aArguments,
                        ErrorType* aError) {
   auto& msg = aArguments->Arg<1, struct msghdr*>();
 
@@ -373,94 +430,52 @@ static PreambleResult MiddlemanPreamble_
   auto msg = aArguments->Arg<1, msghdr*>();
   for (int i = 0; i < msg->msg_iovlen; i++) {
     totalSize += msg->msg_iov[i].iov_len;
   }
   aArguments->Rval<size_t>() = totalSize;
   return PreambleResult::Veto;
 }
 
-static PreambleResult Preamble_mprotect(CallArguments* aArguments) {
-  // Ignore any mprotect calls that occur after taking a snapshot.
-  if (!NumSnapshots()) {
-    return PreambleResult::PassThrough;
-  }
-  aArguments->Rval<ssize_t>() = 0;
-  return PreambleResult::Veto;
-}
-
 static PreambleResult Preamble_mmap(CallArguments* aArguments) {
   auto& address = aArguments->Arg<0, void*>();
   auto& size = aArguments->Arg<1, size_t>();
   auto& prot = aArguments->Arg<2, size_t>();
   auto& flags = aArguments->Arg<3, size_t>();
   auto& fd = aArguments->Arg<4, size_t>();
   auto& offset = aArguments->Arg<5, size_t>();
 
   MOZ_RELEASE_ASSERT(address == PageBase(address));
 
-  // Make sure that fixed mappings do not interfere with snapshot state.
-  if (flags & MAP_FIXED) {
-    CheckFixedMemory(address, RoundupSizeToPageBoundary(size));
+  bool mappingFile = !(flags & MAP_ANON) && !AreThreadEventsPassedThrough();
+  if (IsReplaying() && mappingFile) {
+    flags |= MAP_ANON;
+    prot |= PROT_WRITE;
+    fd = 0;
+    offset = 0;
   }
 
-  void* memory = nullptr;
-  if ((flags & MAP_ANON) ||
-      (IsReplaying() && !AreThreadEventsPassedThrough())) {
-    // Get an anonymous mapping for the result.
-    if (flags & MAP_FIXED) {
-      // For fixed allocations, make sure this memory region is mapped and zero.
-      if (!NumSnapshots()) {
-        // Make sure this memory region is writable.
-        CallFunction<int>(gOriginal_mprotect, address, size,
-                          PROT_READ | PROT_WRITE | PROT_EXEC);
-      }
-      memset(address, 0, size);
-      memory = address;
-    } else {
-      memory = AllocateMemoryTryAddress(
-          address, RoundupSizeToPageBoundary(size), MemoryKind::Tracked);
-    }
-  } else {
-    // We have to call mmap itself, which can change memory protection flags
-    // for memory that is already allocated. If we haven't taken a snapshot
-    // then this is no problem, but after taking a snapshot we have to make
-    // sure that protection flags are what we expect them to be.
-    int newProt = NumSnapshots() ? (PROT_READ | PROT_EXEC) : prot;
-    memory = CallFunction<void*>(gOriginal_mmap, address, size, newProt, flags,
-                                 fd, offset);
-
-    if (flags & MAP_FIXED) {
-      MOZ_RELEASE_ASSERT(memory == address);
-      RestoreWritableFixedMemory(memory, RoundupSizeToPageBoundary(size));
-    } else if (memory && memory != (void*)-1) {
-      RegisterAllocatedMemory(memory, RoundupSizeToPageBoundary(size),
-                              MemoryKind::Tracked);
-    }
+  if (IsReplaying() && !AreThreadEventsPassedThrough()) {
+    flags &= ~MAP_SHARED;
+    flags |= MAP_PRIVATE;
   }
 
-  if (!(flags & MAP_ANON) && !AreThreadEventsPassedThrough()) {
+  void* memory = CallFunction<void*>(gOriginal_mmap, address, size, prot, flags,
+                                     fd, offset);
+
+  if (mappingFile) {
     // Include the data just mapped in the recording.
     MOZ_RELEASE_ASSERT(memory && memory != (void*)-1);
     RecordReplayBytes(memory, size);
   }
 
   aArguments->Rval<void*>() = memory;
   return PreambleResult::Veto;
 }
 
-static PreambleResult Preamble_munmap(CallArguments* aArguments) {
-  auto& address = aArguments->Arg<0, void*>();
-  auto& size = aArguments->Arg<1, size_t>();
-
-  DeallocateMemory(address, size, MemoryKind::Tracked);
-  aArguments->Rval<ssize_t>() = 0;
-  return PreambleResult::Veto;
-}
-
 static PreambleResult MiddlemanPreamble_write(CallArguments* aArguments) {
   // Silently pretend that writes succeed after diverging from the recording.
   aArguments->Rval<size_t>() = aArguments->Arg<2, size_t>();
   return PreambleResult::Veto;
 }
 
 static void RR_getsockopt(Stream& aEvents, CallArguments* aArguments,
                           ErrorType* aError) {
@@ -521,47 +536,41 @@ static PreambleResult MiddlemanPreamble_
       break;
     default:
       MOZ_CRASH();
   }
   aArguments->Rval<ssize_t>() = 0;
   return PreambleResult::Veto;
 }
 
-static PreambleResult Preamble___disable_threadsignal(
-    CallArguments* aArguments) {
-  // __disable_threadsignal is called when a thread finishes. During replay a
-  // terminated thread can cause problems such as changing access bits on
-  // tracked memory behind the scenes.
-  //
-  // Ideally, threads will never try to finish when we are replaying, since we
-  // are supposed to have control over all threads in the system and only spawn
-  // threads which will run forever. Unfortunately, GCD might have already
-  // spawned threads before we were able to install our redirections, so use a
-  // fallback here to keep these threads from terminating.
-  if (IsReplaying()) {
-    Thread::WaitForeverNoIdle();
-  }
-  return PreambleResult::PassThrough;
-}
-
-static void RR___sysctl(Stream& aEvents, CallArguments* aArguments,
-                        ErrorType* aError) {
-  auto& old = aArguments->Arg<2, void*>();
-  auto& oldlenp = aArguments->Arg<3, size_t*>();
+// Record/replay a variety of sysctl-like functions.
+template <size_t BufferArg>
+static void RR_sysctl(Stream& aEvents, CallArguments* aArguments,
+                      ErrorType* aError) {
+  auto& old = aArguments->Arg<BufferArg, void*>();
+  auto& oldlenp = aArguments->Arg<BufferArg + 1, size_t*>();
 
   aEvents.CheckInput((old ? 1 : 0) | (oldlenp ? 2 : 0));
   if (oldlenp) {
     aEvents.RecordOrReplayValue(oldlenp);
   }
   if (old) {
     aEvents.RecordOrReplayBytes(old, *oldlenp);
   }
 }
 
+static PreambleResult Preamble_sysctlbyname(CallArguments* aArguments) {
+  auto name = aArguments->Arg<0, const char*>();
+
+  // Include the environment variable being checked in an assertion, to make it
+  // easier to debug recording mismatches involving sysctlbyname.
+  RecordReplayAssert("sysctlbyname %s", name);
+  return PreambleResult::Redirect;
+}
+
 static PreambleResult Preamble___workq_kernreturn(CallArguments* aArguments) {
   // Busy-wait until initialization is complete.
   while (!gInitialized) {
     ThreadYield();
   }
 
   // Make sure we know this thread exists.
   Thread::Current();
@@ -582,38 +591,50 @@ static PreambleResult Preamble_start_wqt
   RecordReplayInvokeCall(gOriginal_start_wqthread, aArguments);
   return PreambleResult::Veto;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // pthreads redirections
 ///////////////////////////////////////////////////////////////////////////////
 
-static void DirectLockMutex(pthread_mutex_t* aMutex) {
-  AutoPassThroughThreadEvents pt;
+void DirectLockMutex(pthread_mutex_t* aMutex, bool aPassThroughEvents) {
+  Maybe<AutoPassThroughThreadEvents> pt;
+  if (aPassThroughEvents) {
+    pt.emplace();
+  }
   ssize_t rv = CallFunction<ssize_t>(gOriginal_pthread_mutex_lock, aMutex);
+  if (rv != 0) {
+    Print("CRASH DirectLockMutex %d %d\n", rv, errno);
+  }
   MOZ_RELEASE_ASSERT(rv == 0);
 }
 
-static void DirectUnlockMutex(pthread_mutex_t* aMutex) {
-  AutoPassThroughThreadEvents pt;
+void DirectUnlockMutex(pthread_mutex_t* aMutex, bool aPassThroughEvents) {
+  Maybe<AutoPassThroughThreadEvents> pt;
+  if (aPassThroughEvents) {
+    pt.emplace();
+  }
   ssize_t rv = CallFunction<ssize_t>(gOriginal_pthread_mutex_unlock, aMutex);
+  if (rv != 0) {
+    Print("CRASH DirectUnlockMutex %d %d\n", rv, errno);
+  }
   MOZ_RELEASE_ASSERT(rv == 0);
 }
 
 // Handle a redirection which releases a mutex, waits in some way for a cvar,
 // and reacquires the mutex before returning.
 static ssize_t WaitForCvar(pthread_mutex_t* aMutex, pthread_cond_t* aCond,
                            bool aRecordReturnValue,
                            const std::function<ssize_t()>& aCallback) {
   Lock* lock = Lock::Find(aMutex);
   if (!lock) {
     if (IsReplaying() && !AreThreadEventsPassedThrough()) {
       Thread* thread = Thread::Current();
-      if (thread->MaybeWaitForSnapshot(
+      if (thread->MaybeWaitForFork(
               [=]() { pthread_mutex_unlock(aMutex); })) {
         // We unlocked the mutex while the thread idled, so don't wait on the
         // condvar: the state the thread is waiting on may have changed and it
         // might not want to continue waiting. Returning immediately means this
         // this is a spurious wakeup, which is allowed by the pthreads API and
         // should be handled correctly by callers.
         pthread_mutex_lock(aMutex);
         return 0;
@@ -630,18 +651,18 @@ static ssize_t WaitForCvar(pthread_mutex
   }
   ssize_t rv = 0;
   if (IsRecording()) {
     AutoPassThroughThreadEvents pt;
     rv = aCallback();
   } else {
     DirectUnlockMutex(aMutex);
   }
-  lock->Exit();
-  lock->Enter();
+  lock->Exit(aMutex);
+  lock->Enter(aMutex);
   if (IsReplaying()) {
     DirectLockMutex(aMutex);
   }
   if (aRecordReturnValue) {
     return RecordReplayValue(rv);
   }
   MOZ_RELEASE_ASSERT(rv == 0);
   return 0;
@@ -746,30 +767,30 @@ static PreambleResult Preamble_pthread_m
   return PreambleResult::Veto;
 }
 
 static PreambleResult Preamble_pthread_mutex_lock(CallArguments* aArguments) {
   auto& mutex = aArguments->Arg<0, pthread_mutex_t*>();
 
   Lock* lock = Lock::Find(mutex);
   if (!lock) {
-    AutoEnsurePassThroughThreadEventsUseStackPointer pt;
+    AutoEnsurePassThroughThreadEvents pt;
     aArguments->Rval<ssize_t>() =
         CallFunction<ssize_t>(gOriginal_pthread_mutex_lock, mutex);
     return PreambleResult::Veto;
   }
   ssize_t rv = 0;
   if (IsRecording()) {
     AutoPassThroughThreadEvents pt;
     rv = CallFunction<ssize_t>(gOriginal_pthread_mutex_lock, mutex);
   }
   rv = RecordReplayValue(rv);
   MOZ_RELEASE_ASSERT(rv == 0 || rv == EDEADLK);
   if (rv == 0) {
-    lock->Enter();
+    lock->Enter(mutex);
     if (IsReplaying()) {
       DirectLockMutex(mutex);
     }
   }
   aArguments->Rval<ssize_t>() = rv;
   return PreambleResult::Veto;
 }
 
@@ -787,41 +808,166 @@ static PreambleResult Preamble_pthread_m
   ssize_t rv = 0;
   if (IsRecording()) {
     AutoPassThroughThreadEvents pt;
     rv = CallFunction<ssize_t>(gOriginal_pthread_mutex_trylock, mutex);
   }
   rv = RecordReplayValue(rv);
   MOZ_RELEASE_ASSERT(rv == 0 || rv == EBUSY);
   if (rv == 0) {
-    lock->Enter();
+    lock->Enter(mutex);
     if (IsReplaying()) {
       DirectLockMutex(mutex);
     }
   }
   aArguments->Rval<ssize_t>() = rv;
   return PreambleResult::Veto;
 }
 
 static PreambleResult Preamble_pthread_mutex_unlock(CallArguments* aArguments) {
   auto& mutex = aArguments->Arg<0, pthread_mutex_t*>();
 
   Lock* lock = Lock::Find(mutex);
   if (!lock) {
-    AutoEnsurePassThroughThreadEventsUseStackPointer pt;
+    AutoEnsurePassThroughThreadEvents pt;
     aArguments->Rval<ssize_t>() =
         CallFunction<ssize_t>(gOriginal_pthread_mutex_unlock, mutex);
     return PreambleResult::Veto;
   }
-  lock->Exit();
+  lock->Exit(mutex);
   DirectUnlockMutex(mutex);
   aArguments->Rval<ssize_t>() = 0;
   return PreambleResult::Veto;
 }
 
+static PreambleResult Preamble_pthread_getspecific(CallArguments* aArguments) {
+  if (IsReplaying()) {
+    Thread* thread = Thread::Current();
+    if (thread && !thread->IsMainThread()) {
+      auto key = aArguments->Arg<0, pthread_key_t>();
+      void** ptr = thread->GetOrCreateStorage(key);
+      aArguments->Rval<void*>() = *ptr;
+      return PreambleResult::Veto;
+    }
+  }
+  return PreambleResult::PassThrough;
+}
+
+static PreambleResult Preamble_pthread_setspecific(CallArguments* aArguments) {
+  if (IsReplaying()) {
+    Thread* thread = Thread::Current();
+    if (thread && !thread->IsMainThread()) {
+      auto key = aArguments->Arg<0, pthread_key_t>();
+      auto value = aArguments->Arg<1, void*>();
+      void** ptr = thread->GetOrCreateStorage(key);
+      *ptr = value;
+      aArguments->Rval<ssize_t>() = 0;
+      return PreambleResult::Veto;
+    }
+  }
+  return PreambleResult::PassThrough;
+}
+
+static PreambleResult Preamble_pthread_self(CallArguments* aArguments) {
+  if (IsReplaying() && !AreThreadEventsPassedThrough()) {
+    Thread* thread = Thread::Current();
+    if (!thread->IsMainThread()) {
+      aArguments->Rval<pthread_t>() = thread->NativeId();
+      return PreambleResult::Veto;
+    }
+  }
+  return PreambleResult::PassThrough;
+}
+
+static void*
+GetTLVTemplate(void* aPtr, size_t* aTemplateSize, size_t* aTotalSize) {
+  void* tlvTemplate;
+  *aTemplateSize = 0;
+  *aTotalSize = 0;
+
+  Dl_info info;
+  dladdr(aPtr, &info);
+  mach_header_64* header = (mach_header_64*) info.dli_fbase;
+  MOZ_RELEASE_ASSERT(header->magic == MH_MAGIC_64);
+
+  uint32_t offset = sizeof(mach_header_64);
+  for (size_t i = 0; i < header->ncmds; i++) {
+    load_command* cmd = (load_command*) ((uint8_t*)header + offset);
+    if (LC_SEGMENT_64 == (cmd->cmd & ~LC_REQ_DYLD)) {
+      segment_command_64* ncmd = (segment_command_64*) cmd;
+      section_64* sect = (section_64*) (ncmd + 1);
+      for (size_t i = 0; i < ncmd->nsects; i++, sect++) {
+        switch (sect->flags & SECTION_TYPE) {
+        case S_THREAD_LOCAL_REGULAR:
+          MOZ_RELEASE_ASSERT(!*aTotalSize);
+          tlvTemplate = (uint8_t*)header + sect->addr;
+          *aTemplateSize += sect->size;
+          *aTotalSize += sect->size;
+          break;
+        case S_THREAD_LOCAL_ZEROFILL:
+          *aTotalSize += sect->size;
+          break;
+        }
+      }
+    }
+    offset += cmd->cmdsize;
+  }
+
+  return tlvTemplate;
+}
+
+static void*
+GetTLVAddressFunction() {
+  Dl_info info;
+  dladdr(BitwiseCast<void*>(GetTLVAddressFunction), &info);
+  mach_header_64* header = (mach_header_64*) info.dli_fbase;
+  MOZ_RELEASE_ASSERT(header->magic == MH_MAGIC_64);
+
+  uint32_t offset = sizeof(mach_header_64);
+  for (size_t i = 0; i < header->ncmds; i++) {
+    load_command* cmd = (load_command*) ((uint8_t*)header + offset);
+    if (LC_SEGMENT_64 == (cmd->cmd & ~LC_REQ_DYLD)) {
+      segment_command_64* ncmd = (segment_command_64*) cmd;
+      section_64* sect = (section_64*) (ncmd + 1);
+      for (size_t i = 0; i < ncmd->nsects; i++, sect++) {
+        switch (sect->flags & SECTION_TYPE) {
+        case S_THREAD_LOCAL_VARIABLES:
+          tlv_descriptor* desc = (tlv_descriptor*) ((uint8_t*)header + sect->addr);
+          MOZ_RELEASE_ASSERT(desc->thunk);
+          return BitwiseCast<void*>(desc->thunk);
+        }
+      }
+    }
+    offset += cmd->cmdsize;
+  }
+
+  MOZ_CRASH("Couldn't find tlv_get_addr");
+}
+
+static PreambleResult Preamble_tlv_get_addr(CallArguments* aArguments) {
+  if (IsReplaying()) {
+    Thread* thread = Thread::Current();
+    if (thread && !thread->IsMainThread()) {
+      auto desc = aArguments->Arg<0, tlv_descriptor*>();
+      void** ptr = thread->GetOrCreateStorage(desc->key);
+      if (!(*ptr)) {
+        size_t templateSize, totalSize;
+        void* tlvTemplate = GetTLVTemplate(desc, &templateSize, &totalSize);
+        MOZ_RELEASE_ASSERT(desc->offset < totalSize);
+        void* memory = DirectAllocateMemory(totalSize);
+        memcpy(memory, tlvTemplate, templateSize);
+        *ptr = memory;
+      }
+      aArguments->Rval<void*>() = ((uint8_t*)*ptr) + desc->offset;
+      return PreambleResult::Veto;
+    }
+  }
+  return PreambleResult::PassThrough;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // stdlib redirections
 ///////////////////////////////////////////////////////////////////////////////
 
 static void RR_fread(Stream& aEvents, CallArguments* aArguments,
                      ErrorType* aError) {
   auto& buf = aArguments->Arg<0, void*>();
   auto& elemSize = aArguments->Arg<1, size_t>();
@@ -864,89 +1010,67 @@ static PreambleResult Preamble_localtime
 
 // The same concern here applies as for localtime.
 static PreambleResult Preamble_gmtime(CallArguments* aArguments) {
   aArguments->Rval<struct tm*>() =
       gmtime_r(aArguments->Arg<0, const time_t*>(), &gGlobalTM);
   return PreambleResult::Veto;
 }
 
-static PreambleResult Preamble_mach_absolute_time(CallArguments* aArguments) {
-  // This function might be called through OSSpinLock while setting
-  // gTlsThreadKey.
-  Thread* thread = Thread::GetByStackPointer(&thread);
-  if (!thread || thread->PassThroughEvents()) {
-    aArguments->Rval<uint64_t>() =
-        CallFunction<uint64_t>(gOriginal_mach_absolute_time);
-    return PreambleResult::Veto;
-  }
-  return PreambleResult::Redirect;
+static void RR_host_info_or_statistics(Stream& aEvents,
+                                       CallArguments* aArguments,
+                                       ErrorType* aError) {
+  RR_ScalarRval(aEvents, aArguments, aError);
+  auto buf = aArguments->Arg<2, int*>();
+  auto size = aArguments->Arg<3, mach_msg_type_number_t*>();
+
+  aEvents.RecordOrReplayValue(size);
+  aEvents.RecordOrReplayBytes(buf, *size * sizeof(int));
 }
 
-static PreambleResult Preamble_mach_vm_allocate(CallArguments* aArguments) {
-  auto& address = aArguments->Arg<1, void**>();
-  auto& size = aArguments->Arg<2, size_t>();
-  *address = AllocateMemory(size, MemoryKind::Tracked);
-  aArguments->Rval<size_t>() = KERN_SUCCESS;
-  return PreambleResult::Veto;
-}
-
-static PreambleResult Preamble_mach_vm_deallocate(CallArguments* aArguments) {
-  auto& address = aArguments->Arg<1, void*>();
-  auto& size = aArguments->Arg<2, size_t>();
-  DeallocateMemory(address, size, MemoryKind::Tracked);
-  aArguments->Rval<size_t>() = KERN_SUCCESS;
-  return PreambleResult::Veto;
-}
-
-static PreambleResult Preamble_mach_vm_map(CallArguments* aArguments) {
-  if (IsRecording()) {
-    return PreambleResult::PassThrough;
-  } else if (AreThreadEventsPassedThrough()) {
-    // We should only reach this at startup, when initializing the graphics
-    // shared memory block.
-    MOZ_RELEASE_ASSERT(!NumSnapshots());
-    return PreambleResult::PassThrough;
+static void RR_readdir(Stream& aEvents, CallArguments* aArguments,
+                       ErrorType* aError) {
+  auto& rval = aArguments->Rval<dirent*>();
+  size_t nbytes = 0;
+  if (IsRecording() && rval) {
+    nbytes = offsetof(dirent, d_name) + strlen(rval->d_name) + 1;
+  }
+  aEvents.RecordOrReplayValue(&nbytes);
+  if (IsReplaying()) {
+    rval = nbytes ? (dirent*)NewLeakyArray<char>(nbytes) : nullptr;
+  }
+  if (nbytes) {
+    aEvents.RecordOrReplayBytes(rval, nbytes);
   }
-
-  auto size = aArguments->Arg<2, size_t>();
-  auto address = aArguments->Arg<1, void**>();
-
-  *address = AllocateMemory(size, MemoryKind::Tracked);
-  aArguments->Rval<size_t>() = KERN_SUCCESS;
-  return PreambleResult::Veto;
 }
 
-static PreambleResult Preamble_mach_vm_protect(CallArguments* aArguments) {
-  // Ignore any mach_vm_protect calls that occur after taking a snapshot, as
-  // for mprotect.
-  if (!NumSnapshots()) {
-    return PreambleResult::PassThrough;
+static PreambleResult Preamble_mach_thread_self(CallArguments* aArguments) {
+  if (IsReplaying() && !AreThreadEventsPassedThrough()) {
+    Thread* thread = Thread::Current();
+    if (!thread->IsMainThread()) {
+      aArguments->Rval<uintptr_t>() = thread->GetMachId();
+      return PreambleResult::Veto;
+    }
   }
-  aArguments->Rval<size_t>() = KERN_SUCCESS;
-  return PreambleResult::Veto;
+  return PreambleResult::PassThrough;
 }
 
-static PreambleResult Preamble_vm_purgable_control(CallArguments* aArguments) {
-  // Never allow purging of volatile memory, to simplify memory snapshots.
-  auto& state = aArguments->Arg<3, int*>();
-  *state = VM_PURGABLE_NONVOLATILE;
-  aArguments->Rval<size_t>() = KERN_SUCCESS;
-  return PreambleResult::Veto;
-}
-
-static PreambleResult Preamble_vm_copy(CallArguments* aArguments) {
-  // Asking the kernel to copy memory doesn't work right if the destination is
-  // non-writable, so do the copy manually.
-  auto& src = aArguments->Arg<1, void*>();
-  auto& size = aArguments->Arg<2, size_t>();
-  auto& dest = aArguments->Arg<3, void*>();
-  memcpy(dest, src, size);
-  aArguments->Rval<size_t>() = KERN_SUCCESS;
-  return PreambleResult::Veto;
+static void RR_task_threads(Stream& aEvents, CallArguments* aArguments,
+                            ErrorType* aError) {
+  RR_ScalarRval(aEvents, aArguments, aError);
+
+  auto& buf = aArguments->Arg<1, void**>();
+  auto& count = aArguments->Arg<2, mach_msg_type_number_t*>();
+
+  aEvents.RecordOrReplayValue(count);
+
+  if (IsReplaying()) {
+    *buf = NewLeakyArray<void*>(*count);
+  }
+  aEvents.RecordOrReplayBytes(*buf, *count * sizeof(void*));
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // NSPR redirections
 ///////////////////////////////////////////////////////////////////////////////
 
 // Even though NSPR is compiled as part of firefox, it is easier to just
 // redirect this stuff than get record/replay related changes into the source.
@@ -1033,329 +1157,348 @@ static void RR_objc_msgSend(Stream& aEve
     size_t len = 0;
     if (IsRecording()) {
       AutoPassThroughThreadEvents pt;
       len = CFStringGetLength((CFStringRef)object);
     }
     aEvents.RecordOrReplayValue(&len);
     aEvents.RecordOrReplayBytes(aArguments->Arg<2, void*>(),
                                 len * sizeof(wchar_t));
+
+    // Save the length so that EX_NSStringGetCharacters can recover it when
+    // saving output while replaying.
+    Thread::Current()->mRedirectionValue = len;
   }
 
   if (!strcmp(message, "UTF8String") ||
       !strcmp(message, "cStringUsingEncoding:")) {
-    auto& rval = aArguments->Rval<char*>();
-    size_t len = IsRecording() ? strlen(rval) : 0;
-    aEvents.RecordOrReplayValue(&len);
-    if (IsReplaying()) {
-      rval = NewLeakyArray<char>(len + 1);
-    }
-    aEvents.RecordOrReplayBytes(rval, len + 1);
+    RR_CStringRval(aEvents, aArguments, aError);
   }
 }
 
-static void MM_Alloc(MiddlemanCallContext& aCx) {
-  if (aCx.mPhase == MiddlemanCallPhase::MiddlemanInput) {
+static void EX_Alloc(ExternalCallContext& aCx) {
+  if (aCx.mPhase == ExternalCallPhase::RestoreInput) {
     // Refuse to allocate NSAutoreleasePools in the middleman: the order in
     // which middleman calls happen does not guarantee the pools will be
     // created and released in LIFO order, as the pools require. Instead,
     // allocate an NSString instead so we have something to release later.
     // Messages sent to NSAutoreleasePools will all be skipped in the
     // middleman, so no one should notice this subterfuge.
     auto& obj = aCx.mArguments->Arg<0, id>();
     if (obj == (id)objc_lookUpClass("NSAutoreleasePool")) {
       obj = (id)objc_lookUpClass("NSString");
     }
   }
 
-  MM_CreateCFTypeRval(aCx);
+  EX_CreateCFTypeRval(aCx);
 }
 
-static void MM_PerformSelector(MiddlemanCallContext& aCx) {
-  MM_CString<2>(aCx);
-  MM_CFTypeArg<3>(aCx);
+static void EX_PerformSelector(ExternalCallContext& aCx) {
+  EX_CString<2>(aCx);
+  EX_CFTypeArg<3>(aCx);
 
   // The behavior of performSelector:withObject: varies depending on the
   // selector used, so use a whitelist here.
-  if (aCx.mPhase == MiddlemanCallPhase::ReplayPreface) {
+  if (aCx.mPhase == ExternalCallPhase::SaveInput) {
     auto str = aCx.mArguments->Arg<2, const char*>();
     if (strcmp(str, "appearanceNamed:")) {
       aCx.MarkAsFailed();
       return;
     }
   }
 
-  MM_AutoreleaseCFTypeRval(aCx);
+  EX_AutoreleaseCFTypeRval(aCx);
 }
 
-static void MM_DictionaryWithObjectsAndKeys(MiddlemanCallContext& aCx) {
-  // Copy over all possible stack arguments.
-  MM_StackArgumentData<CallArguments::NumStackArguments * sizeof(size_t)>(aCx);
-
-  if (aCx.AccessPreface()) {
-    // Advance through the arguments until there is a null value. If there are
-    // too many arguments for the underlying CallArguments, we will safely
-    // crash when we hit their extent.
-    for (size_t i = 2;; i += 2) {
+static void EX_DictionaryWithObjectsAndKeys(ExternalCallContext& aCx) {
+  if (aCx.AccessInput()) {
+    size_t numArgs = 0;
+    if (aCx.mPhase == ExternalCallPhase::SaveInput) {
+      // Advance through the arguments until there is a null value. If there
+      // are too many arguments for the underlying CallArguments, we will
+      // safely crash when we hit their extent.
+      for (numArgs = 2;; numArgs += 2) {
+        auto& value = aCx.mArguments->Arg<id>(numArgs);
+        if (!value) {
+          break;
+        }
+      }
+    }
+    aCx.ReadOrWriteInputBytes(&numArgs, sizeof(numArgs));
+
+    for (size_t i = 0; i < numArgs; i += 2) {
       auto& value = aCx.mArguments->Arg<id>(i);
-      if (!value) {
-        break;
-      }
       auto& key = aCx.mArguments->Arg<id>(i + 1);
-      MM_ObjCInput(aCx, &value);
-      MM_ObjCInput(aCx, &key);
+      EX_ObjCInput(aCx, &value);
+      EX_ObjCInput(aCx, &key);
     }
   }
 
-  MM_AutoreleaseCFTypeRval(aCx);
+  EX_AutoreleaseCFTypeRval(aCx);
 }
 
-static void MM_DictionaryWithObjects(MiddlemanCallContext& aCx) {
-  MM_Buffer<2, 4, const void*>(aCx);
-  MM_Buffer<3, 4, const void*>(aCx);
-
-  if (aCx.AccessPreface()) {
+static void EX_DictionaryWithObjects(ExternalCallContext& aCx) {
+  EX_Buffer<2, 4, const void*, false>(aCx);
+  EX_Buffer<3, 4, const void*, false>(aCx);
+
+  if (aCx.AccessInput()) {
     auto objects = aCx.mArguments->Arg<2, const void**>();
     auto keys = aCx.mArguments->Arg<3, const void**>();
     auto count = aCx.mArguments->Arg<4, CFIndex>();
 
     for (CFIndex i = 0; i < count; i++) {
-      MM_ObjCInput(aCx, (id*)&objects[i]);
-      MM_ObjCInput(aCx, (id*)&keys[i]);
+      EX_ObjCInput(aCx, (id*)&objects[i]);
+      EX_ObjCInput(aCx, (id*)&keys[i]);
     }
   }
 
-  MM_AutoreleaseCFTypeRval(aCx);
+  EX_AutoreleaseCFTypeRval(aCx);
 }
 
-static void MM_NSStringGetCharacters(MiddlemanCallContext& aCx) {
+static void EX_NSStringGetCharacters(ExternalCallContext& aCx) {
   auto string = aCx.mArguments->Arg<0, CFStringRef>();
   auto& buffer = aCx.mArguments->Arg<2, void*>();
 
-  if (aCx.mPhase == MiddlemanCallPhase::MiddlemanInput) {
+  if (aCx.mPhase == ExternalCallPhase::RestoreInput) {
     size_t len = CFStringGetLength(string);
     buffer = aCx.AllocateBytes(len * sizeof(UniChar));
   }
 
   if (aCx.AccessOutput()) {
-    size_t len = (aCx.mPhase == MiddlemanCallPhase::MiddlemanOutput)
-                     ? CFStringGetLength(string)
-                     : 0;
+    size_t len = 0;
+    if (aCx.mPhase == ExternalCallPhase::SaveOutput) {
+      if (IsReplaying()) {
+        len = Thread::Current()->mRedirectionValue;
+      } else {
+        len = CFStringGetLength(string);
+      }
+    }
     aCx.ReadOrWriteOutputBytes(&len, sizeof(len));
-    if (aCx.mReplayOutputIsOld) {
-      buffer = aCx.AllocateBytes(len * sizeof(UniChar));
-    }
     aCx.ReadOrWriteOutputBytes(buffer, len * sizeof(UniChar));
   }
 }
 
 struct ObjCMessageInfo {
   const char* mMessage;
-  MiddlemanCallFn mMiddlemanCall;
+  ExternalCallFn mExternalCall;
   bool mUpdatesObject;
 };
 
 // All Objective C messages that can be called in the middleman, and hooks for
 // capturing any inputs and outputs other than the object, message, and scalar
 // arguments / return values.
-static ObjCMessageInfo gObjCMiddlemanCallMessages[] = {
+static ObjCMessageInfo gObjCExternalCallMessages[] = {
     // Generic
-    {"alloc", MM_Alloc},
-    {"init", MM_AutoreleaseCFTypeRval},
-    {"performSelector:withObject:", MM_PerformSelector},
-    {"release", MM_SkipInMiddleman},
-    {"respondsToSelector:", MM_CString<2>},
+    {"alloc", EX_Alloc},
+    {"init", EX_AutoreleaseCFTypeRval},
+    {"performSelector:withObject:", EX_PerformSelector},
+    {"release", EX_SkipExecuting},
+    {"respondsToSelector:", EX_CString<2>},
 
     // NSAppearance
     {"_drawInRect:context:options:",
-     MM_Compose<MM_StackArgumentData<sizeof(CGRect)>, MM_CFTypeArg<2>,
-                MM_CFTypeArg<3>>},
+     EX_Compose<EX_StackArgumentData<sizeof(CGRect)>, EX_CFTypeArg<2>,
+                EX_CFTypeArg<3>>},
 
     // NSAutoreleasePool
-    {"drain", MM_SkipInMiddleman},
+    {"drain", EX_SkipExecuting},
 
     // NSArray
     {"count"},
-    {"objectAtIndex:", MM_AutoreleaseCFTypeRval},
+    {"objectAtIndex:", EX_Compose<EX_ScalarArg<2>, EX_AutoreleaseCFTypeRval>},
 
     // NSBezierPath
-    {"addClip", MM_NoOp, true},
-    {"bezierPathWithRoundedRect:xRadius:yRadius:", MM_AutoreleaseCFTypeRval},
+    {"addClip", EX_NoOp, true},
+    {"bezierPathWithRoundedRect:xRadius:yRadius:",
+     EX_Compose<EX_StackArgumentData<sizeof(CGRect)>,
+                EX_FloatArg<0>, EX_FloatArg<1>, EX_AutoreleaseCFTypeRval>},
 
     // NSCell
     {"drawFocusRingMaskWithFrame:inView:",
-     MM_Compose<MM_CFTypeArg<2>, MM_StackArgumentData<sizeof(CGRect)>>},
+     EX_Compose<EX_CFTypeArg<2>, EX_StackArgumentData<sizeof(CGRect)>>},
     {"drawWithFrame:inView:",
-     MM_Compose<MM_CFTypeArg<2>, MM_StackArgumentData<sizeof(CGRect)>>},
-    {"initTextCell:", MM_Compose<MM_CFTypeArg<2>, MM_AutoreleaseCFTypeRval>},
+     EX_Compose<EX_CFTypeArg<2>, EX_StackArgumentData<sizeof(CGRect)>>},
+    {"initTextCell:", EX_Compose<EX_CFTypeArg<2>, EX_AutoreleaseCFTypeRval>},
     {"initTextCell:pullsDown:",
-     MM_Compose<MM_CFTypeArg<2>, MM_AutoreleaseCFTypeRval>},
-    {"setAllowsMixedState:", MM_NoOp, true},
-    {"setBezeled:", MM_NoOp, true},
-    {"setBezelStyle:", MM_NoOp, true},
-    {"setButtonType:", MM_NoOp, true},
-    {"setControlSize:", MM_NoOp, true},
-    {"setControlTint:", MM_NoOp, true},
-    {"setCriticalValue:", MM_NoOp, true},
-    {"setDoubleValue:", MM_NoOp, true},
-    {"setEditable:", MM_NoOp, true},
-    {"setEnabled:", MM_NoOp, true},
-    {"setFocusRingType:", MM_NoOp, true},
-    {"setHighlighted:", MM_NoOp, true},
-    {"setHighlightsBy:", MM_NoOp, true},
-    {"setHorizontal:", MM_NoOp, true},
-    {"setIndeterminate:", MM_NoOp, true},
-    {"setMax:", MM_NoOp, true},
-    {"setMaxValue:", MM_NoOp, true},
-    {"setMinValue:", MM_NoOp, true},
-    {"setPlaceholderString:", MM_NoOp, true},
-    {"setPullsDown:", MM_NoOp, true},
-    {"setShowsFirstResponder:", MM_NoOp, true},
-    {"setState:", MM_NoOp, true},
-    {"setValue:", MM_NoOp, true},
-    {"setWarningValue:", MM_NoOp, true},
+     EX_Compose<EX_CFTypeArg<2>, EX_ScalarArg<3>, EX_AutoreleaseCFTypeRval>},
+    {"setAllowsMixedState:", EX_ScalarArg<2>, true},
+    {"setBezeled:", EX_ScalarArg<2>, true},
+    {"setBezelStyle:", EX_ScalarArg<2>, true},
+    {"setButtonType:", EX_ScalarArg<2>, true},
+    {"setControlSize:", EX_ScalarArg<2>, true},
+    {"setControlTint:", EX_ScalarArg<2>, true},
+    {"setCriticalValue:", EX_FloatArg<0>, true},
+    {"setDoubleValue:", EX_FloatArg<0>, true},
+    {"setEditable:", EX_ScalarArg<2>, true},
+    {"setEnabled:", EX_ScalarArg<2>, true},
+    {"setFocusRingType:", EX_ScalarArg<2>, true},
+    {"setHighlighted:", EX_ScalarArg<2>, true},
+    {"setHighlightsBy:", EX_ScalarArg<2>, true},
+    {"setHorizontal:", EX_ScalarArg<2>, true},
+    {"setIndeterminate:", EX_ScalarArg<2>, true},
+    {"setMax:", EX_FloatArg<0>, true},
+    {"setMaxValue:", EX_FloatArg<0>, true},
+    {"setMinValue:", EX_FloatArg<0>, true},
+    {"setPlaceholderString:", EX_CFTypeArg<2>, true},
+    {"setPullsDown:", EX_ScalarArg<2>, true},
+    {"setShowsFirstResponder:", EX_ScalarArg<2>, true},
+    {"setState:", EX_ScalarArg<2>, true},
+    {"setValue:", EX_FloatArg<0>, true},
+    {"setWarningValue:", EX_FloatArg<0>, true},
     {"showsFirstResponder"},
 
     // NSColor
     {"alphaComponent"},
     {"colorWithDeviceRed:green:blue:alpha:",
-     MM_Compose<MM_StackArgumentData<sizeof(CGFloat)>,
-                MM_AutoreleaseCFTypeRval>},
+     EX_Compose<EX_FloatArg<0>, EX_FloatArg<1>, EX_FloatArg<2>,
+                EX_StackArgumentData<sizeof(CGFloat)>,
+                EX_AutoreleaseCFTypeRval>},
     {"currentControlTint"},
-    {"set", MM_NoOp, true},
+    {"set", EX_NoOp, true},
 
     // NSDictionary
-    {"dictionaryWithObjectsAndKeys:", MM_DictionaryWithObjectsAndKeys},
-    {"dictionaryWithObjects:forKeys:count:", MM_DictionaryWithObjects},
-    {"mutableCopy", MM_AutoreleaseCFTypeRval},
-    {"setObject:forKey:", MM_Compose<MM_CFTypeArg<2>, MM_CFTypeArg<3>>, true},
+    {"dictionaryWithObjectsAndKeys:", EX_DictionaryWithObjectsAndKeys},
+    {"dictionaryWithObjects:forKeys:count:", EX_DictionaryWithObjects},
+    {"mutableCopy", EX_AutoreleaseCFTypeRval},
+    {"setObject:forKey:", EX_Compose<EX_CFTypeArg<2>, EX_CFTypeArg<3>>, true},
 
     // NSFont
-    {"boldSystemFontOfSize:", MM_AutoreleaseCFTypeRval},
-    {"controlContentFontOfSize:", MM_AutoreleaseCFTypeRval},
-    {"familyName", MM_AutoreleaseCFTypeRval},
-    {"fontDescriptor", MM_AutoreleaseCFTypeRval},
-    {"menuBarFontOfSize:", MM_AutoreleaseCFTypeRval},
+    {"boldSystemFontOfSize:",
+     EX_Compose<EX_FloatArg<0>, EX_AutoreleaseCFTypeRval>},
+    {"controlContentFontOfSize:",
+     EX_Compose<EX_FloatArg<0>, EX_AutoreleaseCFTypeRval>},
+    {"familyName", EX_AutoreleaseCFTypeRval},
+    {"fontDescriptor", EX_AutoreleaseCFTypeRval},
+    {"menuBarFontOfSize:",
+     EX_Compose<EX_FloatArg<0>, EX_AutoreleaseCFTypeRval>},
     {"pointSize"},
     {"smallSystemFontSize"},
-    {"systemFontOfSize:", MM_AutoreleaseCFTypeRval},
-    {"toolTipsFontOfSize:", MM_AutoreleaseCFTypeRval},
-    {"userFontOfSize:", MM_AutoreleaseCFTypeRval},
+    {"systemFontOfSize:",
+     EX_Compose<EX_FloatArg<0>, EX_AutoreleaseCFTypeRval>},
+    {"toolTipsFontOfSize:",
+     EX_Compose<EX_FloatArg<0>, EX_AutoreleaseCFTypeRval>},
+    {"userFontOfSize:",
+     EX_Compose<EX_FloatArg<0>, EX_AutoreleaseCFTypeRval>},
 
     // NSFontManager
     {"availableMembersOfFontFamily:",
-     MM_Compose<MM_CFTypeArg<2>, MM_AutoreleaseCFTypeRval>},
-    {"sharedFontManager", MM_AutoreleaseCFTypeRval},
+     EX_Compose<EX_CFTypeArg<2>, EX_AutoreleaseCFTypeRval>},
+    {"sharedFontManager", EX_AutoreleaseCFTypeRval},
 
     // NSGraphicsContext
-    {"currentContext", MM_AutoreleaseCFTypeRval},
+    {"currentContext", EX_AutoreleaseCFTypeRval},
     {"graphicsContextWithGraphicsPort:flipped:",
-     MM_Compose<MM_CFTypeArg<2>, MM_AutoreleaseCFTypeRval>},
-    {"graphicsPort", MM_AutoreleaseCFTypeRval},
+     EX_Compose<EX_CFTypeArg<2>, EX_ScalarArg<3>, EX_AutoreleaseCFTypeRval>},
+    {"graphicsPort", EX_AutoreleaseCFTypeRval},
     {"restoreGraphicsState"},
     {"saveGraphicsState"},
-    {"setCurrentContext:", MM_CFTypeArg<2>},
+    {"setCurrentContext:", EX_CFTypeArg<2>},
 
     // NSNumber
-    {"numberWithBool:", MM_AutoreleaseCFTypeRval},
+    {"numberWithBool:", EX_Compose<EX_ScalarArg<2>, EX_AutoreleaseCFTypeRval>},
     {"unsignedIntValue"},
 
     // NSString
-    {"getCharacters:", MM_NSStringGetCharacters},
-    {"hasSuffix:", MM_CFTypeArg<2>},
-    {"isEqualToString:", MM_CFTypeArg<2>},
+    {"getCharacters:", EX_NSStringGetCharacters},
+    {"hasSuffix:", EX_CFTypeArg<2>},
+    {"isEqualToString:", EX_CFTypeArg<2>},
     {"length"},
-    {"rangeOfString:options:", MM_CFTypeArg<2>},
+    {"rangeOfString:options:", EX_Compose<EX_CFTypeArg<2>, EX_ScalarArg<3>>},
     {"stringWithCharacters:length:",
-     MM_Compose<MM_Buffer<2, 3, UniChar>, MM_AutoreleaseCFTypeRval>},
+     EX_Compose<EX_Buffer<2, 3, UniChar>, EX_AutoreleaseCFTypeRval>},
 
     // NSWindow
-    {"coreUIRenderer", MM_AutoreleaseCFTypeRval},
+    {"coreUIRenderer", EX_AutoreleaseCFTypeRval},
 
     // UIFontDescriptor
     {"symbolicTraits"},
 };
 
-static void MM_objc_msgSend(MiddlemanCallContext& aCx) {
-  auto message = aCx.mArguments->Arg<1, const char*>();
-
-  for (const ObjCMessageInfo& info : gObjCMiddlemanCallMessages) {
+static void EX_objc_msgSend(ExternalCallContext& aCx) {
+  EX_CString<1>(aCx);
+  auto& message = aCx.mArguments->Arg<1, const char*>();
+
+  if (aCx.mPhase == ExternalCallPhase::RestoreInput) {
+    message = (const char*) sel_registerName(message);
+  }
+
+  for (const ObjCMessageInfo& info : gObjCExternalCallMessages) {
     if (!strcmp(info.mMessage, message)) {
       if (info.mUpdatesObject) {
-        MM_UpdateCFTypeArg<0>(aCx);
+        EX_UpdateCFTypeArg<0>(aCx);
       } else {
-        MM_CFTypeArg<0>(aCx);
+        EX_CFTypeArg<0>(aCx);
       }
-      if (info.mMiddlemanCall && !aCx.mFailed) {
-        info.mMiddlemanCall(aCx);
+      if (info.mExternalCall && !aCx.mFailed) {
+        info.mExternalCall(aCx);
       }
-      if (child::CurrentRepaintCannotFail() && aCx.mFailed) {
-        child::ReportFatalError(Nothing(), "Middleman message failure: %s\n",
-                                message);
+      if (aCx.mFailed && HasDivergedFromRecording()) {
+        PrintSpew("Middleman message failure: %s\n", message);
+        if (child::CurrentRepaintCannotFail()) {
+          child::ReportFatalError("Middleman message failure: %s\n", message);
+        }
       }
       return;
     }
   }
 
-  if (aCx.mPhase == MiddlemanCallPhase::ReplayPreface) {
+  if (aCx.mPhase == ExternalCallPhase::SaveInput) {
     aCx.MarkAsFailed();
-    if (child::CurrentRepaintCannotFail()) {
-      child::ReportFatalError(
-          Nothing(), "Could not perform middleman message: %s\n", message);
+    if (HasDivergedFromRecording()) {
+      PrintSpew("Middleman message failure: %s\n", message);
+      if (child::CurrentRepaintCannotFail()) {
+        child::ReportFatalError("Could not perform middleman message: %s\n",
+                                message);
+      }
     }
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Cocoa redirections
 ///////////////////////////////////////////////////////////////////////////////
 
-static void MM_CFArrayCreate(MiddlemanCallContext& aCx) {
-  MM_Buffer<1, 2, const void*>(aCx);
-
-  if (aCx.AccessPreface()) {
+static void EX_CFArrayCreate(ExternalCallContext& aCx) {
+  EX_RequireDefaultAllocator<0>(aCx);
+  EX_Buffer<1, 2, const void*, false>(aCx);
+  EX_RequireFixed<3, FixedInput::kCFTypeArrayCallBacks>(aCx);
+
+  if (aCx.AccessInput()) {
     auto values = aCx.mArguments->Arg<1, const void**>();
     auto numValues = aCx.mArguments->Arg<2, CFIndex>();
-    auto& callbacks = aCx.mArguments->Arg<3, const CFArrayCallBacks*>();
-
-    // For now we only support creating arrays with CFType elements.
-    if (aCx.mPhase == MiddlemanCallPhase::MiddlemanInput) {
-      callbacks = &kCFTypeArrayCallBacks;
-    } else {
-      MOZ_RELEASE_ASSERT(callbacks == &kCFTypeArrayCallBacks);
-    }
 
     for (CFIndex i = 0; i < numValues; i++) {
-      MM_ObjCInput(aCx, (id*)&values[i]);
+      EX_ObjCInput(aCx, (id*)&values[i]);
     }
   }
 
-  MM_CreateCFTypeRval(aCx);
+  EX_CreateCFTypeRval(aCx);
 }
 
-static void MM_CFArrayGetValueAtIndex(MiddlemanCallContext& aCx) {
-  MM_CFTypeArg<0>(aCx);
+static void EX_CFArrayGetValueAtIndex(ExternalCallContext& aCx) {
+  EX_CFTypeArg<0>(aCx);
+  EX_ScalarArg<1>(aCx);
 
   auto array = aCx.mArguments->Arg<0, id>();
 
   // We can't probe the array to see what callbacks it uses, so look at where
   // it came from to see whether its elements should be treated as CFTypes.
-  MiddlemanCall* call = LookupMiddlemanCall(array);
+  ExternalCall* call = LookupExternalCall(array);
   bool isCFTypeRval = false;
   if (call) {
     const char* name = GetRedirection(call->mCallId).mName;
     if (!strcmp(name, "CTLineGetGlyphRuns") ||
         !strcmp(name, "CTFontCopyVariationAxes") ||
         !strcmp(name, "CTFontDescriptorCreateMatchingFontDescriptors")) {
       isCFTypeRval = true;
     }
   }
 
   if (isCFTypeRval) {
-    MM_CFTypeRval(aCx);
+    EX_CFTypeRval(aCx);
   }
 }
 
 static void RR_CFDataGetBytePtr(Stream& aEvents, CallArguments* aArguments,
                                 ErrorType* aError) {
   auto& rval = aArguments->Rval<UInt8*>();
 
   size_t len = 0;
@@ -1363,66 +1506,64 @@ static void RR_CFDataGetBytePtr(Stream& 
     len = CallFunction<size_t>(gOriginal_CFDataGetLength,
                                aArguments->Arg<0, CFDataRef>());
   }
   aEvents.RecordOrReplayValue(&len);
   if (IsReplaying()) {
     rval = NewLeakyArray<UInt8>(len);
   }
   aEvents.RecordOrReplayBytes(rval, len);
+
+  // Save the length so that EX_CFDataGetBytePtr can recover it when saving
+  // output while replaying.
+  Thread::Current()->mRedirectionValue = len;
 }
 
-static void MM_CFDataGetBytePtr(MiddlemanCallContext& aCx) {
-  MM_CFTypeArg<0>(aCx);
+static void EX_CFDataGetBytePtr(ExternalCallContext& aCx) {
+  EX_CFTypeArg<0>(aCx);
 
   auto data = aCx.mArguments->Arg<0, CFDataRef>();
   auto& buffer = aCx.mArguments->Rval<void*>();
 
   if (aCx.AccessOutput()) {
-    size_t len = (aCx.mPhase == MiddlemanCallPhase::MiddlemanOutput)
-                     ? CFDataGetLength(data)
-                     : 0;
+    size_t len = 0;
+    if (aCx.mPhase == ExternalCallPhase::SaveOutput) {
+      if (IsReplaying()) {
+        len = Thread::Current()->mRedirectionValue;
+      } else {
+        len = CFDataGetLength(data);
+      }
+    }
     aCx.ReadOrWriteOutputBytes(&len, sizeof(len));
-    if (aCx.mPhase == MiddlemanCallPhase::ReplayOutput) {
+    if (aCx.mPhase == ExternalCallPhase::RestoreOutput) {
       buffer = aCx.AllocateBytes(len);
     }
     aCx.ReadOrWriteOutputBytes(buffer, len);
   }
 }
 
-static void MM_CFDictionaryCreate(MiddlemanCallContext& aCx) {
-  MM_Buffer<1, 3, const void*>(aCx);
-  MM_Buffer<2, 3, const void*>(aCx);
-
-  if (aCx.AccessPreface()) {
+static void EX_CFDictionaryCreate(ExternalCallContext& aCx) {
+  EX_RequireDefaultAllocator<0>(aCx);
+  EX_Buffer<1, 3, const void*, false>(aCx);
+  EX_Buffer<2, 3, const void*, false>(aCx);
+  EX_RequireFixed<4, FixedInput::kCFTypeDictionaryKeyCallBacks>(aCx);
+  EX_RequireFixed<5, FixedInput::kCFTypeDictionaryValueCallBacks>(aCx);
+
+  if (aCx.AccessInput()) {
     auto keys = aCx.mArguments->Arg<1, const void**>();
     auto values = aCx.mArguments->Arg<2, const void**>();
     auto numValues = aCx.mArguments->Arg<3, CFIndex>();
-    auto& keyCallbacks =
-        aCx.mArguments->Arg<4, const CFDictionaryKeyCallBacks*>();
-    auto& valueCallbacks =
-        aCx.mArguments->Arg<5, const CFDictionaryValueCallBacks*>();
-
-    // For now we only support creating dictionaries with CFType keys and
-    // values.
-    if (aCx.mPhase == MiddlemanCallPhase::MiddlemanInput) {
-      keyCallbacks = &kCFTypeDictionaryKeyCallBacks;
-      valueCallbacks = &kCFTypeDictionaryValueCallBacks;
-    } else {
-      MOZ_RELEASE_ASSERT(keyCallbacks == &kCFTypeDictionaryKeyCallBacks);
-      MOZ_RELEASE_ASSERT(valueCallbacks == &kCFTypeDictionaryValueCallBacks);
-    }
 
     for (CFIndex i = 0; i < numValues; i++) {
-      MM_ObjCInput(aCx, (id*)&keys[i]);
-      MM_ObjCInput(aCx, (id*)&values[i]);
+      EX_ObjCInput(aCx, (id*)&keys[i]);
+      EX_ObjCInput(aCx, (id*)&values[i]);
     }
   }
 
-  MM_CreateCFTypeRval(aCx);
+  EX_CreateCFTypeRval(aCx);
 }
 
 static void DummyCFNotificationCallback(CFNotificationCenterRef aCenter,
                                         void* aObserver, CFStringRef aName,
                                         const void* aObject,
                                         CFDictionaryRef aUserInfo) {
   // FIXME
   // MOZ_CRASH();
@@ -1471,37 +1612,41 @@ static size_t CFNumberTypeBytes(CFNumber
       return sizeof(long);
     case kCFNumberCGFloatType:
       return sizeof(CGFloat);
     default:
       MOZ_CRASH();
   }
 }
 
-static void MM_CFNumberCreate(MiddlemanCallContext& aCx) {
-  if (aCx.AccessPreface()) {
+static void EX_CFNumberCreate(ExternalCallContext& aCx) {
+  EX_RequireDefaultAllocator<0>(aCx);
+  EX_ScalarArg<1>(aCx);
+
+  if (aCx.AccessInput()) {
     auto numberType = aCx.mArguments->Arg<1, CFNumberType>();
     auto& valuePtr = aCx.mArguments->Arg<2, void*>();
-    aCx.ReadOrWritePrefaceBuffer(&valuePtr, CFNumberTypeBytes(numberType));
+    aCx.ReadOrWriteInputBuffer(&valuePtr, CFNumberTypeBytes(numberType));
   }
 
-  MM_CreateCFTypeRval(aCx);
+  EX_CreateCFTypeRval(aCx);
 }
 
 static void RR_CFNumberGetValue(Stream& aEvents, CallArguments* aArguments,
                                 ErrorType* aError) {
   auto& type = aArguments->Arg<1, CFNumberType>();
   auto& value = aArguments->Arg<2, void*>();
 
   aEvents.CheckInput(type);
   aEvents.RecordOrReplayBytes(value, CFNumberTypeBytes(type));
 }
 
-static void MM_CFNumberGetValue(MiddlemanCallContext& aCx) {
-  MM_CFTypeArg<0>(aCx);
+static void EX_CFNumberGetValue(ExternalCallContext& aCx) {
+  EX_CFTypeArg<0>(aCx);
+  EX_ScalarArg<1>(aCx);
 
   auto& buffer = aCx.mArguments->Arg<2, void*>();
   auto type = aCx.mArguments->Arg<1, CFNumberType>();
   aCx.ReadOrWriteOutputBuffer(&buffer, CFNumberTypeBytes(type));
 }
 
 static PreambleResult MiddlemanPreamble_CFRetain(CallArguments* aArguments) {
   aArguments->Rval<size_t>() = aArguments->Arg<0, size_t>();
@@ -1542,41 +1687,47 @@ static void RR_CGBitmapContextCreateWith
                                              ErrorType* aError) {
   auto& data = aArguments->Arg<0, void*>();
   auto& height = aArguments->Arg<2, size_t>();
   auto& bytesPerRow = aArguments->Arg<4, size_t>();
   auto& rval = aArguments->Rval<CGContextRef>();
 
   MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread());
 
-  // When replaying, MM_CGBitmapContextCreateWithData will take care of
-  // updating gContextData with the right context pointer (after being mangled
-  // in MM_SystemOutput).
+  // When replaying, EX_CGBitmapContextCreateWithData will take care of
+  // updating gContextData with the right context pointer.
   if (IsRecording()) {
     gContextData.emplaceBack(rval, data, height * bytesPerRow);
   }
 }
 
-static void MM_CGBitmapContextCreateWithData(MiddlemanCallContext& aCx) {
-  MM_CFTypeArg<5>(aCx);
-  MM_StackArgumentData<3 * sizeof(size_t)>(aCx);
-  MM_CreateCFTypeRval(aCx);
+static void EX_CGBitmapContextCreateWithData(ExternalCallContext& aCx) {
+  EX_ScalarArg<1>(aCx);
+  EX_ScalarArg<2>(aCx);
+  EX_ScalarArg<3>(aCx);
+  EX_ScalarArg<4>(aCx);
+  EX_CFTypeArg<5>(aCx);
+  EX_ScalarArg<6>(aCx);
+  EX_CreateCFTypeRval(aCx);
 
   auto& data = aCx.mArguments->Arg<0, void*>();
   auto height = aCx.mArguments->Arg<2, size_t>();
   auto bytesPerRow = aCx.mArguments->Arg<4, size_t>();
   auto rval = aCx.mArguments->Rval<CGContextRef>();
 
-  if (aCx.mPhase == MiddlemanCallPhase::MiddlemanInput) {
+  if (aCx.mPhase == ExternalCallPhase::RestoreInput) {
     data = aCx.AllocateBytes(height * bytesPerRow);
+
+    auto& releaseCallback = aCx.mArguments->Arg<7, void*>();
+    auto& releaseInfo = aCx.mArguments->Arg<8, void*>();
+    releaseCallback = nullptr;
+    releaseInfo = nullptr;
   }
 
-  if ((aCx.mPhase == MiddlemanCallPhase::ReplayPreface &&
-       !HasDivergedFromRecording()) ||
-      (aCx.AccessOutput() && !aCx.mReplayOutputIsOld)) {
+  if (aCx.AccessOutput()) {
     gContextData.emplaceBack(rval, data, height * bytesPerRow);
   }
 }
 
 template <size_t ContextArgument>
 static void RR_FlushCGContext(Stream& aEvents, CallArguments* aArguments,
                               ErrorType* aError) {
   auto& context = aArguments->Arg<ContextArgument, CGContextRef>();
@@ -1587,43 +1738,49 @@ static void RR_FlushCGContext(Stream& aE
                                   gContextData[i].mDataSize);
       return;
     }
   }
   MOZ_CRASH("RR_FlushCGContext");
 }
 
 template <size_t ContextArgument>
-static void MM_FlushCGContext(MiddlemanCallContext& aCx) {
+static void EX_FlushCGContext(ExternalCallContext& aCx) {