Bug 1470795 Part 9 - Add ReplayDebugger and record/replay sandbox JS files, r=jimb.
authorBrian Hackett <bhackett1024@gmail.com>
Sun, 22 Jul 2018 11:59:42 +0000
changeset 427743 dd58259f140ade2a5f38b41978c5e8f0bdb4ca28
parent 427742 1fede6c8a76a69c35aa93b234e551c1937a1f891
child 427744 a4a06f53d689f3dedfca8eaf23f417ec54696927
push id34314
push usercsabou@mozilla.com
push dateMon, 23 Jul 2018 09:31:12 +0000
treeherdermozilla-central@143984185dce [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs1470795
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1470795 Part 9 - Add ReplayDebugger and record/replay sandbox JS files, r=jimb.
devtools/server/actors/moz.build
devtools/server/actors/replay/debugger.js
devtools/server/actors/replay/moz.build
devtools/server/actors/replay/replay.js
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -6,16 +6,17 @@
 
 DIRS += [
     'addon',
     'canvas',
     'emulation',
     'highlighters',
     'inspector',
     'object',
+    'replay',
     'targets',
     'utils',
     'webconsole',
     'worker',
 ]
 
 DevToolsModules(
     'accessibility-parent.js',
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/debugger.js
@@ -0,0 +1,673 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint-disable spaced-comment, brace-style, indent-legacy */
+
+// When recording/replaying an execution with Web Replay, Devtools server code
+// runs in the middleman process instead of the recording/replaying process the
+// code is interested in.
+//
+// This file defines replay objects analogous to those constructed by the
+// C++ Debugger (Debugger, Debugger.Object, etc.), which implement similar
+// methods and properties to those C++ objects. These replay objects are
+// created in the middleman process, and describe things that exist in the
+// recording/replaying process, inspecting them via the RecordReplayControl
+// interface.
+
+"use strict";
+
+const RecordReplayControl = require("RecordReplayControl");
+
+///////////////////////////////////////////////////////////////////////////////
+// ReplayDebugger
+///////////////////////////////////////////////////////////////////////////////
+
+function ReplayDebugger() {
+  RecordReplayControl.registerReplayDebugger(this);
+
+  // All breakpoints (per BreakpointPosition) installed by this debugger.
+  this._breakpoints = [];
+
+  // All ReplayDebuggerFramees that have been created while paused at the
+  // current position, indexed by their index (zero is the oldest frame, with
+  // the index increasing for newer frames). These are invalidated when
+  // unpausing.
+  this._frames = [];
+
+  // All ReplayDebuggerObjects and ReplayDebuggerEnvironments that have been
+  // created while paused at the current position, indexed by their id. These
+  // are invalidated when unpausing.
+  this._objects = [];
+
+  // All ReplayDebuggerScripts and ReplayDebuggerScriptSources that have been
+  // created, indexed by their id. These stay valid even after unpausing.
+  this._scripts = [];
+  this._scriptSources = [];
+}
+
+// Frame index used to refer to the newest frame in the child process.
+const NewestFrameIndex = -1;
+
+ReplayDebugger.prototype = {
+
+  /////////////////////////////////////////////////////////
+  // General methods
+  /////////////////////////////////////////////////////////
+
+  replaying: true,
+
+  canRewind: RecordReplayControl.canRewind,
+  replayResumeBackward() { RecordReplayControl.resume(/* forward = */ false); },
+  replayResumeForward() { RecordReplayControl.resume(/* forward = */ true); },
+  replayPause: RecordReplayControl.pause,
+
+  addDebuggee() {},
+  removeAllDebuggees() {},
+
+  replayingContent(url) {
+    return this._sendRequest({ type: "getContent", url });
+  },
+
+  _sendRequest(request) {
+    const data = RecordReplayControl.sendRequest(request);
+    //dump("SendRequest: " +
+    //     JSON.stringify(request) + " -> " + JSON.stringify(data) + "\n");
+    if (data.exception) {
+      ThrowError(data.exception);
+    }
+    return data;
+  },
+
+  _setBreakpoint(handler, position, data) {
+    const id = RecordReplayControl.setBreakpoint(handler, position);
+    this._breakpoints.push({id, position, data});
+  },
+
+  _clearMatchingBreakpoints(callback) {
+    this._breakpoints = this._breakpoints.filter(breakpoint => {
+      if (callback(breakpoint)) {
+        RecordReplayControl.clearBreakpoint(breakpoint.id);
+        return false;
+      }
+      return true;
+    });
+  },
+
+  _searchBreakpoints(callback) {
+    for (const breakpoint of this._breakpoints) {
+      const v = callback(breakpoint);
+      if (v) {
+        return v;
+      }
+    }
+    return undefined;
+  },
+
+  // 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.length = 0;
+
+    this._objects.forEach(obj => obj._invalidate());
+    this._objects.length = 0;
+  },
+
+  /////////////////////////////////////////////////////////
+  // Script methods
+  /////////////////////////////////////////////////////////
+
+  _getScript(id) {
+    if (!id) {
+      return null;
+    }
+    const rv = this._scripts[id];
+    if (rv) {
+      return rv;
+    }
+    return this._addScript(this._sendRequest({ type: "getScript", id }));
+  },
+
+  _addScript(data) {
+    if (!this._scripts[data.id]) {
+      this._scripts[data.id] = new ReplayDebuggerScript(this, data);
+    }
+    return this._scripts[data.id];
+  },
+
+  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));
+  },
+
+  /////////////////////////////////////////////////////////
+  // ScriptSource methods
+  /////////////////////////////////////////////////////////
+
+  _getSource(id) {
+    if (!this._scriptSources[id]) {
+      const data = this._sendRequest({ type: "getSource", id });
+      this._scriptSources[id] = new ReplayDebuggerScriptSource(this, data);
+    }
+    return this._scriptSources[id];
+  },
+
+  /////////////////////////////////////////////////////////
+  // Object methods
+  /////////////////////////////////////////////////////////
+
+  _getObject(id) {
+    if (id && !this._objects[id]) {
+      const data = this._sendRequest({ type: "getObject", id });
+      switch (data.kind) {
+      case "Object":
+        this._objects[id] = new ReplayDebuggerObject(this, data);
+        break;
+      case "Environment":
+        this._objects[id] = new ReplayDebuggerEnvironment(this, data);
+        break;
+      default:
+        ThrowError("Unknown object kind");
+      }
+    }
+    return this._objects[id];
+  },
+
+  _convertValue(value) {
+    if (value && typeof value == "object") {
+      if (value.object) {
+        return this._getObject(value.object);
+      } else if (value.special == "undefined") {
+        return undefined;
+      } else if (value.special == "NaN") {
+        return NaN;
+      } else if (value.special == "Infinity") {
+        return Infinity;
+      } else if (value.special == "-Infinity") {
+        return -Infinity;
+      }
+    }
+    return value;
+  },
+
+  _convertCompletionValue(value) {
+    if ("return" in value) {
+      return { return: this._convertValue(value.return) };
+    }
+    if ("throw" in value) {
+      return { throw: this._convertValue(value.throw) };
+    }
+    ThrowError("Unexpected completion value");
+    return null; // For eslint
+  },
+
+  /////////////////////////////////////////////////////////
+  // Frame methods
+  /////////////////////////////////////////////////////////
+
+  _getFrame(index) {
+    if (index == NewestFrameIndex) {
+      if (this._frames.length) {
+        return this._frames[this._frames.length - 1];
+      }
+    } else {
+      assert(index < this._frames.length);
+      if (this._frames[index]) {
+        return this._frames[index];
+      }
+    }
+
+    const data = this._sendRequest({ type: "getFrame", index });
+
+    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;
+    });
+  },
+
+  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");
+    }
+  },
+
+  get onEnterFrame() {
+    return this._searchBreakpoints(({position, data}) => {
+      return position.kind == "EnterFrame" ? data : null;
+    });
+  },
+
+  set onEnterFrame(handler) {
+    if (handler) {
+      this._setBreakpoint(() => handler.call(this, this.getNewestFrame()),
+                          { kind: "EnterFrame" }, handler);
+    } else {
+      this._clearMatchingBreakpoints(({position}) => position.kind == "EnterFrame");
+    }
+  },
+
+  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;
+      });
+    }
+  },
+
+  clearAllBreakpoints: NYI,
+
+}; // ReplayDebugger.prototype
+
+///////////////////////////////////////////////////////////////////////////////
+// ReplayDebuggerScript
+///////////////////////////////////////////////////////////////////////////////
+
+function ReplayDebuggerScript(dbg, data) {
+  this._dbg = dbg;
+  this._data = data;
+}
+
+ReplayDebuggerScript.prototype = {
+  get displayName() { return this._data.displayName; },
+  get url() { return this._data.url; },
+  get startLine() { return this._data.startLine; },
+  get lineCount() { return this._data.lineCount; },
+  get source() { return this._dbg._getSource(this._data.sourceId); },
+  get sourceStart() { return this._data.sourceStart; },
+  get sourceLength() { return this._data.sourceLength; },
+
+  _forward(type, value) {
+    return this._dbg._sendRequest({ type, id: this._data.id, value });
+  },
+
+  getLineOffsets(line) { return this._forward("getLineOffsets", line); },
+  getOffsetLocation(pc) { return this._forward("getOffsetLocation", pc); },
+  getSuccessorOffsets(pc) { return this._forward("getSuccessorOffsets", pc); },
+  getPredecessorOffsets(pc) { return this._forward("getPredecessorOffsets", pc); },
+
+  setBreakpoint(offset, handler) {
+    this._dbg._setBreakpoint(() => { handler.hit(this._dbg.getNewestFrame()); },
+                             { kind: "Break", script: this._data.id, offset },
+                             handler);
+  },
+
+  clearBreakpoint(handler) {
+    this._dbg._clearMatchingBreakpoints(({position, data}) => {
+      return position.script == this._data.id && handler == data;
+    });
+  },
+
+  get isGeneratorFunction() { NYI(); },
+  get isAsyncFunction() { NYI(); },
+  get format() { NYI(); },
+  getChildScripts: NYI,
+  getAllOffsets: NYI,
+  getAllColumnOffsets: NYI,
+  getBreakpoints: NYI,
+  clearAllBreakpoints: NYI,
+  isInCatchScope: NYI,
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// ReplayDebuggerScriptSource
+///////////////////////////////////////////////////////////////////////////////
+
+function ReplayDebuggerScriptSource(dbg, data) {
+  this._dbg = dbg;
+  this._data = data;
+}
+
+ReplayDebuggerScriptSource.prototype = {
+  get text() { return this._data.text; },
+  get url() { return this._data.url; },
+  get displayURL() { return this._data.displayURL; },
+  get elementAttributeName() { return this._data.elementAttributeName; },
+  get introductionOffset() { return this._data.introductionOffset; },
+  get introductionType() { return this._data.introductionType; },
+  get sourceMapURL() { return this._data.sourceMapURL; },
+  get element() { return null; },
+
+  get introductionScript() {
+    return this._dbg._getScript(this._data.introductionScript);
+  },
+
+  get binary() { NYI(); },
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// ReplayDebuggerFrame
+///////////////////////////////////////////////////////////////////////////////
+
+function ReplayDebuggerFrame(dbg, data) {
+  this._dbg = dbg;
+  this._data = data;
+  if (this._data.arguments) {
+    this._data.arguments =
+      this._data.arguments.map(this._dbg._convertValue.bind(this._dbg));
+  }
+}
+
+ReplayDebuggerFrame.prototype = {
+  _invalidate() {
+    this._data = null;
+  },
+
+  get type() { return this._data.type; },
+  get callee() { return this._dbg._getObject(this._data.callee); },
+  get environment() { return this._dbg._getObject(this._data.environment); },
+  get generator() { return this._data.generator; },
+  get constructing() { return this._data.constructing; },
+  get this() { return this._dbg._convertValue(this._data.this); },
+  get script() { return this._dbg._getScript(this._data.script); },
+  get offset() { return this._data.offset; },
+  get arguments() { return this._data.arguments; },
+  get live() { return true; },
+
+  eval(text, options) {
+    const rv = this._dbg._sendRequest({ type: "frameEvaluate",
+                                        index: this._data.index, text, options });
+    return this._dbg._convertCompletionValue(rv);
+  },
+
+  _positionMatches(position, kind) {
+    return position.kind == kind
+        && position.script == this._data.script
+        && position.frameIndex == this._data.index;
+  },
+
+  get onStep() {
+    return this._dbg._searchBreakpoints(({position, data}) => {
+      return this._positionMatches(position, "OnStep") ? data : null;
+    });
+  },
+
+  set onStep(handler) {
+    if (handler) {
+      // Use setReplayingOnStep instead.
+      NotAllowed();
+    }
+    this._clearOnStepBreakpoints();
+  },
+
+  _clearOnStepBreakpoints() {
+    this._dbg._clearMatchingBreakpoints(
+      ({position}) => this._positionMatches(position, "OnStep")
+    );
+  },
+
+  setReplayingOnStep(handler, offsets) {
+    this._clearOnStepBreakpoints();
+    offsets.forEach(offset => {
+      this._dbg._setBreakpoint(
+        () => handler.call(this._dbg.getNewestFrame()),
+        { kind: "OnStep",
+          script: this._data.script,
+          offset,
+          frameIndex: this._data.index },
+        handler);
+    });
+  },
+
+  get onPop() {
+    return this._dbg._searchBreakpoints(({position, data}) => {
+      return this._positionMatches(position, "OnPop");
+    });
+  },
+
+  set onPop(handler) {
+    if (handler) {
+      this._dbg._setBreakpoint(() => {
+          const result = this._dbg._sendRequest({ type: "popFrameResult" });
+          handler.call(this._dbg.getNewestFrame(),
+                       this._dbg._convertCompletionValue(result));
+        },
+        { kind: "OnPop", script: this._data.script, frameIndex: this._data.index },
+        handler);
+    } else {
+      this._dbg._clearMatchingBreakpoints(
+        ({position}) => this._positionMatches(position, "OnPop")
+      );
+    }
+  },
+
+  get older() {
+    if (this._data.index == 0) {
+      // This is the oldest frame.
+      return null;
+    }
+    return this._dbg._getFrame(this._data.index - 1);
+  },
+
+  get implementation() { NYI(); },
+  evalWithBindings: NYI,
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// ReplayDebuggerObject
+///////////////////////////////////////////////////////////////////////////////
+
+function ReplayDebuggerObject(dbg, data) {
+  this._dbg = dbg;
+  this._data = data;
+  this._properties = null;
+}
+
+ReplayDebuggerObject.prototype = {
+  _invalidate() {
+    this._data = null;
+    this._properties = null;
+  },
+
+  get callable() { return this._data.callable; },
+  get isBoundFunction() { return this._data.isBoundFunction; },
+  get isArrowFunction() { return this._data.isArrowFunction; },
+  get isGeneratorFunction() { return this._data.isGeneratorFunction; },
+  get isAsyncFunction() { return this._data.isAsyncFunction; },
+  get proto() { return this._dbg._getObject(this._data.proto); },
+  get class() { return this._data.class; },
+  get name() { return this._data.name; },
+  get displayName() { return this._data.displayName; },
+  get parameterNames() { return this._data.parameterNames; },
+  get script() { return this._dbg._getScript(this._data.script); },
+  get environment() { return this._dbg._getObject(this._data.environment); },
+  get boundTargetFunction() { return this.isBoundFunction ? NYI() : undefined; },
+  get boundThis() { return this.isBoundFunction ? NYI() : undefined; },
+  get boundArguments() { return this.isBoundFunction ? NYI() : undefined; },
+  get global() { return this._dbg._getObject(this._data.global); },
+  get isProxy() { return this._data.isProxy; },
+
+  isExtensible() { return this._data.isExtensible; },
+  isSealed() { return this._data.isSealed; },
+  isFrozen() { return this._data.isFrozen; },
+  unwrap() { return this.isProxy ? NYI() : this; },
+
+  unsafeDereference() {
+    // Direct access to the referent is not currently available.
+    return null;
+  },
+
+  getOwnPropertyNames() {
+    this._ensureProperties();
+    return Object.keys(this._properties);
+  },
+
+  getOwnPropertySymbols() {
+    // Symbol properties are not handled yet.
+    return [];
+  },
+
+  getOwnPropertyDescriptor(name) {
+    this._ensureProperties();
+    return this._properties[name];
+  },
+
+  _ensureProperties() {
+    if (!this._properties) {
+      const properties = this._dbg._sendRequest({
+        type: "getObjectProperties",
+        id: this._data.id
+      });
+      this._properties = {};
+      properties.forEach(({name, desc}) => {
+        if ("value" in desc) {
+          desc.value = this._dbg._convertValue(desc.value);
+        }
+        if ("get" in desc) {
+          desc.get = this._dbg._getObject(desc.get);
+        }
+        if ("set" in desc) {
+          desc.set = this._dbg._getObject(desc.set);
+        }
+        this._properties[name] = desc;
+      });
+    }
+  },
+
+  get allocationSite() { NYI(); },
+  get errorMessageName() { NYI(); },
+  get errorNotes() { NYI(); },
+  get errorLineNumber() { NYI(); },
+  get errorColumnNumber() { NYI(); },
+  get proxyTarget() { NYI(); },
+  get proxyHandler() { NYI(); },
+  get isPromise() { NYI(); },
+  call: NYI,
+  apply: NYI,
+  asEnvironment: NYI,
+  executeInGlobal: NYI,
+  executeInGlobalWithBindings: NYI,
+  makeDebuggeeValue: NYI,
+
+  preventExtensions: NotAllowed,
+  seal: NotAllowed,
+  freeze: NotAllowed,
+  defineProperty: NotAllowed,
+  defineProperties: NotAllowed,
+  deleteProperty: NotAllowed,
+  forceLexicalInitializationByName: NotAllowed,
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// ReplayDebuggerEnvironment
+///////////////////////////////////////////////////////////////////////////////
+
+function ReplayDebuggerEnvironment(dbg, data) {
+  this._dbg = dbg;
+  this._data = data;
+  this._names = null;
+}
+
+ReplayDebuggerEnvironment.prototype = {
+  _invalidate() {
+    this._data = null;
+    this._names = null;
+  },
+
+  get type() { return this._data.type; },
+  get parent() { return this._dbg._getObject(this._data.parent); },
+  get object() { return this._dbg._getObject(this._data.object); },
+  get callee() { return this._dbg._getObject(this._data.callee); },
+  get optimizedOut() { return this._data.optimizedOut; },
+
+  _ensureNames() {
+    if (!this._names) {
+      const names =
+        this._dbg._sendRequest({ type: "getEnvironmentNames", id: this._data.id });
+      this._names = {};
+      names.forEach(({ name, value }) => {
+        this._names[name] = this._dbg._convertValue(value);
+      });
+    }
+  },
+
+  names() {
+    this._ensureNames();
+    return Object.keys(this._names);
+  },
+
+  getVariable(name) {
+    this._ensureNames();
+    return this._names[name];
+  },
+
+  get inspectable() {
+    // All ReplayDebugger environments are inspectable, as all compartments in
+    // the replayed process are considered to be debuggees.
+    return true;
+  },
+
+  find: NYI,
+  setVariable: NotAllowed,
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// Utilities
+///////////////////////////////////////////////////////////////////////////////
+
+function NYI() {
+  ThrowError("Not yet implemented");
+}
+
+function NotAllowed() {
+  ThrowError("Not allowed");
+}
+
+function ThrowError(msg)
+{
+  const error = new Error(msg);
+  dump("ReplayDebugger Server Error: " + msg + " Stack: " + error.stack + "\n");
+  throw error;
+}
+
+function assert(v) {
+  if (!v) {
+    throw new Error("Assertion Failed!");
+  }
+}
+
+module.exports = ReplayDebugger;
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'debugger.js',
+    'replay.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/replay/replay.js
@@ -0,0 +1,598 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint-disable spaced-comment, brace-style, indent-legacy */
+
+// This file defines the logic that runs in the record/replay devtools sandbox.
+// This code is loaded into all recording/replaying processes, and responds to
+// requests and other instructions from the middleman via the exported symbols
+// defined at the end of this file.
+//
+// Like all other JavaScript in the recording/replaying process, this code's
+// state is included in memory snapshots and reset when checkpoints are
+// restored. In the process of handling the middleman's requests, however, its
+// state may vary between recording and replaying, or between different
+// replays. As a result, we have to be very careful about performing operations
+// that might interact with the recording --- any time we enter the debuggee
+// and evaluate code or perform other operations.
+// The RecordReplayControl.maybeDivergeFromRecording function should be used at
+// any point where such interactions might occur.
+// eslint-disable spaced-comment
+
+"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');" +
+  "addDebuggerToGlobal(this);",
+  sandbox
+);
+const Debugger = sandbox.Debugger;
+const RecordReplayControl = sandbox.RecordReplayControl;
+
+const dbg = new Debugger();
+
+// We are interested in debugging all globals in the process.
+dbg.onNewGlobalObject = function(global) {
+  dbg.addDebuggee(global);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// Utilities
+///////////////////////////////////////////////////////////////////////////////
+
+function assert(v) {
+  if (!v) {
+    RecordReplayControl.dump("Assertion Failed: " + (new Error()).stack + "\n");
+    throw new Error("Assertion Failed!");
+  }
+}
+
+// Bidirectional map between objects and IDs.
+function IdMap() {
+  this._idToObject = [ undefined ];
+  this._objectToId = new Map();
+}
+
+IdMap.prototype = {
+  add(object) {
+    assert(object && !this._objectToId.has(object));
+    const id = this._idToObject.length;
+    this._idToObject.push(object);
+    this._objectToId.set(object, id);
+    return id;
+  },
+
+  getId(object) {
+    const id = this._objectToId.get(object);
+    return (id === undefined) ? 0 : id;
+  },
+
+  getObject(id) {
+    return this._idToObject[id];
+  },
+
+  forEach(callback) {
+    for (let i = 1; i < this._idToObject.length; i++) {
+      callback(i, this._idToObject[i]);
+    }
+  },
+
+  lastId() {
+    return this._idToObject.length - 1;
+  },
+};
+
+function countScriptFrames() {
+  let count = 0;
+  let frame = dbg.getNewestFrame();
+  while (frame) {
+    if (considerScript(frame.script)) {
+      count++;
+    }
+    frame = frame.older;
+  }
+  return count;
+}
+
+function scriptFrameForIndex(index) {
+  let indexFromTop = countScriptFrames() - 1 - index;
+  let frame = dbg.getNewestFrame();
+  while (true) {
+    if (considerScript(frame.script)) {
+      if (indexFromTop-- == 0) {
+        break;
+      }
+    }
+    frame = frame.older;
+  }
+  return frame;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Persistent State
+///////////////////////////////////////////////////////////////////////////////
+
+// Association between Debugger.Scripts and their IDs. The indices that this
+// table assigns to scripts are stable across the entire recording, even though
+// this table (like all JS state) is included in snapshots, rolled back when
+// rewinding, and so forth.  In debuggee time, this table only grows (there is
+// no way to remove entries). Scripts created for debugger activity (e.g. eval)
+// are ignored, and off thread compilation is disabled, so this table acquires
+// the same scripts in the same order as we roll back and run forward in the
+// recording.
+const gScripts = new IdMap();
+
+function addScript(script) {
+  gScripts.add(script);
+  script.getChildScripts().forEach(addScript);
+}
+
+// Association between Debugger.ScriptSources and their IDs. As for gScripts,
+// the indices assigned to a script source are consistent across all replays
+// and rewinding.
+const gScriptSources = new IdMap();
+
+function addScriptSource(source) {
+  gScriptSources.add(source);
+}
+
+function considerScript(script) {
+  return script.url
+      && !script.url.startsWith("resource:")
+      && !script.url.startsWith("chrome:");
+}
+
+dbg.onNewScript = function(script) {
+  if (RecordReplayControl.areThreadEventsDisallowed()) {
+    // This script is part of an eval on behalf of the debugger.
+    return;
+  }
+
+  if (!considerScript(script)) {
+    return;
+  }
+
+  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" });
+  }
+
+  // Check in case any handlers we need to install are on the scripts just
+  // created.
+  installPendingHandlers();
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// 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;
+
+// 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 = [];
+
+// Script/offset pairs where we have installed a breakpoint handler. We have to
+// avoid installing duplicate handlers here because they will both be called.
+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;
+  gPendingPcHandlers.length = 0;
+  gInstalledPcHandlers.length = 0;
+  gOnPopFilters.length = 0;
+}
+
+function installPendingHandlers() {
+  const pending = gPendingPcHandlers.map(position => position);
+  gPendingPcHandlers.length = 0;
+
+  pending.forEach(EnsurePositionHandler);
+}
+
+// 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" });
+  }
+
+  if (considerScript(frame.script)) {
+    gOnPopFilters.forEach(filter => {
+      if (filter(frame)) {
+        frame.onPop = onPopFrame;
+      }
+    });
+  }
+}
+
+function addOnPopFilter(filter) {
+  let frame = dbg.getNewestFrame();
+  while (frame) {
+    if (considerScript(frame.script) && filter(frame)) {
+      frame.onPop = onPopFrame;
+    }
+    frame = frame.older;
+  }
+
+  gOnPopFilters.push(filter);
+  dbg.onEnterFrame = onEnterFrame;
+}
+
+function EnsurePositionHandler(position) {
+  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
+        // can't install a handler for it. Add a pending handler so that we
+        // can install the handler once the script is created.
+        gPendingPcHandlers.push(position);
+        return;
+      }
+    }
+
+    const match = function({script, offset}) {
+      return script == position.script && offset == position.offset;
+    };
+    if (gInstalledPcHandlers.some(match)) {
+      return;
+    }
+    gInstalledPcHandlers.push({ script: position.script, offset: position.offset });
+
+    debugScript.setBreakpoint(position.offset, {
+      hit() {
+        RecordReplayControl.positionHit({
+          kind: "OnStep",
+          script: position.script,
+          offset: position.offset,
+          frameIndex: countScriptFrames() - 1,
+        });
+      }
+    });
+    break;
+  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) {
+      return {
+        kind: "Break",
+        script: position.script,
+        offset: script.mainOffset,
+      };
+    }
+  }
+  return null;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Paused State
+///////////////////////////////////////////////////////////////////////////////
+
+let gPausedObjects = new IdMap();
+
+function getObjectId(obj) {
+  const id = gPausedObjects.getId(obj);
+  if (!id && obj) {
+    assert((obj instanceof Debugger.Object) ||
+           (obj instanceof Debugger.Environment));
+    return gPausedObjects.add(obj);
+  }
+  return id;
+}
+
+function convertValue(value) {
+  if (value instanceof Debugger.Object) {
+    return { object: getObjectId(value) };
+  }
+  if (value === undefined) {
+    return { special: "undefined" };
+  }
+  if (value !== value) { // eslint-disable-line no-self-compare
+    return { special: "NaN" };
+  }
+  if (value == Infinity) {
+    return { special: "Infinity" };
+  }
+  if (value == -Infinity) {
+    return { special: "-Infinity" };
+  }
+  return value;
+}
+
+function convertCompletionValue(value) {
+  if ("return" in value) {
+    return { return: convertValue(value.return) };
+  }
+  if ("throw" in value) {
+    return { throw: convertValue(value.throw) };
+  }
+  throw new Error("Unexpected completion value");
+}
+
+// eslint-disable-next-line no-unused-vars
+function ClearPausedState() {
+  gPausedObjects = new IdMap();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Handler Helpers
+///////////////////////////////////////////////////////////////////////////////
+
+function getScriptData(id) {
+  const script = gScripts.getObject(id);
+  return {
+    id,
+    sourceId: gScriptSources.getId(script.source),
+    startLine: script.startLine,
+    lineCount: script.lineCount,
+    sourceStart: script.sourceStart,
+    sourceLength: script.sourceLength,
+    displayName: script.displayName,
+    url: script.url,
+  };
+}
+
+function forwardToScript(name) {
+  return request => gScripts.getObject(request.id)[name](request.value);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Handlers
+///////////////////////////////////////////////////////////////////////////////
+
+const gRequestHandlers = {
+
+  findScripts(request) {
+    const rv = [];
+    gScripts.forEach((id) => {
+      rv.push(getScriptData(id));
+    });
+    return rv;
+  },
+
+  getScript(request) {
+    return getScriptData(request.id);
+  },
+
+  getNewScript(request) {
+    return getScriptData(gScripts.lastId());
+  },
+
+  getContent(request) {
+    return RecordReplayControl.getContent(request.url);
+  },
+
+  getSource(request) {
+    const source = gScriptSources.getObject(request.id);
+    const introductionScript = gScripts.getId(source.introductionScript);
+    return {
+      id: request.id,
+      text: source.text,
+      url: source.url,
+      displayURL: source.displayURL,
+      elementAttributeName: source.elementAttributeName,
+      introductionScript,
+      introductionOffset: introductionScript ? source.introductionOffset : undefined,
+      introductionType: source.introductionType,
+      sourceMapURL: source.sourceMapURL,
+    };
+  },
+
+  getObject(request) {
+    const object = gPausedObjects.getObject(request.id);
+    if (object instanceof Debugger.Object) {
+      return {
+        id: request.id,
+        kind: "Object",
+        callable: object.callable,
+        isBoundFunction: object.isBoundFunction,
+        isArrowFunction: object.isArrowFunction,
+        isGeneratorFunction: object.isGeneratorFunction,
+        isAsyncFunction: object.isAsyncFunction,
+        proto: getObjectId(object.proto),
+        class: object.class,
+        name: object.name,
+        displayName: object.displayName,
+        parameterNames: object.parameterNames,
+        script: gScripts.getId(object.script),
+        environment: getObjectId(object.environment),
+        global: getObjectId(object.global),
+        isProxy: object.isProxy,
+        isExtensible: object.isExtensible(),
+        isSealed: object.isSealed(),
+        isFrozen: object.isFrozen(),
+      };
+    }
+    if (object instanceof Debugger.Environment) {
+      return {
+        id: request.id,
+        kind: "Environment",
+        type: object.type,
+        parent: getObjectId(object.parent),
+        object: object.type == "declarative" ? 0 : getObjectId(object.object),
+        callee: getObjectId(object.callee),
+        optimizedOut: object.optimizedOut,
+      };
+    }
+    throw new Error("Unknown object kind");
+  },
+
+  getObjectProperties(request) {
+    if (!RecordReplayControl.maybeDivergeFromRecording()) {
+      return [{
+        name: "Unknown properties",
+        desc: {
+          value: "Recording divergence in getObjectProperties",
+          enumerable: true
+        },
+      }];
+    }
+
+    const object = gPausedObjects.getObject(request.id);
+    const names = object.getOwnPropertyNames();
+
+    return names.map(name => {
+      const desc = object.getOwnPropertyDescriptor(name);
+      if ("value" in desc) {
+        desc.value = convertValue(desc.value);
+      }
+      if ("get" in desc) {
+        desc.get = getObjectId(desc.get);
+      }
+      if ("set" in desc) {
+        desc.set = getObjectId(desc.set);
+      }
+      return { name, desc };
+    });
+  },
+
+  getEnvironmentNames(request) {
+    if (!RecordReplayControl.maybeDivergeFromRecording()) {
+      return [{name: "Unknown names",
+               value: "Recording divergence in getEnvironmentNames" }];
+    }
+
+    const env = gPausedObjects.getObject(request.id);
+    const names = env.names();
+
+    return names.map(name => {
+      return { name, value: convertValue(env.getVariable(name)) };
+    });
+  },
+
+  getFrame(request) {
+    if (request.index == -1 /* NewestFrameIndex */) {
+      const numFrames = countScriptFrames();
+      if (!numFrames) {
+        // Return an empty object when there are no frames.
+        return {};
+      }
+      request.index = numFrames - 1;
+    }
+
+    const frame = scriptFrameForIndex(request.index);
+
+    let _arguments = null;
+    if (frame.arguments) {
+      _arguments = [];
+      for (let i = 0; i < frame.arguments.length; i++) {
+        _arguments.push(convertValue(frame.arguments[i]));
+      }
+    }
+
+    return {
+      index: request.index,
+      type: frame.type,
+      callee: getObjectId(frame.callee),
+      environment: getObjectId(frame.environment),
+      generator: frame.generator,
+      constructing: frame.constructing,
+      this: convertValue(frame.this),
+      script: gScripts.getId(frame.script),
+      offset: frame.offset,
+      arguments: _arguments,
+    };
+  },
+
+  getLineOffsets: forwardToScript("getLineOffsets"),
+  getOffsetLocation: forwardToScript("getOffsetLocation"),
+  getSuccessorOffsets: forwardToScript("getSuccessorOffsets"),
+  getPredecessorOffsets: forwardToScript("getPredecessorOffsets"),
+
+  frameEvaluate(request) {
+    if (!RecordReplayControl.maybeDivergeFromRecording()) {
+      return { throw: "Recording divergence in frameEvaluate" };
+    }
+
+    const frame = scriptFrameForIndex(request.index);
+    const rv = frame.eval(request.text, request.options);
+    return convertCompletionValue(rv);
+  },
+
+  popFrameResult(request) {
+    return gPopFrameResult ? convertCompletionValue(gPopFrameResult) : {};
+  },
+};
+
+// eslint-disable-next-line no-unused-vars
+function ProcessRequest(request) {
+  try {
+    if (gRequestHandlers[request.type]) {
+      return gRequestHandlers[request.type](request);
+    }
+    return { exception: "No handler for " + request.type };
+  } catch (e) {
+    RecordReplayControl.dump("ReplayDebugger Record/Replay Error: " + e + "\n");
+    return { exception: "" + e };
+  }
+}
+
+// eslint-disable-next-line no-unused-vars
+var EXPORTED_SYMBOLS = [
+  "EnsurePositionHandler",
+  "ClearPositionHandlers",
+  "ClearPausedState",
+  "ProcessRequest",
+  "GetEntryPosition",
+];