Bug 1526832 Part 1 - Allow snapshots when sending objects to the middleman, r=lsmyth.
authorBrian Hackett <bhackett1024@gmail.com>
Sun, 10 Feb 2019 14:51:26 -1000
changeset 459557 b845c53036f494719110f400338dcae05031039f
parent 459556 cb355b458bbf165b878f9ea76c09145a8c29f4c1
child 459558 b23892c07b8591cb99262bac3d97719c537036b0
push id111967
push userbhackett@mozilla.com
push dateFri, 15 Feb 2019 20:03:08 +0000
treeherdermozilla-inbound@b23892c07b85 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsmyth
bugs1526832
milestone67.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 1526832 Part 1 - Allow snapshots when sending objects to the middleman, r=lsmyth.
devtools/server/actors/replay/debugger.js
devtools/server/actors/replay/replay.js
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -377,18 +377,19 @@ ReplayDebugger.prototype = {
               type: "getFrame",
               index: NewestFrameIndex,
             });
             if ("index" in frameData) {
               const rv = this._searchControl.sendRequest({
                 type: "frameEvaluate",
                 index: frameData.index,
                 text,
+                convertOptions: { snapshot: true },
               });
-              evaluateResult = this._convertCompletionValue(rv, { forSearch: true });
+              evaluateResult = this._convertCompletionValue(rv);
             }
           }
           results.push(point);
           callback(point, evaluateResult);
         }
       }
     }
   },
@@ -525,69 +526,59 @@ ReplayDebugger.prototype = {
     const data = this._sendRequest({ type: "findSources" });
     return data.map(source => this._addSource(source));
   },
 
   /////////////////////////////////////////////////////////
   // Object methods
   /////////////////////////////////////////////////////////
 
-  _getObject(id, options) {
-    if (options && options.forSearch) {
-      // Returning objects through searches is NYI.
-      return "<UnknownSearchObject>";
-    }
-    const forConsole = options && options.forConsole;
-
+  _getObject(id) {
     if (id && !this._objects[id]) {
       const data = this._sendRequest({ type: "getObject", id });
       switch (data.kind) {
       case "Object":
-        // Objects which |forConsole| is set are objects that were logged in
-        // console messages, and had their properties recorded so that they can
-        // be inspected without switching to a replaying child.
-        this._objects[id] = new ReplayDebuggerObject(this, data, forConsole);
+        this._objects[id] = new ReplayDebuggerObject(this, data);
         break;
       case "Environment":
         this._objects[id] = new ReplayDebuggerEnvironment(this, data);
         break;
       default:
         ThrowError("Unknown object kind");
       }
     }
-    const rv = this._objects[id];
-    if (forConsole) {
-      rv._forConsole = true;
-    }
-    return rv;
+    return this._objects[id];
   },
 
   // Convert a value we received from the child.
-  _convertValue(value, options) {
+  _convertValue(value) {
     if (isNonNullObject(value)) {
       if (value.object) {
-        return this._getObject(value.object, options);
+        return this._getObject(value.object);
+      }
+      if (value.snapshot) {
+        return new ReplayDebuggerObjectSnapshot(this, value.snapshot);
       }
       switch (value.special) {
       case "undefined": return undefined;
       case "Infinity": return Infinity;
       case "-Infinity": return -Infinity;
       case "NaN": return NaN;
       case "0": return -0;
       }
     }
     return value;
   },
 
-  _convertCompletionValue(value, options) {
+  _convertCompletionValue(value) {
     if ("return" in value) {
-      return { return: this._convertValue(value.return, options) };
+      return { return: this._convertValue(value.return) };
     }
     if ("throw" in value) {
-      return { throw: this._convertValue(value.throw, options) };
+      return { throw: this._convertValue(value.throw) };
     }
     ThrowError("Unexpected completion value");
     return null; // For eslint
   },
 
   // Convert a value for sending to the child.
   _convertValueForChild(value) {
     if (isNonNullObject(value)) {
@@ -642,18 +633,17 @@ ReplayDebugger.prototype = {
   // Console Message methods
   /////////////////////////////////////////////////////////
 
   _convertConsoleMessage(message) {
     // Console API message arguments need conversion to debuggee values, but
     // other contents of the message can be left alone.
     if (message.messageType == "ConsoleAPI" && message.arguments) {
       for (let i = 0; i < message.arguments.length; i++) {
-        message.arguments[i] = this._convertValue(message.arguments[i],
-                                                  { forConsole: true });
+        message.arguments[i] = this._convertValue(message.arguments[i]);
       }
     }
     return message;
   },
 
   /////////////////////////////////////////////////////////
   // Handlers
   /////////////////////////////////////////////////////////
@@ -897,20 +887,19 @@ ReplayDebuggerFrame.prototype = {
   get implementation() { NYI(); },
   evalWithBindings: NYI,
 };
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerObject
 ///////////////////////////////////////////////////////////////////////////////
 
-function ReplayDebuggerObject(dbg, data, forConsole) {
+function ReplayDebuggerObject(dbg, data) {
   this._dbg = dbg;
   this._data = data;
-  this._forConsole = forConsole;
   this._properties = null;
   this._proxyData = null;
 }
 
 ReplayDebuggerObject.prototype = {
   _invalidate() {
     this._data = null;
     this._properties = null;
@@ -928,28 +917,22 @@ ReplayDebuggerObject.prototype = {
   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; },
+  get proto() { return this._dbg._getObject(this._data.proto); },
 
   isExtensible() { return this._data.isExtensible; },
   isSealed() { return this._data.isSealed; },
   isFrozen() { return this._data.isFrozen; },
 
-  get proto() {
-    // Don't allow inspection of the prototypes of objects logged to the
-    // console. This is a hack that prevents the object inspector from crawling
-    // the object's prototype chain.
-    return this._forConsole ? null : this._dbg._getObject(this._data.proto);
-  },
-
   unsafeDereference() {
     // Direct access to the referent is not currently available.
     return null;
   },
 
   getOwnPropertyNames() {
     this._ensureProperties();
     return Object.keys(this._properties);
@@ -964,19 +947,18 @@ ReplayDebuggerObject.prototype = {
     this._ensureProperties();
     const desc = this._properties[name];
     return desc ? this._convertPropertyDescriptor(desc) : undefined;
   },
 
   _ensureProperties() {
     if (!this._properties) {
       const id = this._data.id;
-      const properties = this._forConsole
-        ? this._dbg._sendRequest({ type: "getObjectPropertiesForConsole", id })
-        : this._dbg._sendRequestAllowDiverge({ type: "getObjectProperties", id });
+      const properties =
+        this._dbg._sendRequestAllowDiverge({ type: "getObjectProperties", id });
       this._properties = Object.create(null);
       properties.forEach(({name, desc}) => { this._properties[name] = desc; });
     }
   },
 
   _convertPropertyDescriptor(desc) {
     const rv = Object.assign({}, desc);
     if ("value" in desc) {
@@ -1055,16 +1037,33 @@ ReplayDebuggerObject.prototype = {
   freeze: NotAllowed,
   defineProperty: NotAllowed,
   defineProperties: NotAllowed,
   deleteProperty: NotAllowed,
   forceLexicalInitializationByName: NotAllowed,
 };
 
 ///////////////////////////////////////////////////////////////////////////////
+// ReplayDebuggerObjectSnapshot
+///////////////////////////////////////////////////////////////////////////////
+
+// Create an object based on snapshot data which can be consulted without
+// communicating with the child process. This uses data provided by the child
+// process in the same format as for normal ReplayDebuggerObjects, except that
+// it does not contain references to any other objects.
+function ReplayDebuggerObjectSnapshot(dbg, data) {
+  this._dbg = dbg;
+  this._data = data;
+  this._properties = Object.create(null);
+  data.properties.forEach(({name, desc}) => { this._properties[name] = desc; });
+}
+
+ReplayDebuggerObjectSnapshot.prototype = ReplayDebuggerObject.prototype;
+
+///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerEnvironment
 ///////////////////////////////////////////////////////////////////////////////
 
 function ReplayDebuggerEnvironment(dbg, data) {
   this._dbg = dbg;
   this._data = data;
   this._names = null;
 }
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -186,34 +186,58 @@ dbg.onNewScript = function(script) {
 
   hitGlobalHandler("NewScript");
 
   // Check in case any handlers we need to install are on the scripts just
   // created.
   installPendingHandlers();
 };
 
-const gConsoleObjectProperties = new Map();
+///////////////////////////////////////////////////////////////////////////////
+// Object Snapshots
+///////////////////////////////////////////////////////////////////////////////
 
-function shouldSaveConsoleProperty({ desc }) {
-  // When logging an object to the console, only properties captured here will
-  // be shown. We limit this to non-object data properties, as more complex
-  // properties have two problems: A) to inspect them we will need to switch to
-  // a replaying child process, which is very slow when there are many console
-  // messages, and B) trying to access objects transitively referred to by
-  // logged console objects will fail when unpaused, and depends on the current
-  // state of the process otherwise.
-  return "value" in desc && !isNonNullObject(desc.value);
+// 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 }) {
+  // Only capture primitive properties in object snapshots.
+  if ("value" in desc && !convertedValueIsObject(desc.value)) {
+    return { name, desc };
+  }
+  return { name, desc: { value: "<unavailable>" } };
 }
 
-function saveConsoleObjectProperties(obj) {
-  if (obj instanceof Debugger.Object) {
-    const properties = getObjectProperties(obj).filter(shouldSaveConsoleProperty);
-    gConsoleObjectProperties.set(obj, properties);
-  }
+function makeObjectSnapshot(object) {
+  assert(object instanceof Debugger.Object);
+
+  // Include properties that would be included in a normal object's data packet,
+  // except do not allow inspection of any other referenced objects.
+  // In particular, don't set the prototype so that the object inspector will
+  // not attempt to crawl the object's prototype chain.
+  return {
+    kind: "Object",
+    callable: object.callable,
+    isBoundFunction: object.isBoundFunction,
+    isArrowFunction: object.isArrowFunction,
+    isGeneratorFunction: object.isGeneratorFunction,
+    isAsyncFunction: object.isAsyncFunction,
+    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),
+  };
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Console Message State
 ///////////////////////////////////////////////////////////////////////////////
 
 const gConsoleMessages = [];
 
@@ -276,37 +300,25 @@ Services.obs.addObserver({
     for (const id in apiMessage) {
       if (id != "wrappedJSObject" && id != "arguments") {
         contents[id] = JSON.parse(JSON.stringify(apiMessage[id]));
       }
     }
 
     // Message arguments are preserved as debuggee values.
     if (apiMessage.arguments) {
-      contents.arguments = apiMessage.arguments.map(makeDebuggeeValue);
-      contents.arguments.forEach(saveConsoleObjectProperties);
+      contents.arguments = apiMessage.arguments.map(v => {
+        return convertValue(makeDebuggeeValue(v), { snapshot: true });
+      });
     }
 
     newConsoleMessage("ConsoleAPI", null, contents);
   },
 }, "console-api-log-event");
 
-function convertConsoleMessage(contents) {
-  const result = {};
-  for (const id in contents) {
-    if (id == "arguments" && contents.messageType == "ConsoleAPI") {
-      // Copy arguments over as debuggee values.
-      result.arguments = contents.arguments.map(convertValue);
-    } else {
-      result[id] = contents[id];
-    }
-  }
-  return result;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // Position Handler State
 ///////////////////////////////////////////////////////////////////////////////
 
 // 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.
@@ -464,35 +476,43 @@ function getObjectId(obj) {
     assert((obj instanceof Debugger.Object) ||
            (obj instanceof Debugger.Environment));
     return gPausedObjects.add(obj);
   }
   return id;
 }
 
 // Convert a value for sending to the parent.
-function convertValue(value) {
+function convertValue(value, options) {
   if (value instanceof Debugger.Object) {
+    if (options && options.snapshot) {
+      return { snapshot: makeObjectSnapshot(value) };
+    }
     return { object: getObjectId(value) };
-  } else if (value === undefined ||
-             value == Infinity ||
-             value == -Infinity ||
-             Object.is(value, NaN) ||
-             Object.is(value, -0)) {
-      return { special: "" + value };
-    }
+  }
+  if (value === undefined ||
+      value == Infinity ||
+      value == -Infinity ||
+      Object.is(value, NaN) ||
+      Object.is(value, -0)) {
+    return { special: "" + value };
+  }
   return value;
 }
 
-function convertCompletionValue(value) {
+function convertedValueIsObject(value) {
+  return isNonNullObject(value) && "object" in value;
+}
+
+function convertCompletionValue(value, options) {
   if ("return" in value) {
-    return { return: convertValue(value.return) };
+    return { return: convertValue(value.return, options) };
   }
   if ("throw" in value) {
-    return { throw: convertValue(value.throw) };
+    return { throw: convertValue(value.throw, options) };
   }
   throw new Error("Unexpected completion value");
 }
 
 // Convert a value we received from the parent.
 function convertValueFromParent(value) {
   if (isNonNullObject(value)) {
     if (value.object) {
@@ -688,25 +708,16 @@ const gRequestHandlers = {
         },
       }];
     }
 
     const object = gPausedObjects.getObject(request.id);
     return getObjectProperties(object);
   },
 
-  getObjectPropertiesForConsole(request) {
-    const object = gPausedObjects.getObject(request.id);
-    const properties = gConsoleObjectProperties.get(object);
-    if (!properties) {
-      throw new Error("Console object properties not saved");
-    }
-    return properties;
-  },
-
   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),
@@ -789,29 +800,29 @@ const gRequestHandlers = {
 
   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);
+    return convertCompletionValue(rv, request.convertOptions);
   },
 
   popFrameResult(request) {
     return gPopFrameResult ? convertCompletionValue(gPopFrameResult) : {};
   },
 
   findConsoleMessages(request) {
-    return gConsoleMessages.map(convertConsoleMessage);
+    return gConsoleMessages;
   },
 
   getNewConsoleMessage(request) {
-    return convertConsoleMessage(gConsoleMessages[gConsoleMessages.length - 1]);
+    return gConsoleMessages[gConsoleMessages.length - 1];
   },
 
   currentExecutionPoint(request) {
     return RecordReplayControl.currentExecutionPoint();
   },
 
   recordingEndpoint(request) {
     return RecordReplayControl.recordingEndpoint();