Bug 1023386 - Split and filter properties remotely for objects. r=past
authorAlexandre Poirot <poirot.alex@gmail.com>
Mon, 25 May 2015 20:11:59 +0200
changeset 245599 8653d81c62e4bdf541d9dd4748ed3282e85c8fb7
parent 245598 f8f27a8cc29876453607e1b32e55d5d1c8ac7f10
child 245600 b85bc83d5a15e92fd32ca3476a012e1360b5c102
push id13165
push userapoirot@mozilla.com
push dateTue, 26 May 2015 16:07:06 +0000
treeherderfx-team@8653d81c62e4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs1023386
milestone41.0a1
Bug 1023386 - Split and filter properties remotely for objects. r=past
browser/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js
browser/devtools/shared/widgets/VariablesView.jsm
browser/devtools/shared/widgets/VariablesViewController.jsm
toolkit/components/telemetry/Histograms.json
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/script.js
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js
@@ -65,20 +65,17 @@ function initialChecks() {
 
   is(bufferVar.expanded, false,
     "The 'buffer' variable shouldn't be expanded.");
   is(arrayVar.expanded, false,
     "The 'largeArray' variable shouldn't be expanded.");
   is(objectVar.expanded, false,
     "The 'largeObject' variable shouldn't be expanded.");
 
-  let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2);
-  arrayVar.expand();
-  objectVar.expand();
-  return finished;
+  return promise.all([arrayVar.expand(),objectVar.expand()]);
 }
 
 function verifyFirstLevel() {
   let localScope = gVariables.getScopeAtIndex(0);
   let arrayVar = localScope.get("largeArray");
   let objectVar = localScope.get("largeObject");
 
   let arrayEnums = arrayVar.target.querySelector(".variables-view-element-details.enum").childNodes;
@@ -91,147 +88,134 @@ function verifyFirstLevel() {
   let objectEnums = objectVar.target.querySelector(".variables-view-element-details.enum").childNodes;
   let objectNonEnums = objectVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
   is(objectEnums.length, 0,
     "The 'largeObject' shouldn't contain any enumerable elements.");
   is(objectNonEnums.length, 5,
     "The 'largeObject' should contain all the created non-enumerable elements.");
 
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    0 + gEllipsis + 1999, "The first page in the 'largeArray' is named correctly.");
+    "[0" + gEllipsis + "2499]", "The first page in the 'largeArray' is named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
     "", "The first page in the 'largeArray' should not have a corresponding value.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    2000 + gEllipsis + 3999, "The second page in the 'largeArray' is named correctly.");
+    "[2500" + gEllipsis + "4999]", "The second page in the 'largeArray' is named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
     "", "The second page in the 'largeArray' should not have a corresponding value.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
-    4000 + gEllipsis + 5999, "The third page in the 'largeArray' is named correctly.");
+    "[5000" + gEllipsis + "7499]", "The third page in the 'largeArray' is named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"),
     "", "The third page in the 'largeArray' should not have a corresponding value.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
-    6000 + gEllipsis + 9999, "The fourth page in the 'largeArray' is named correctly.");
+    "[7500" + gEllipsis + "9999]", "The fourth page in the 'largeArray' is named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"),
     "", "The fourth page in the 'largeArray' should not have a corresponding value.");
 
   is(objectVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    0 + gEllipsis + 1999, "The first page in the 'largeObject' is named correctly.");
+    "[0" + gEllipsis + "2499]", "The first page in the 'largeObject' is named correctly.");
   is(objectVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
     "", "The first page in the 'largeObject' should not have a corresponding value.");
   is(objectVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    2000 + gEllipsis + 3999, "The second page in the 'largeObject' is named correctly.");
+    "[2500" + gEllipsis + "4999]", "The second page in the 'largeObject' is named correctly.");
   is(objectVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
     "", "The second page in the 'largeObject' should not have a corresponding value.");
   is(objectVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
-    4000 + gEllipsis + 5999, "The thrid page in the 'largeObject' is named correctly.");
+    "[5000" + gEllipsis + "7499]", "The thrid page in the 'largeObject' is named correctly.");
   is(objectVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"),
     "", "The thrid page in the 'largeObject' should not have a corresponding value.");
   is(objectVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
-    6000 + gEllipsis + 9999, "The fourth page in the 'largeObject' is named correctly.");
+    "[7500" + gEllipsis + "9999]", "The fourth page in the 'largeObject' is named correctly.");
   is(objectVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"),
     "", "The fourth page in the 'largeObject' should not have a corresponding value.");
 
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"),
-    "length", "The other properties 'largeArray' are named correctly.");
+    "buffer", "The other properties 'largeArray' are named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
-    "10000", "The other properties 'largeArray' have the correct value.");
+    "ArrayBuffer", "The other properties 'largeArray' have the correct value.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"),
-    "buffer", "The other properties 'largeArray' are named correctly.");
+    "byteLength", "The other properties 'largeArray' are named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"),
-    "ArrayBuffer", "The other properties 'largeArray' have the correct value.");
+    "10000", "The other properties 'largeArray' have the correct value.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"),
-    "byteLength", "The other properties 'largeArray' are named correctly.");
+    "byteOffset", "The other properties 'largeArray' are named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"),
-    "10000", "The other properties 'largeArray' have the correct value.");
+    "0", "The other properties 'largeArray' have the correct value.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"),
-    "byteOffset", "The other properties 'largeArray' are named correctly.");
+    "length", "The other properties 'largeArray' are named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"),
-    "0", "The other properties 'largeArray' have the correct value.");
+    "10000", "The other properties 'largeArray' have the correct value.");
+
   is(arrayVar.target.querySelectorAll(".variables-view-property .name")[8].getAttribute("value"),
     "__proto__", "The other properties 'largeArray' are named correctly.");
   is(arrayVar.target.querySelectorAll(".variables-view-property .value")[8].getAttribute("value"),
     "Int8ArrayPrototype", "The other properties 'largeArray' have the correct value.");
 
   is(objectVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"),
     "__proto__", "The other properties 'largeObject' are named correctly.");
   is(objectVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
     "Object", "The other properties 'largeObject' have the correct value.");
 }
 
 function verifyNextLevels() {
   let localScope = gVariables.getScopeAtIndex(0);
   let objectVar = localScope.get("largeObject");
 
-  let lastPage1 = objectVar.get(6000 + gEllipsis + 9999);
+  let lastPage1 = objectVar.get("[7500" + gEllipsis + "9999]");
   ok(lastPage1, "The last page in the first level was retrieved successfully.");
-  lastPage1.expand();
+  return lastPage1.expand()
+                  .then(verifyNextLevels2.bind(null, lastPage1));
+}
 
+function verifyNextLevels2(lastPage1) {
   let pageEnums1 = lastPage1.target.querySelector(".variables-view-element-details.enum").childNodes;
   let pageNonEnums1 = lastPage1.target.querySelector(".variables-view-element-details.nonenum").childNodes;
   is(pageEnums1.length, 0,
     "The last page in the first level shouldn't contain any enumerable elements.");
   is(pageNonEnums1.length, 4,
     "The last page in the first level should contain all the created non-enumerable elements.");
 
   is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    6000 + gEllipsis + 6999, "The first page in this level named correctly (1).");
+    "[7500" + gEllipsis + "8124]", "The first page in this level named correctly (1).");
   is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    7000 + gEllipsis + 7999, "The second page in this level named correctly (1).");
+    "[8125" + gEllipsis + "8749]", "The second page in this level named correctly (1).");
   is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
-    8000 + gEllipsis + 8999, "The third page in this level named correctly (1).");
+    "[8750" + gEllipsis + "9374]", "The third page in this level named correctly (1).");
   is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
-    9000 + gEllipsis + 9999, "The fourth page in this level named correctly (1).");
+    "[9375" + gEllipsis + "9999]", "The fourth page in this level named correctly (1).");
 
-  let lastPage2 = lastPage1.get(9000 + gEllipsis + 9999);
+  let lastPage2 = lastPage1.get("[9375" + gEllipsis + "9999]");
   ok(lastPage2, "The last page in the second level was retrieved successfully.");
-  lastPage2.expand();
+  return lastPage2.expand()
+                  .then(verifyNextLevels3.bind(null, lastPage2));
+}
 
+function verifyNextLevels3(lastPage2) {
   let pageEnums2 = lastPage2.target.querySelector(".variables-view-element-details.enum").childNodes;
   let pageNonEnums2 = lastPage2.target.querySelector(".variables-view-element-details.nonenum").childNodes;
-  is(pageEnums2.length, 0,
-    "The last page in the second level shouldn't contain any enumerable elements.");
-  is(pageNonEnums2.length, 4,
-    "The last page in the second level should contain all the created non-enumerable elements.");
-
-  is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    9000 + gEllipsis + 9199, "The first page in this level named correctly (2).");
-  is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    9200 + gEllipsis + 9399, "The second page in this level named correctly (2).");
-  is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
-    9400 + gEllipsis + 9599, "The third page in this level named correctly (2).");
-  is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
-    9600 + gEllipsis + 9999, "The fourth page in this level named correctly (2).");
-
-  let lastPage3 = lastPage2.get(9600 + gEllipsis + 9999);
-  ok(lastPage3, "The last page in the third level was retrieved successfully.");
-  lastPage3.expand();
-
-  let pageEnums3 = lastPage3.target.querySelector(".variables-view-element-details.enum").childNodes;
-  let pageNonEnums3 = lastPage3.target.querySelector(".variables-view-element-details.nonenum").childNodes;
-  is(pageEnums3.length, 400,
+  is(pageEnums2.length, 625,
     "The last page in the third level should contain all the created enumerable elements.");
-  is(pageNonEnums3.length, 0,
+  is(pageNonEnums2.length, 0,
     "The last page in the third level shouldn't contain any non-enumerable elements.");
 
-  is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    9600, "The properties in this level are named correctly (3).");
-  is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    9601, "The properties in this level are named correctly (3).");
-  is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[398].getAttribute("value"),
+  is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
+    9375, "The properties in this level are named correctly (3).");
+  is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
+    9376, "The properties in this level are named correctly (3).");
+  is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[623].getAttribute("value"),
     9998, "The properties in this level are named correctly (3).");
-  is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[399].getAttribute("value"),
+  is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[624].getAttribute("value"),
     9999, "The properties in this level are named correctly (3).");
 
-  is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
-    399, "The properties in this level have the correct value (3).");
-  is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
-    398, "The properties in this level have the correct value (3).");
-  is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[398].getAttribute("value"),
+  is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
+    624, "The properties in this level have the correct value (3).");
+  is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
+    623, "The properties in this level have the correct value (3).");
+  is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[623].getAttribute("value"),
     1, "The properties in this level have the correct value (3).");
-  is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[399].getAttribute("value"),
+  is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[624].getAttribute("value"),
     0, "The properties in this level have the correct value (3).");
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gPanel = null;
   gDebugger = null;
   gVariables = null;
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -7,17 +7,16 @@
 
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 const LAZY_EMPTY_DELAY = 150; // ms
 const LAZY_EXPAND_DELAY = 50; // ms
 const SCROLL_PAGE_SIZE_DEFAULT = 0;
-const APPEND_PAGE_SIZE_DEFAULT = 500;
 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
 const PAGE_SIZE_MAX_JUMPS = 30;
 const SEARCH_ACTION_MAX_DELAY = 300; // ms
 const ITEM_FLASH_DURATION = 300 // ms
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
@@ -242,22 +241,16 @@ VariablesView.prototype = {
   /**
    * The number of elements in this container to jump when Page Up or Page Down
    * keys are pressed. If falsy, then the page size will be based on the
    * container height.
    */
   scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
 
   /**
-   * The maximum number of elements allowed in a scope, variable or property
-   * that allows pagination when appending children.
-   */
-  appendPageSize: APPEND_PAGE_SIZE_DEFAULT,
-
-  /**
    * Function called each time a variable or property's value is changed via
    * user interaction. If null, then value changes are disabled.
    *
    * This property is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   eval: null,
 
@@ -551,16 +544,24 @@ VariablesView.prototype = {
    * If aToken is falsy, then all the scopes are unhidden and expanded,
    * while the available variables and properties inside those scopes are
    * just unhidden.
    *
    * @param string aToken
    *        The variable or property to search for.
    */
   _doSearch: function(aToken) {
+    if (this.controller.supportsSearch()) {
+      this.empty();
+      let scope = this.addScope(aToken);
+      scope.expanded = true; // Expand the scope by default.
+      scope.locked = true; // Prevent collapsing the scope.
+      this.controller.performSearch(scope, aToken);
+      return;
+    }
     for (let scope of this._store) {
       switch (aToken) {
         case "":
         case null:
         case undefined:
           scope.expand();
           scope._performSearch("");
           break;
@@ -1209,17 +1210,16 @@ function Scope(aView, aName, aFlags = {}
 
   this._onClick = this._onClick.bind(this);
   this._openEnum = this._openEnum.bind(this);
   this._openNonEnum = this._openNonEnum.bind(this);
 
   // Inherit properties and flags from the parent view. You can override
   // each of these directly onto any scope, variable or property instance.
   this.scrollPageSize = aView.scrollPageSize;
-  this.appendPageSize = aView.appendPageSize;
   this.eval = aView.eval;
   this.switch = aView.switch;
   this.delete = aView.delete;
   this.new = aView.new;
   this.preventDisableOnChange = aView.preventDisableOnChange;
   this.preventDescriptorModifiers = aView.preventDescriptorModifiers;
   this.editableNameTooltip = aView.editableNameTooltip;
   this.editableValueTooltip = aView.editableValueTooltip;
@@ -1315,91 +1315,22 @@ Scope.prototype = {
    *                 someProp4: { value: { type: "null" } },
    *                 someProp5: { value: { type: "object", class: "Object" } },
    *                 someProp6: { get: { type: "object", class: "Function" },
    *                              set: { type: "undefined" } } }
    * @param object aOptions [optional]
    *        Additional options for adding the properties. Supported options:
    *        - sorted: true to sort all the properties before adding them
    *        - callback: function invoked after each item is added
-   * @param string aKeysType [optional]
-   *        Helper argument in the case of paginated items. Can be either
-   *        "just-strings" or "just-numbers". Humans shouldn't use this argument.
    */
-  addItems: function(aItems, aOptions = {}, aKeysType = "") {
+  addItems: function(aItems, aOptions = {}) {
     let names = Object.keys(aItems);
 
-    // Building the view when inspecting an object with a very large number of
-    // properties may take a long time. To avoid blocking the UI, group
-    // the items into several lazily populated pseudo-items.
-    let exceedsThreshold = names.length >= this.appendPageSize;
-    let shouldPaginate = exceedsThreshold && aKeysType != "just-strings";
-    if (shouldPaginate && this.allowPaginate) {
-      // Group the items to append into two separate arrays, one containing
-      // number-like keys, the other one containing string keys.
-      if (aKeysType == "just-numbers") {
-        var numberKeys = names;
-        var stringKeys = [];
-      } else {
-        var numberKeys = [];
-        var stringKeys = [];
-        for (let name of names) {
-          // Be very careful. Avoid Infinity, NaN and non Natural number keys.
-          let coerced = +name;
-          if (Number.isInteger(coerced) && coerced > -1) {
-            numberKeys.push(name);
-          } else {
-            stringKeys.push(name);
-          }
-        }
-      }
-
-      // This object contains a very large number of properties, but they're
-      // almost all strings that can't be coerced to numbers. Don't paginate.
-      if (numberKeys.length < this.appendPageSize) {
-        this.addItems(aItems, aOptions, "just-strings");
-        return;
-      }
-
-      // Slices a section of the { name: descriptor } data properties.
-      let paginate = (aArray, aBegin = 0, aEnd = aArray.length) => {
-        let store = {}
-        for (let i = aBegin; i < aEnd; i++) {
-          let name = aArray[i];
-          store[name] = aItems[name];
-        }
-        return store;
-      };
-
-      // Creates a pseudo-item that populates itself with the data properties
-      // from the corresponding page range.
-      let createRangeExpander = (aArray, aBegin, aEnd, aOptions, aKeyTypes) => {
-        let rangeVar = this.addItem(aArray[aBegin] + Scope.ellipsis + aArray[aEnd - 1]);
-        rangeVar.onexpand = () => {
-          let pageItems = paginate(aArray, aBegin, aEnd);
-          rangeVar.addItems(pageItems, aOptions, aKeyTypes);
-        }
-        rangeVar.showArrow();
-        rangeVar.target.setAttribute("pseudo-item", "");
-      };
-
-      // Divide the number keys into quarters.
-      let page = +Math.round(numberKeys.length / 4).toPrecision(1);
-      createRangeExpander(numberKeys, 0, page, aOptions, "just-numbers");
-      createRangeExpander(numberKeys, page, page * 2, aOptions, "just-numbers");
-      createRangeExpander(numberKeys, page * 2, page * 3, aOptions, "just-numbers");
-      createRangeExpander(numberKeys, page * 3, numberKeys.length, aOptions, "just-numbers");
-
-      // Append all the string keys together.
-      this.addItems(paginate(stringKeys), aOptions, "just-strings");
-      return;
-    }
-
     // Sort all of the properties before adding them, if preferred.
-    if (aOptions.sorted && aKeysType != "just-numbers") {
+    if (aOptions.sorted) {
       names.sort(this._naturalSort);
     }
 
     // Add the properties to the current scope.
     for (let name of names) {
       let descriptor = aItems[name];
       let item = this.addItem(name, descriptor);
 
@@ -1531,17 +1462,21 @@ Scope.prototype = {
       this._openEnum();
     }
     if (this._variablesView._nonEnumVisible) {
       Services.tm.currentThread.dispatch({ run: this._openNonEnum }, 0);
     }
     this._isExpanded = true;
 
     if (this.onexpand) {
-      this.onexpand(this);
+      // We return onexpand as it sometimes returns a promise
+      // (up to the user of VariableView to do it)
+      // that can indicate when the view is done expanding
+      // and attributes are available. (Mostly used for tests)
+      return this.onexpand(this);
     }
   },
 
   /**
    * Collapses the scope, hiding all the added details.
    */
   collapse: function() {
     if (!this._isExpanded || this._isLocked) {
--- a/browser/devtools/shared/widgets/VariablesViewController.jsm
+++ b/browser/devtools/shared/widgets/VariablesViewController.jsm
@@ -27,18 +27,21 @@ Object.defineProperty(this, "WebConsoleU
 XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
   Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
 );
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/devtools/Console.jsm");
 
 const MAX_LONG_STRING_LENGTH = 200000;
+const MAX_PROPERTY_ITEMS = 2000;
 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 
+const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data
+
 this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"];
 
 
 /**
  * Controller for a VariablesView that handles interfacing with the debugger
  * protocol. Is able to populate scopes and variables via the protocol as well
  * as manage actor lifespans.
  *
@@ -154,40 +157,218 @@ VariablesViewController.prototype = {
 
       deferred.resolve();
     });
 
     return deferred.promise;
   },
 
   /**
+   * Adds pseudo items in case there is too many properties to display.
+   * Each item can expand into property slices.
+   *
+   * @param Scope aTarget
+   *        The Scope where the properties will be placed into.
+   * @param object aGrip
+   *        The property iterator grip.
+   * @param object aIterator
+   *        The property iterator client.
+   */
+  _populatePropertySlices: function(aTarget, aGrip, aIterator) {
+    if (aGrip.count < MAX_PROPERTY_ITEMS) {
+      return this._populateFromPropertyIterator(aTarget, aGrip);
+    }
+
+    // Divide the keys into quarters.
+    let items = Math.ceil(aGrip.count / 4);
+
+    let promises = [];
+    for(let i = 0; i < 4; i++) {
+      let start = aGrip.start + i * items;
+      let count = i != 3 ? items : aGrip.count - i * items;
+
+      // Create a new kind of grip, with additional fields to define the slice
+      let sliceGrip = {
+        type: "property-iterator",
+        propertyIterator: aIterator,
+        start: start,
+        count: count
+      };
+
+      // Query the name of the first and last items for this slice
+      let deferred = promise.defer();
+      aIterator.names([start, start + count - 1], ({ names }) => {
+        let label = "[" + names[0] + ELLIPSIS + names[1] + "]";
+        let item = aTarget.addItem(label);
+        item.showArrow();
+        this.addExpander(item, sliceGrip);
+        deferred.resolve();
+      });
+      promises.push(deferred.promise);
+    }
+
+    return promise.all(promises);
+  },
+
+  /**
+   * Adds a property slice for a Variable in the view using the already
+   * property iterator
+   *
+   * @param Scope aTarget
+   *        The Scope where the properties will be placed into.
+   * @param object aGrip
+   *        The property iterator grip.
+   */
+  _populateFromPropertyIterator: function(aTarget, aGrip) {
+    if (aGrip.count >= MAX_PROPERTY_ITEMS) {
+      // We already started to split, but there is still too many properties, split again.
+      return this._populatePropertySlices(aTarget, aGrip, aGrip.propertyIterator);
+    }
+    // We started slicing properties, and the slice is now small enough to be displayed
+    let deferred = promise.defer();
+    aGrip.propertyIterator.slice(aGrip.start, aGrip.count,
+      ({ ownProperties }) => {
+        // Add all the variable properties.
+        if (Object.keys(ownProperties).length > 0) {
+          aTarget.addItems(ownProperties, {
+            sorted: true,
+            // Expansion handlers must be set after the properties are added.
+            callback: this.addExpander
+          });
+        }
+        deferred.resolve();
+      });
+    return deferred.promise;
+  },
+
+  /**
+   * Adds the properties for a Variable in the view using a new feature in FF40+
+   * that allows iteration over properties in slices.
+   *
+   * @param Scope aTarget
+   *        The Scope where the properties will be placed into.
+   * @param object aGrip
+   *        The grip to use to populate the target.
+   * @param string aQuery [optional]
+   *        The query string used to fetch only a subset of properties
+   */
+  _populateFromObjectWithIterator: function(aTarget, aGrip, aQuery) {
+    // FF40+ starts exposing `ownPropertyLength` on ObjectActor's grip,
+    // as well as `enumProperties` request.
+    let deferred = promise.defer();
+    let objectClient = this._getObjectClient(aGrip);
+    let isArray = aGrip.preview && aGrip.preview.kind === "ArrayLike";
+    if (isArray) {
+      // First enumerate array items, e.g. properties from `0` to `array.length`.
+      let options = {
+        ignoreNonIndexedProperties: true,
+        ignoreSafeGetters: true,
+        query: aQuery
+      };
+      objectClient.enumProperties(options, ({ iterator }) => {
+        let sliceGrip = {
+          type: "property-iterator",
+          propertyIterator: iterator,
+          start: 0,
+          count: iterator.count
+        };
+        this._populatePropertySlices(aTarget, sliceGrip, iterator)
+            .then(() => {
+          // Then enumerate the rest of the properties, like length, buffer, etc.
+          let options = {
+            ignoreIndexedProperties: true,
+            sort: true,
+            query: aQuery
+          };
+          objectClient.enumProperties(options, ({ iterator }) => {
+            let sliceGrip = {
+              type: "property-iterator",
+              propertyIterator: iterator,
+              start: 0,
+              count: iterator.count
+            };
+            deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip, iterator));
+          });
+        });
+      });
+    } else {
+      // For objects, we just enumerate all the properties sorted by name.
+      objectClient.enumProperties({ sort: true, query: aQuery }, ({ iterator }) => {
+        let sliceGrip = {
+          type: "property-iterator",
+          propertyIterator: iterator,
+          start: 0,
+          count: iterator.count
+        };
+        deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip, iterator));
+      });
+
+    }
+    return deferred.promise;
+  },
+
+  /**
+   * Adds the given prototype in the view.
+   *
+   * @param Scope aTarget
+   *        The Scope where the properties will be placed into.
+   * @param object aProtype
+   *        The prototype grip.
+   */
+  _populateObjectPrototype: function(aTarget, aPrototype) {
+    // Add the variable's __proto__.
+    if (aPrototype && aPrototype.type != "null") {
+      let proto = aTarget.addItem("__proto__", { value: aPrototype });
+      this.addExpander(proto, aPrototype);
+    }
+  },
+
+  /**
    * Adds properties to a Scope, Variable, or Property in the view. Triggered
    * when a scope is expanded or certain variables are hovered.
    *
    * @param Scope aTarget
    *        The Scope where the properties will be placed into.
    * @param object aGrip
    *        The grip to use to populate the target.
    */
   _populateFromObject: function(aTarget, aGrip) {
-    let deferred = promise.defer();
+    // Fetch properties by slices if there is too many in order to prevent UI freeze.
+    if ("ownPropertyLength" in aGrip && aGrip.ownPropertyLength >= MAX_PROPERTY_ITEMS) {
+      return this._populateFromObjectWithIterator(aTarget, aGrip)
+                 .then(() => {
+                   let deferred = promise.defer();
+                   let objectClient = this._getObjectClient(aGrip);
+                   objectClient.getPrototype(({ prototype }) => {
+                     this._populateObjectPrototype(aTarget, prototype);
+                     deferred.resolve();
+                   });
+                   return deferred.promise;
+                 });
+    }
 
     if (aGrip.class === "Promise" && aGrip.promiseState) {
       const { state, value, reason } = aGrip.promiseState;
       aTarget.addItem("<state>", { value: state });
       if (state === "fulfilled") {
         this.addExpander(aTarget.addItem("<value>", { value }), value);
       } else if (state === "rejected") {
         this.addExpander(aTarget.addItem("<reason>", { value: reason }), reason);
       }
     }
+    return this._populateProperties(aTarget, aGrip);
+  },
+
+  _populateProperties: function(aTarget, aGrip, aOptions) {
+    let deferred = promise.defer();
 
     let objectClient = this._getObjectClient(aGrip);
     objectClient.getPrototypeAndProperties(aResponse => {
-      let { ownProperties, prototype } = aResponse;
+      let ownProperties = aResponse.ownProperties || {};
+      let prototype = aResponse.prototype || null;
       // 'safeGetterValues' is new and isn't necessary defined on old actors.
       let safeGetterValues = aResponse.safeGetterValues || {};
       let sortable = VariablesView.isSortable(aGrip.class);
 
       // Merge the safe getter values into one object such that we can use it
       // in VariablesView.
       for (let name of Object.keys(safeGetterValues)) {
         if (name in ownProperties) {
@@ -195,31 +376,25 @@ VariablesViewController.prototype = {
           ownProperties[name].getterValue = getterValue;
           ownProperties[name].getterPrototypeLevel = getterPrototypeLevel;
         } else {
           ownProperties[name] = safeGetterValues[name];
         }
       }
 
       // Add all the variable properties.
-      if (ownProperties) {
-        aTarget.addItems(ownProperties, {
-          // Not all variables need to force sorted properties.
-          sorted: sortable,
-          // Expansion handlers must be set after the properties are added.
-          callback: this.addExpander
-        });
-      }
+      aTarget.addItems(ownProperties, {
+        // Not all variables need to force sorted properties.
+        sorted: sortable,
+        // Expansion handlers must be set after the properties are added.
+        callback: this.addExpander
+      });
 
       // Add the variable's __proto__.
-      if (prototype && prototype.type != "null") {
-        let proto = aTarget.addItem("__proto__", { value: prototype });
-        // Expansion handlers must be set after the properties are added.
-        this.addExpander(proto, prototype);
-      }
+      this._populateObjectPrototype(aTarget, prototype);
 
       // If the object is a function we need to fetch its scope chain
       // to show them as closures for the respective function.
       if (aGrip.class == "Function") {
         objectClient.getScope(aResponse => {
           if (aResponse.error) {
             // This function is bound to a built-in object or it's not present
             // in the current scope chain. Not necessarily an actual error,
@@ -384,16 +559,20 @@ VariablesViewController.prototype = {
     // Make sure the source grip is available.
     if (!aSource) {
       return promise.reject(new Error("No actor grip was given for the variable."));
     }
 
     let deferred = promise.defer();
     aTarget._fetched = deferred.promise;
 
+    if (aSource.type === "property-iterator") {
+      return this._populateFromPropertyIterator(aTarget, aSource);
+    }
+
     // If the target is a Variable or Property then we're fetching properties.
     if (VariablesView.isVariable(aTarget)) {
       this._populateFromObject(aTarget, aSource).then(() => {
         // Signal that properties have been fetched.
         this.view.emit("fetched", "properties", aTarget);
         // Commit the hierarchy because new items were added.
         this.view.commitHierarchy();
         deferred.resolve();
@@ -434,16 +613,39 @@ VariablesViewController.prototype = {
         Cu.reportError(error);
         deferred.reject(error);
     }
 
     return deferred.promise;
   },
 
   /**
+   * Indicates to the view if the targeted actor supports properties search
+   *
+   * @return boolean True, if the actor supports enumProperty request
+   */
+  supportsSearch: function () {
+    // FF40+ starts exposing ownPropertyLength on object actor's grip
+    // as well as enumProperty which allows to query a subset of properties.
+    return this.objectActor && ("ownPropertyLength" in this.objectActor);
+  },
+
+  /**
+   * Try to use the actor to perform an attribute search.
+   *
+   * @param Scope aScope
+   *        The Scope instance to populate with properties
+   * @param string aToken
+   *        The query string
+   */
+  performSearch: function(aScope, aToken) {
+    this._populateFromObjectWithIterator(aScope, this.objectActor, aToken);
+  },
+
+  /**
    * Release an actor from the controller.
    *
    * @param object aActor
    *        The actor to release.
    */
   releaseActor: function(aActor){
     if (this._releaseActor) {
       this._releaseActor(aActor);
@@ -492,16 +694,18 @@ VariablesViewController.prototype = {
     let scope = this.view.addScope(aOptions.label);
     scope.expanded = true; // Expand the scope by default.
     scope.locked = true; // Prevent collpasing the scope.
 
     let variable = scope.addItem("", { enumerable: true });
     let populated;
 
     if (aOptions.objectActor) {
+      // Save objectActor for properties filtering
+      this.objectActor = aOptions.objectActor;
       populated = this.populate(variable, aOptions.objectActor);
       variable.expand();
     } else if (aOptions.rawObject) {
       variable.populate(aOptions.rawObject, { expanded: true });
       populated = promise.resolve();
     }
 
     return { variable: variable, expanded: populated };
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5340,16 +5340,30 @@
   },
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_PROTOTYPEANDPROPERTIES_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'prototypeAndProperties' request to go round trip."
   },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_ENUMPROPERTIES_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'enumProperties' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_ENUMPROPERTIES_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'enumProperties' request to go round trip."
+  },
   "DEVTOOLS_DEBUGGER_RDP_LOCAL_PROTOTYPESANDPROPERTIES_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'prototypesAndProperties' request to go round trip."
   },
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_PROTOTYPESANDPROPERTIES_MS": {
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -2323,16 +2323,49 @@ ObjectClient.prototype = {
    */
   getPrototypeAndProperties: DebuggerClient.requester({
     type: "prototypeAndProperties"
   }, {
     telemetry: "PROTOTYPEANDPROPERTIES"
   }),
 
   /**
+   * Request a PropertyIteratorClient instance to ease listing
+   * properties for this object.
+   *
+   * @param options Object
+   *        A dictionary object with various boolean attributes:
+   *        - ignoreSafeGetters Boolean
+   *          If true, do not iterate over safe getters.
+   *        - ignoreIndexedProperties Boolean
+   *          If true, filters out Array items.
+   *          e.g. properties names between `0` and `object.length`.
+   *        - ignoreNonIndexedProperties Boolean
+   *          If true, filters out items that aren't array items
+   *          e.g. properties names that are not a number between `0`
+   *          and `object.length`.
+   *        - sort Boolean
+   *          If true, the iterator will sort the properties by name
+   *          before dispatching them.
+   * @param aOnResponse function Called with the client instance.
+   */
+  enumProperties: DebuggerClient.requester({
+    type: "enumProperties",
+    options: args(0)
+  }, {
+    after: function(aResponse) {
+      if (aResponse.iterator) {
+        return { iterator: new PropertyIteratorClient(this._client, aResponse.iterator) };
+      }
+      return aResponse;
+    },
+    telemetry: "ENUMPROPERTIES"
+  }),
+
+  /**
    * Request the property descriptor of the object's specified property.
    *
    * @param aName string The name of the requested property.
    * @param aOnResponse function Called with the request's response.
    */
   getProperty: DebuggerClient.requester({
     type: "property",
     name: args(0)
@@ -2376,16 +2409,84 @@ ObjectClient.prototype = {
       }
       return aPacket;
     },
     telemetry: "SCOPE"
   })
 };
 
 /**
+ * A PropertyIteratorClient provides a way to access to property names and
+ * values of an object efficiently, slice by slice.
+ * Note that the properties can be sorted in the backend,
+ * this is controled while creating the PropertyIteratorClient
+ * from ObjectClient.enumProperties.
+ *
+ * @param aClient DebuggerClient
+ *        The debugger client parent.
+ * @param aGrip Object
+ *        A PropertyIteratorActor grip returned by the protocol via
+ *        TabActor.enumProperties request.
+ */
+function PropertyIteratorClient(aClient, aGrip) {
+  this._grip = aGrip;
+  this._client = aClient;
+  this.request = this._client.request;
+}
+
+PropertyIteratorClient.prototype = {
+  get actor() { return this._grip.actor; },
+
+  /**
+   * Get the total number of properties available in the iterator.
+   */
+  get count() { return this._grip.count; },
+
+  /**
+   * Get one or more property names that correspond to the positions in the
+   * indexes parameter.
+   *
+   * @param indexes Array
+   *        An array of property indexes.
+   * @param aCallback Function
+   *        The function called when we receive the property names.
+   */
+  names: DebuggerClient.requester({
+    type: "names",
+    indexes: args(0)
+  }, {}),
+
+  /**
+   * Get a set of following property value(s).
+   *
+   * @param start Number
+   *        The index of the first property to fetch.
+   * @param count Number
+   *        The number of properties to fetch.
+   * @param aCallback Function
+   *        The function called when we receive the property values.
+   */
+  slice: DebuggerClient.requester({
+    type: "slice",
+    start: args(0),
+    count: args(1)
+  }, {}),
+
+  /**
+   * Get all the property values.
+   *
+   * @param aCallback Function
+   *        The function called when we receive the property values.
+   */
+  all: DebuggerClient.requester({
+    type: "all"
+  }, {}),
+};
+
+/**
  * A LongStringClient provides a way to access "very long" strings from the
  * debugger server.
  *
  * @param aClient DebuggerClient
  *        The debugger client parent.
  * @param aGrip Object
  *        A pause-lifetime long string grip returned by the protocol.
  */
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -3242,29 +3242,187 @@ let stringifiers = {
       statePreview += ": " + (typeof settledValue === "object" && settledValue !== null
                                 ? stringify(settledValue)
                                 : settledValue);
     }
     return "Promise (" + statePreview + ")";
   },
 };
 
+
+/**
+ * Creates an actor to iterate over an object's property names and values.
+ *
+ * @param aObjectActor ObjectActor
+ *        The object actor.
+ * @param aOptions Object
+ *        A dictionary object with various boolean attributes:
+ *        - ignoreSafeGetters Boolean
+ *          If true, do not iterate over safe getters.
+ *        - ignoreIndexedProperties Boolean
+ *          If true, filters out Array items.
+ *          e.g. properties names between `0` and `object.length`.
+ *        - ignoreNonIndexedProperties Boolean
+ *          If true, filters out items that aren't array items
+ *          e.g. properties names that are not a number between `0`
+ *          and `object.length`.
+ *        - sort Boolean
+ *          If true, the iterator will sort the properties by name
+ *          before dispatching them.
+ *        - query String
+ *          If non-empty, will filter the properties by names containing
+ *          this query string. The match is not case-sensitive.
+ */
+function PropertyIteratorActor(aObjectActor, aOptions)
+{
+  this.objectActor = aObjectActor;
+
+  let ownProperties = Object.create(null);
+  let names = [];
+  try {
+    names = this.objectActor.obj.getOwnPropertyNames();
+  } catch (ex) {}
+
+
+  let safeGetterValues = {};
+  let safeGetterNames = [];
+  if (!aOptions.ignoreSafeGetters) {
+    // Merge the safe getter values into the existing properties list.
+    safeGetterValues = this.objectActor._findSafeGetterValues(names);
+    safeGetterNames = Object.keys(safeGetterValues);
+    for (let name of safeGetterNames) {
+      if (names.indexOf(name) === -1) {
+        names.push(name);
+      }
+    }
+  }
+
+  if (aOptions.ignoreIndexedProperties || aOptions.ignoreNonIndexedProperties) {
+    let length = DevToolsUtils.getProperty(this.objectActor.obj, "length");
+    if (typeof(length) !== "number") {
+      // Pseudo arrays are flagged as ArrayLike if they have
+      // subsequent indexed properties without having any length attribute.
+      length = 0;
+      for (let key of names) {
+        if (isNaN(key) || key != length++) {
+          break;
+        }
+      }
+    }
+
+    if (aOptions.ignoreIndexedProperties) {
+      names = names.filter(i => {
+        // Use parseFloat in order to reject floats...
+        // (parseInt converts floats to integer)
+        // (Number(str) converts spaces to 0)
+        i = parseFloat(i);
+        return !Number.isInteger(i) || i < 0 || i >= length;
+      });
+    }
+
+    if (aOptions.ignoreNonIndexedProperties) {
+      names = names.filter(i => {
+        i = parseFloat(i);
+        return Number.isInteger(i) && i >= 0 && i < length;
+      });
+    }
+  }
+
+  if (aOptions.query) {
+    let { query } = aOptions;
+    query = query.toLowerCase();
+    names = names.filter(name => {
+      return name.toLowerCase().includes(query);
+    });
+  }
+
+  if (aOptions.sort) {
+    names.sort();
+  }
+
+  // Now build the descriptor list
+  for (let name of names) {
+    let desc = this.objectActor._propertyDescriptor(name);
+    if (!desc) {
+      desc = safeGetterValues[name];
+    }
+    else if (name in safeGetterValues) {
+      // Merge the safe getter values into the existing properties list.
+      let { getterValue, getterPrototypeLevel } = safeGetterValues[name];
+      desc.getterValue = getterValue;
+      desc.getterPrototypeLevel = getterPrototypeLevel;
+    }
+    ownProperties[name] = desc;
+  }
+
+  this.names = names;
+  this.ownProperties = ownProperties;
+}
+
+PropertyIteratorActor.prototype = {
+  actorPrefix: "propertyIterator",
+
+  grip: function () {
+    return {
+      type: "propertyIterator",
+      actor: this.actorID,
+      count: this.names.length
+    };
+  },
+
+  names: function ({ indexes }) {
+    let list = [];
+    for (let idx of indexes) {
+      list.push(this.names[idx]);
+    }
+    return {
+      names: list
+    };
+  },
+
+  slice: function ({ start, count }) {
+    let names = this.names.slice(start, start + count);
+    let props = Object.create(null);
+    for (let name of names) {
+      props[name] = this.ownProperties[name];
+    }
+    return {
+      ownProperties: props
+    };
+  },
+
+  all: function () {
+    return {
+      ownProperties: this.ownProperties
+    };
+  }
+};
+
+PropertyIteratorActor.prototype.requestTypes = {
+  "names": PropertyIteratorActor.prototype.names,
+  "slice": PropertyIteratorActor.prototype.slice,
+  "all": PropertyIteratorActor.prototype.all,
+};
+
+exports.PropertyIteratorActor = PropertyIteratorActor;
+
 /**
  * Creates an actor for the specified object.
  *
  * @param aObj Debugger.Object
  *        The debuggee object.
  * @param aThreadActor ThreadActor
  *        The parent thread actor for this object.
  */
 function ObjectActor(aObj, aThreadActor)
 {
   dbg_assert(!aObj.optimizedOut, "Should not create object actors for optimized out values!");
   this.obj = aObj;
   this.threadActor = aThreadActor;
+  this.iterators = new Set();
 }
 
 ObjectActor.prototype = {
   actorPrefix: "obj",
 
   /**
    * Returns a grip for this actor for returning in a protocol message.
    */
@@ -3287,16 +3445,26 @@ ObjectActor.prototype = {
         g.promiseState = { state };
         if (state == "fulfilled") {
           g.promiseState.value = this.threadActor.createValueGrip(value);
         } else if (state == "rejected") {
           g.promiseState.reason = this.threadActor.createValueGrip(reason);
         }
       }
 
+      // FF40+: Allow to know how many properties an object has
+      // to lazily display them when there is a bunch.
+      // Throws on some MouseEvent object in tests.
+      try {
+        // Bug 1163520: Assert on internal functions
+        if (this.obj.class != "Function") {
+          g.ownPropertyLength = this.obj.getOwnPropertyNames().length;
+        }
+      } catch(e) {}
+
       let raw = this.obj.unsafeDereference();
 
       // If Cu is not defined, we are running on a worker thread, where xrays
       // don't exist.
       if (Cu) {
         raw = Cu.unwaiveXrays(raw);
       }
 
@@ -3323,16 +3491,18 @@ ObjectActor.prototype = {
 
   /**
    * Releases this actor from the pool.
    */
   release: function () {
     if (this.registeredPool.objectActors) {
       this.registeredPool.objectActors.delete(this.obj);
     }
+    this.iterators.forEach(actor => this.registeredPool.removeActor(actor));
+    this.iterators.clear();
     this.registeredPool.removeActor(this);
   },
 
   /**
    * Handle a protocol request to provide the definition site of this function
    * object.
    *
    * @param aRequest object
@@ -3376,16 +3546,30 @@ ObjectActor.prototype = {
    *        The protocol request object.
    */
   onOwnPropertyNames: function (aRequest) {
     return { from: this.actorID,
              ownPropertyNames: this.obj.getOwnPropertyNames() };
   },
 
   /**
+   * Creates an actor to iterate over an object property names and values.
+   * See PropertyIteratorActor constructor for more info about options param.
+   *
+   * @param aRequest object
+   *        The protocol request object.
+   */
+  onEnumProperties: function (aRequest) {
+    let actor = new PropertyIteratorActor(this, aRequest.options);
+    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.
    *
    * @param aRequest object
    *        The protocol request object.
    */
   onPrototypeAndProperties: function (aRequest) {
     let ownProperties = Object.create(null);
@@ -3401,25 +3585,25 @@ ObjectActor.prototype = {
                safeGetterValues: Object.create(null) };
     }
     for (let name of names) {
       ownProperties[name] = this._propertyDescriptor(name);
     }
     return { from: this.actorID,
              prototype: this.threadActor.createValueGrip(this.obj.proto),
              ownProperties: ownProperties,
-             safeGetterValues: this._findSafeGetterValues(ownProperties) };
+             safeGetterValues: this._findSafeGetterValues(names) };
   },
 
   /**
    * Find the safe getter values for the current Debugger.Object, |this.obj|.
    *
    * @private
-   * @param object aOwnProperties
-   *        The object that holds the list of known ownProperties for
+   * @param array aOwnProperties
+   *        The array that holds the list of known ownProperties names for
    *        |this.obj|.
    * @param number [aLimit=0]
    *        Optional limit of getter values to find.
    * @return object
    *         An object that maps property names to safe getter descriptors as
    *         defined by the remote debugging protocol.
    */
   _findSafeGetterValues: function (aOwnProperties, aLimit = 0)
@@ -3430,17 +3614,17 @@ ObjectActor.prototype = {
 
     while (obj) {
       let getters = this._findSafeGetters(obj);
       for (let name of getters) {
         // Avoid overwriting properties from prototypes closer to this.obj. Also
         // avoid providing safeGetterValues from prototypes if property |name|
         // is already defined as an own property.
         if (name in safeGetterValues ||
-            (obj != this.obj && name in aOwnProperties)) {
+            (obj != this.obj && aOwnProperties.indexOf(name) !== -1)) {
           continue;
         }
 
         // Ignore __proto__ on Object.prototye.
         if (!obj.proto && name == "__proto__") {
           continue;
         }
 
@@ -3697,16 +3881,17 @@ ObjectActor.prototype = {
     return { from: this.actorID, scope: envActor.form() };
   }
 };
 
 ObjectActor.prototype.requestTypes = {
   "definitionSite": ObjectActor.prototype.onDefinitionSite,
   "parameterNames": ObjectActor.prototype.onParameterNames,
   "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties,
+  "enumProperties": ObjectActor.prototype.onEnumProperties,
   "prototype": ObjectActor.prototype.onPrototype,
   "property": ObjectActor.prototype.onProperty,
   "displayString": ObjectActor.prototype.onDisplayString,
   "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
   "decompile": ObjectActor.prototype.onDecompile,
   "release": ObjectActor.prototype.onRelease,
   "scope": ObjectActor.prototype.onScope,
 };
@@ -4418,17 +4603,17 @@ DebuggerServer.ObjectActorPreviewers.Obj
       preview.ownProperties[name] = desc;
       if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
         break;
       }
     }
 
     if (i < OBJECT_PREVIEW_MAX_ITEMS) {
       preview.safeGetterValues = aObjectActor.
-                                 _findSafeGetterValues(preview.ownProperties,
+                                 _findSafeGetterValues(Object.keys(preview.ownProperties),
                                                        OBJECT_PREVIEW_MAX_ITEMS - i);
     }
 
     return true;
   }, // GenericObject
 ]; // DebuggerServer.ObjectActorPreviewers.Object
 
 /**