Merge inbound to mozilla-central. a=merge
authorNarcis Beleuzu <nbeleuzu@mozilla.com>
Sun, 05 Aug 2018 12:45:36 +0300
changeset 485291 b3e7de2df93d
parent 485275 5c5509a1a351 (current diff)
parent 485290 2b34cf65dba5 (diff)
child 485293 eccde880daee
child 485297 87a0ebcd004f
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.0a1
first release with
nightly linux32
b3e7de2df93d / 63.0a1 / 20180805100054 / files
nightly linux64
b3e7de2df93d / 63.0a1 / 20180805100054 / files
nightly mac
b3e7de2df93d / 63.0a1 / 20180805100054 / files
nightly win32
b3e7de2df93d / 63.0a1 / 20180805100054 / files
nightly win64
b3e7de2df93d / 63.0a1 / 20180805100054 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
testing/web-platform/meta/navigation-timing/idlharness.window.js.ini
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -713,9 +713,9 @@ skip-if = os == 'linux' && !asan # bug 1
 [browser_dbg-stepping.js]
 skip-if = debug || (verify && (os == 'win')) || (os == "win" && os_version == "6.1")
 [browser_dbg-tabs.js]
 [browser_dbg-tabs-pretty-print.js]
 [browser_dbg-toggling-tools.js]
 [browser_dbg-wasm-sourcemaps.js]
 skip-if = true
 [browser_dbg_rr_breakpoints-01.js]
-skip-if = os != "mac" || debug
+skip-if = os != "mac" || debug || !nightly_build
--- a/devtools/client/debugger/new/test/mochitest/head.js
+++ b/devtools/client/debugger/new/test/mochitest/head.js
@@ -62,38 +62,38 @@ async function takeScreenshot(dbg) {
   canvas.width = dbg.win.innerWidth;
   canvas.height = dbg.win.innerHeight;
   context.drawWindow(dbg.win, 0, 0, canvas.width, canvas.height, "white");
   await waitForTime(1000);
   dump(`[SCREENSHOT] ${canvas.toDataURL()}\n`);
 }
 
 // Attach a debugger to a tab, returning a promise that resolves with the
-// debugger's thread client.
+// debugger's toolbox.
 async function attachDebugger(tab) {
   let target = TargetFactory.forTab(tab);
   let toolbox = await gDevTools.showToolbox(target, "jsdebugger");
-  ok(toolbox.threadClient.state == "resuming", "Thread is attached");
   return toolbox;
 }
 
 // Return a promise that resolves when a breakpoint has been set.
 async function setBreakpoint(threadClient, expectedFile, lineno) {
   let {sources} = await threadClient.getSources();
   ok(sources.length == 1, "Got one source");
   ok(RegExp(expectedFile).test(sources[0].url), "Source is " + expectedFile);
   let sourceClient = threadClient.source(sources[0]);
   await sourceClient.setBreakpoint({ line: lineno });
 }
 
 function resumeThenPauseAtLineFunctionFactory(method) {
   return async function(threadClient, lineno) {
     threadClient[method]();
-    await threadClient.addOneTimeListener("paused", function(event, packet) {
-      let frameLine = ("frame" in packet) ? packet.frame.where.line : undefined;
+    await threadClient.addOneTimeListener("paused", async function(event, packet) {
+      let {frames} = await threadClient.getFrames(0, 1);
+      let frameLine = frames[0] ? frames[0].where.line : undefined;
       ok(frameLine == lineno, "Paused at line " + frameLine + " expected " + lineno);
     });
   };
 }
 
 // Define various methods that resume a thread in a specific way and ensure it
 // pauses at a specified line.
 var rewindToLine = resumeThenPauseAtLineFunctionFactory("rewind");
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -187,16 +187,21 @@ webconsole.menu.selectAll.label=Select a
 webconsole.menu.selectAll.accesskey=A
 
 # LOCALIZATION NOTE (webconsole.menu.openInSidebar.label)
 # Label used for a context-menu item displayed for object/variable logs. Clicking on it
 # opens the webconsole sidebar for the logged variable.
 webconsole.menu.openInSidebar.label=Open in sidebar
 webconsole.menu.openInSidebar.accesskey=V
 
+# LOCALIZATION NOTE (webconsole.menu.timeWarp.label)
+# Label used for a context-menu item displayed for any log. Clicking on it will
+# jump to the execution point where the log item was generated.
+webconsole.menu.timeWarp.label=Jump here
+
 # LOCALIZATION NOTE (webconsole.clearButton.tooltip)
 # Label used for the tooltip on the clear logs button in the console top toolbar bar.
 # Clicking on it will clear the content of the console.
 webconsole.clearButton.tooltip=Clear the Web Console output
 
 # LOCALIZATION NOTE (webconsole.toggleFilterButton.tooltip)
 # Label used for the tooltip on the toggle filter bar button in the console top
 # toolbar bar. Clicking on it will toggle the visibility of an additional bar which
--- a/devtools/client/webconsole/utils/context-menu.js
+++ b/devtools/client/webconsole/utils/context-menu.js
@@ -29,25 +29,29 @@ loader.lazyRequireGetter(this, "openCont
  *        - {String} variableText (optional) which is the textual frontend
  *            representation of the variable
  *        - {Object} message (optional) message object containing metadata such as:
  *          - {String} source
  *          - {String} request
  *        - {Function} openSidebar (optional) function that will open the object
  *            inspector sidebar
  *        - {String} rootActorId (optional) actor id for the root object being clicked on
+ *        - {Object} executionPoint (optional) when replaying, the execution point where
+ *            this message was logged
  */
 function createContextMenu(hud, parentNode, {
   actor,
   clipboardText,
   variableText,
   message,
   serviceContainer,
   openSidebar,
   rootActorId,
+  executionPoint,
+  toolbox,
 }) {
   const win = parentNode.ownerDocument.defaultView;
   const selection = win.getSelection();
 
   const { source, request } = message || {};
 
   const menu = new Menu({
     id: "webconsole-menu"
@@ -172,16 +176,29 @@ function createContextMenu(hud, parentNo
       id: "console-menu-open-sidebar",
       label: l10n.getStr("webconsole.menu.openInSidebar.label"),
       accesskey: l10n.getStr("webconsole.menu.openInSidebar.accesskey"),
       disabled: !rootActorId,
       click: () => openSidebar(message.messageId),
     }));
   }
 
+  // Add time warp option if available.
+  if (executionPoint) {
+    menu.append(new MenuItem({
+      id: "console-menu-time-warp",
+      label: l10n.getStr("webconsole.menu.timeWarp.label"),
+      disabled: false,
+      click: () => {
+        const threadClient = toolbox.threadClient;
+        threadClient.timeWarp(executionPoint);
+      },
+    }));
+  }
+
   return menu;
 }
 
 exports.createContextMenu = createContextMenu;
 
 /**
  * Return an 'edit' menu for a input field. This integrates directly
  * with docshell commands to provide the right enabled state and editor
--- a/devtools/client/webconsole/utils/messages.js
+++ b/devtools/client/webconsole/utils/messages.js
@@ -184,16 +184,17 @@ function transformConsoleAPICallPacket(p
     parameters,
     messageText,
     stacktrace: message.stacktrace ? message.stacktrace : null,
     frame,
     timeStamp: message.timeStamp,
     userProvidedStyles: message.styles,
     prefix: message.prefix,
     private: message.private,
+    executionPoint: message.executionPoint,
   });
 }
 
 function transformNavigationMessagePacket(packet) {
   const { message } = packet;
   return new ConsoleMessage({
     source: MESSAGE_SOURCE.CONSOLE_API,
     type: MESSAGE_TYPE.LOG,
@@ -244,16 +245,17 @@ function transformPageErrorPacket(packet
     level,
     messageText: pageError.errorMessage,
     stacktrace: pageError.stacktrace ? pageError.stacktrace : null,
     frame,
     exceptionDocURL: pageError.exceptionDocURL,
     timeStamp: pageError.timeStamp,
     notes: pageError.notes,
     private: pageError.private,
+    executionPoint: pageError.executionPoint,
   });
 }
 
 function transformNetworkEventPacket(packet) {
   const { networkEvent } = packet;
 
   return new NetworkEventMessage({
     actor: networkEvent.actor,
--- a/devtools/client/webconsole/webconsole-output-wrapper.js
+++ b/devtools/client/webconsole/webconsole-output-wrapper.js
@@ -1,8 +1,9 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
 /* 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/. */
 "use strict";
 
 const Services = require("Services");
 const { createElement, createFactory } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
@@ -149,24 +150,29 @@ WebConsoleOutputWrapper.prototype = {
                         rootObjectInspector.querySelector("[data-link-actor-id]") : null;
         const rootActorId = rootActor ? rootActor.dataset.linkActorId : null;
 
         const sidebarTogglePref = store.getState().prefs.sidebarToggle;
         const openSidebar = sidebarTogglePref ? (messageId) => {
           store.dispatch(actions.showMessageObjectInSidebar(rootActorId, messageId));
         } : null;
 
+        const messageData = getMessage(store.getState(), message.messageId);
+        const executionPoint = messageData && messageData.executionPoint;
+
         const menu = createContextMenu(this.hud, this.parentNode, {
           actor,
           clipboardText,
           variableText,
           message,
           serviceContainer,
           openSidebar,
-          rootActorId
+          rootActorId,
+          executionPoint,
+          toolbox: this.toolbox,
         });
 
         // Emit the "menu-open" event for testing.
         menu.once("open", () => this.emit("menu-open"));
         menu.popup(screenX, screenY, { doc: this.owner.chromeWindow.document });
 
         return menu;
       };
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -56,16 +56,17 @@ ReplayDebugger.prototype = {
   // General methods
   /////////////////////////////////////////////////////////
 
   replaying: true,
 
   canRewind: RecordReplayControl.canRewind,
   replayResumeBackward() { RecordReplayControl.resume(/* forward = */ false); },
   replayResumeForward() { RecordReplayControl.resume(/* forward = */ true); },
+  replayTimeWarp: RecordReplayControl.timeWarp,
   replayPause: RecordReplayControl.pause,
 
   addDebuggee() {},
   removeAllDebuggees() {},
 
   replayingContent(url) {
     return this._sendRequest({ type: "getContent", url });
   },
@@ -100,24 +101,36 @@ ReplayDebugger.prototype = {
       const v = callback(breakpoint);
       if (v) {
         return v;
       }
     }
     return undefined;
   },
 
+  // Getter for a breakpoint kind that has no script/offset/frameIndex.
+  _breakpointKindGetter(kind) {
+    return this._searchBreakpoints(({position, data}) => {
+      return (position.kind == kind) ? data : null;
+    });
+  },
+
+  // Setter for a breakpoint kind that has no script/offset/frameIndex.
+  _breakpointKindSetter(kind, handler, callback) {
+    if (handler) {
+      this._setBreakpoint(callback, { kind }, handler);
+    } else {
+      this._clearMatchingBreakpoints(({position}) => position.kind == kind);
+    }
+  },
+
   // This is called on all ReplayDebuggers whenever the child process is about
   // to unpause. Clear out all data that is invalidated as a result.
   invalidateAfterUnpause() {
-    this._frames.forEach(frame => {
-      if (frame) {
-        frame._invalidate();
-      }
-    });
+    this._frames.forEach(frame => frame._invalidate());
     this._frames.length = 0;
 
     this._objects.forEach(obj => obj._invalidate());
     this._objects.length = 0;
   },
 
   /////////////////////////////////////////////////////////
   // Script methods
@@ -143,16 +156,20 @@ ReplayDebugger.prototype = {
 
   findScripts() {
     // Note: Debugger's findScripts() method takes a query argument, which
     // we ignore here.
     const data = this._sendRequest({ type: "findScripts" });
     return data.map(script => this._addScript(script));
   },
 
+  findAllConsoleMessages() {
+    return this._sendRequest({ type: "findConsoleMessages" });
+  },
+
   /////////////////////////////////////////////////////////
   // ScriptSource methods
   /////////////////////////////////////////////////////////
 
   _getSource(id) {
     if (!this._scriptSources[id]) {
       const data = this._sendRequest({ type: "getSource", id });
       this._scriptSources[id] = new ReplayDebuggerScriptSource(this, data);
@@ -229,81 +246,81 @@ ReplayDebugger.prototype = {
 
     if (index == NewestFrameIndex) {
       if ("index" in data) {
         index = data.index;
       } else {
         // There are no frames on the stack.
         return null;
       }
-
-      // Fill in the older frames.
-      while (index >= this._frames.length) {
-        this._frames.push(null);
-      }
     }
 
     this._frames[index] = new ReplayDebuggerFrame(this, data);
     return this._frames[index];
   },
 
   getNewestFrame() {
     return this._getFrame(NewestFrameIndex);
   },
 
-  get onNewScript() {
-    return this._searchBreakpoints(({position, data}) => {
-      return position.kind == "NewScript" ? data : null;
-    });
+  /////////////////////////////////////////////////////////
+  // Handlers
+  /////////////////////////////////////////////////////////
+
+  _getNewScript() {
+    return this._addScript(this._sendRequest({ type: "getNewScript" }));
   },
 
+  get onNewScript() { return this._breakpointKindGetter("NewScript"); },
   set onNewScript(handler) {
-    if (handler) {
-      this._setBreakpoint(() => {
-        const script = this._sendRequest({ type: "getNewScript" });
-        const debugScript = this._addScript(script);
-        handler.call(this, debugScript);
-      }, { kind: "NewScript" }, handler);
-    } else {
-      this._clearMatchingBreakpoints(({position}) => position.kind == "NewScript");
-    }
+    this._breakpointKindSetter("NewScript", handler,
+                               () => handler.call(this, this._getNewScript()));
   },
 
-  get onEnterFrame() {
-    return this._searchBreakpoints(({position, data}) => {
-      return position.kind == "EnterFrame" ? data : null;
-    });
-  },
-
+  get onEnterFrame() { return this._breakpointKindGetter("EnterFrame"); },
   set onEnterFrame(handler) {
-    if (handler) {
-      this._setBreakpoint(() => handler.call(this, this.getNewestFrame()),
-                          { kind: "EnterFrame" }, handler);
-    } else {
-      this._clearMatchingBreakpoints(({position}) => position.kind == "EnterFrame");
-    }
+    this._breakpointKindSetter("EnterFrame", handler,
+                               () => handler.call(this, this.getNewestFrame()));
   },
 
   get replayingOnPopFrame() {
     return this._searchBreakpoints(({position, data}) => {
       return (position.kind == "OnPop" && !position.script) ? data : null;
     });
   },
 
   set replayingOnPopFrame(handler) {
     if (handler) {
       this._setBreakpoint(() => handler.call(this, this.getNewestFrame()),
                           { kind: "OnPop" }, handler);
     } else {
       this._clearMatchingBreakpoints(({position}) => {
-        return position.kind == "EnterFrame" && !position.script;
+        return position.kind == "OnPop" && !position.script;
       });
     }
   },
 
+  get replayingOnForcedPause() {
+    return this._breakpointKindGetter("ForcedPause");
+  },
+  set replayingOnForcedPause(handler) {
+    this._breakpointKindSetter("ForcedPause", handler,
+                               () => handler.call(this, this.getNewestFrame()));
+  },
+
+  _getNewConsoleMessage() { return this._sendRequest({ type: "getNewConsoleMessage" }); },
+
+  get onConsoleMessage() {
+    return this._breakpointKindGetter("ConsoleMessage");
+  },
+  set onConsoleMessage(handler) {
+    this._breakpointKindSetter("ConsoleMessage", handler,
+                               () => handler.call(this, this._getNewConsoleMessage()));
+  },
+
   clearAllBreakpoints: NYI,
 
 }; // ReplayDebugger.prototype
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerScript
 ///////////////////////////////////////////////////////////////////////////////
 
@@ -450,17 +467,17 @@ ReplayDebuggerFrame.prototype = {
           offset,
           frameIndex: this._data.index },
         handler);
     });
   },
 
   get onPop() {
     return this._dbg._searchBreakpoints(({position, data}) => {
-      return this._positionMatches(position, "OnPop");
+      return this._positionMatches(position, "OnPop") ? data : null;
     });
   },
 
   set onPop(handler) {
     if (handler) {
       this._dbg._setBreakpoint(() => {
           const result = this._dbg._sendRequest({ type: "popFrameResult" });
           handler.call(this._dbg.getNewestFrame(),
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -24,21 +24,23 @@
 "use strict";
 
 const CC = Components.Constructor;
 
 // Create a sandbox with the resources we need. require() doesn't work here.
 const sandbox = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
 Cu.evalInSandbox(
   "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
+  "Components.utils.import('resource://gre/modules/Services.jsm');" +
   "addDebuggerToGlobal(this);",
   sandbox
 );
 const Debugger = sandbox.Debugger;
 const RecordReplayControl = sandbox.RecordReplayControl;
+const Services = sandbox.Services;
 
 const dbg = new Debugger();
 
 // We are interested in debugging all globals in the process.
 dbg.onNewGlobalObject = function(global) {
   dbg.addDebuggee(global);
 };
 
@@ -110,17 +112,17 @@ function scriptFrameForIndex(index) {
       }
     }
     frame = frame.older;
   }
   return frame;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
-// Persistent State
+// 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
@@ -161,34 +163,101 @@ dbg.onNewScript = function(script) {
   addScript(script);
   addScriptSource(script.source);
 
   // Each onNewScript call advances the progress counter, to preserve the
   // ProgressCounter invariant when onNewScript is called multiple times
   // without executing any scripts.
   RecordReplayControl.advanceProgressCounter();
 
-  if (gHasNewScriptHandler) {
-    RecordReplayControl.positionHit({ kind: "NewScript" });
-  }
+  hitGlobalHandler("NewScript");
 
   // Check in case any handlers we need to install are on the scripts just
   // created.
   installPendingHandlers();
 };
 
 ///////////////////////////////////////////////////////////////////////////////
+// Console Message State
+///////////////////////////////////////////////////////////////////////////////
+
+const gConsoleMessages = [];
+
+function newConsoleMessage(messageType, executionPoint, contents) {
+  // Each new console message advances the progress counter, to make sure
+  // that different messages have different progress values.
+  RecordReplayControl.advanceProgressCounter();
+
+  if (!executionPoint) {
+    executionPoint =
+      RecordReplayControl.currentExecutionPoint({ kind: "ConsoleMessage" });
+  }
+
+  contents.messageType = messageType;
+  contents.executionPoint = executionPoint;
+  gConsoleMessages.push(contents);
+
+  hitGlobalHandler("ConsoleMessage");
+}
+
+function convertStack(stack) {
+  if (stack) {
+    const { source, line, column, functionDisplayName } = stack;
+    const parent = convertStack(stack.parent);
+    return { source, line, column, functionDisplayName, parent };
+  }
+  return null;
+}
+
+// Listen to all console messages in the process.
+Services.console.registerListener({
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]),
+
+  observe(message) {
+    if (message instanceof Ci.nsIScriptError) {
+      // If there is a warp target associated with the execution point, use
+      // that. This will take users to the point where the error was originally
+      // generated, rather than where it was reported to the console.
+      let executionPoint;
+      if (message.timeWarpTarget) {
+        executionPoint =
+          RecordReplayControl.timeWarpTargetExecutionPoint(message.timeWarpTarget);
+      }
+
+      const contents = JSON.parse(JSON.stringify(message));
+      contents.stack = convertStack(message.stack);
+      newConsoleMessage("PageError", executionPoint, contents);
+    }
+  },
+});
+
+// Listen to all console API messages in the process.
+Services.obs.addObserver({
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
+
+  observe(message, topic, data) {
+    const apiMessage = message.wrappedJSObject;
+
+    const contents = {};
+    for (const id in apiMessage) {
+      if (id != "wrappedJSObject") {
+        contents[id] = JSON.parse(JSON.stringify(apiMessage[id]));
+      }
+    }
+
+    newConsoleMessage("ConsoleAPI", null, contents);
+  },
+}, "console-api-log-event");
+
+///////////////////////////////////////////////////////////////////////////////
 // Position Handler State
 ///////////////////////////////////////////////////////////////////////////////
 
-// Whether there is a position handler for NewScript.
-let gHasNewScriptHandler = false;
-
-// Whether there is a position handler for EnterFrame.
-let gHasEnterFrameHandler = false;
+// Position kinds we are expected to hit.
+let gPositionHandlerKinds = Object.create(null);
 
 // Handlers we tried to install but couldn't due to a script not existing.
 // Breakpoints requested by the middleman --- which are preserved when
 // restoring earlier checkpoints --- identify target scripts by their stable ID
 // in gScripts. This array holds the breakpoints for scripts whose IDs we know
 // but which have not been created yet.
 const gPendingPcHandlers = [];
 
@@ -199,47 +268,52 @@ const gInstalledPcHandlers = [];
 // Callbacks to test whether a frame should have an OnPop handler.
 const gOnPopFilters = [];
 
 // eslint-disable-next-line no-unused-vars
 function ClearPositionHandlers() {
   dbg.clearAllBreakpoints();
   dbg.onEnterFrame = undefined;
 
-  gHasNewScriptHandler = false;
-  gHasEnterFrameHandler = false;
+  gPositionHandlerKinds = Object.create(null);
   gPendingPcHandlers.length = 0;
   gInstalledPcHandlers.length = 0;
   gOnPopFilters.length = 0;
 }
 
 function installPendingHandlers() {
   const pending = gPendingPcHandlers.map(position => position);
   gPendingPcHandlers.length = 0;
 
   pending.forEach(EnsurePositionHandler);
 }
 
+// Hit a position with the specified kind if we are expected to. This is for
+// use with position kinds that have no script/offset/frameIndex information.
+function hitGlobalHandler(kind) {
+  if (gPositionHandlerKinds[kind]) {
+    RecordReplayControl.positionHit({ kind });
+  }
+}
+
 // The completion state of any frame that is being popped.
 let gPopFrameResult = null;
 
 function onPopFrame(completion) {
   gPopFrameResult = completion;
   RecordReplayControl.positionHit({
     kind: "OnPop",
     script: gScripts.getId(this.script),
     frameIndex: countScriptFrames() - 1,
   });
   gPopFrameResult = null;
 }
 
 function onEnterFrame(frame) {
-  if (gHasEnterFrameHandler) {
-    RecordReplayControl.positionHit({ kind: "EnterFrame" });
-  }
+  hitGlobalHandler("EnterFrame");
 
   if (considerScript(frame.script)) {
     gOnPopFilters.forEach(filter => {
       if (filter(frame)) {
         frame.onPop = onPopFrame;
       }
     });
   }
@@ -254,16 +328,18 @@ function addOnPopFilter(filter) {
     frame = frame.older;
   }
 
   gOnPopFilters.push(filter);
   dbg.onEnterFrame = onEnterFrame;
 }
 
 function EnsurePositionHandler(position) {
+  gPositionHandlerKinds[position.kind] = true;
+
   switch (position.kind) {
   case "Break":
   case "OnStep":
     let debugScript;
     if (position.script) {
       debugScript = gScripts.getObject(position.script);
       if (!debugScript) {
         // The script referred to in this position does not exist yet, so we
@@ -296,22 +372,18 @@ function EnsurePositionHandler(position)
   case "OnPop":
     if (position.script) {
       addOnPopFilter(frame => gScripts.getId(frame.script) == position.script);
     } else {
       addOnPopFilter(frame => true);
     }
     break;
   case "EnterFrame":
-    gHasEnterFrameHandler = true;
     dbg.onEnterFrame = onEnterFrame;
     break;
-  case "NewScript":
-    gHasNewScriptHandler = true;
-    break;
   }
 }
 
 // eslint-disable-next-line no-unused-vars
 function GetEntryPosition(position) {
   if (position.kind == "Break" || position.kind == "OnStep") {
     const script = gScripts.getObject(position.script);
     if (script) {
@@ -568,16 +640,24 @@ const gRequestHandlers = {
     const frame = scriptFrameForIndex(request.index);
     const rv = frame.eval(request.text, request.options);
     return convertCompletionValue(rv);
   },
 
   popFrameResult(request) {
     return gPopFrameResult ? convertCompletionValue(gPopFrameResult) : {};
   },
+
+  findConsoleMessages(request) {
+    return gConsoleMessages;
+  },
+
+  getNewConsoleMessage(request) {
+    return gConsoleMessages[gConsoleMessages.length - 1];
+  },
 };
 
 // eslint-disable-next-line no-unused-vars
 function ProcessRequest(request) {
   try {
     if (gRequestHandlers[request.type]) {
       return gRequestHandlers[request.type](request);
     }
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -103,16 +103,19 @@ const ThreadActor = ActorClassWithSpec(t
 
   get dbg() {
     if (!this._dbg) {
       this._dbg = this._parent.makeDebugger();
       this._dbg.uncaughtExceptionHook = this.uncaughtExceptionHook;
       this._dbg.onDebuggerStatement = this.onDebuggerStatement;
       this._dbg.onNewScript = this.onNewScript;
       this._dbg.on("newGlobal", this.onNewGlobal);
+      if (this._dbg.replaying) {
+        this._dbg.replayingOnForcedPause = this.replayingOnForcedPause.bind(this);
+      }
       // Keep the debugger disabled until a client attaches.
       this._dbg.enabled = this._state != "detached";
     }
     return this._dbg;
   },
 
   get globalDebugObject() {
     if (!this._parent.window || this.dbg.replaying) {
@@ -640,32 +643,38 @@ const ThreadActor = ActorClassWithSpec(t
    * @param Object request
    *        The request packet received over the RDP.
    * @returns A promise that resolves to true once the hooks are attached, or is
    *          rejected with an error packet.
    */
   _handleResumeLimit: async function(request) {
     const steppingType = request.resumeLimit.type;
     const rewinding = request.rewind;
-    if (!["break", "step", "next", "finish"].includes(steppingType)) {
+    if (!["break", "step", "next", "finish", "warp"].includes(steppingType)) {
       return Promise.reject({
         error: "badParameterType",
         message: "Unknown resumeLimit type"
       });
     }
 
+    if (steppingType == "warp") {
+      // Time warp resume limits are handled by the caller.
+      return true;
+    }
+
     const generatedLocation = this.sources.getFrameLocation(this.youngestFrame);
     const originalLocation = await this.sources.getOriginalLocation(generatedLocation);
     const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(
       originalLocation,
       steppingType,
       rewinding
     );
 
-    // Make sure there is still a frame on the stack if we are to continue stepping.
+    // Make sure there is still a frame on the stack if we are to continue
+    // stepping.
     const stepFrame = this._getNextStepFrame(this.youngestFrame, rewinding);
     if (stepFrame) {
       switch (steppingType) {
         case "step":
           if (rewinding) {
             this.dbg.replayingOnPopFrame = onEnterFrame;
           } else {
             this.dbg.onEnterFrame = onEnterFrame;
@@ -794,17 +803,19 @@ const ThreadActor = ActorClassWithSpec(t
         this._options.ignoreCaughtExceptions = request.ignoreCaughtExceptions;
         this.maybePauseOnExceptions();
         this._maybeListenToEvents(request);
       }
 
       // When replaying execution in a separate process we need to explicitly
       // notify that process when to resume execution.
       if (this.dbg.replaying) {
-        if (rewinding) {
+        if (request && request.resumeLimit && request.resumeLimit.type == "warp") {
+          this.dbg.replayTimeWarp(request.resumeLimit.target);
+        } else if (rewinding) {
           this.dbg.replayResumeBackward();
         } else {
           this.dbg.replayResumeForward();
         }
       }
 
       const packet = this._resumed();
       this._popThreadPause();
@@ -1631,16 +1642,39 @@ const ThreadActor = ActorClassWithSpec(t
   },
 
   onSkipBreakpoints: function({ skip }) {
     this.skipBreakpoints = skip;
     return { skip };
   },
 
   /**
+   * A function that the engine calls when replay has hit a point where it will
+   * pause, even if no breakpoint has been set. Such points include hitting the
+   * beginning or end of the replay, or reaching the target of a time warp.
+   *
+   * @param frame Debugger.Frame
+   *        The youngest stack frame, or null.
+   */
+  replayingOnForcedPause: function(frame) {
+    if (frame) {
+      this._pauseAndRespond(frame, { type: "replayForcedPause" });
+    } else {
+      const packet = this._paused(frame);
+      if (!packet) {
+        return;
+      }
+      packet.why = "replayForcedPause";
+
+      this.conn.send(packet);
+      this._pushThreadPause();
+    }
+  },
+
+  /**
    * A function that the engine calls when an exception has been thrown and has
    * propagated to the specified frame.
    *
    * @param youngestFrame Debugger.Frame
    *        The youngest remaining stack frame.
    * @param value object
    *        The exception that was thrown.
    */
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -89,16 +89,20 @@ function WebConsoleActor(connection, par
   }
 
   this.traits = {
     evaluateJSAsync: true,
     transferredResponseSize: true,
     selectedObjectActor: true, // 44+
     fetchCacheDescriptor: true,
   };
+
+  if (this.dbg.replaying && !isWorker) {
+    this.dbg.onConsoleMessage = this.onReplayingMessage.bind(this);
+  }
 }
 
 WebConsoleActor.prototype =
 {
   /**
    * Debugger instance.
    *
    * @see jsdebugger.jsm
@@ -422,16 +426,23 @@ WebConsoleActor.prototype =
    *        The value you want to get a debuggee value for.
    * @param boolean useObjectGlobal
    *        If |true| the object global is determined and added as a debuggee,
    *        otherwise |this.window| is used when makeDebuggeeValue() is invoked.
    * @return object
    *         Debuggee value for |value|.
    */
   makeDebuggeeValue: function(value, useObjectGlobal) {
+    if (this.dbg.replaying) {
+      if (typeof value == "object") {
+        throw new Error("Object makeDebuggeeValue not supported with replaying debugger");
+      } else {
+        return value;
+      }
+    }
     if (useObjectGlobal && isObject(value)) {
       try {
         const global = Cu.getGlobalForObject(value);
         const dbgGlobal = this.dbg.makeGlobalObjectReference(global);
         return dbgGlobal.makeDebuggeeValue(value);
       } catch (ex) {
         // The above can throw an exception if value is not an actual object
         // or 'Object in compartment marked as invisible to Debugger'
@@ -535,16 +546,41 @@ WebConsoleActor.prototype =
    */
   inspectObject(dbgObj, inspectFromAnnotation) {
     this.conn.sendActorEvent(this.actorID, "inspectObject", {
       objectActor: this.createValueGrip(dbgObj),
       inspectFromAnnotation,
     });
   },
 
+  /**
+   * When using a replaying debugger, all messages we have seen so far.
+   */
+  replayingMessages: null,
+
+  /**
+   * When using a replaying debugger, this helper returns whether a message has
+   * been seen before. When the process rewinds or plays back through regions
+   * of execution that have executed before, we will see the same messages
+   * again.
+   */
+  isDuplicateReplayingMessage: function(msg) {
+    if (!this.replayingMessages) {
+      this.replayingMessages = {};
+    }
+    // The progress counter on the message is unique across all messages in the
+    // replaying process.
+    const progress = msg.executionPoint.progress;
+    if (this.replayingMessages[progress]) {
+      return true;
+    }
+    this.replayingMessages[progress] = true;
+    return false;
+  },
+
   // Request handlers for known packet types.
 
   /**
    * Handler for the "startListeners" request.
    *
    * @param object request
    *        The JSON request object received from the Web Console client.
    * @return object
@@ -805,20 +841,35 @@ WebConsoleActor.prototype =
       return {
         error: "missingParameter",
         message: "The messageTypes parameter is missing.",
       };
     }
 
     const messages = [];
 
+    let replayingMessages = [];
+    if (this.dbg.replaying) {
+      replayingMessages = this.dbg.findAllConsoleMessages().filter(msg => {
+        return !this.isDuplicateReplayingMessage(msg);
+      });
+    }
+
     while (types.length > 0) {
       const type = types.shift();
       switch (type) {
         case "ConsoleAPI": {
+          replayingMessages.forEach((msg) => {
+            if (msg.messageType == "ConsoleAPI") {
+              const message = this.prepareConsoleMessageForRemote(msg);
+              message._type = type;
+              messages.push(message);
+            }
+          });
+
           if (!this.consoleAPIListener) {
             break;
           }
 
           // See `window` definition. It isn't always a DOM Window.
           const winStartTime = this.window && this.window.performance ?
             this.window.performance.timing.navigationStart : 0;
 
@@ -834,16 +885,24 @@ WebConsoleActor.prototype =
 
             const message = this.prepareConsoleMessageForRemote(cachedMessage);
             message._type = type;
             messages.push(message);
           });
           break;
         }
         case "PageError": {
+          replayingMessages.forEach((msg) => {
+            if (msg.messageType == "PageError") {
+              const message = this.preparePageErrorForRemote(msg);
+              message._type = type;
+              messages.push(message);
+            }
+          });
+
           if (!this.consoleServiceListener) {
             break;
           }
           const cache = this.consoleServiceListener
                       .getCachedMessages(!this.parentActor.isRootActor);
           cache.forEach((cachedMessage) => {
             let message = null;
             if (cachedMessage instanceof Ci.nsIScriptError) {
@@ -1609,16 +1668,39 @@ WebConsoleActor.prototype =
       window: dbgWindow,
     };
   },
   /* eslint-enable complexity */
 
   // Event handlers for various listeners.
 
   /**
+   * Handle console messages sent to us from a replaying process via the
+   * debugger.
+   */
+  onReplayingMessage: function(msg) {
+    if (this.isDuplicateReplayingMessage(msg)) {
+      return;
+    }
+
+    if (msg.messageType == "ConsoleAPI") {
+      this.onConsoleAPICall(msg);
+    }
+
+    if (msg.messageType == "PageError") {
+      const packet = {
+        from: this.actorID,
+        type: "pageError",
+        pageError: this.preparePageErrorForRemote(msg),
+      };
+      this.conn.send(packet);
+    }
+  },
+
+  /**
    * Handler for messages received from the ConsoleServiceListener. This method
    * sends the nsIConsoleMessage to the remote Web Console client.
    *
    * @param nsIConsoleMessage message
    *        The message we need to send to the client.
    */
   onConsoleServiceMessage: function(message) {
     let packet;
@@ -1700,16 +1782,17 @@ WebConsoleActor.prototype =
       warning: !!(pageError.flags & pageError.warningFlag),
       error: !!(pageError.flags & pageError.errorFlag),
       exception: !!(pageError.flags & pageError.exceptionFlag),
       strict: !!(pageError.flags & pageError.strictFlag),
       info: !!(pageError.flags & pageError.infoFlag),
       private: pageError.isFromPrivateWindow,
       stacktrace: stack,
       notes: notesArray,
+      executionPoint: pageError.executionPoint,
     };
   },
 
   /**
    * Handler for window.console API calls received from the ConsoleAPIListener.
    * This method sends the object to the remote Web Console client.
    *
    * @see ConsoleAPIListener
--- a/devtools/shared/client/thread-client.js
+++ b/devtools/shared/client/thread-client.js
@@ -241,16 +241,35 @@ ThreadClient.prototype = {
    * @param function onResponse
    *        Called with the response packet.
    */
   breakOnNext: function(onResponse) {
     return this._doInterrupt("onNext", onResponse);
   },
 
   /**
+   * Warp through time to an execution point in the past or future.
+   *
+   * @param object aTarget
+   *        Description of the warp destination.
+   * @param function aOnResponse
+   *        Called with the response packet.
+   */
+  timeWarp: function(target, onResponse) {
+    const warp = () => {
+      this._doResume({ type: "warp", target }, true, onResponse);
+    };
+    if (this.paused) {
+      warp();
+    } else {
+      this.interrupt(warp);
+    }
+  },
+
+  /**
    * Interrupt a running thread.
    *
    * @param function onResponse
    *        Called with the response packet.
    */
   _doInterrupt: DebuggerClient.requester({
     type: "interrupt",
     when: arg(0)
--- a/dom/base/nsJSEnvironment.cpp
+++ b/dom/base/nsJSEnvironment.cpp
@@ -480,17 +480,17 @@ public:
                                         &status);
     }
 
     if (status != nsEventStatus_eConsumeNoDefault) {
       JS::Rooted<JSObject*> stack(rootingCx);
       JS::Rooted<JSObject*> stackGlobal(rootingCx);
       xpc::FindExceptionStackForConsoleReport(win, mError,
                                               &stack, &stackGlobal);
-      mReport->LogToConsoleWithStack(stack, stackGlobal);
+      mReport->LogToConsoleWithStack(stack, stackGlobal, JS::ExceptionTimeWarpTarget(mError));
     }
 
     return NS_OK;
   }
 
 private:
   nsCOMPtr<nsPIDOMWindowInner>  mWindow;
   RefPtr<xpc::ErrorReport>      mReport;
--- a/dom/bindings/nsIScriptError.idl
+++ b/dom/bindings/nsIScriptError.idl
@@ -93,16 +93,22 @@ interface nsIScriptError : nsIConsoleMes
     /**
      * The name of a template string, as found in js.msg, associated with the
      * error message.
      */
     attribute AString errorMessageName;
 
     readonly attribute nsIArray notes;
 
+    /**
+     * If we are recording or replaying, this value may identify the point
+     * where the error was generated, otherwise zero.
+     */
+    attribute unsigned long long timeWarpTarget;
+
     void init(in AString message,
               in AString sourceName,
               in AString sourceLine,
               in uint32_t lineNumber,
               in uint32_t columnNumber,
               in uint32_t flags,
               in string category,
               [optional] in bool fromPrivateWindow);
--- a/dom/bindings/nsScriptError.cpp
+++ b/dom/bindings/nsScriptError.cpp
@@ -36,16 +36,17 @@ nsScriptErrorBase::nsScriptErrorBase()
        mLineNumber(0),
        mSourceLine(),
        mColumnNumber(0),
        mFlags(0),
        mCategory(),
        mOuterWindowID(0),
        mInnerWindowID(0),
        mTimeStamp(0),
+       mTimeWarpTarget(0),
        mInitializedOnMainThread(false),
        mIsFromPrivateWindow(false)
 {
 }
 
 nsScriptErrorBase::~nsScriptErrorBase() {}
 
 void
@@ -430,16 +431,30 @@ nsScriptErrorBase::GetIsFromPrivateWindo
         InitializeOnMainThread();
     }
 
     *aIsFromPrivateWindow = mIsFromPrivateWindow;
     return NS_OK;
 }
 
 NS_IMETHODIMP
+nsScriptErrorBase::SetTimeWarpTarget(uint64_t aTarget)
+{
+    mTimeWarpTarget = aTarget;
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsScriptErrorBase::GetTimeWarpTarget(uint64_t* aTarget)
+{
+    *aTarget = mTimeWarpTarget;
+    return NS_OK;
+}
+
+NS_IMETHODIMP
 nsScriptErrorBase::GetNotes(nsIArray** aNotes)
 {
     nsresult rv = NS_OK;
     nsCOMPtr<nsIMutableArray> array =
         do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
 
     uint32_t len = mNotes.Length();
--- a/dom/bindings/nsScriptError.h
+++ b/dom/bindings/nsScriptError.h
@@ -69,16 +69,17 @@ protected:
   nsString mSourceLine;
   uint32_t mColumnNumber;
   uint32_t mFlags;
   nsCString mCategory;
   // mOuterWindowID is set on the main thread from InitializeOnMainThread().
   uint64_t mOuterWindowID;
   uint64_t mInnerWindowID;
   int64_t mTimeStamp;
+  uint64_t mTimeWarpTarget;
   // mInitializedOnMainThread and mIsFromPrivateWindow are set on the main
   // thread from InitializeOnMainThread().
   mozilla::Atomic<bool> mInitializedOnMainThread;
   bool mIsFromPrivateWindow;
 };
 
 class nsScriptError final : public nsScriptErrorBase {
 public:
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -5582,16 +5582,25 @@ namespace JS {
  * cross-compartment wrapper for one), return the stack for that exception, if
  * any.  Will return null if the given object is not an exception object
  * (including if it's null or a security wrapper that can't be unwrapped) or if
  * the exception has no stack.
  */
 extern JS_PUBLIC_API(JSObject*)
 ExceptionStackOrNull(JS::HandleObject obj);
 
+/**
+ * If this process is recording or replaying and the given value is an
+ * exception object (or an unwrappable cross-compartment wrapper for one),
+ * return the point where this exception was thrown, for time warping later.
+ * Returns zero otherwise.
+ */
+extern JS_PUBLIC_API(uint64_t)
+ExceptionTimeWarpTarget(JS::HandleValue exn);
+
 } /* namespace JS */
 
 /**
  * A JS context always has an "owner thread". The owner thread is set when the
  * context is created (to the current thread) and practically all entry points
  * into the JS engine check that a context (or anything contained in the
  * context: runtime, compartment, object, etc) is only touched by its owner
  * thread. Embeddings may check this invariant outside the JS engine by calling
--- a/js/src/jsexn.cpp
+++ b/js/src/jsexn.cpp
@@ -421,16 +421,29 @@ JS::ExceptionStackOrNull(HandleObject ob
     JSObject* obj = CheckedUnwrap(objArg);
     if (!obj || !obj->is<ErrorObject>()) {
       return nullptr;
     }
 
     return obj->as<ErrorObject>().stack();
 }
 
+JS_PUBLIC_API(uint64_t)
+JS::ExceptionTimeWarpTarget(JS::HandleValue value)
+{
+    if (!value.isObject())
+        return 0;
+
+    JSObject* obj = CheckedUnwrap(&value.toObject());
+    if (!obj || !obj->is<ErrorObject>())
+        return 0;
+
+    return obj->as<ErrorObject>().timeWarpTarget();
+}
+
 bool
 Error(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // ES6 19.5.1.1 mandates the .prototype lookup happens before the toString
     RootedObject proto(cx);
     if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto))
--- a/js/src/vm/ErrorObject-inl.h
+++ b/js/src/vm/ErrorObject-inl.h
@@ -33,9 +33,16 @@ js::ErrorObject::columnNumber() const
 }
 
 inline JSObject*
 js::ErrorObject::stack() const
 {
     return getReservedSlotRef(STACK_SLOT).toObjectOrNull();
 }
 
+inline uint64_t
+js::ErrorObject::timeWarpTarget() const
+{
+    const HeapSlot& slot = getReservedSlotRef(TIME_WARP_SLOT);
+    return slot.isDouble() ? slot.toDouble() : 0;
+}
+
 #endif /* vm_ErrorObject_inl_h */
--- a/js/src/vm/ErrorObject.cpp
+++ b/js/src/vm/ErrorObject.cpp
@@ -79,16 +79,26 @@ js::ErrorObject::init(JSContext* cx, Han
     obj->initReservedSlot(STACK_SLOT, ObjectOrNullValue(stack));
     obj->setReservedSlot(ERROR_REPORT_SLOT, PrivateValue(report));
     obj->initReservedSlot(FILENAME_SLOT, StringValue(fileName));
     obj->initReservedSlot(LINENUMBER_SLOT, Int32Value(lineNumber));
     obj->initReservedSlot(COLUMNNUMBER_SLOT, Int32Value(columnNumber));
     if (message)
         obj->setSlotWithType(cx, messageShape, StringValue(message));
 
+    // When recording/replaying and running on the main thread, get a counter
+    // which the devtools can use to warp to this point in the future.
+    if (mozilla::recordreplay::IsRecordingOrReplaying() && !cx->runtime()->parentRuntime) {
+        uint64_t timeWarpTarget = mozilla::recordreplay::NewTimeWarpTarget();
+
+        // Make sure we don't truncate the time warp target by storing it as a double.
+        MOZ_RELEASE_ASSERT(timeWarpTarget < uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT));
+        obj->initReservedSlot(TIME_WARP_SLOT, DoubleValue(timeWarpTarget));
+    }
+
     return true;
 }
 
 /* static */ ErrorObject*
 js::ErrorObject::create(JSContext* cx, JSExnType errorType, HandleObject stack,
                         HandleString fileName, uint32_t lineNumber, uint32_t columnNumber,
                         UniquePtr<JSErrorReport> report, HandleString message,
                         HandleObject protoArg /* = nullptr */)
--- a/js/src/vm/ErrorObject.h
+++ b/js/src/vm/ErrorObject.h
@@ -45,18 +45,19 @@ class ErrorObject : public NativeObject
   protected:
     static const uint32_t EXNTYPE_SLOT          = 0;
     static const uint32_t STACK_SLOT            = EXNTYPE_SLOT + 1;
     static const uint32_t ERROR_REPORT_SLOT     = STACK_SLOT + 1;
     static const uint32_t FILENAME_SLOT         = ERROR_REPORT_SLOT + 1;
     static const uint32_t LINENUMBER_SLOT       = FILENAME_SLOT + 1;
     static const uint32_t COLUMNNUMBER_SLOT     = LINENUMBER_SLOT + 1;
     static const uint32_t MESSAGE_SLOT          = COLUMNNUMBER_SLOT + 1;
+    static const uint32_t TIME_WARP_SLOT        = MESSAGE_SLOT + 1;
 
-    static const uint32_t RESERVED_SLOTS = MESSAGE_SLOT + 1;
+    static const uint32_t RESERVED_SLOTS = TIME_WARP_SLOT + 1;
 
   public:
     static const Class classes[JSEXN_ERROR_LIMIT];
 
     static const Class * classForType(JSExnType type) {
         MOZ_ASSERT(type < JSEXN_WARN);
         return &classes[type];
     }
@@ -94,16 +95,17 @@ class ErrorObject : public NativeObject
     }
 
     JSErrorReport * getOrCreateErrorReport(JSContext* cx);
 
     inline JSString * fileName(JSContext* cx) const;
     inline uint32_t lineNumber() const;
     inline uint32_t columnNumber() const;
     inline JSObject * stack() const;
+    inline uint64_t timeWarpTarget() const;
 
     JSString * getMessage() const {
         const HeapSlot& slot = getReservedSlotRef(MESSAGE_SLOT);
         return slot.isString() ? slot.toString() : nullptr;
     }
 
     // Getter and setter for the Error.prototype.stack accessor.
     static bool getStack(JSContext* cx, unsigned argc, Value* vp);
--- a/js/xpconnect/src/nsXPConnect.cpp
+++ b/js/xpconnect/src/nsXPConnect.cpp
@@ -319,17 +319,18 @@ xpc::ErrorReport::LogToStderr()
 void
 xpc::ErrorReport::LogToConsole()
 {
     LogToConsoleWithStack(nullptr, nullptr);
 }
 
 void
 xpc::ErrorReport::LogToConsoleWithStack(JS::HandleObject aStack,
-                                        JS::HandleObject aStackGlobal)
+                                        JS::HandleObject aStackGlobal,
+                                        uint64_t aTimeWarpTarget /* = 0 */)
 {
     // Don't log failures after diverging from a recording during replay, as
     // this will cause the associated debugger operation to fail.
     if (recordreplay::HasDivergedFromRecording())
         return;
 
     if (aStack) {
         MOZ_ASSERT(aStackGlobal);
@@ -359,16 +360,17 @@ xpc::ErrorReport::LogToConsoleWithStack(
       // As we cache messages in the console service,
       // we have to ensure not leaking them after the related
       // context is destroyed and we only track document lifecycle for now.
       errorObject = new nsScriptErrorWithStack(aStack, aStackGlobal);
     } else {
       errorObject = new nsScriptError();
     }
     errorObject->SetErrorMessageName(mErrorMsgName);
+    errorObject->SetTimeWarpTarget(aTimeWarpTarget);
 
     nsresult rv = errorObject->InitWithWindowID(mErrorMsg, mFileName, mSourceLine,
                                                 mLineNumber, mColumn, mFlags,
                                                 mCategory, mWindowID);
     NS_ENSURE_SUCCESS_VOID(rv);
 
     for (size_t i = 0, len = mNotes.Length(); i < len; i++) {
         ErrorNote& note = mNotes[i];
--- a/js/xpconnect/src/xpcpublic.h
+++ b/js/xpconnect/src/xpcpublic.h
@@ -608,18 +608,20 @@ class ErrorReport : public ErrorBase {
 
     // Log the error report to the console.  Which console will depend on the
     // window id it was initialized with.
     void LogToConsole();
     // Log to console, using the given stack object (which should be a stack of
     // the sort that JS::CaptureCurrentStack produces).  aStack is allowed to be
     // null. If aStack is non-null, aStackGlobal must be a non-null global
     // object that's same-compartment with aStack. Note that aStack might be a
-    // CCW.
-    void LogToConsoleWithStack(JS::HandleObject aStack, JS::HandleObject aStackGlobal);
+    // CCW. When recording/replaying, aTimeWarpTarget optionally indicates
+    // where the error occurred in the process' execution.
+    void LogToConsoleWithStack(JS::HandleObject aStack, JS::HandleObject aStackGlobal,
+                               uint64_t aTimeWarpTarget = 0);
 
     // Produce an error event message string from the given JSErrorReport.  Note
     // that this may produce an empty string if aReport doesn't have a
     // message attached.
     static void ErrorReportToMessageString(JSErrorReport* aReport,
                                            nsAString& aString);
 
     // Log the error report to the stderr.
--- a/mfbt/RecordReplay.cpp
+++ b/mfbt/RecordReplay.cpp
@@ -31,16 +31,17 @@ namespace recordreplay {
   Macro(InternalHasDivergedFromRecording, bool, (), ())         \
   Macro(InternalGeneratePLDHashTableCallbacks, const PLDHashTableOps*, \
         (const PLDHashTableOps* aOps), (aOps))                  \
   Macro(InternalUnwrapPLDHashTableCallbacks, const PLDHashTableOps*, \
         (const PLDHashTableOps* aOps), (aOps))                  \
   Macro(InternalThingIndex, size_t, (void* aThing), (aThing))   \
   Macro(InternalVirtualThingName, const char*, (void* aThing), (aThing)) \
   Macro(ExecutionProgressCounter, ProgressCounter*, (), ())     \
+  Macro(NewTimeWarpTarget, ProgressCounter, (), ())             \
   Macro(IsInternalScript, bool, (const char* aURL), (aURL))     \
   Macro(DefineRecordReplayControlObject, bool, (JSContext* aCx, JSObject* aObj), (aCx, aObj))
 
 #define FOR_EACH_INTERFACE_VOID(Macro)                          \
   Macro(InternalBeginOrderedAtomicAccess, (), ())               \
   Macro(InternalEndOrderedAtomicAccess, (), ())                 \
   Macro(InternalBeginPassThroughThreadEvents, (), ())           \
   Macro(InternalEndPassThroughThreadEvents, (), ())             \
--- a/mfbt/RecordReplay.h
+++ b/mfbt/RecordReplay.h
@@ -337,16 +337,20 @@ typedef uint64_t ProgressCounter;
 MFBT_API ProgressCounter* ExecutionProgressCounter();
 
 static inline void
 AdvanceExecutionProgressCounter()
 {
   ++*ExecutionProgressCounter();
 }
 
+// Get an identifier for the current execution point which can be used to warp
+// here later.
+MFBT_API ProgressCounter NewTimeWarpTarget();
+
 // Return whether a script is internal to the record/replay infrastructure,
 // may run non-deterministically between recording and replaying, and whose
 // execution must not update the progress counter.
 MFBT_API bool IsInternalScript(const char* aURL);
 
 // Define a RecordReplayControl object on the specified global object, with
 // methods specialized to the current recording/replaying or middleman process
 // kind.
deleted file mode 100644
--- a/testing/web-platform/meta/navigation-timing/idlharness.window.js.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[idlharness.window.html]
-  [Untitled]
-    expected: FAIL
-
--- a/toolkit/recordreplay/ipc/Channel.h
+++ b/toolkit/recordreplay/ipc/Channel.h
@@ -74,16 +74,20 @@ namespace recordreplay {
   /* Unpause the child and play execution either to the next point when a */ \
   /* breakpoint is hit, or to the next checkpoint. Resumption may be either */ \
   /* forward or backward. */                                   \
   _Macro(Resume)                                               \
                                                                \
   /* Rewind to a particular saved checkpoint in the past. */   \
   _Macro(RestoreCheckpoint)                                    \
                                                                \
+  /* Run forward to a particular execution point between the current checkpoint */ \
+  /* and the next one. */                                      \
+  _Macro(RunToPoint)                                           \
+                                                               \
   /* Notify the child whether it is the active child and should send paint and similar */ \
   /* messages to the middleman. */                             \
   _Macro(SetIsActive)                                          \
                                                                \
   /* Set whether to perform intentional crashes, for testing. */ \
   _Macro(SetAllowIntentionalCrashes)                           \
                                                                \
   /* Set whether to save a particular checkpoint. */           \
@@ -278,16 +282,27 @@ struct RestoreCheckpointMessage : public
   size_t mCheckpoint;
 
   explicit RestoreCheckpointMessage(size_t aCheckpoint)
     : Message(MessageType::RestoreCheckpoint, sizeof(*this))
     , mCheckpoint(aCheckpoint)
   {}
 };
 
+struct RunToPointMessage : public Message
+{
+  // The target execution point.
+  js::ExecutionPoint mTarget;
+
+  explicit RunToPointMessage(const js::ExecutionPoint& aTarget)
+    : Message(MessageType::RunToPoint, sizeof(*this))
+    , mTarget(aTarget)
+  {}
+};
+
 struct SetIsActiveMessage : public Message
 {
   // Whether this is the active child process (see ParentIPC.cpp).
   bool mActive;
 
   explicit SetIsActiveMessage(bool aActive)
     : Message(MessageType::SetIsActive, sizeof(*this))
     , mActive(aActive)
--- a/toolkit/recordreplay/ipc/ChildIPC.cpp
+++ b/toolkit/recordreplay/ipc/ChildIPC.cpp
@@ -131,16 +131,23 @@ ChannelMessageHandler(Message* aMsg)
   }
   case MessageType::RestoreCheckpoint: {
     const RestoreCheckpointMessage& nmsg = (const RestoreCheckpointMessage&) *aMsg;
     PauseMainThreadAndInvokeCallback([=]() {
         navigation::RestoreCheckpoint(nmsg.mCheckpoint);
       });
     break;
   }
+  case MessageType::RunToPoint: {
+    const RunToPointMessage& nmsg = (const RunToPointMessage&) *aMsg;
+    PauseMainThreadAndInvokeCallback([=]() {
+        navigation::RunToPoint(nmsg.mTarget);
+      });
+    break;
+  }
   default:
     MOZ_CRASH();
   }
 
   free(aMsg);
 }
 
 // Main routine for a thread whose sole purpose is to listen to requests from
--- a/toolkit/recordreplay/ipc/ChildInternal.h
+++ b/toolkit/recordreplay/ipc/ChildInternal.h
@@ -37,25 +37,33 @@ void SetRecordingEndpoint(size_t aIndex,
 // Save temporary checkpoints at all opportunities during navigation.
 void AlwaysSaveTemporaryCheckpoints();
 
 // Process incoming requests from the middleman.
 void DebuggerRequest(js::CharBuffer* aBuffer);
 void SetBreakpoint(size_t aId, const js::BreakpointPosition& aPosition);
 void Resume(bool aForward);
 void RestoreCheckpoint(size_t aId);
+void RunToPoint(const js::ExecutionPoint& aPoint);
 
 // Attempt to diverge from the recording so that new recorded events cause
 // the process to rewind. Returns false if the divergence failed: either we
 // can't rewind, or already diverged here and then had an unhandled divergence.
 bool MaybeDivergeFromRecording();
 
 // Notify navigation that a position was hit.
 void PositionHit(const js::BreakpointPosition& aPosition);
 
+// Get an execution point for hitting the specified position right now.
+js::ExecutionPoint CurrentExecutionPoint(const js::BreakpointPosition& aPosition);
+
+// Convert an identifier from NewTimeWarpTarget() which we have seen while
+// executing into an ExecutionPoint.
+js::ExecutionPoint TimeWarpTargetExecutionPoint(ProgressCounter aTarget);
+
 // Called when running forward, immediately before hitting a normal or
 // temporary checkpoint.
 void BeforeCheckpoint();
 
 // Called immediately after hitting a normal or temporary checkpoint, either
 // when running forward or immediately after rewinding.
 void AfterCheckpoint(const CheckpointId& aCheckpoint);
 
--- a/toolkit/recordreplay/ipc/ChildNavigation.cpp
+++ b/toolkit/recordreplay/ipc/ChildNavigation.cpp
@@ -94,16 +94,21 @@ public:
     Unsupported("Resume");
   }
 
   // Called after the middleman tells us to rewind to a specific checkpoint.
   virtual void RestoreCheckpoint(size_t aCheckpoint) {
     Unsupported("RestoreCheckpoint");
   }
 
+  // Called after the middleman tells us to run forward to a specific point.
+  virtual void RunToPoint(const ExecutionPoint& aTarget) {
+    Unsupported("RunToPoint");
+  }
+
   // Process an incoming debugger request from the middleman.
   virtual void HandleDebuggerRequest(js::CharBuffer* aRequestBuffer) {
     Unsupported("HandleDebuggerRequest");
   }
 
   // Called when a debugger request wants to try an operation that may
   // trigger an unhandled divergence from the recording.
   virtual bool MaybeDivergeFromRecording() {
@@ -161,17 +166,18 @@ class BreakpointPausedPhase final : publ
   // last entry in |mRequests|, though may be earlier if we are recovering
   // from an unhandled divergence.
   size_t mRequestIndex;
 
   // Set when we were told to resume forward and need to clean up our state.
   bool mResumeForward;
 
 public:
-  void Enter(const ExecutionPoint& aPoint, const BreakpointVector& aBreakpoints);
+  void Enter(const ExecutionPoint& aPoint, bool aRecordingEndpoint,
+             const BreakpointVector& aBreakpoints);
 
   void ToString(nsAutoCString& aStr) override {
     aStr.AppendPrintf("BreakpointPaused RecoveringFromDivergence %d", mRecoveringFromDivergence);
   }
 
   void AfterCheckpoint(const CheckpointId& aCheckpoint) override;
   void PositionHit(const ExecutionPoint& aPoint) override;
   void Resume(bool aForward) override;
@@ -185,26 +191,27 @@ public:
 
 // Phase when the replaying process is paused at a normal checkpoint.
 class CheckpointPausedPhase final : public NavigationPhase
 {
   size_t mCheckpoint;
   bool mAtRecordingEndpoint;
 
 public:
-  void Enter(size_t aCheckpoint, bool aRewind, bool aAtRecordingEndpoint);
+  void Enter(size_t aCheckpoint, bool aRewind, bool aRecordingEndpoint);
 
   void ToString(nsAutoCString& aStr) override {
     aStr.AppendPrintf("CheckpointPaused");
   }
 
   void AfterCheckpoint(const CheckpointId& aCheckpoint) override;
   void PositionHit(const ExecutionPoint& aPoint) override;
   void Resume(bool aForward) override;
   void RestoreCheckpoint(size_t aCheckpoint) override;
+  void RunToPoint(const ExecutionPoint& aTarget) override;
   void HandleDebuggerRequest(js::CharBuffer* aRequestBuffer) override;
   ExecutionPoint GetRecordingEndpoint() override;
 };
 
 // Phase when execution is proceeding forwards in search of breakpoint hits.
 class ForwardPhase final : public NavigationPhase
 {
   // Some execution point in the recent past. There are no checkpoints or
@@ -240,18 +247,18 @@ private:
   // Whether we have saved a temporary checkpoint at the specified point.
   bool mSavedTemporaryCheckpoint;
 
   // The time at which we started running forward from the initial
   // checkpoint, in microseconds.
   double mStartTime;
 
 public:
-  // Note: this always rewinds.
   void Enter(const CheckpointId& aStart,
+             bool aRewind,
              const ExecutionPoint& aPoint,
              const Maybe<ExecutionPoint>& aTemporaryCheckpoint);
 
   void ToString(nsAutoCString& aStr) override {
     aStr.AppendPrintf("ReachBreakpoint: ");
     ExecutionPointToString(mPoint, aStr);
     if (mTemporaryCheckpoint.isSome()) {
       aStr.AppendPrintf(" TemporaryCheckpoint: ");
@@ -361,16 +368,19 @@ public:
   ForwardPhase mForwardPhase;
   ReachBreakpointPhase mReachBreakpointPhase;
   FindLastHitPhase mFindLastHitPhase;
 
   // For testing, specify that temporary checkpoints should be taken regardless
   // of how much time has elapsed.
   bool mAlwaysSaveTemporaryCheckpoints;
 
+  // Checkpoints for all time warp targets that have been generated.
+  InfallibleVector<std::pair<ProgressCounter, size_t>, 0, UntrackedAllocPolicy> mTimeWarpTargetCheckpoints;
+
   // Note: NavigationState is initially zeroed.
   NavigationState()
     : mPhase(&mForwardPhase)
   {
     if (IsReplaying()) {
       // The recording must include everything up to the first
       // checkpoint. After that point we will ask the record/replay
       // system to notify us about any further endpoints.
@@ -411,16 +421,20 @@ public:
   void Resume(bool aForward) {
     mPhase->Resume(aForward);
   }
 
   void RestoreCheckpoint(size_t aCheckpoint) {
     mPhase->RestoreCheckpoint(aCheckpoint);
   }
 
+  void RunToPoint(const ExecutionPoint& aTarget) {
+    mPhase->RunToPoint(aTarget);
+  }
+
   void HandleDebuggerRequest(js::CharBuffer* aRequestBuffer) {
     mPhase->HandleDebuggerRequest(aRequestBuffer);
   }
 
   bool MaybeDivergeFromRecording() {
     return mPhase->MaybeDivergeFromRecording();
   }
 
@@ -496,17 +510,18 @@ GetAllBreakpointHits(const ExecutionPoin
 
 static bool
 ThisProcessCanRewind()
 {
   return HasSavedCheckpoint();
 }
 
 void
-BreakpointPausedPhase::Enter(const ExecutionPoint& aPoint, const BreakpointVector& aBreakpoints)
+BreakpointPausedPhase::Enter(const ExecutionPoint& aPoint, bool aRecordingEndpoint,
+                             const BreakpointVector& aBreakpoints)
 {
   MOZ_RELEASE_ASSERT(aPoint.HasPosition());
 
   mPoint = aPoint;
   mRequests.clear();
   mRecoveringFromDivergence = false;
   mRequestIndex = 0;
   mResumeForward = false;
@@ -536,18 +551,17 @@ BreakpointPausedPhase::Enter(const Execu
           });
         Unreachable();
       }
       gNavigation->PositionHit(aPoint);
       return;
     }
   }
 
-  bool endpoint = aBreakpoints.empty();
-  child::HitBreakpoint(endpoint, aBreakpoints.begin(), aBreakpoints.length());
+  child::HitBreakpoint(aRecordingEndpoint, aBreakpoints.begin(), aBreakpoints.length());
 
   // When rewinding is allowed we will rewind before resuming to erase side effects.
   MOZ_RELEASE_ASSERT(!ThisProcessCanRewind());
 }
 
 void
 BreakpointPausedPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
 {
@@ -592,17 +606,17 @@ BreakpointPausedPhase::Resume(bool aForw
   gNavigation->mFindLastHitPhase.Enter(start, Some(mPoint));
   Unreachable();
 }
 
 void
 BreakpointPausedPhase::RestoreCheckpoint(size_t aCheckpoint)
 {
   gNavigation->mCheckpointPausedPhase.Enter(aCheckpoint, /* aRewind = */ true,
-                                            /* aAtRecordingEndpoint = */ false);
+                                            /* aRecordingEndpoint = */ false);
 }
 
 void
 BreakpointPausedPhase::HandleDebuggerRequest(js::CharBuffer* aRequestBuffer)
 {
   MOZ_RELEASE_ASSERT(!mRecoveringFromDivergence);
 
   mRequests.emplaceBack();
@@ -731,17 +745,26 @@ CheckpointPausedPhase::Resume(bool aForw
     gNavigation->mFindLastHitPhase.Enter(start, Nothing());
     Unreachable();
   }
 }
 
 void
 CheckpointPausedPhase::RestoreCheckpoint(size_t aCheckpoint)
 {
-  Enter(aCheckpoint, /* aRewind = */ true, /* aAtRecordingEndpoint = */ false);
+  Enter(aCheckpoint, aCheckpoint != mCheckpoint, /* aRecordingEndpoint = */ false);
+}
+
+void
+CheckpointPausedPhase::RunToPoint(const ExecutionPoint& aTarget)
+{
+  MOZ_RELEASE_ASSERT(aTarget.mCheckpoint == mCheckpoint);
+  ResumeExecution();
+  gNavigation->mReachBreakpointPhase.Enter(CheckpointId(mCheckpoint), /* aRewind = */ false,
+                                           aTarget, /* aTemporaryCheckpoint = */ Nothing());
 }
 
 void
 CheckpointPausedPhase::HandleDebuggerRequest(js::CharBuffer* aRequestBuffer)
 {
   js::CharBuffer responseBuffer;
   js::ProcessRequest(aRequestBuffer->begin(), aRequestBuffer->length(), &responseBuffer);
 
@@ -778,64 +801,71 @@ ForwardPhase::Enter(const ExecutionPoint
 }
 
 void
 ForwardPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
 {
   MOZ_RELEASE_ASSERT(!aCheckpoint.mTemporary &&
                      aCheckpoint.mNormal == mPoint.mCheckpoint + 1);
   gNavigation->mCheckpointPausedPhase.Enter(aCheckpoint.mNormal, /* aRewind = */ false,
-                                            /* aAtRecordingEndpoint = */ false);
+                                            /* aRecordingEndpoint = */ false);
 }
 
 void
 ForwardPhase::PositionHit(const ExecutionPoint& aPoint)
 {
   BreakpointVector hitBreakpoints;
   GetAllBreakpointHits(aPoint, hitBreakpoints);
 
   if (!hitBreakpoints.empty()) {
-    gNavigation->mBreakpointPausedPhase.Enter(aPoint, hitBreakpoints);
+    gNavigation->mBreakpointPausedPhase.Enter(aPoint, /* aRecordingEndpoint = */ false,
+                                              hitBreakpoints);
   }
 }
 
 void
 ForwardPhase::HitRecordingEndpoint(const ExecutionPoint& aPoint)
 {
   if (aPoint.HasPosition()) {
     BreakpointVector emptyBreakpoints;
-    gNavigation->mBreakpointPausedPhase.Enter(aPoint, emptyBreakpoints);
+    gNavigation->mBreakpointPausedPhase.Enter(aPoint, /* aRecordingEndpoint = */ true,
+                                              emptyBreakpoints);
   } else {
     gNavigation->mCheckpointPausedPhase.Enter(aPoint.mCheckpoint, /* aRewind = */ false,
-                                              /* aAtRecordingEndpoint = */ true);
+                                              /* aRecordingEndpoint = */ true);
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReachBreakpointPhase
 ///////////////////////////////////////////////////////////////////////////////
 
 void
 ReachBreakpointPhase::Enter(const CheckpointId& aStart,
+                            bool aRewind,
                             const ExecutionPoint& aPoint,
                             const Maybe<ExecutionPoint>& aTemporaryCheckpoint)
 {
   MOZ_RELEASE_ASSERT(aPoint.HasPosition());
   MOZ_RELEASE_ASSERT(aTemporaryCheckpoint.isNothing() ||
                      (aTemporaryCheckpoint.ref().HasPosition() &&
                       aTemporaryCheckpoint.ref() != aPoint));
   mStart = aStart;
   mPoint = aPoint;
   mTemporaryCheckpoint = aTemporaryCheckpoint;
   mSavedTemporaryCheckpoint = false;
 
   gNavigation->SetPhase(this);
 
-  RestoreCheckpointAndResume(aStart);
-  Unreachable();
+  if (aRewind) {
+    RestoreCheckpointAndResume(aStart);
+    Unreachable();
+  } else {
+    AfterCheckpoint(aStart);
+  }
 }
 
 void
 ReachBreakpointPhase::AfterCheckpoint(const CheckpointId& aCheckpoint)
 {
   if (aCheckpoint == mStart && mTemporaryCheckpoint.isSome()) {
     js::EnsurePositionHandler(mTemporaryCheckpoint.ref().mPosition);
 
@@ -879,19 +909,19 @@ ReachBreakpointPhase::PositionHit(const 
         return;
       }
     }
   }
 
   if (mPoint == aPoint) {
     BreakpointVector hitBreakpoints;
     GetAllBreakpointHits(aPoint, hitBreakpoints);
-    MOZ_RELEASE_ASSERT(!hitBreakpoints.empty());
 
-    gNavigation->mBreakpointPausedPhase.Enter(aPoint, hitBreakpoints);
+    gNavigation->mBreakpointPausedPhase.Enter(aPoint, /* aRecordingEndpoint = */ false,
+                                              hitBreakpoints);
   }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // FindLastHitPhase
 ///////////////////////////////////////////////////////////////////////////////
 
 void
@@ -1010,17 +1040,17 @@ FindLastHitPhase::OnRegionEnd()
       CheckpointId start = mStart;
       start.mTemporary--;
       ExecutionPoint end = gNavigation->LastTemporaryCheckpointLocation();
       gNavigation->mFindLastHitPhase.Enter(start, Some(end));
       Unreachable();
     } else {
       // Rewind to the last normal checkpoint and pause.
       gNavigation->mCheckpointPausedPhase.Enter(mStart.mNormal, /* aRewind = */ true,
-                                                /* aAtRecordingEndpoint = */ false);
+                                                /* aRecordingEndpoint = */ false);
       Unreachable();
     }
   }
 
   // When running backwards, we don't want to place temporary checkpoints at
   // the breakpoint where we are going to stop at. If the user continues
   // rewinding then we will just have to discard the checkpoint and waste the
   // work we did in saving it.
@@ -1029,25 +1059,27 @@ FindLastHitPhase::OnRegionEnd()
   // breakpoint's script was entered. This optimizes for the case of stepping
   // around within a frame.
   Maybe<BreakpointPosition> baseEntry = GetEntryPosition(lastBreakpoint.ref().mPosition);
   if (baseEntry.isSome()) {
     const TrackedPosition& tracked = FindTrackedPosition(baseEntry.ref());
     if (tracked.mLastHit.HasPosition() &&
         tracked.mLastHitCount < lastBreakpoint.ref().mLastHitCount)
     {
-      gNavigation->mReachBreakpointPhase.Enter(mStart, lastBreakpoint.ref().mLastHit,
+      gNavigation->mReachBreakpointPhase.Enter(mStart, /* aRewind = */ true,
+                                               lastBreakpoint.ref().mLastHit,
                                                Some(tracked.mLastHit));
       Unreachable();
     }
   }
 
   // There was no suitable place for a temporary checkpoint, so rewind to the
   // last checkpoint and play forward to the last breakpoint hit we found.
-  gNavigation->mReachBreakpointPhase.Enter(mStart, lastBreakpoint.ref().mLastHit, Nothing());
+  gNavigation->mReachBreakpointPhase.Enter(mStart, /* aRewind = */ true,
+                                           lastBreakpoint.ref().mLastHit, Nothing());
   Unreachable();
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Hooks
 ///////////////////////////////////////////////////////////////////////////////
 
 bool
@@ -1107,16 +1139,22 @@ Resume(bool aForward)
 }
 
 void
 RestoreCheckpoint(size_t aId)
 {
   gNavigation->RestoreCheckpoint(aId);
 }
 
+void
+RunToPoint(const ExecutionPoint& aTarget)
+{
+  gNavigation->RunToPoint(aTarget);
+}
+
 ExecutionPoint
 GetRecordingEndpoint()
 {
   MOZ_RELEASE_ASSERT(IsRecording());
   return gNavigation->GetRecordingEndpoint();
 }
 
 void
@@ -1133,28 +1171,78 @@ extern "C" {
 MOZ_EXPORT ProgressCounter*
 RecordReplayInterface_ExecutionProgressCounter()
 {
   return &gProgressCounter;
 }
 
 } // extern "C"
 
-static ExecutionPoint
-NewExecutionPoint(const BreakpointPosition& aPosition)
+ExecutionPoint
+CurrentExecutionPoint(const BreakpointPosition& aPosition)
 {
   return ExecutionPoint(gNavigation->LastCheckpoint().mNormal,
                         gProgressCounter, aPosition);
 }
 
 void
 PositionHit(const BreakpointPosition& position)
 {
   AutoDisallowThreadEvents disallow;
-  gNavigation->PositionHit(NewExecutionPoint(position));
+  gNavigation->PositionHit(CurrentExecutionPoint(position));
+}
+
+extern "C" {
+
+MOZ_EXPORT ProgressCounter
+RecordReplayInterface_NewTimeWarpTarget()
+{
+  // NewTimeWarpTarget() must be called at consistent points between recording
+  // and replaying.
+  recordreplay::RecordReplayAssert("NewTimeWarpTarget");
+
+  if (!gNavigation) {
+    return 0;
+  }
+
+  // Advance the progress counter for each time warp target. This can be called
+  // at any place and any number of times where recorded events are allowed.
+  ProgressCounter progress = ++gProgressCounter;
+
+  PositionHit(BreakpointPosition(BreakpointPosition::WarpTarget));
+
+  // Remember the checkpoint associated with each time warp target we have
+  // generated, so we can convert them to ExecutionPoints later. Ignore warp
+  // targets we have already encountered.
+  if (gNavigation->mTimeWarpTargetCheckpoints.empty() ||
+      progress > gNavigation->mTimeWarpTargetCheckpoints.back().first)
+  {
+    size_t checkpoint = gNavigation->LastCheckpoint().mNormal;
+    gNavigation->mTimeWarpTargetCheckpoints.emplaceBack(progress, checkpoint);
+  }
+
+  return progress;
+}
+
+} // extern "C"
+
+ExecutionPoint
+TimeWarpTargetExecutionPoint(ProgressCounter aTarget)
+{
+  Maybe<size_t> checkpoint;
+  for (auto entry : gNavigation->mTimeWarpTargetCheckpoints) {
+    if (entry.first == aTarget) {
+      checkpoint.emplace(entry.second);
+      break;
+    }
+  }
+  MOZ_RELEASE_ASSERT(checkpoint.isSome());
+
+  return ExecutionPoint(checkpoint.ref(), aTarget,
+                        BreakpointPosition(BreakpointPosition::WarpTarget));
 }
 
 bool
 MaybeDivergeFromRecording()
 {
   return gNavigation->MaybeDivergeFromRecording();
 }
 
--- a/toolkit/recordreplay/ipc/ChildProcess.cpp
+++ b/toolkit/recordreplay/ipc/ChildProcess.cpp
@@ -107,48 +107,78 @@ ChildProcessInfo::IsPausedAtRecordingEnd
     return static_cast<HitCheckpointMessage*>(mPausedMessage)->mRecordingEndpoint;
   }
   if (mPausedMessage->mType == MessageType::HitBreakpoint) {
     return static_cast<HitBreakpointMessage*>(mPausedMessage)->mRecordingEndpoint;
   }
   return false;
 }
 
+void
+ChildProcessInfo::GetInstalledBreakpoints(Vector<SetBreakpointMessage*>& aBreakpoints)
+{
+  for (Message* msg : mMessages) {
+    if (msg->mType == MessageType::SetBreakpoint) {
+      SetBreakpointMessage* nmsg = static_cast<SetBreakpointMessage*>(msg);
+      for (SetBreakpointMessage*& existing : aBreakpoints) {
+        if (existing->mId == nmsg->mId) {
+          aBreakpoints.erase(&existing);
+          break;
+        }
+      }
+      if (nmsg->mPosition.IsValid()) {
+        if (!aBreakpoints.append(nmsg)) {
+          MOZ_CRASH("OOM");
+        }
+      }
+    }
+  }
+}
+
 bool
 ChildProcessInfo::IsPausedAtMatchingBreakpoint(const BreakpointFilter& aFilter)
 {
   if (!IsPaused() || mPausedMessage->mType != MessageType::HitBreakpoint) {
     return false;
   }
 
+  Vector<SetBreakpointMessage*> installed;
+  GetInstalledBreakpoints(installed);
+
   HitBreakpointMessage* npaused = static_cast<HitBreakpointMessage*>(mPausedMessage);
   for (size_t i = 0; i < npaused->NumBreakpoints(); i++) {
     uint32_t breakpointId = npaused->Breakpoints()[i];
 
-    // Find the last time we sent a SetBreakpoint message to this process for
-    // this breakpoint ID.
-    SetBreakpointMessage* lastSet = nullptr;
-    for (Message* msg : mMessages) {
-      if (msg->mType == MessageType::SetBreakpoint) {
-        SetBreakpointMessage* nmsg = static_cast<SetBreakpointMessage*>(msg);
-        if (nmsg->mId == breakpointId) {
-          lastSet = nmsg;
-        }
+    // Note: this test isn't quite right if new breakpoints have been installed
+    // since the child paused, though this does not affect current callers.
+    for (SetBreakpointMessage* msg : installed) {
+      if (msg->mId == breakpointId && aFilter(msg->mPosition.mKind)) {
+        return true;
       }
     }
-    MOZ_RELEASE_ASSERT(lastSet && lastSet->mPosition.IsValid());
-    if (aFilter(lastSet->mPosition.mKind)) {
-      return true;
-    }
   }
 
   return false;
 }
 
 void
+ChildProcessInfo::GetMatchingInstalledBreakpoints(const BreakpointFilter& aFilter,
+                                                  Vector<uint32_t>& aBreakpointIds)
+{
+  Vector<SetBreakpointMessage*> installed;
+  GetInstalledBreakpoints(installed);
+
+  for (SetBreakpointMessage* msg : installed) {
+    if (aFilter(msg->mPosition.mKind) && !aBreakpointIds.append(msg->mId)) {
+      MOZ_CRASH("OOM");
+    }
+  }
+}
+
+void
 ChildProcessInfo::AddMajorCheckpoint(size_t aId)
 {
   // Major checkpoints should be listed in order.
   MOZ_RELEASE_ASSERT(mMajorCheckpoints.empty() || aId > mMajorCheckpoints.back());
   mMajorCheckpoints.append(aId);
 }
 
 void
@@ -248,31 +278,33 @@ ChildProcessInfo::SendMessage(const Mess
 
   // Update paused state.
   MOZ_RELEASE_ASSERT(IsPaused() ||
                      aMsg.mType == MessageType::CreateCheckpoint ||
                      aMsg.mType == MessageType::Terminate);
   switch (aMsg.mType) {
   case MessageType::Resume:
   case MessageType::RestoreCheckpoint:
+  case MessageType::RunToPoint:
     free(mPausedMessage);
     mPausedMessage = nullptr;
     MOZ_FALLTHROUGH;
   case MessageType::DebuggerRequest:
   case MessageType::FlushRecording:
     mPaused = false;
     break;
   default:
     break;
   }
 
   // Keep track of messages which affect the child's behavior.
   switch (aMsg.mType) {
   case MessageType::Resume:
   case MessageType::RestoreCheckpoint:
+  case MessageType::RunToPoint:
   case MessageType::DebuggerRequest:
   case MessageType::SetBreakpoint:
     mMessages.emplaceBack(aMsg.Clone());
     break;
   default:
     break;
   }
 
--- a/toolkit/recordreplay/ipc/JSControl.cpp
+++ b/toolkit/recordreplay/ipc/JSControl.cpp
@@ -32,16 +32,46 @@ NonNullObject(JSContext* aCx, HandleValu
 {
   if (!aValue.isObject()) {
     JS_ReportErrorASCII(aCx, "Expected object");
     return nullptr;
   }
   return &aValue.toObject();
 }
 
+template <typename T>
+static bool
+MaybeGetNumberProperty(JSContext* aCx, HandleObject aObject, const char* aProperty, T* aResult)
+{
+  RootedValue v(aCx);
+  if (!JS_GetProperty(aCx, aObject, aProperty, &v)) {
+    return false;
+  }
+  if (v.isNumber()) {
+    *aResult = v.toNumber();
+  }
+  return true;
+}
+
+template <typename T>
+static bool
+GetNumberProperty(JSContext* aCx, HandleObject aObject, const char* aProperty, T* aResult)
+{
+  RootedValue v(aCx);
+  if (!JS_GetProperty(aCx, aObject, aProperty, &v)) {
+    return false;
+  }
+  if (!v.isNumber()) {
+    JS_ReportErrorASCII(aCx, "Object missing required property");
+    return false;
+  }
+  *aResult = v.toNumber();
+  return true;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // BreakpointPosition Conversion
 ///////////////////////////////////////////////////////////////////////////////
 
 // Names of properties which JS code uses to specify the contents of a BreakpointPosition.
 static const char gKindProperty[] = "kind";
 static const char gScriptProperty[] = "script";
 static const char gOffsetProperty[] = "offset";
@@ -61,29 +91,16 @@ BreakpointPosition::Encode(JSContext* aC
       (mFrameIndex != BreakpointPosition::EMPTY_FRAME_INDEX &&
        !JS_DefineProperty(aCx, obj, gFrameIndexProperty, mFrameIndex, JSPROP_ENUMERATE)))
   {
     return nullptr;
   }
   return obj;
 }
 
-static bool
-MaybeGetNumberProperty(JSContext* aCx, HandleObject aObject, const char* aProperty, uint32_t* aResult)
-{
-  RootedValue v(aCx);
-  if (!JS_GetProperty(aCx, aObject, aProperty, &v)) {
-    return false;
-  }
-  if (v.isNumber()) {
-    *aResult = (size_t) v.toNumber();
-  }
-  return true;
-}
-
 bool
 BreakpointPosition::Decode(JSContext* aCx, HandleObject aObject)
 {
   RootedValue v(aCx);
   if (!JS_GetProperty(aCx, aObject, gKindProperty, &v)) {
     return false;
   }
 
@@ -109,16 +126,57 @@ BreakpointPosition::Decode(JSContext* aC
   {
     return false;
   }
 
   return true;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
+// ExecutionPoint Conversion
+///////////////////////////////////////////////////////////////////////////////
+
+// Names of properties which JS code uses to specify the contents of an ExecutionPoint.
+static const char gCheckpointProperty[] = "checkpoint";
+static const char gProgressProperty[] = "progress";
+static const char gPositionProperty[] = "position";
+
+JSObject*
+ExecutionPoint::Encode(JSContext* aCx) const
+{
+  RootedObject obj(aCx, JS_NewObject(aCx, nullptr));
+  RootedObject position(aCx, mPosition.Encode(aCx));
+  if (!obj || !position ||
+      !JS_DefineProperty(aCx, obj, gCheckpointProperty,
+                         (double) mCheckpoint, JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(aCx, obj, gProgressProperty,
+                         (double) mProgress, JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(aCx, obj, gPositionProperty, position, JSPROP_ENUMERATE))
+  {
+    return nullptr;
+  }
+  return obj;
+}
+
+bool
+ExecutionPoint::Decode(JSContext* aCx, HandleObject aObject)
+{
+  RootedValue v(aCx);
+  if (!JS_GetProperty(aCx, aObject, gPositionProperty, &v)) {
+    return false;
+  }
+
+  RootedObject positionObject(aCx, NonNullObject(aCx, v));
+  return positionObject
+      && mPosition.Decode(aCx, positionObject)
+      && GetNumberProperty(aCx, aObject, gCheckpointProperty, &mCheckpoint)
+      && GetNumberProperty(aCx, aObject, gProgressProperty, &mProgress);
+}
+
+///////////////////////////////////////////////////////////////////////////////
 // Middleman Methods
 ///////////////////////////////////////////////////////////////////////////////
 
 // Keep track of all replay debuggers in existence, so that they can all be
 // invalidated when the process is unpaused.
 static StaticInfallibleVector<PersistentRootedObject*> gReplayDebuggers;
 
 static bool
@@ -173,16 +231,40 @@ Middleman_Resume(JSContext* aCx, unsigne
 
   parent::Resume(forward);
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool
+Middleman_TimeWarp(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  RootedObject targetObject(aCx, NonNullObject(aCx, args.get(0)));
+  if (!targetObject) {
+    return false;
+  }
+
+  ExecutionPoint target;
+  if (!target.Decode(aCx, targetObject)) {
+    return false;
+  }
+
+  if (!InvalidateReplayDebuggersAfterUnpause(aCx)) {
+    return false;
+  }
+
+  parent::TimeWarp(target);
+
+  args.rval().setUndefined();
+  return true;
+}
+
+static bool
 Middleman_Pause(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   CallArgs args = CallArgsFromVp(aArgc, aVp);
 
   parent::Pause();
 
   args.rval().setUndefined();
   return true;
@@ -638,16 +720,59 @@ RecordReplay_GetContent(JSContext* aCx, 
     return false;
   }
 
   args.rval().setObject(*obj);
   return true;
 }
 
 static bool
+RecordReplay_CurrentExecutionPoint(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  RootedObject obj(aCx, NonNullObject(aCx, args.get(0)));
+  if (!obj) {
+    return false;
+  }
+
+  BreakpointPosition position;
+  if (!position.Decode(aCx, obj)) {
+    return false;
+  }
+
+  ExecutionPoint point = navigation::CurrentExecutionPoint(position);
+  RootedObject result(aCx, point.Encode(aCx));
+  if (!result) {
+    return false;
+  }
+
+  args.rval().setObject(*result);
+  return true;
+}
+
+static bool
+RecordReplay_TimeWarpTargetExecutionPoint(JSContext* aCx, unsigned aArgc, Value* aVp)
+{
+  CallArgs args = CallArgsFromVp(aArgc, aVp);
+  double timeWarpTarget;
+  if (!ToNumber(aCx, args.get(0), &timeWarpTarget)) {
+    return false;
+  }
+
+  ExecutionPoint point = navigation::TimeWarpTargetExecutionPoint((ProgressCounter) timeWarpTarget);
+  RootedObject result(aCx, point.Encode(aCx));
+  if (!result) {
+    return false;
+  }
+
+  args.rval().setObject(*result);
+  return true;
+}
+
+static bool
 RecordReplay_Dump(JSContext* aCx, unsigned aArgc, Value* aVp)
 {
   // This method is an alternative to dump() that can be used in places where
   // thread events are disallowed.
   CallArgs args = CallArgsFromVp(aArgc, aVp);
   for (size_t i = 0; i < args.length(); i++) {
     RootedString str(aCx, ToString(aCx, args[i]));
     if (!str) {
@@ -668,29 +793,32 @@ RecordReplay_Dump(JSContext* aCx, unsign
 ///////////////////////////////////////////////////////////////////////////////
 // Plumbing
 ///////////////////////////////////////////////////////////////////////////////
 
 static const JSFunctionSpec gMiddlemanMethods[] = {
   JS_FN("registerReplayDebugger", Middleman_RegisterReplayDebugger, 1, 0),
   JS_FN("canRewind", Middleman_CanRewind, 0, 0),
   JS_FN("resume", Middleman_Resume, 1, 0),
+  JS_FN("timeWarp", Middleman_TimeWarp, 1, 0),
   JS_FN("pause", Middleman_Pause, 0, 0),
   JS_FN("sendRequest", Middleman_SendRequest, 1, 0),
   JS_FN("setBreakpoint", Middleman_SetBreakpoint, 2, 0),
   JS_FN("clearBreakpoint", Middleman_ClearBreakpoint, 1, 0),
   JS_FS_END
 };
 
 static const JSFunctionSpec gRecordReplayMethods[] = {
   JS_FN("areThreadEventsDisallowed", RecordReplay_AreThreadEventsDisallowed, 0, 0),
   JS_FN("maybeDivergeFromRecording", RecordReplay_MaybeDivergeFromRecording, 0, 0),
   JS_FN("advanceProgressCounter", RecordReplay_AdvanceProgressCounter, 0, 0),
   JS_FN("positionHit", RecordReplay_PositionHit, 1, 0),
   JS_FN("getContent", RecordReplay_GetContent, 1, 0),
+  JS_FN("currentExecutionPoint", RecordReplay_CurrentExecutionPoint, 1, 0),
+  JS_FN("timeWarpTargetExecutionPoint", RecordReplay_TimeWarpTargetExecutionPoint, 1, 0),
   JS_FN("dump", RecordReplay_Dump, 1, 0),
   JS_FS_END
 };
 
 extern "C" {
 
 MOZ_EXPORT bool
 RecordReplayInterface_DefineRecordReplayControlObject(JSContext* aCx, JSObject* aObjectArg)
--- a/toolkit/recordreplay/ipc/JSControl.h
+++ b/toolkit/recordreplay/ipc/JSControl.h
@@ -50,17 +50,28 @@ struct BreakpointPosition
     // Break either when any frame is popped, or when a specific frame is
     // popped. Requires script/frameIndex in the latter case.
     OnPop,
 
     // Break when entering any frame.
     EnterFrame,
 
     // Break when a new top-level script is created.
-    NewScript
+    NewScript,
+
+    // Break when a message is logged to the web console.
+    ConsoleMessage,
+
+    // Break when NewTimeWarpTarget() is called.
+    WarpTarget,
+
+    // Break when the debugger should pause even if no breakpoint has been
+    // set: the beginning or end of the replay has been reached, or a time
+    // warp has reached its destination.
+    ForcedPause
   ));
 
   Kind mKind;
 
   // Optional information associated with the breakpoint.
   uint32_t mScript;
   uint32_t mOffset;
   uint32_t mFrameIndex;
@@ -101,16 +112,19 @@ struct BreakpointPosition
   static const char* StaticKindString(Kind aKind) {
     switch (aKind) {
     case Invalid: return "Invalid";
     case Break: return "Break";
     case OnStep: return "OnStep";
     case OnPop: return "OnPop";
     case EnterFrame: return "EnterFrame";
     case NewScript: return "NewScript";
+    case ConsoleMessage: return "ConsoleMessage";
+    case WarpTarget: return "WarpTarget";
+    case ForcedPause: return "ForcedPause";
     }
     MOZ_CRASH("Bad BreakpointPosition kind");
   }
 
   const char* KindString() const {
     return StaticKindString(mKind);
   }
 
@@ -163,16 +177,19 @@ struct ExecutionPoint
 
   inline bool operator==(const ExecutionPoint& o) const {
     return mCheckpoint == o.mCheckpoint
         && mProgress == o.mProgress
         && mPosition == o.mPosition;
   }
 
   inline bool operator!=(const ExecutionPoint& o) const { return !(*this == o); }
+
+  JSObject* Encode(JSContext* aCx) const;
+  bool Decode(JSContext* aCx, JS::HandleObject aObject);
 };
 
 // Buffer type used for encoding object data.
 typedef InfallibleVector<char16_t> CharBuffer;
 
 // Called in the middleman when a breakpoint with the specified id has been hit.
 bool HitBreakpoint(JSContext* aCx, size_t id);
 
--- a/toolkit/recordreplay/ipc/ParentIPC.cpp
+++ b/toolkit/recordreplay/ipc/ParentIPC.cpp
@@ -344,22 +344,23 @@ static ChildProcessInfo*
 ReplayingChildResponsibleForSavingCheckpoint(size_t aId)
 {
   MOZ_RELEASE_ASSERT(CanRewind() && gFirstReplayingChild && gSecondReplayingChild);
   size_t firstMajor = LastMajorCheckpointPreceding(gFirstReplayingChild, aId);
   size_t secondMajor = LastMajorCheckpointPreceding(gSecondReplayingChild, aId);
   return (firstMajor < secondMajor) ? gSecondReplayingChild : gFirstReplayingChild;
 }
 
-// Return whether the active child is explicitly paused somewhere, or has
-// started rewinding after being explicitly paused. Standby roles must save all
-// intermediate checkpoints they are responsible for, in the range from their
-// most recent major checkpoint up to the checkpoint where the active child can
-// rewind to.
-static bool ActiveChildIsPausedOrRewinding();
+// Returns a checkpoint if the active child is explicitly paused somewhere,
+// has started rewinding after being explicitly paused, or is attempting to
+// warp to an execution point. The checkpoint returned is the latest one which
+// should be saved, and standby roles must save all intermediate checkpoints
+// they are responsible for, in the range from their most recent major
+// checkpoint up to the returned checkpoint.
+static Maybe<size_t> ActiveChildTargetCheckpoint();
 
 // Notify a child it does not need to save aCheckpoint, unless it is a major
 // checkpoint for the child.
 static void
 ClearIfSavedNonMajorCheckpoint(ChildProcessInfo* aChild, size_t aCheckpoint)
 {
   if (aChild->ShouldSaveCheckpoint(aCheckpoint) &&
       !aChild->IsMajorCheckpoint(aCheckpoint) &&
@@ -378,50 +379,51 @@ ChildRoleStandby::Poke()
   if (mProcess->PauseNeeded()) {
     return;
   }
 
   // Check if we need to save a range of intermediate checkpoints.
   do {
     // Intermediate checkpoints are only saved when the active child is paused
     // or rewinding.
-    if (!ActiveChildIsPausedOrRewinding()) {
+    Maybe<size_t> targetCheckpoint = ActiveChildTargetCheckpoint();
+    if (targetCheckpoint.isNothing()) {
       break;
     }
 
     // The startpoint of the range is the most recent major checkpoint prior to
-    // the active child's position.
-    size_t targetCheckpoint = gActiveChild->RewindTargetCheckpoint();
-    size_t lastMajorCheckpoint = LastMajorCheckpointPreceding(mProcess, targetCheckpoint);
+    // the target.
+    size_t lastMajorCheckpoint = LastMajorCheckpointPreceding(mProcess, targetCheckpoint.ref());
 
-    // If there is no major checkpoint prior to the active child's position,
-    // just idle.
+    // If there is no major checkpoint prior to the target, just idle.
     if (lastMajorCheckpoint == CheckpointId::Invalid) {
       return;
     }
 
     // If we haven't reached the last major checkpoint, we need to run forward
     // without saving intermediate checkpoints.
     if (mProcess->LastCheckpoint() < lastMajorCheckpoint) {
-      break;
+      ClearIfSavedNonMajorCheckpoint(mProcess, mProcess->LastCheckpoint() + 1);
+      mProcess->SendMessage(ResumeMessage(/* aForward = */ true));
+      return;
     }
 
     // The endpoint of the range is the checkpoint prior to either the active
     // child's current position, or the other replaying child's most recent
     // major checkpoint.
     size_t otherMajorCheckpoint =
-      LastMajorCheckpointPreceding(OtherReplayingChild(mProcess), targetCheckpoint);
+      LastMajorCheckpointPreceding(OtherReplayingChild(mProcess), targetCheckpoint.ref());
     if (otherMajorCheckpoint > lastMajorCheckpoint) {
-      MOZ_RELEASE_ASSERT(otherMajorCheckpoint <= targetCheckpoint);
-      targetCheckpoint = otherMajorCheckpoint - 1;
+      MOZ_RELEASE_ASSERT(otherMajorCheckpoint <= targetCheckpoint.ref());
+      targetCheckpoint.ref() = otherMajorCheckpoint - 1;
     }
 
     // Find the first checkpoint in the fill range which we have not saved.
     Maybe<size_t> missingCheckpoint;
-    for (size_t i = lastMajorCheckpoint; i <= targetCheckpoint; i++) {
+    for (size_t i = lastMajorCheckpoint; i <= targetCheckpoint.ref(); i++) {
       if (!mProcess->HasSavedCheckpoint(i)) {
         missingCheckpoint.emplace(i);
         break;
       }
     }
 
     // If we have already saved everything we need to, we can idle.
     if (!missingCheckpoint.isSome()) {
@@ -568,23 +570,31 @@ SpawnReplayingChildren()
     new ChildProcessInfo(std::move(firstRole), Nothing());
   gSecondReplayingChild =
     new ChildProcessInfo(MakeUnique<ChildRoleStandby>(), Nothing());
   AssignMajorCheckpoint(gSecondReplayingChild, CheckpointId::First);
 }
 
 // Change the current active child, and select a new role for the old one.
 static void
-SwitchActiveChild(ChildProcessInfo* aChild)
+SwitchActiveChild(ChildProcessInfo* aChild, bool aRecoverPosition = true)
 {
   MOZ_RELEASE_ASSERT(aChild != gActiveChild);
   ChildProcessInfo* oldActiveChild = gActiveChild;
   aChild->WaitUntilPaused();
   if (!aChild->IsRecording()) {
-    aChild->Recover(gActiveChild);
+    if (aRecoverPosition) {
+      aChild->Recover(gActiveChild);
+    } else {
+      Vector<SetBreakpointMessage*> breakpoints;
+      gActiveChild->GetInstalledBreakpoints(breakpoints);
+      for (SetBreakpointMessage* msg : breakpoints) {
+        aChild->SendMessage(*msg);
+      }
+    }
   }
   aChild->SetRole(MakeUnique<ChildRoleActive>());
   if (oldActiveChild->IsRecording()) {
     oldActiveChild->SetRole(MakeUnique<ChildRoleInert>());
   } else {
     oldActiveChild->RecoverToCheckpoint(oldActiveChild->MostRecentSavedCheckpoint());
     oldActiveChild->SetRole(MakeUnique<ChildRoleStandby>());
   }
@@ -740,16 +750,19 @@ SaveRecording(const ipc::FileDescriptor&
 ///////////////////////////////////////////////////////////////////////////////
 // Explicit Pauses
 ///////////////////////////////////////////////////////////////////////////////
 
 // At the last time the active child was explicitly paused, the ID of the
 // checkpoint that needs to be saved for the child to rewind.
 static size_t gLastExplicitPause;
 
+// Any checkpoint we are trying to warp to and pause.
+static Maybe<size_t> gTimeWarpTarget;
+
 static bool
 HasSavedCheckpointsInRange(ChildProcessInfo* aChild, size_t aStart, size_t aEnd)
 {
   for (size_t i = aStart; i <= aEnd; i++) {
     if (!aChild->HasSavedCheckpoint(i)) {
       return false;
     }
   }
@@ -759,17 +772,18 @@ HasSavedCheckpointsInRange(ChildProcessI
 // Return whether a child is paused at a breakpoint set by the user or by
 // stepping around, at which point the debugger will send requests to the
 // child to inspect its state. This excludes breakpoints set for things
 // internal to the debugger.
 static bool
 IsUserBreakpoint(js::BreakpointPosition::Kind aKind)
 {
   MOZ_RELEASE_ASSERT(aKind != js::BreakpointPosition::Invalid);
-  return aKind != js::BreakpointPosition::NewScript;
+  return aKind != js::BreakpointPosition::NewScript
+      && aKind != js::BreakpointPosition::ConsoleMessage;
 }
 
 static void
 MarkActiveChildExplicitPause()
 {
   MOZ_RELEASE_ASSERT(gActiveChild->IsPaused());
   size_t targetCheckpoint = gActiveChild->RewindTargetCheckpoint();
 
@@ -805,20 +819,26 @@ MarkActiveChildExplicitPause()
   }
 
   gLastExplicitPause = targetCheckpoint;
   PrintSpew("MarkActiveChildExplicitPause %d\n", (int) gLastExplicitPause);
 
   PokeChildren();
 }
 
-static bool
-ActiveChildIsPausedOrRewinding()
+static Maybe<size_t>
+ActiveChildTargetCheckpoint()
 {
-  return gActiveChild->RewindTargetCheckpoint() <= gLastExplicitPause;
+  if (gTimeWarpTarget.isSome()) {
+    return gTimeWarpTarget;
+  }
+  if (gActiveChild->RewindTargetCheckpoint() <= gLastExplicitPause) {
+    return Some(gActiveChild->RewindTargetCheckpoint());
+  }
+  return Nothing();
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Initialization
 ///////////////////////////////////////////////////////////////////////////////
 
 // Message loop processed on the main thread.
 static MessageLoop* gMainThreadMessageLoop;
@@ -916,35 +936,38 @@ SetBreakpoint(size_t aId, const js::Brea
 // according to the last direction we were explicitly given.
 static bool gChildExecuteForward = true;
 static bool gChildExecuteBackward = false;
 
 // Whether there is a ResumeForwardOrBackward task which should execute on the
 // main thread. This will continue execution in the preferred direction.
 static bool gResumeForwardOrBackward = false;
 
+// Hit any breakpoints installed for forced pauses.
+static void HitForcedPauseBreakpoints(bool aRecordingBoundary);
+
 void
 Resume(bool aForward)
 {
   gActiveChild->WaitUntilPaused();
 
   // Set the preferred direction of travel.
   gResumeForwardOrBackward = false;
   gChildExecuteForward = aForward;
   gChildExecuteBackward = !aForward;
 
   // When rewinding, make sure the active child can rewind to the previous
   // checkpoint.
   if (!aForward && !gActiveChild->HasSavedCheckpoint(gActiveChild->RewindTargetCheckpoint())) {
-    MOZ_RELEASE_ASSERT(ActiveChildIsPausedOrRewinding());
     size_t targetCheckpoint = gActiveChild->RewindTargetCheckpoint();
 
     // Don't rewind if we are at the beginning of the recording.
     if (targetCheckpoint == CheckpointId::Invalid) {
       SendMessageToUIProcess("HitRecordingBeginning");
+      HitForcedPauseBreakpoints(true);
       return;
     }
 
     // Find the replaying child responsible for saving the target checkpoint.
     // We should have explicitly paused before rewinding and given fill roles
     // to the replaying children.
     ChildProcessInfo* targetChild = ReplayingChildResponsibleForSavingCheckpoint(targetCheckpoint);
     MOZ_RELEASE_ASSERT(targetChild != gActiveChild);
@@ -960,18 +983,18 @@ Resume(bool aForward)
   }
 
   if (aForward) {
     // Don't send a replaying process past the recording endpoint.
     if (gActiveChild->IsPausedAtRecordingEndpoint()) {
       // Look for a recording child we can transition into.
       MOZ_RELEASE_ASSERT(!gActiveChild->IsRecording());
       if (!gRecordingChild) {
-        MarkActiveChildExplicitPause();
         SendMessageToUIProcess("HitRecordingEndpoint");
+        HitForcedPauseBreakpoints(true);
         return;
       }
 
       // Switch to the recording child as the active child and continue execution.
       SwitchActiveChild(gRecordingChild);
     }
 
     ClearIfSavedNonMajorCheckpoint(gActiveChild, gActiveChild->LastCheckpoint() + 1);
@@ -979,16 +1002,67 @@ Resume(bool aForward)
     // Idle children might change their behavior as we run forward.
     PokeChildren();
   }
 
   gActiveChild->SendMessage(ResumeMessage(aForward));
 }
 
 void
+TimeWarp(const js::ExecutionPoint& aTarget)
+{
+  gActiveChild->WaitUntilPaused();
+
+  // There is no preferred direction of travel after warping.
+  gResumeForwardOrBackward = false;
+  gChildExecuteForward = false;
+  gChildExecuteBackward = false;
+
+  // Make sure the active child can rewind to the checkpoint prior to the
+  // warp target.
+  MOZ_RELEASE_ASSERT(gTimeWarpTarget.isNothing());
+  gTimeWarpTarget.emplace(aTarget.mCheckpoint);
+
+  PokeChildren();
+
+  if (!gActiveChild->HasSavedCheckpoint(aTarget.mCheckpoint)) {
+    // Find the replaying child responsible for saving the target checkpoint.
+    ChildProcessInfo* targetChild = ReplayingChildResponsibleForSavingCheckpoint(aTarget.mCheckpoint);
+
+    if (targetChild == gActiveChild) {
+      // Switch to the other replaying child while this one saves the necessary
+      // checkpoint.
+      SwitchActiveChild(OtherReplayingChild(gActiveChild));
+    }
+
+    // This process will be the new active child, so make sure it has saved the
+    // checkpoint we need it to.
+    targetChild->WaitUntil([=]() {
+        return targetChild->HasSavedCheckpoint(aTarget.mCheckpoint)
+            && targetChild->IsPaused();
+      });
+
+    SwitchActiveChild(targetChild, /* aRecoverPosition = */ false);
+  }
+
+  gTimeWarpTarget.reset();
+
+  if (!gActiveChild->IsPausedAtCheckpoint() || gActiveChild->LastCheckpoint() != aTarget.mCheckpoint) {
+    gActiveChild->SendMessage(RestoreCheckpointMessage(aTarget.mCheckpoint));
+    gActiveChild->WaitUntilPaused();
+  }
+
+  gActiveChild->SendMessage(RunToPointMessage(aTarget));
+
+  gActiveChild->WaitUntilPaused();
+  SendMessageToUIProcess("TimeWarpFinished");
+  HitForcedPauseBreakpoints(false);
+}
+
+void
 Pause()
 {
   MaybeCreateCheckpointInRecordingChild();
   gActiveChild->WaitUntilPaused();
 
   // If the debugger has explicitly paused then there is no preferred direction
   // of travel.
   gChildExecuteForward = false;
@@ -1021,45 +1095,70 @@ RecvHitCheckpoint(const HitCheckpointMes
   } else if (!gResumeForwardOrBackward) {
     gResumeForwardOrBackward = true;
     gMainThreadMessageLoop->PostTask(NewRunnableFunction("ResumeForwardOrBackward",
                                                          ResumeForwardOrBackward));
   }
 }
 
 static void
-HitBreakpoint(uint32_t* aBreakpoints, size_t aNumBreakpoints)
+HitBreakpoint(uint32_t* aBreakpoints, size_t aNumBreakpoints, bool aRecordingBoundary)
 {
+  if (!gActiveChild->IsPaused()) {
+    if (aNumBreakpoints) {
+      Print("Warning: Process resumed before breakpoints were hit.\n");
+    }
+    delete[] aBreakpoints;
+    return;
+  }
+
   MarkActiveChildExplicitPause();
 
-  MOZ_RELEASE_ASSERT(!gResumeForwardOrBackward);
   gResumeForwardOrBackward = true;
 
   // Call breakpoint handlers until one of them explicitly resumes forward or
   // backward travel.
   for (size_t i = 0; i < aNumBreakpoints && gResumeForwardOrBackward; i++) {
     AutoSafeJSContext cx;
     if (!js::HitBreakpoint(cx, aBreakpoints[i])) {
       Print("Warning: hitBreakpoint hook threw an exception.\n");
     }
   }
 
-  // If the child was not explicitly resumed by any breakpoint handler, resume
-  // travel in whichever direction it was going previously.
-  if (gResumeForwardOrBackward) {
+  // If the child was not explicitly resumed by any breakpoint handler, and we
+  // are not at a forced pause at the recording boundary, resume travel in
+  // whichever direction it was going previously.
+  if (gResumeForwardOrBackward && !aRecordingBoundary) {
     ResumeForwardOrBackward();
   }
 
   delete[] aBreakpoints;
 }
 
 static void
 RecvHitBreakpoint(const HitBreakpointMessage& aMsg)
 {
   uint32_t* breakpoints = new uint32_t[aMsg.NumBreakpoints()];
   PodCopy(breakpoints, aMsg.Breakpoints(), aMsg.NumBreakpoints());
   gMainThreadMessageLoop->PostTask(NewRunnableFunction("HitBreakpoint", HitBreakpoint,
-                                                       breakpoints, aMsg.NumBreakpoints()));
+                                                       breakpoints, aMsg.NumBreakpoints(),
+                                                       /* aRecordingBoundary = */ false));
+}
+
+static void
+HitForcedPauseBreakpoints(bool aRecordingBoundary)
+{
+  Vector<uint32_t> breakpoints;
+  gActiveChild->GetMatchingInstalledBreakpoints([=](js::BreakpointPosition::Kind aKind) {
+      return aKind == js::BreakpointPosition::ForcedPause;
+    }, breakpoints);
+  if (!breakpoints.empty()) {
+    uint32_t* newBreakpoints = new uint32_t[breakpoints.length()];
+    PodCopy(newBreakpoints, breakpoints.begin(), breakpoints.length());
+    gMainThreadMessageLoop->PostTask(NewRunnableFunction("HitBreakpoint", HitBreakpoint,
+                                                         newBreakpoints, breakpoints.length(),
+                                                         aRecordingBoundary));
+  }
 }
 
 } // namespace parent
 } // namespace recordreplay
 } // namespace mozilla
--- a/toolkit/recordreplay/ipc/ParentInternal.h
+++ b/toolkit/recordreplay/ipc/ParentInternal.h
@@ -52,16 +52,19 @@ void Shutdown();
 static Monitor* gMonitor;
 
 // Allow the child process to resume execution.
 void Resume(bool aForward);
 
 // Pause the child process at the next opportunity.
 void Pause();
 
+// Direct the child process to warp to a specific point.
+void TimeWarp(const js::ExecutionPoint& target);
+
 // Send a JSON request to the child process, and synchronously wait for a
 // response.
 void SendRequest(const js::CharBuffer& aBuffer, js::CharBuffer* aResponse);
 
 // Set or clear a breakpoint in the child process.
 void SetBreakpoint(size_t aId, const js::BreakpointPosition& aPosition);
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -280,21 +283,28 @@ public:
   bool IsRecovering() { return mRecoveryStage != RecoveryStage::None; }
   bool PauseNeeded() { return mPauseNeeded; }
   const InfallibleVector<size_t>& MajorCheckpoints() { return mMajorCheckpoints; }
 
   bool IsPaused() { return mPaused; }
   bool IsPausedAtCheckpoint();
   bool IsPausedAtRecordingEndpoint();
 
-  // Return whether this process is paused at a breakpoint whose kind matches
-  // the supplied filter.
+  // Get all breakpoints currently installed for this process.
+  void GetInstalledBreakpoints(Vector<SetBreakpointMessage*>& aBreakpoints);
+
   typedef std::function<bool(js::BreakpointPosition::Kind)> BreakpointFilter;
+
+  // Return whether this process is paused at a breakpoint matching a filter.
   bool IsPausedAtMatchingBreakpoint(const BreakpointFilter& aFilter);
 
+  // Get the ids of all installed breakpoints matching a filter.
+  void GetMatchingInstalledBreakpoints(const BreakpointFilter& aFilter,
+                                       Vector<uint32_t>& aBreakpointIds);
+
   // Get the checkpoint at or earlier to the process' position. This is either
   // the last reached checkpoint or the previous one.
   size_t MostRecentCheckpoint() {
     return (GetDisposition() == BeforeLastCheckpoint) ? mLastCheckpoint - 1 : mLastCheckpoint;
   }
 
   // Get the checkpoint which needs to be saved in order for this process
   // (or another at the same place) to rewind.