Bug 1392614 - Add an onEnumSymbols function on the object actor. r=ochameau draft
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Tue, 22 Aug 2017 12:07:48 +0200
changeset 651926 377526321e04e28ffc58ed7af7f4325b6e1ee66d
parent 650941 7c50f0c999c5bf8ee915261997597a5a9b8fb2ae
child 727922 9adbe35b2dbb84866395744cd2cb3dcacb42009f
push id75875
push userbmo:nchevobbe@mozilla.com
push dateThu, 24 Aug 2017 07:10:11 +0000
reviewersochameau
bugs1392614
milestone57.0a1
Bug 1392614 - Add an onEnumSymbols function on the object actor. r=ochameau This will enable lazy loading symbols for a grip if needed. This patch introduce a new SymbolIteratorActor, which is similar to PropertyIteratorActor but with a data structure that better fits symbols, and how we already handle them, i.e. an `ownSymbols` array property (and not an `ownProperties` object like PropertyIteratorActor). We take this as an opportunity to add a test for enumSymbols, but also for enumProperties that did not have server unit tests (it is only tested in the frontend with the variable view, which might be deprecated at some point). MozReview-Commit-ID: IEIKA8zwH90
devtools/server/actors/object.js
devtools/server/tests/unit/test_objectgrips-18.js
devtools/server/tests/unit/xpcshell.ini
devtools/shared/client/main.js
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -258,18 +258,41 @@ ObjectActor.prototype = {
   onEnumEntries: function () {
     let actor = new PropertyIteratorActor(this, { enumEntries: true });
     this.registeredPool.addActor(actor);
     this.iterators.add(actor);
     return { iterator: actor.grip() };
   },
 
   /**
+   * Creates an actor to iterate over an object symbols properties.
+   */
+  onEnumSymbols: function () {
+    let actor = new SymbolIteratorActor(this);
+    this.registeredPool.addActor(actor);
+    this.iterators.add(actor);
+    return { iterator: actor.grip() };
+  },
+
+  /**
    * Handle a protocol request to provide the prototype and own properties of
    * the object.
+   *
+   * @returns {Object} An object containing the data of this.obj, of the following form:
+   *          - {string} from: this.obj's actorID.
+   *          - {Object} prototype: The descriptor of this.obj's prototype.
+   *          - {Object} ownProperties: an object where the keys are the names of the
+   *                     this.obj's ownProperties, and the values the descriptors of
+   *                     the properties.
+   *          - {Array} ownSymbols: An array containing all descriptors of this.obj's
+   *                    ownSymbols. Here we have an array, and not an object like for
+   *                    ownProperties, because we can have multiple symbols with the same
+   *                    name in this.obj, e.g. `{[Symbol()]: "a", [Symbol()]: "b"}`.
+   *          - {Object} safeGetterValues: an object that maps this.obj's property names
+   *                     with safe getters descriptors.
    */
   onPrototypeAndProperties: function () {
     let ownProperties = Object.create(null);
     let ownSymbols = [];
     let names;
     let symbols;
     try {
       names = this.obj.getOwnPropertyNames();
@@ -746,16 +769,17 @@ ObjectActor.prototype.requestTypes = {
   "decompile": ObjectActor.prototype.onDecompile,
   "release": ObjectActor.prototype.onRelease,
   "scope": ObjectActor.prototype.onScope,
   "dependentPromises": ObjectActor.prototype.onDependentPromises,
   "allocationStack": ObjectActor.prototype.onAllocationStack,
   "fulfillmentStack": ObjectActor.prototype.onFulfillmentStack,
   "rejectionStack": ObjectActor.prototype.onRejectionStack,
   "enumEntries": ObjectActor.prototype.onEnumEntries,
+  "enumSymbols": ObjectActor.prototype.onEnumSymbols,
 };
 
 /**
  * Creates an actor to iterate over an object's property names and values.
  *
  * @param objectActor ObjectActor
  *        The object actor.
  * @param options Object
@@ -828,17 +852,17 @@ PropertyIteratorActor.prototype = {
       ownProperties[name] = this.iterator.propertyDescription(i);
     }
     return {
       ownProperties
     };
   },
 
   all() {
-    return this.slice({ start: 0, count: this.length });
+    return this.slice({ start: 0, count: this.iterator.size });
   }
 };
 
 PropertyIteratorActor.prototype.requestTypes = {
   "names": PropertyIteratorActor.prototype.names,
   "slice": PropertyIteratorActor.prototype.slice,
   "all": PropertyIteratorActor.prototype.all,
 };
@@ -1121,16 +1145,68 @@ function enumWeakSetEntries(objectActor)
         enumerable: true,
         value: gripFromEntry(objectActor, val)
       };
     }
   };
 }
 
 /**
+ * Creates an actor to iterate over an object's symbols.
+ *
+ * @param objectActor ObjectActor
+ *        The object actor.
+ */
+function SymbolIteratorActor(objectActor) {
+  const symbols =  objectActor.obj.getOwnPropertySymbols();
+
+  this.iterator = {
+    size: symbols.length,
+    symbolDescription(index) {
+      const symbol = symbols[index];
+      return {
+        name: symbol.toString(),
+        descriptor: objectActor._propertyDescriptor(symbol, true)
+      };
+    }
+  };
+}
+
+SymbolIteratorActor.prototype = {
+  actorPrefix: "symbolIterator",
+
+  grip() {
+    return {
+      type: this.actorPrefix,
+      actor: this.actorID,
+      count: this.iterator.size
+    };
+  },
+
+  slice({ start, count }) {
+    let ownSymbols = [];
+    for (let i = start, m = start + count; i < m; i++) {
+      ownSymbols.push(this.iterator.symbolDescription(i));
+    }
+    return {
+      ownSymbols
+    };
+  },
+
+  all() {
+    return this.slice({ start: 0, count: this.iterator.size });
+  }
+};
+
+SymbolIteratorActor.prototype.requestTypes = {
+  "slice": SymbolIteratorActor.prototype.slice,
+  "all": SymbolIteratorActor.prototype.all,
+};
+
+/**
  * Functions for adding information to ObjectActor grips for the purpose of
  * having customized output. This object holds arrays mapped by
  * Debugger.Object.prototype.class.
  *
  * In each array you can add functions that take three
  * arguments:
  *   - the ObjectActor instance and its hooks to make a preview for,
  *   - the grip object being prepared for the client,
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_objectgrips-18.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+var gCallback;
+
+function run_test() {
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+}
+
+async function run_test_with_server(server, callback) {
+  gCallback = callback;
+  initTestDebuggerServer(server);
+  gDebuggee = addTestGlobal("test-grips", server);
+  gDebuggee.eval(function stopMe(arg1) {
+    debugger;
+  }.toString());
+
+  gClient = new DebuggerClient(server.connectPipe());
+  await gClient.connect();
+  const [,, threadClient] = await attachTestTabAndResume(gClient, "test-grips");
+  gThreadClient = threadClient;
+  test_object_grip();
+}
+
+async function test_object_grip() {
+  gThreadClient.addOneTimeListener("paused", async function (event, packet) {
+    let [grip] = packet.frame.arguments;
+
+    let objClient = gThreadClient.pauseGrip(grip);
+
+    // Checks the result of enumProperties.
+    let response = await objClient.enumProperties({});
+    await check_enum_properties(response);
+
+    // Checks the result of enumSymbols.
+    response = await objClient.enumSymbols();
+    await check_enum_symbols(response);
+
+    await gThreadClient.resume();
+    await gClient.close();
+    gCallback();
+  });
+
+  gDebuggee.eval(`
+    var obj = Array.from({length: 10})
+      .reduce((res, _, i) => {
+        res["property_" + i + "_key"] = "property_" + i + "_value";
+        res[Symbol("symbol_" + i)] = "symbol_" + i + "_value";
+        return res;
+      }, {});
+
+    obj[Symbol()] = "first unnamed symbol";
+    obj[Symbol()] = "second unnamed symbol";
+    obj[Symbol.iterator] = function* () {
+      yield 1;
+      yield 2;
+    };
+
+    stopMe(obj);
+  `);
+}
+
+async function check_enum_properties(response) {
+  do_print("Check enumProperties response");
+  ok(response && Object.getOwnPropertyNames(response).includes("iterator"),
+    "The response object has an iterator property");
+
+  const {iterator} = response;
+  equal(iterator.count, 10, "iterator.count has the expected value");
+
+  do_print("Check iterator.slice response for all properties");
+  let sliceResponse = await iterator.slice(0, iterator.count);
+  ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
+    "The response object has an ownProperties property");
+
+  let {ownProperties} = sliceResponse;
+  let names = Object.keys(ownProperties);
+  equal(names.length, iterator.count,
+    "The response has the expected number of properties");
+  for (let i = 0; i < names.length; i++) {
+    const name = names[i];
+    equal(name, `property_${i}_key`);
+    equal(ownProperties[name].value, `property_${i}_value`);
+  }
+
+  do_print("Check iterator.all response");
+  let allResponse = await iterator.all();
+  deepEqual(allResponse, sliceResponse, "iterator.all response has the expected data");
+
+  do_print("Check iterator response for 2 properties only");
+  sliceResponse = await iterator.slice(2, 2);
+  ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
+    "The response object has an ownProperties property");
+
+  ownProperties = sliceResponse.ownProperties;
+  names = Object.keys(ownProperties);
+  equal(names.length, 2, "The response has the expected number of properties");
+  equal(names[0], `property_2_key`);
+  equal(names[1], `property_3_key`);
+  equal(ownProperties[names[0]].value, `property_2_value`);
+  equal(ownProperties[names[1]].value, `property_3_value`);
+}
+
+async function check_enum_symbols(response) {
+  do_print("Check enumProperties response");
+  ok(response && Object.getOwnPropertyNames(response).includes("iterator"),
+    "The response object has an iterator property");
+
+  const {iterator} = response;
+  equal(iterator.count, 13, "iterator.count has the expected value");
+
+  do_print("Check iterator.slice response for all symbols");
+  let sliceResponse = await iterator.slice(0, iterator.count);
+  ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"),
+    "The response object has an ownSymbols property");
+
+  let {ownSymbols} = sliceResponse;
+  equal(ownSymbols.length, iterator.count,
+    "The response has the expected number of symbols");
+  for (let i = 0; i < 10; i++) {
+    const symbol = ownSymbols[i];
+    equal(symbol.name, `Symbol(symbol_${i})`);
+    equal(symbol.descriptor.value, `symbol_${i}_value`);
+  }
+  const firstUnnamedSymbol = ownSymbols[10];
+  equal(firstUnnamedSymbol.name, "Symbol()");
+  equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol");
+
+  const secondUnnamedSymbol = ownSymbols[11];
+  equal(secondUnnamedSymbol.name, "Symbol()");
+  equal(secondUnnamedSymbol.descriptor.value, "second unnamed symbol");
+
+  const iteratorSymbol = ownSymbols[12];
+  equal(iteratorSymbol.name, "Symbol(Symbol.iterator)");
+  equal(iteratorSymbol.descriptor.value.class, "Function");
+
+  do_print("Check iterator.all response");
+  let allResponse = await iterator.all();
+  deepEqual(allResponse, sliceResponse, "iterator.all response has the expected data");
+
+  do_print("Check iterator response for 2 symbols only");
+  sliceResponse = await iterator.slice(9, 2);
+  ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"),
+    "The response object has an ownSymbols property");
+
+  ownSymbols = sliceResponse.ownSymbols;
+  equal(ownSymbols.length, 2, "The response has the expected number of symbols");
+  equal(ownSymbols[0].name, "Symbol(symbol_9)");
+  equal(ownSymbols[0].descriptor.value, "symbol_9_value");
+  equal(ownSymbols[1].name, "Symbol()");
+  equal(ownSymbols[1].descriptor.value, "first unnamed symbol");
+}
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -168,16 +168,17 @@ reason = only ran on B2G
 [test_objectgrips-10.js]
 [test_objectgrips-11.js]
 [test_objectgrips-12.js]
 [test_objectgrips-13.js]
 [test_objectgrips-14.js]
 [test_objectgrips-15.js]
 [test_objectgrips-16.js]
 [test_objectgrips-17.js]
+[test_objectgrips-18.js]
 [test_promise_state-01.js]
 [test_promise_state-02.js]
 [test_promise_state-03.js]
 [test_interrupt.js]
 [test_stepping-01.js]
 [test_stepping-02.js]
 [test_stepping-03.js]
 [test_stepping-04.js]
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -2655,16 +2655,40 @@ ObjectClient.prototype = {
           iterator: new PropertyIteratorClient(this._client, response.iterator)
         };
       }
       return response;
     }
   }),
 
   /**
+   * Request a SymbolIteratorClient instance to enumerate symbols in an object.
+   *
+   * @param onResponse function Called with the request's response.
+   */
+  enumSymbols: DebuggerClient.requester({
+    type: "enumSymbols"
+  }, {
+    before: function (packet) {
+      if (this._grip.type !== "object") {
+        throw new Error("enumSymbols is only valid for objects grips.");
+      }
+      return packet;
+    },
+    after: function (response) {
+      if (response.iterator) {
+        return {
+          iterator: new SymbolIteratorClient(this._client, response.iterator)
+        };
+      }
+      return response;
+    }
+  }),
+
+  /**
    * Request the property descriptor of the object's specified property.
    *
    * @param name string The name of the requested property.
    * @param onResponse function Called with the request's response.
    */
   getProperty: DebuggerClient.requester({
     type: "property",
     name: arg(0)
@@ -2832,16 +2856,71 @@ PropertyIteratorClient.prototype = {
    *        The function called when we receive the property values.
    */
   all: DebuggerClient.requester({
     type: "all"
   }, {}),
 };
 
 /**
+ * A SymbolIteratorClient provides a way to access to symbols
+ * of an object efficiently, slice by slice.
+ *
+ * @param client DebuggerClient
+ *        The debugger client parent.
+ * @param grip Object
+ *        A SymbolIteratorActor grip returned by the protocol via
+ *        TabActor.enumSymbols request.
+ */
+function SymbolIteratorClient(client, grip) {
+  this._grip = grip;
+  this._client = client;
+  this.request = this._client.request;
+}
+
+SymbolIteratorClient.prototype = {
+  get actor() {
+    return this._grip.actor;
+  },
+
+  /**
+   * Get the total number of symbols available in the iterator.
+   */
+  get count() {
+    return this._grip.count;
+  },
+
+  /**
+   * Get a set of following symbols.
+   *
+   * @param start Number
+   *        The index of the first symbol to fetch.
+   * @param count Number
+   *        The number of symbols to fetch.
+   * @param callback Function
+   *        The function called when we receive the symbols.
+   */
+  slice: DebuggerClient.requester({
+    type: "slice",
+    start: arg(0),
+    count: arg(1)
+  }, {}),
+
+  /**
+   * Get all the symbols.
+   *
+   * @param callback Function
+   *        The function called when we receive the symbols.
+   */
+  all: DebuggerClient.requester({
+    type: "all"
+  }, {}),
+};
+
+/**
  * A ArrayBufferClient provides a way to access ArrayBuffer from the
  * debugger server.
  *
  * @param client DebuggerClient
  *        The debugger client parent.
  * @param grip Object
  *        A pause-lifetime ArrayBuffer grip returned by the protocol.
  */