Bug 1547864 - Use a single debugger request to gather information needed to pause at a location, r=loganfsmyth.
authorBrian Hackett <bhackett1024@gmail.com>
Mon, 29 Apr 2019 06:40:23 -1000
changeset 532519 3c5ff599ce3c6f922b8bb31f2344c199b82889e2
parent 532518 60085ea6ff728115e1f61568702fd2348c15814d
child 532520 22a3a17e5a419109cb17d83d1803ae652145b6a7
push id11268
push usercsabou@mozilla.com
push dateTue, 14 May 2019 15:24:22 +0000
treeherdermozilla-beta@5fb7fcd568d6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersloganfsmyth
bugs1547864
milestone68.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 1547864 - Use a single debugger request to gather information needed to pause at a location, r=loganfsmyth. Differential Revision: https://phabricator.services.mozilla.com/D29292
devtools/server/actors/object.js
devtools/server/actors/object/previewers.js
devtools/server/actors/replay/debugger.js
devtools/server/actors/replay/replay.js
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -133,16 +133,20 @@ const proto = {
 
     // FF40+: Allow to know how many properties an object has to lazily display them
     // when there is a bunch.
     if (isTypedArray(g)) {
       // Bug 1348761: getOwnPropertyNames is unnecessary slow on TypedArrays
       g.ownPropertyLength = getArrayLength(this.obj);
     } else if (isStorage(g)) {
       g.ownPropertyLength = getStorageLength(this.obj);
+    } else if (isReplaying) {
+      // When replaying we can get the number of properties directly, to avoid
+      // needing to enumerate all of them.
+      g.ownPropertyLength = this.obj.getOwnPropertyNamesCount();
     } else {
       try {
         g.ownPropertyLength = this.obj.getOwnPropertyNames().length;
       } catch (err) {
         // The above can throw when the debuggee does not subsume the object's
         // compartment, or for some WrappedNatives like Cu.Sandbox.
       }
     }
@@ -330,16 +334,23 @@ const proto = {
     let obj = this.obj;
     let level = 0, i = 0;
 
     // Do not search safe getters in unsafe objects.
     if (!DevToolsUtils.isSafeDebuggerObject(obj)) {
       return safeGetterValues;
     }
 
+    // Do not search for safe getters while replaying. While this would be nice
+    // to support, it involves a lot of back-and-forth between processes and
+    // would be better to do entirely in the replaying process.
+    if (isReplaying) {
+      return safeGetterValues;
+    }
+
     // Most objects don't have any safe getters but inherit some from their
     // prototype. Avoid calling getOwnPropertyNames on objects that may have
     // many properties like Array, strings or js objects. That to avoid
     // freezing firefox when doing so.
     if (isArray(this.obj) || ["Object", "String"].includes(this.obj.class)) {
       obj = obj.proto;
       level++;
     }
--- a/devtools/server/actors/object/previewers.js
+++ b/devtools/server/actors/object/previewers.js
@@ -372,16 +372,20 @@ function GenericObject(objectActor, grip
     if (ObjectUtils.isStorage(obj)) {
       // local and session storage cannot be iterated over using
       // Object.getOwnPropertyNames() because it skips keys that are duplicated
       // on the prototype e.g. "key", "getKeys" so we need to gather the real
       // keys using the storage.key() function.
       for (let j = 0; j < rawObj.length; j++) {
         names.push(rawObj.key(j));
       }
+    } else if (isReplaying) {
+      // When replaying we can access a batch of properties for use in generating
+      // the preview. This avoids needing to enumerate all properties.
+      names = obj.getEnumerableOwnPropertyNamesForPreview();
     } else {
       names = obj.getOwnPropertyNames();
     }
     symbols = obj.getOwnPropertySymbols();
   } catch (ex) {
     // Calling getOwnPropertyNames() on some wrapped native prototypes is not
     // allowed: "cannot modify properties of a WrappedNative". See bug 952093.
   }
@@ -776,16 +780,22 @@ previewers.Object = [
 
   function PseudoArray({obj, hooks}, grip, rawObj) {
     // An object is considered a pseudo-array if all the following apply:
     // - All its properties are array indices except, optionally, a "length" property.
     // - At least it has the "0" array index.
     // - The array indices are consecutive.
     // - The value of "length", if present, is the number of array indices.
 
+    // Don't generate pseudo array previews when replaying. We don't want to
+    // have to enumerate all the properties in order to determine this.
+    if (isReplaying) {
+      return false;
+    }
+
     let keys;
     try {
       keys = obj.getOwnPropertyNames();
     } catch (err) {
       // The above can throw when the debuggee does not subsume the object's
       // compartment, or for some WrappedNatives like Cu.Sandbox.
       return false;
     }
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -189,31 +189,33 @@ ReplayDebugger.prototype = {
     this._ensurePaused();
     this._setResume(() => {
       this._direction = forward ? Direction.FORWARD : Direction.BACKWARD;
       dumpv("Resuming " + this._direction);
       this._control.resume(forward);
       if (this._paused) {
         // If we resume and immediately pause, we are at an endpoint of the
         // recording. Force the thread to pause.
+        this._capturePauseData();
         this.replayingOnForcedPause(this.getNewestFrame());
       }
     });
   },
 
   replayTimeWarp(target) {
     this._ensurePaused();
     this._setResume(() => {
       this._direction = Direction.NONE;
       dumpv("Warping " + JSON.stringify(target));
       this._control.timeWarp(target);
 
       // timeWarp() doesn't return until the child has reached the target of
       // the warp, after which we force the thread to pause.
       assert(this._paused);
+      this._capturePauseData();
       this.replayingOnForcedPause(this.getNewestFrame());
     });
   },
 
   replayPause() {
     this._ensurePaused();
 
     // Cancel any pending resume.
@@ -347,16 +349,58 @@ ReplayDebugger.prototype = {
   _invalidateAfterUnpause() {
     this._frames.forEach(frame => frame._invalidate());
     this._frames.length = 0;
 
     this._objects.forEach(obj => obj._invalidate());
     this._objects.length = 0;
   },
 
+  // Fill in the debugger with (hopefully) all data the client/server need to
+  // pause at the current location.
+  _capturePauseData() {
+    if (this._frames.length) {
+      return;
+    }
+
+    const pauseData = this._sendRequestAllowDiverge({ type: "pauseData" });
+    if (!pauseData.frames) {
+      return;
+    }
+
+    for (const data of Object.values(pauseData.scripts)) {
+      this._addScript(data);
+    }
+
+    for (const { scriptId, offset, metadata} of pauseData.offsetMetadata) {
+      if (this._scripts[scriptId]) {
+        const script = this._getScript(scriptId);
+        script._addOffsetMetadata(offset, metadata);
+      }
+    }
+
+    for (const { data, preview } of Object.values(pauseData.objects)) {
+      if (!this._objects[data.id]) {
+        this._addObject(data);
+      }
+      this._getObject(data.id)._preview = preview;
+    }
+
+    for (const { data, names } of Object.values(pauseData.environments)) {
+      if (!this._objects[data.id]) {
+        this._addObject(data);
+      }
+      this._getObject(data.id)._names = names;
+    }
+
+    for (const frame of pauseData.frames) {
+      this._frames[frame.index] = new ReplayDebuggerFrame(this, frame);
+    }
+  },
+
   /////////////////////////////////////////////////////////
   // Search management
   /////////////////////////////////////////////////////////
 
   _forEachSearch(callback) {
     for (const { position } of this._searches) {
       callback(position);
     }
@@ -551,30 +595,34 @@ ReplayDebugger.prototype = {
 
   /////////////////////////////////////////////////////////
   // 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");
-      }
+      this._addObject(data);
     }
     return this._objects[id];
   },
 
+  _addObject(data) {
+    switch (data.kind) {
+    case "Object":
+      this._objects[data.id] = new ReplayDebuggerObject(this, data);
+      break;
+    case "Environment":
+      this._objects[data.id] = new ReplayDebuggerEnvironment(this, data);
+      break;
+    default:
+      ThrowError("Unknown object kind");
+    }
+  },
+
   // Convert a value we received from the child.
   _convertValue(value) {
     if (isNonNullObject(value)) {
       if (value.object) {
         return this._getObject(value.object);
       }
       if (value.snapshot) {
         return new ReplayDebuggerObjectSnapshot(this, value.snapshot);
@@ -689,18 +737,20 @@ ReplayDebugger.prototype = {
   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);
+      this._setBreakpoint(() => {
+        this._capturePauseData();
+        handler.call(this, this.getNewestFrame());
+      }, { kind: "OnPop" }, handler);
     } else {
       this._clearMatchingBreakpoints(({position}) => {
         return position.kind == "OnPop" && !position.script;
       });
     }
   },
 
   getNewConsoleMessage() {
@@ -722,16 +772,17 @@ ReplayDebugger.prototype = {
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerScript
 ///////////////////////////////////////////////////////////////////////////////
 
 function ReplayDebuggerScript(dbg, data) {
   this._dbg = dbg;
   this._data = data;
+  this._offsetMetadata = [];
 }
 
 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); },
@@ -744,28 +795,39 @@ ReplayDebuggerScript.prototype = {
     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); },
   getAllColumnOffsets() { return this._forward("getAllColumnOffsets"); },
-  getOffsetMetadata(pc) { return this._forward("getOffsetMetadata", pc); },
   getPossibleBreakpoints(query) {
     return this._forward("getPossibleBreakpoints", query);
   },
   getPossibleBreakpointOffsets(query) {
     return this._forward("getPossibleBreakpointOffsets", query);
   },
 
+  getOffsetMetadata(pc) {
+    if (!this._offsetMetadata[pc]) {
+      this._addOffsetMetadata(pc, this._forward("getOffsetMetadata", pc));
+    }
+    return this._offsetMetadata[pc];
+  },
+
+  _addOffsetMetadata(pc, metadata) {
+    this._offsetMetadata[pc] = metadata;
+  },
+
   setBreakpoint(offset, handler) {
-    this._dbg._setBreakpoint(() => { handler.hit(this._dbg.getNewestFrame()); },
-                             { kind: "Break", script: this._data.id, offset },
-                             handler);
+    this._dbg._setBreakpoint(() => {
+      this._dbg._capturePauseData();
+      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;
     });
   },
 
@@ -862,35 +924,38 @@ ReplayDebuggerFrame.prototype = {
 
   set onStep(handler) {
     // Use setReplayingOnStep or replayClearSteppingHooks instead.
     NotAllowed();
   },
 
   setReplayingOnStep(handler, offsets) {
     offsets.forEach(offset => {
-      this._dbg._setBreakpoint(
-        () => { handler.call(this._dbg.getNewestFrame()); },
-        { kind: "OnStep",
-          script: this._data.script,
-          offset,
-          frameIndex: this._data.index },
-        handler);
+      this._dbg._setBreakpoint(() => {
+        this._dbg._capturePauseData();
+        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") ? data : null;
     });
   },
 
   set onPop(handler) {
     if (handler) {
       this._dbg._setBreakpoint(() => {
+          this._dbg._capturePauseData();
           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 {
       // Use replayClearSteppingHooks instead.
@@ -912,25 +977,25 @@ ReplayDebuggerFrame.prototype = {
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerObject
 ///////////////////////////////////////////////////////////////////////////////
 
 function ReplayDebuggerObject(dbg, data) {
   this._dbg = dbg;
   this._data = data;
+  this._preview = null;
   this._properties = null;
-  this._proxyData = null;
 }
 
 ReplayDebuggerObject.prototype = {
   _invalidate() {
     this._data = null;
+    this._preview = null;
     this._properties = null;
-    this._proxyData = 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 class() { return this._data.class; },
@@ -951,80 +1016,92 @@ ReplayDebuggerObject.prototype = {
     return null;
   },
 
   getOwnPropertyNames() {
     this._ensureProperties();
     return Object.keys(this._properties);
   },
 
+  getEnumerableOwnPropertyNamesForPreview() {
+    if (this._preview) {
+      return Object.keys(this._preview.enumerableOwnProperties);
+    }
+    return this.getOwnPropertyNames();
+  },
+
+  getOwnPropertyNamesCount() {
+    if (this._preview) {
+      return this._preview.ownPropertyNamesCount;
+    }
+    return this.getOwnPropertyNames().length;
+  },
+
   getOwnPropertySymbols() {
     // Symbol properties are not handled yet.
     return [];
   },
 
   getOwnPropertyDescriptor(name) {
+    if (this._preview) {
+      if (this._preview.enumerableOwnProperties) {
+        const desc = this._preview.enumerableOwnProperties[name];
+        if (desc) {
+          return this._convertPropertyDescriptor(desc);
+        }
+      }
+      if (name == "length") {
+        return this._convertPropertyDescriptor(this._preview.lengthProperty);
+      }
+      if (name == "displayName") {
+        return this._convertPropertyDescriptor(this._preview.displayNameProperty);
+      }
+    }
     this._ensureProperties();
-    const desc = this._properties[name];
-    return desc ? this._convertPropertyDescriptor(desc) : undefined;
+    return this._convertPropertyDescriptor(this._properties[name]);
   },
 
   _ensureProperties() {
     if (!this._properties) {
       const id = this._data.id;
-      const properties =
+      this._properties =
         this._dbg._sendRequestAllowDiverge({ type: "getObjectProperties", id });
-      this._properties = Object.create(null);
-      properties.forEach(({name, desc}) => { this._properties[name] = desc; });
     }
   },
 
   _convertPropertyDescriptor(desc) {
+    if (!desc) {
+      return undefined;
+    }
     const rv = Object.assign({}, desc);
     if ("value" in desc) {
       rv.value = this._dbg._convertValue(desc.value);
     }
     if ("get" in desc) {
       rv.get = this._dbg._getObject(desc.get);
     }
     if ("set" in desc) {
       rv.set = this._dbg._getObject(desc.set);
     }
     return rv;
   },
 
-  _ensureProxyData() {
-    if (!this._proxyData) {
-      const data = this._dbg._sendRequestAllowDiverge({
-        type: "objectProxyData",
-        id: this._data.id,
-      });
-      if (data.exception) {
-        throw new Error(data.exception);
-      }
-      this._proxyData = data;
-    }
-  },
-
   unwrap() {
     if (!this.isProxy) {
       return this;
     }
-    this._ensureProxyData();
-    return this._dbg._convertValue(this._proxyData.unwrapped);
+    return this._dbg._convertValue(this._data.proxyUnwrapped);
   },
 
   get proxyTarget() {
-    this._ensureProxyData();
-    return this._dbg._convertValue(this._proxyData.target);
+    return this._dbg._convertValue(this._data.proxyTarget);
   },
 
   get proxyHandler() {
-    this._ensureProxyData();
-    return this._dbg._convertValue(this._proxyData.handler);
+    return this._dbg._convertValue(this._data.proxyHandler);
   },
 
   get boundTargetFunction() {
     if (this.isBoundFunction) {
       return this._dbg._getObject(this._data.boundTargetFunction);
     }
     return undefined;
   },
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -208,17 +208,17 @@ dbg.onNewScript = function(script) {
 ///////////////////////////////////////////////////////////////////////////////
 
 // Snapshots are generated for objects that might be inspected at times when we
 // are not paused at the point where the snapshot was originally taken. The
 // snapshot data is provided to the server, which can use it to provide limited
 // answers to the client about the object's contents, without having to consult
 // a child process.
 
-function snapshotObjectProperty({ name, desc }) {
+function snapshotObjectProperty([ name, desc ]) {
   // Only capture primitive properties in object snapshots.
   if ("value" in desc && !convertedValueIsObject(desc.value)) {
     return { name, desc };
   }
   return { name, desc: { value: "<unavailable>" } };
 }
 
 function makeObjectSnapshot(object) {
@@ -238,17 +238,17 @@ function makeObjectSnapshot(object) {
     class: object.class,
     name: object.name,
     displayName: object.displayName,
     parameterNames: object.parameterNames,
     isProxy: object.isProxy,
     isExtensible: object.isExtensible(),
     isSealed: object.isSealed(),
     isFrozen: object.isFrozen(),
-    properties: getObjectProperties(object).map(snapshotObjectProperty),
+    properties: Object.entries(getObjectProperties(object)).map(snapshotObjectProperty),
   };
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Console Message State
 ///////////////////////////////////////////////////////////////////////////////
 
 const gConsoleMessages = [];
@@ -619,62 +619,309 @@ function getSourceData(id) {
     sourceMapURL: source.sourceMapURL,
   };
 }
 
 function forwardToScript(name) {
   return request => gScripts.getObject(request.id)[name](request.value);
 }
 
+function getFrameData(index) {
+  const frame = scriptFrameForIndex(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,
+    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,
+  };
+}
+
 function unknownObjectProperties(why) {
   return [{
     name: "Unknown properties",
     desc: {
       value: why,
       enumerable: true,
     },
   }];
 }
 
+function getObjectData(id) {
+  const object = gPausedObjects.getObject(id);
+  if (object instanceof Debugger.Object) {
+    const rv = {
+      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),
+      isProxy: object.isProxy,
+      isExtensible: object.isExtensible(),
+      isSealed: object.isSealed(),
+      isFrozen: object.isFrozen(),
+    };
+    if (rv.isBoundFunction) {
+      rv.boundTargetFunction = getObjectId(object.boundTargetFunction);
+      rv.boundThis = convertValue(object.boundThis);
+      rv.boundArguments = getObjectId(makeDebuggeeValue(object.boundArguments));
+    }
+    if (rv.isProxy) {
+      rv.proxyUnwrapped = convertValue(object.unwrap());
+      rv.proxyTarget = convertValue(object.proxyTarget);
+      rv.proxyHandler = convertValue(object.proxyHandler);
+    }
+    return rv;
+  }
+  if (object instanceof Debugger.Environment) {
+    return {
+      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");
+}
+
 function getObjectProperties(object) {
   let names;
   try {
     names = object.getOwnPropertyNames();
   } catch (e) {
     return unknownObjectProperties(e.toString());
   }
 
-  return names.map(name => {
+  const rv = Object.create(null);
+  names.forEach(name => {
     let desc;
     try {
       desc = object.getOwnPropertyDescriptor(name);
     } catch (e) {
-      return { name, desc: { value: "Unknown: " + e, enumerable: true } };
+      desc = { name, desc: { value: "Unknown: " + e, enumerable: true } };
     }
     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 };
+    rv[name] = desc;
   });
+  return rv;
+}
+
+function getEnvironmentNames(env) {
+  try {
+    const names = env.names();
+
+    return names.map(name => {
+      return { name, value: convertValue(env.getVariable(name)) };
+    });
+  } catch (e) {
+    return [{name: "Unknown names",
+             value: "Exception thrown in getEnvironmentNames" }];
+  }
 }
 
 function getWindow() {
   // Hopefully there is exactly one window in this enumerator.
   for (const window of Services.ww.getWindowEnumerator()) {
     return window;
   }
   return null;
 }
 
+// Maximum number of properties the server is interested in when previewing an
+// object.
+const OBJECT_PREVIEW_MAX_ITEMS = 10;
+
+// When the replaying process pauses, the server needs to inspect a lot of state
+// around frames, objects, etc. in order to fill in all the information the
+// client needs to update the UI for the pause location. Done naively, this
+// inspection requires a lot of back and forth with the replaying process to
+// get all this data. This is bad for performance, and especially so if the
+// replaying process is on a different machine from the server. Instead, the
+// debugger running in the server can request a pause data packet which includes
+// everything the server will need.
+//
+// This should avoid overapproximation, so that we can quickly send pause data
+// across a network connection, and especially should not underapproximate
+// as the server will end up needing to make more requests before the client can
+// finish pausing.
+function getPauseData() {
+  const numFrames = countScriptFrames();
+  if (!numFrames) {
+    return {};
+  }
+
+  const rv = {
+    frames: [],
+    scripts: {},
+    offsetMetadata: [],
+    objects: {},
+    environments: {},
+  };
+
+  function addValue(value, includeProperties) {
+    if (value && typeof value == "object" && value.object) {
+      addObject(value.object, includeProperties);
+    }
+  }
+
+  function addObject(id, includeProperties) {
+    if (!id) {
+      return;
+    }
+
+    // If includeProperties is set then previewing the object requires knowledge
+    // of its enumerable properties.
+    const needObject = !rv.objects[id];
+    const needProperties =
+      includeProperties &&
+      (needObject || !rv.objects[id].preview.enumerableOwnProperties);
+
+    if (!needObject && !needProperties) {
+      return;
+    }
+
+    const object = gPausedObjects.getObject(id);
+    assert(object instanceof Debugger.Object);
+
+    const properties = getObjectProperties(object);
+    const propertyEntries = Object.entries(properties);
+
+    if (needObject) {
+      rv.objects[id] = {
+        data: getObjectData(id),
+        preview: {
+          ownPropertyNamesCount: propertyEntries.length,
+        },
+      };
+
+      const preview = rv.objects[id].preview;
+
+      // Add some properties (if present) which the server might ask for
+      // even when it isn't interested in the rest of the properties.
+      if (properties.length) {
+        preview.lengthProperty = properties.length;
+      }
+      if (properties.displayName) {
+        preview.displayNameProperty = properties.displayName;
+      }
+    }
+
+    if (needProperties) {
+      const preview = rv.objects[id].preview;
+
+      // The server is only interested in enumerable properties, and at most
+      // OBJECT_PREVIEW_MAX_ITEMS of them. Limiting the properties we send to
+      // only those the server needs avoids having to send the contents of huge
+      // objects like Windows, most of which will not be used.
+      const enumerableOwnProperties = Object.create(null);
+      let enumerablePropertyCount = 0;
+      for (const [ name, desc ] of propertyEntries) {
+        if (desc.enumerable) {
+          enumerableOwnProperties[name] = desc;
+          addPropertyDescriptor(desc, false);
+          if (++enumerablePropertyCount == OBJECT_PREVIEW_MAX_ITEMS) {
+            break;
+          }
+        }
+      }
+      preview.enumerableOwnProperties = enumerableOwnProperties;
+    }
+  }
+
+  function addPropertyDescriptor(desc, includeProperties) {
+    if (desc.value) {
+      addValue(desc.value, includeProperties);
+    }
+    if (desc.get) {
+      addObject(desc.get, includeProperties);
+    }
+    if (desc.set) {
+      addObject(desc.set, includeProperties);
+    }
+  }
+
+  function addEnvironment(id) {
+    if (!id || rv.environments[id]) {
+      return;
+    }
+
+    const env = gPausedObjects.getObject(id);
+    assert(env instanceof Debugger.Environment);
+
+    const data = getObjectData(id);
+    const names = getEnvironmentNames(env);
+    rv.environments[id] = { data, names };
+
+    addEnvironment(data.parent);
+  }
+
+  // eslint-disable-next-line no-shadow
+  function addScript(id) {
+    if (!rv.scripts[id]) {
+      rv.scripts[id] = getScriptData(id);
+    }
+  }
+
+  for (let i = 0; i < numFrames; i++) {
+    const frame = getFrameData(i);
+    const script = gScripts.getObject(frame.script);
+    rv.frames.push(frame);
+    rv.offsetMetadata.push({
+      scriptId: frame.script,
+      offset: frame.offset,
+      metadata: script.getOffsetMetadata(frame.offset),
+    });
+    addScript(frame.script);
+    addValue(frame.this, true);
+    if (frame.arguments) {
+      for (const arg of frame.arguments) {
+        addValue(arg, true);
+      }
+    }
+    addObject(frame.callee, false);
+    addEnvironment(frame.environment, true);
+  }
+
+  return rv;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Handlers
 ///////////////////////////////////////////////////////////////////////////////
 
 const gRequestHandlers = {
 
   repaint() {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
@@ -728,80 +975,28 @@ const gRequestHandlers = {
     return sources;
   },
 
   getSource(request) {
     return getSourceData(request.id);
   },
 
   getObject(request) {
-    const object = gPausedObjects.getObject(request.id);
-    if (object instanceof Debugger.Object) {
-      const rv = {
-        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),
-        isProxy: object.isProxy,
-        isExtensible: object.isExtensible(),
-        isSealed: object.isSealed(),
-        isFrozen: object.isFrozen(),
-      };
-      if (rv.isBoundFunction) {
-        rv.boundTargetFunction = getObjectId(object.boundTargetFunction);
-        rv.boundThis = convertValue(object.boundThis);
-        rv.boundArguments = getObjectId(makeDebuggeeValue(object.boundArguments));
-      }
-      return rv;
-    }
-    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");
+    return getObjectData(request.id);
   },
 
   getObjectProperties(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return unknownObjectProperties("Recording divergence in getObjectProperties");
     }
 
     const object = gPausedObjects.getObject(request.id);
     return getObjectProperties(object);
   },
 
-  objectProxyData(request) {
-    if (!RecordReplayControl.maybeDivergeFromRecording()) {
-      return { exception: "Recording divergence in unwrapObject" };
-    }
-    const obj = gPausedObjects.getObject(request.id);
-    return {
-      unwrapped: convertValue(obj.unwrap()),
-      target: convertValue(obj.proxyTarget),
-      handler: convertValue(obj.proxyHandler),
-    };
-  },
-
   objectApply(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return { throw: "Recording divergence in objectApply" };
     }
     const obj = gPausedObjects.getObject(request.id);
     const thisv = convertValueFromParent(request.thisv);
     const args = request.args.map(v => convertValueFromParent(v));
     const rv = obj.apply(thisv, args);
@@ -809,61 +1004,40 @@ const gRequestHandlers = {
   },
 
   getEnvironmentNames(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return [{name: "Unknown names",
                value: "Recording divergence in getEnvironmentNames" }];
     }
 
-    try {
-      const env = gPausedObjects.getObject(request.id);
-      const names = env.names();
-
-      return names.map(name => {
-        return { name, value: convertValue(env.getVariable(name)) };
-      });
-    } catch (e) {
-      return [{name: "Unknown names",
-               value: "Exception thrown in getEnvironmentNames" }];
-    }
+    const env = gPausedObjects.getObject(request.id);
+    return getEnvironmentNames(env);
   },
 
   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);
+    return getFrameData(request.index);
+  },
 
-    let _arguments = null;
-    if (frame.arguments) {
-      _arguments = [];
-      for (let i = 0; i < frame.arguments.length; i++) {
-        _arguments.push(convertValue(frame.arguments[i]));
-      }
+  pauseData(request) {
+    if (!RecordReplayControl.maybeDivergeFromRecording()) {
+      return { error: "Recording divergence in pauseData" };
     }
 
-    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,
-    };
+    return getPauseData();
   },
 
   getLineOffsets: forwardToScript("getLineOffsets"),
   getOffsetLocation: forwardToScript("getOffsetLocation"),
   getSuccessorOffsets: forwardToScript("getSuccessorOffsets"),
   getPredecessorOffsets: forwardToScript("getPredecessorOffsets"),
   getAllColumnOffsets: forwardToScript("getAllColumnOffsets"),
   getOffsetMetadata: forwardToScript("getOffsetMetadata"),