Bug 1172920: DevTools: Map/Set entries should be visible in the Variables view r=vporof,tromey a=kwierso
authorJarda Snajdr <jsnajdr@gmail.com>
Thu, 31 Mar 2016 16:12:38 -0700
changeset 291292 50c354c8516f0cf1ea64b216ca09f5de69c5968c
parent 291096 bccb11375f2af838cda714d42fd8cef78f5c7bf1
child 291293 6ecf26c604a3f6a80e6757b9107a2deb1a689937
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof, tromey, kwierso
bugs1172920
milestone48.0a1
Bug 1172920: DevTools: Map/Set entries should be visible in the Variables view r=vporof,tromey a=kwierso MozReview-Commit-ID: HzwnqO1uQ4o
devtools/client/debugger/test/mochitest/browser.ini
devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js
devtools/client/debugger/test/mochitest/browser_dbg_variables-view-map-set.js
devtools/client/debugger/test/mochitest/doc_large-array-buffer.html
devtools/client/debugger/test/mochitest/doc_map-set.html
devtools/client/shared/widgets/VariablesView.jsm
devtools/client/shared/widgets/VariablesViewController.jsm
devtools/server/actors/object.js
devtools/shared/client/main.js
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -80,16 +80,17 @@ support-files =
   doc_function-search.html
   doc_global-method-override.html
   doc_iframes.html
   doc_included-script.html
   doc_inline-debugger-statement.html
   doc_inline-script.html
   doc_large-array-buffer.html
   doc_listworkers-tab.html
+  doc_map-set.html
   doc_minified.html
   doc_minified_bogus_map.html
   doc_native-event-handler.html
   doc_no-page-sources.html
   doc_pause-exceptions.html
   doc_pretty-print.html
   doc_pretty-print-2.html
   doc_pretty-print-3.html
@@ -536,18 +537,18 @@ skip-if = e10s && debug
 skip-if = e10s && debug
 [browser_dbg_variables-view-frame-parameters-03.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-frame-with.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-frozen-sealed-nonext.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-hide-non-enums.js]
-skip-if = e10s && debug
 [browser_dbg_variables-view-large-array-buffer.js]
+[browser_dbg_variables-view-map-set.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-override-01.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-override-02.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-popup-01.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-popup-02.js]
--- a/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-large-array-buffer.js
@@ -3,222 +3,247 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Make sure that the variables view remains responsive when faced with
  * huge ammounts of data.
  */
 
+"use strict";
+
 const TAB_URL = EXAMPLE_URL + "doc_large-array-buffer.html";
 
 var gTab, gPanel, gDebugger;
 var gVariables, gEllipsis;
 
 function test() {
+  // this test does a lot of work on large objects, default 45s is not enough
+  requestLongerTimeout(4);
+
   initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gVariables = gDebugger.DebuggerView.Variables;
     gEllipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
 
-    waitForSourceAndCaretAndScopes(gPanel, ".html", 23)
-      .then(() => initialChecks())
-      .then(() => verifyFirstLevel())
-      .then(() => verifyNextLevels())
+    waitForSourceAndCaretAndScopes(gPanel, ".html", 28)
+      .then(() => performTests())
       .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
-      .then(null, aError => {
-        ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+      .then(null, error => {
+        ok(false, "Got an error: " + error.message + "\n" + error.stack);
       });
 
     generateMouseClickInTab(gTab, "content.document.querySelector('button')");
   });
 }
 
-function initialChecks() {
-  let localScope = gVariables.getScopeAtIndex(0);
-  let bufferVar = localScope.get("buffer");
-  let arrayVar = localScope.get("largeArray");
-  let objectVar = localScope.get("largeObject");
+const VARS_TO_TEST = [
+  {
+    varName: "buffer",
+    stringified: "ArrayBuffer",
+    doNotExpand: true
+  },
+  {
+    varName: "largeArray",
+    stringified: "Int8Array[10000]",
+    extraProps: [
+      [ "buffer", "ArrayBuffer" ],
+      [ "byteLength", "10000" ],
+      [ "byteOffset", "0" ],
+      [ "length", "10000" ],
+      [ "__proto__", "Int8ArrayPrototype" ]
+    ]
+  },
+  {
+    varName: "largeObject",
+    stringified: "Object[10000]",
+    extraProps: [
+      [ "__proto__", "Object" ]
+    ]
+  },
+  {
+    varName: "largeMap",
+    stringified: "Map[10000]",
+    hasEntries: true,
+    extraProps: [
+      [ "size", "10000" ],
+      [ "__proto__", "Object" ]
+    ]
+  },
+  {
+    varName: "largeSet",
+    stringified: "Set[10000]",
+    hasEntries: true,
+    extraProps: [
+      [ "size", "10000" ],
+      [ "__proto__", "Object" ]
+    ]
+  }
+];
 
-  ok(bufferVar, "There should be a 'buffer' variable present in the scope.");
-  ok(arrayVar, "There should be a 'largeArray' variable present in the scope.");
-  ok(objectVar, "There should be a 'largeObject' variable present in the scope.");
-
-  is(bufferVar.target.querySelector(".name").getAttribute("value"), "buffer",
-    "Should have the right property name for 'buffer'.");
-  is(bufferVar.target.querySelector(".value").getAttribute("value"), "ArrayBuffer",
-    "Should have the right property value for 'buffer'.");
-  ok(bufferVar.target.querySelector(".value").className.includes("token-other"),
-    "Should have the right token class for 'buffer'.");
+const PAGE_RANGES = [
+  [0, 2499], [2500, 4999], [5000, 7499], [7500, 9999]
+];
 
-  is(arrayVar.target.querySelector(".name").getAttribute("value"), "largeArray",
-    "Should have the right property name for 'largeArray'.");
-  is(arrayVar.target.querySelector(".value").getAttribute("value"), "Int8Array[10000]",
-    "Should have the right property value for 'largeArray'.");
-  ok(arrayVar.target.querySelector(".value").className.includes("token-other"),
-    "Should have the right token class for 'largeArray'.");
+function toPageNames(ranges) {
+  return ranges.map(([ from, to ]) => "[" + from + gEllipsis + to + "]");
+}
+
+function performTests() {
+  let localScope = gVariables.getScopeAtIndex(0);
+
+  return promise.all(VARS_TO_TEST.map(spec => {
+    let { varName, stringified, doNotExpand } = spec;
+
+    let variable = localScope.get(varName);
+    ok(variable,
+      `There should be a '${varName}' variable present in the scope.`);
+
+    is(variable.target.querySelector(".name").getAttribute("value"), varName,
+      `Should have the right property name for '${varName}'.`);
+    is(variable.target.querySelector(".value").getAttribute("value"), stringified,
+      `Should have the right property value for '${varName}'.`);
+    ok(variable.target.querySelector(".value").className.includes("token-other"),
+      `Should have the right token class for '${varName}'.`);
+
+    is(variable.expanded, false,
+      `The '${varName}' variable shouldn't be expanded.`);
 
-  is(objectVar.target.querySelector(".name").getAttribute("value"), "largeObject",
-    "Should have the right property name for 'largeObject'.");
-  is(objectVar.target.querySelector(".value").getAttribute("value"), "Object[10000]",
-    "Should have the right property value for 'largeObject'.");
-  ok(objectVar.target.querySelector(".value").className.includes("token-other"),
-    "Should have the right token class for 'largeObject'.");
+    if (doNotExpand) {
+      return promise.resolve();
+    }
+
+    return variable.expand()
+      .then(() => verifyFirstLevel(variable, spec));
+  }));
+}
 
-  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.");
+// In objects and arrays, the sliced pages are at the top-level of
+// the expanded object, but with Maps and Sets, we have to expand
+// <entries> first and look there.
+function getExpandedPages(variable, hasEntries) {
+  let expandedPages = promise.defer();
+  if (hasEntries) {
+    let entries = variable.get("<entries>");
+    ok(entries, "<entries> retrieved");
+    entries.expand().then(() => expandedPages.resolve(entries));
+  } else {
+    expandedPages.resolve(variable);
+  }
 
-  return promise.all([arrayVar.expand(),objectVar.expand()]);
+  return expandedPages.promise;
 }
 
-function verifyFirstLevel() {
-  let localScope = gVariables.getScopeAtIndex(0);
-  let arrayVar = localScope.get("largeArray");
-  let objectVar = localScope.get("largeObject");
+function verifyFirstLevel(variable, spec) {
+  let { varName, hasEntries, extraProps } = spec;
 
-  let arrayEnums = arrayVar.target.querySelector(".variables-view-element-details.enum").childNodes;
-  let arrayNonEnums = arrayVar.target.querySelector(".variables-view-element-details.nonenum").childNodes;
-  is(arrayEnums.length, 0,
-    "The 'largeArray' shouldn't contain any enumerable elements.");
-  is(arrayNonEnums.length, 9,
-    "The 'largeArray' should contain all the created non-enumerable elements.");
+  let enums = variable._enum.childNodes;
+  let nonEnums = variable._nonenum.childNodes;
 
-  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(enums.length, hasEntries ? 1 : 4,
+    `The '${varName}' contains the right number of enumerable elements.`);
+  is(nonEnums.length, extraProps.length,
+    `The '${varName}' contains the right number of non-enumerable elements.`);
 
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    "[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"),
-    "[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"),
-    "[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"),
-    "[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.");
+  // the sliced pages begin after <entries> row
+  let pagesOffset = hasEntries ? 1 : 0;
+  let expandedPages = getExpandedPages(variable, hasEntries);
+
+  return expandedPages.then((pagesList) => {
+    toPageNames(PAGE_RANGES).forEach((pageName, i) => {
+      let index = i + pagesOffset;
 
-  is(objectVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    "[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"),
-    "[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"),
-    "[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"),
-    "[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(pagesList.target.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+        pageName, `The page #${i + 1} in the '${varName}' is named correctly.`);
+      is(pagesList.target.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+        "", `The page #${i + 1} in the '${varName}' should not have a corresponding value.`);
+    });
+  }).then(() => {
+    extraProps.forEach(([ propName, propValue ], i) => {
+      // the extra props start after the 4 pages
+      let index = i + pagesOffset + 4;
 
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"),
-    "buffer", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
-    "ArrayBuffer", "The other properties 'largeArray' have the correct value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"),
-    "byteLength", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"),
-    "10000", "The other properties 'largeArray' have the correct value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"),
-    "byteOffset", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"),
-    "0", "The other properties 'largeArray' have the correct value.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"),
-    "length", "The other properties 'largeArray' are named correctly.");
-  is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"),
-    "10000", "The other properties 'largeArray' have the correct value.");
+      is(variable.target.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+        propName, `The other properties in '${varName}' are named correctly.`);
+      is(variable.target.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+        propValue, `The other properties in '${varName}' have the correct value.`);
+    });
+  }).then(() => verifyNextLevels(variable, spec));
+}
 
-  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.");
+function verifyNextLevels(variable, spec) {
+  let { varName, hasEntries } = spec;
+
+  // the entries are already expanded in verifyFirstLevel
+  let pagesList = hasEntries ? variable.get("<entries>") : variable;
 
-  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.");
+  let lastPage = pagesList.get(toPageNames(PAGE_RANGES)[3]);
+  ok(lastPage, `The last page in the 1st level of '${varName}' was retrieved successfully.`);
+
+  return lastPage.expand()
+    .then(() => verifyNextLevels2(lastPage, varName));
 }
 
-function verifyNextLevels() {
-  let localScope = gVariables.getScopeAtIndex(0);
-  let objectVar = localScope.get("largeObject");
+function verifyNextLevels2(lastPage1, varName) {
+  const PAGE_RANGES_IN_LAST_PAGE = [
+    [7500, 8124], [8125, 8749], [8750, 9374], [9375, 9999]
+  ];
+
+  let pageEnums1 = lastPage1._enum.childNodes;
+  let pageNonEnums1 = lastPage1._nonenum.childNodes;
+  is(pageEnums1.length, 4,
+    `The last page in the 1st level of '${varName}' should contain all the created enumerable elements.`);
+  is(pageNonEnums1.length, 0,
+    `The last page in the 1st level of '${varName}' should not contain any non-enumerable elements.`);
 
-  let lastPage1 = objectVar.get("[7500" + gEllipsis + "9999]");
-  ok(lastPage1, "The last page in the first level was retrieved successfully.");
-  return lastPage1.expand()
-                  .then(verifyNextLevels2.bind(null, lastPage1));
+  let pageNames = toPageNames(PAGE_RANGES_IN_LAST_PAGE);
+  pageNames.forEach((pageName, i) => {
+    is(lastPage1._enum.querySelectorAll(".variables-view-property .name")[i].getAttribute("value"),
+      pageName, `The page #${i + 1} in the 2nd level of '${varName}' is named correctly.`);
+  });
+
+  let lastPage2 = lastPage1.get(pageNames[3]);
+  ok(lastPage2, "The last page in the 2nd level was retrieved successfully.");
+
+  return lastPage2.expand()
+    .then(() => verifyNextLevels3(lastPage2, varName));
 }
 
-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.");
+function verifyNextLevels3(lastPage2, varName) {
+  let pageEnums2 = lastPage2._enum.childNodes;
+  let pageNonEnums2 = lastPage2._nonenum.childNodes;
+  is(pageEnums2.length, 625,
+    `The last page in the 3rd level of '${varName}' should contain all the created enumerable elements.`);
+  is(pageNonEnums2.length, 0,
+    `The last page in the 3rd level of '${varName}' shouldn't contain any non-enumerable elements.`);
 
-  is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
-    "[7500" + gEllipsis + "8124]", "The first page in this level named correctly (1).");
-  is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
-    "[8125" + gEllipsis + "8749]", "The second page in this level named correctly (1).");
-  is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
-    "[8750" + gEllipsis + "9374]", "The third page in this level named correctly (1).");
-  is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
-    "[9375" + gEllipsis + "9999]", "The fourth page in this level named correctly (1).");
-
-  let lastPage2 = lastPage1.get("[9375" + gEllipsis + "9999]");
-  ok(lastPage2, "The last page in the second level was retrieved successfully.");
-  return lastPage2.expand()
-                  .then(verifyNextLevels3.bind(null, lastPage2));
-}
+  const LEAF_ITEMS = [
+    [0, 9375, 624],
+    [1, 9376, 623],
+    [623, 9998, 1],
+    [624, 9999, 0]
+  ];
 
-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, 625,
-    "The last page in the third level should contain all the created enumerable elements.");
-  is(pageNonEnums2.length, 0,
-    "The last page in the third level shouldn't contain any non-enumerable elements.");
+  function expectedValue(name, value) {
+    switch (varName) {
+      case "largeArray": return 0;
+      case "largeObject": return value;
+      case "largeMap": return name + " \u2192 " + value;
+      case "largeSet": return 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(lastPage2._enum.querySelectorAll(".variables-view-property .name")[624].getAttribute("value"),
-    9999, "The properties in this level are named correctly (3).");
-
-  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(lastPage2._enum.querySelectorAll(".variables-view-property .value")[624].getAttribute("value"),
-    0, "The properties in this level have the correct value (3).");
+  LEAF_ITEMS.forEach(([index, name, value]) => {
+    is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[index].getAttribute("value"),
+      name, `The properties in the leaf level of '${varName}' are named correctly.`);
+    is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[index].getAttribute("value"),
+      expectedValue(name, value), `The properties in the leaf level of '${varName}' have the correct value.`);
+  });
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gPanel = null;
   gDebugger = null;
   gVariables = null;
   gEllipsis = null;
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_variables-view-map-set.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that Map and Set and their Weak friends are displayed in variables view.
+ */
+
+"use strict";
+
+const TAB_URL = EXAMPLE_URL + "doc_map-set.html";
+
+var test = Task.async(function*() {
+  const [tab,, panel] = yield initDebugger(TAB_URL);
+  yield ensureSourceIs(panel, "doc_map-set.html", true);
+
+  const scopes = waitForCaretAndScopes(panel, 37);
+  callInTab(tab, "startTest");
+  yield scopes;
+
+  const variables = panel.panelWin.DebuggerView.Variables;
+  ok(variables, "Should get the variables view.");
+
+  const scope = variables.getScopeAtIndex(0);
+  ok(scope, "Should get the current function's scope.");
+
+  /* Test the maps */
+  for (let varName of ["map", "weakMap"]) {
+    const mapVar = scope.get(varName);
+    ok(mapVar, `Retrieved the '${varName}' variable from the scope`);
+
+    info(`Expanding '${varName}' variable`);
+    yield mapVar.expand();
+
+    const entries = mapVar.get("<entries>");
+    ok(entries, `Retrieved the '${varName}' entries`);
+
+    info(`Expanding '${varName}' entries`);
+    yield entries.expand();
+
+    // Check the entries. WeakMap returns its entries in a nondeterministic
+    // order, so we make our job easier by not testing the exact values.
+    let i = 0;
+    for (let [ name, entry ] of entries) {
+      is(name, i, `The '${varName}' entry's property name is correct`);
+      ok(entry.displayValue.startsWith("Object \u2192 "),
+        `The '${varName}' entry's property value is correct`);
+      yield entry.expand();
+
+      let key = entry.get("key");
+      ok(key, `The '${varName}' entry has the 'key' property`);
+      yield key.expand();
+
+      let keyProperty = key.get("a");
+      ok(keyProperty,
+        `The '${varName}' entry's 'key' has the correct property`);
+
+      let value = entry.get("value");
+      ok(value, `The '${varName}' entry has the 'value' property`);
+
+      i++;
+    }
+
+    is(i, 2, `The '${varName}' entry count is correct`);
+
+    // Check the extra property on the object
+    let extraProp = mapVar.get("extraProp");
+    ok(extraProp, `Retrieved the '${varName}' extraProp`);
+    is(extraProp.displayValue, "true",
+      `The '${varName}' extraProp's value is correct`);
+  }
+
+  /* Test the sets */
+  for (let varName of ["set", "weakSet"]) {
+    const setVar = scope.get(varName);
+    ok(setVar, `Retrieved the '${varName}' variable from the scope`);
+
+    info(`Expanding '${varName}' variable`);
+    yield setVar.expand();
+
+    const entries = setVar.get("<entries>");
+    ok(entries, `Retrieved the '${varName}' entries`);
+
+    info(`Expanding '${varName}' entries`);
+    yield entries.expand();
+
+    // Check the entries. WeakSet returns its entries in a nondeterministic
+    // order, so we make our job easier by not testing the exact values.
+    let i = 0;
+    for (let [ name, entry ] of entries) {
+      is(name, i, `The '${varName}' entry's property name is correct`);
+      is(entry.displayValue, "Object",
+        `The '${varName}' entry's property value is correct`);
+      yield entry.expand();
+
+      let entryProperty = entry.get("a");
+      ok(entryProperty,
+        `The '${varName}' entry's value has the correct property`);
+
+      i++;
+    }
+
+    is(i, 2, `The '${varName}' entry count is correct`);
+
+    // Check the extra property on the object
+    let extraProp = setVar.get("extraProp");
+    ok(extraProp, `Retrieved the '${varName}' extraProp`);
+    is(extraProp.displayValue, "true",
+      `The '${varName}' extraProp's value is correct`);
+  }
+
+  resumeDebuggerThenCloseAndFinish(panel);
+});
--- a/devtools/client/debugger/test/mochitest/doc_large-array-buffer.html
+++ b/devtools/client/debugger/test/mochitest/doc_large-array-buffer.html
@@ -11,17 +11,22 @@
   <body>
     <button onclick="test(10000)">Click me!</button>
 
     <script type="text/javascript">
       function test(aNumber) {
         var buffer = new ArrayBuffer(aNumber);
         var largeArray = new Int8Array(buffer);
         var largeObject = {};
+        var largeMap = new Map();
+        var largeSet = new Set();
 
         for (var i = 0; i < aNumber; i++) {
-          largeObject[i] = aNumber - i - 1;
+          let value = aNumber - i - 1;
+          largeObject[i] = value;
+          largeMap.set(i, value);
+          largeSet.add(value);
         }
         debugger;
       }
     </script>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_map-set.html
@@ -0,0 +1,42 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger test page for Maps and Sets</title>
+  </head>
+
+  <body>
+    <script>
+      function startTest() {
+        let obj0 = { a: 0 };
+        let obj1 = { a: 1 };
+
+        let map = new Map();
+        map.set(obj0, 0);
+        map.set(obj1, 1);
+        map.extraProp = true;
+
+        let weakMap = new WeakMap();
+        weakMap.set(obj0, 0);
+        weakMap.set(obj1, 1);
+        weakMap.extraProp = true;
+
+        let set = new Set();
+        set.add(obj0);
+        set.add(obj1);
+        set.extraProp = true;
+
+        let weakSet = new WeakSet();
+        weakSet.add(obj0);
+        weakSet.add(obj1);
+        weakSet.extraProp = true;
+
+        debugger;
+      };
+    </script>
+  </body>
+
+</html>
--- a/devtools/client/shared/widgets/VariablesView.jsm
+++ b/devtools/client/shared/widgets/VariablesView.jsm
@@ -2533,19 +2533,17 @@ Variable.prototype = Heritage.extend(Sco
     this._displayVariable();
     this._customizeVariable();
     this._prepareTooltips();
     this._setAttributes();
     this._addEventListeners();
 
     if (this._initialDescriptor.enumerable ||
         this._nameString == "this" ||
-        (this._internalItem &&
-         (this._nameString == "<return>" ||
-          this._nameString == "<exception>"))) {
+        this._internalItem) {
       this.ownerView._enum.appendChild(this._target);
       this.ownerView._enumItems.push(this);
     } else {
       this.ownerView._nonenum.appendChild(this._target);
       this.ownerView._nonEnumItems.push(this);
     }
   },
 
@@ -3483,16 +3481,29 @@ VariablesView.stringifiers.byType = {
     }
     return null;
   },
 
   symbol: function(aGrip, aOptions) {
     const name = aGrip.name || "";
     return "Symbol(" + name + ")";
   },
+
+  mapEntry: function(aGrip, {concise}) {
+    let { preview: { key, value }} = aGrip;
+
+    let keyString = VariablesView.getString(key, {
+      concise: true,
+      noStringQuotes: true,
+    });
+    let valueString = VariablesView.getString(value, { concise: true });
+
+    return keyString + " \u2192 " + valueString;
+  },
+
 }; // VariablesView.stringifiers.byType
 
 VariablesView.stringifiers.byObjectClass = {
   Function: function(aGrip, {concise}) {
     // TODO: Bug 948484 - support arrow functions and ES6 generators
 
     let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || "";
     name = VariablesView.getString(name, { noStringQuotes: true });
--- a/devtools/client/shared/widgets/VariablesViewController.jsm
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -166,45 +166,43 @@ VariablesViewController.prototype = {
   /**
    * 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) {
+  _populatePropertySlices: function(aTarget, aGrip) {
     if (aGrip.count < MAX_PROPERTY_ITEMS) {
       return this._populateFromPropertyIterator(aTarget, aGrip);
     }
 
     // Divide the keys into quarters.
     let items = Math.ceil(aGrip.count / 4);
-
+    let iterator = aGrip.propertyIterator;
     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,
+        propertyIterator: iterator,
         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 }) => {
+      iterator.names([start, start + count - 1], ({ names }) => {
         let label = "[" + names[0] + L10N.ellipsis + names[1] + "]";
-        let item = aTarget.addItem(label);
+        let item = aTarget.addItem(label, {}, { internalItem: true });
         item.showArrow();
         this.addExpander(item, sliceGrip);
         deferred.resolve();
       });
       promises.push(deferred.promise);
     }
 
     return promise.all(promises);
@@ -217,17 +215,17 @@ VariablesViewController.prototype = {
    * @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);
+      return this._populatePropertySlices(aTarget, aGrip);
     }
     // 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, {
@@ -267,45 +265,45 @@ VariablesViewController.prototype = {
       };
       objectClient.enumProperties(options, ({ iterator }) => {
         let sliceGrip = {
           type: "property-iterator",
           propertyIterator: iterator,
           start: 0,
           count: iterator.count
         };
-        this._populatePropertySlices(aTarget, sliceGrip, iterator)
+        this._populatePropertySlices(aTarget, sliceGrip)
             .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));
+            deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip));
           });
         });
       });
     } 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));
+        deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip));
       });
 
     }
     return deferred.promise;
   },
 
   /**
    * Adds the given prototype in the view.
@@ -328,39 +326,51 @@ VariablesViewController.prototype = {
    * 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) {
+    if (aGrip.class === "Promise" && aGrip.promiseState) {
+      const { state, value, reason } = aGrip.promiseState;
+      aTarget.addItem("<state>", { value: state }, { internalItem: true });
+      if (state === "fulfilled") {
+        this.addExpander(
+          aTarget.addItem("<value>", { value }, { internalItem: true }),
+          value);
+      } else if (state === "rejected") {
+        this.addExpander(
+          aTarget.addItem("<reason>", { value: reason }, { internalItem: true }),
+          reason);
+      }
+    } else if (["Map", "WeakMap", "Set", "WeakSet"].includes(aGrip.class)) {
+      let entriesList = aTarget.addItem("<entries>", {}, { internalItem: true });
+      entriesList.showArrow();
+      this.addExpander(entriesList, {
+        type: "entries-list",
+        obj: aGrip
+      });
+    }
+
     // 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 => {
@@ -483,16 +493,40 @@ VariablesViewController.prototype = {
     aTarget.addItems(aBindings.variables, {
       // Not all variables need to force sorted properties.
       sorted: VARIABLES_SORTING_ENABLED,
       // Expansion handlers must be set after the properties are added.
       callback: this.addExpander
     });
   },
 
+  _populateFromEntries: function(target, grip) {
+    let objGrip = grip.obj;
+    let objectClient = this._getObjectClient(objGrip);
+
+    return new promise((resolve, reject) => {
+      objectClient.enumEntries((response) => {
+        if (response.error) {
+          // Older server might not support the enumEntries method
+          console.warn(response.error + ": " + response.message);
+          resolve();
+        } else {
+          let sliceGrip = {
+            type: "property-iterator",
+            propertyIterator: response.iterator,
+            start: 0,
+            count: response.iterator.count
+          };
+
+          resolve(this._populatePropertySlices(target, sliceGrip));
+        }
+      });
+    });
+  },
+
   /**
    * Adds an 'onexpand' callback for a variable, lazily handling
    * the addition of new properties.
    *
    * @param Variable aTarget
    *        The variable where the properties will be placed into.
    * @param any aSource
    *        The source to use to populate the target.
@@ -565,16 +599,31 @@ VariablesViewController.prototype = {
 
     let deferred = promise.defer();
     aTarget._fetched = deferred.promise;
 
     if (aSource.type === "property-iterator") {
       return this._populateFromPropertyIterator(aTarget, aSource);
     }
 
+    if (aSource.type === "entries-list") {
+      return this._populateFromEntries(aTarget, aSource);
+    }
+
+    if (aSource.type === "mapEntry") {
+      aTarget.addItems({
+        key: { value: aSource.preview.key },
+        value: { value: aSource.preview.value }
+      }, {
+        callback: this.addExpander
+      });
+
+      return promise.resolve();
+    }
+
     // 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();
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -221,16 +221,26 @@ ObjectActor.prototype = {
   onEnumProperties: function(request) {
     let actor = new PropertyIteratorActor(this, request.options);
     this.registeredPool.addActor(actor);
     this.iterators.add(actor);
     return { iterator: actor.grip() };
   },
 
   /**
+   * Creates an actor to iterate over entries of a Map/Set-like object.
+   */
+  onEnumEntries: function() {
+    let actor = new PropertyIteratorActor(this, { enumEntries: true });
+    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.
    */
   onPrototypeAndProperties: function() {
     let ownProperties = Object.create(null);
     let names;
     try {
       names = this.obj.getOwnPropertyNames();
@@ -675,26 +685,30 @@ ObjectActor.prototype.requestTypes = {
   "displayString": ObjectActor.prototype.onDisplayString,
   "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
   "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
+  "rejectionStack": ObjectActor.prototype.onRejectionStack,
+  "enumEntries": ObjectActor.prototype.onEnumEntries,
 };
 
 /**
  * Creates an actor to iterate over an object's property names and values.
  *
  * @param objectActor ObjectActor
  *        The object actor.
  * @param options Object
  *        A dictionary object with various boolean attributes:
+ *        - enumEntries Boolean
+ *          If true, enumerates the entries of a Map or Set object
+ *          instead of enumerating properties.
  *        - 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`
@@ -703,115 +717,176 @@ ObjectActor.prototype.requestTypes = {
  *          If true, the iterator will sort the properties by name
  *          before dispatching them.
  *        - query String
  *          If non-empty, will filter the properties by names and values
  *          containing this query string. The match is not case-sensitive.
  *          Regarding value filtering it just compare to the stringification
  *          of the property value.
  */
-function PropertyIteratorActor(objectActor, options){
+function PropertyIteratorActor(objectActor, options) {
   this.objectActor = objectActor;
 
-  let ownProperties = Object.create(null);
-  let names = [];
-  try {
-    names = this.objectActor.obj.getOwnPropertyNames();
-  } catch (ex) {}
+  if (options.enumEntries) {
+    this._initEntries();
+  } else {
+    this._initProperties(options);
+  }
+}
+
+PropertyIteratorActor.prototype = {
+  actorPrefix: "propertyIterator",
+
+  _initEntries: function() {
+    let names = [];
+    let ownProperties = Object.create(null);
+
+    switch (this.objectActor.obj.class) {
+      case "Map":
+      case "WeakMap": {
+        let idx = 0;
+        let enumFn = this.objectActor.obj.class === "Map" ?
+          enumMapEntries : enumWeakMapEntries;
+        for (let entry of enumFn(this.objectActor)) {
+          names.push(idx);
+          ownProperties[idx] = {
+            enumerable: true,
+            value: {
+              type: "mapEntry",
+              preview: {
+                key: entry[0],
+                value: entry[1]
+              }
+            }
+          };
 
-  let safeGetterValues = {};
-  let safeGetterNames = [];
-  if (!options.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);
+          idx++;
+        }
+        break;
       }
+      case "Set":
+      case "WeakSet": {
+        let idx = 0;
+        let enumFn = this.objectActor.obj.class === "Set" ?
+          enumSetEntries : enumWeakSetEntries;
+        for (let item of enumFn(this.objectActor)) {
+          names.push(idx);
+          ownProperties[idx] = {
+            enumerable: true,
+            value: item
+          };
+
+          idx++;
+        }
+        break;
+      }
+      default:
+        // the ownProperties and names are left empty
+        break;
     }
-  }
+
+    this.names = names;
+    this.ownProperties = ownProperties;
+  },
+
+  _initProperties: function(options) {
+    let names = [];
+    let ownProperties = Object.create(null);
 
-  if (options.ignoreIndexedProperties || options.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;
+    try {
+      names = this.objectActor.obj.getOwnPropertyNames();
+    } catch (ex) {}
+
+    let safeGetterValues = {};
+    let safeGetterNames = [];
+    if (!options.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 (options.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 (options.ignoreIndexedProperties || options.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 (options.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 (options.ignoreNonIndexedProperties) {
+        names = names.filter(i => {
+          i = parseFloat(i);
+          return Number.isInteger(i) && i >= 0 && i < length;
+        });
+      }
+    }
+
+    if (options.query) {
+      let { query } = options;
+      query = query.toLowerCase();
+      names = names.filter(name => {
+        // Filter on attribute names
+        if (name.toLowerCase().includes(query)) {
+          return true;
+        }
+        // and then on attribute values
+        let desc;
+        try {
+          desc = this.obj.getOwnPropertyDescriptor(name);
+        } catch(e) {}
+        if (desc && desc.value &&
+            String(desc.value).includes(query)) {
+          return true;
+        }
+        return false;
       });
     }
 
-    if (options.ignoreNonIndexedProperties) {
-      names = names.filter(i => {
-        i = parseFloat(i);
-        return Number.isInteger(i) && i >= 0 && i < length;
-      });
+    if (options.sort) {
+      names.sort();
     }
-  }
 
-  if (options.query) {
-    let { query } = options;
-    query = query.toLowerCase();
-    names = names.filter(name => {
-      // Filter on attribute names
-      if (name.toLowerCase().includes(query)) {
-        return true;
-      }
-      // and then on attribute values
-      let desc;
-      try {
-        desc = this.obj.getOwnPropertyDescriptor(name);
-      } catch(e) {}
-      if (desc && desc.value &&
-          String(desc.value).includes(query)) {
-        return true;
+    // Now build the descriptor list
+    for (let name of names) {
+      let desc = this.objectActor._propertyDescriptor(name);
+      if (!desc) {
+        desc = safeGetterValues[name];
       }
-      return false;
-    });
-  }
-
-  if (options.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;
     }
-    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",
+    this.names = names;
+    this.ownProperties = ownProperties;
+  },
 
   grip: function() {
     return {
       type: "propertyIterator",
       actor: this.actorID,
       count: this.names.length
     };
   },
@@ -846,16 +921,119 @@ PropertyIteratorActor.prototype = {
 
 PropertyIteratorActor.prototype.requestTypes = {
   "names": PropertyIteratorActor.prototype.names,
   "slice": PropertyIteratorActor.prototype.slice,
   "all": PropertyIteratorActor.prototype.all,
 };
 
 /**
+ * Helper function to create a grip from a Map/Set entry
+ */
+function gripFromEntry({ obj, hooks }, entry) {
+  return hooks.createValueGrip(
+    makeDebuggeeValueIfNeeded(obj, Cu.unwaiveXrays(entry)));
+}
+
+function enumMapEntries(objectActor) {
+  // Iterating over a Map via .entries goes through various intermediate
+  // objects - an Iterator object, then a 2-element Array object, then the
+  // actual values we care about. We don't have Xrays to Iterator objects,
+  // so we get Opaque wrappers for them. And even though we have Xrays to
+  // Arrays, the semantics often deny access to the entires based on the
+  // nature of the values. So we need waive Xrays for the iterator object
+  // and the tupes, and then re-apply them on the underlying values until
+  // we fix bug 1023984.
+  //
+  // Even then though, we might want to continue waiving Xrays here for the
+  // same reason we do so for Arrays above - this filtering behavior is likely
+  // to be more confusing than beneficial in the case of Object previews.
+  let raw = objectActor.obj.unsafeDereference();
+
+  return {
+    [Symbol.iterator]: function*() {
+      for (let keyValuePair of Cu.waiveXrays(Map.prototype.entries.call(raw))) {
+        yield keyValuePair.map(val => gripFromEntry(objectActor, val));
+      }
+    }
+  };
+}
+
+function enumWeakMapEntries(objectActor) {
+  // We currently lack XrayWrappers for WeakMap, so when we iterate over
+  // the values, the temporary iterator objects get created in the target
+  // compartment. However, we _do_ have Xrays to Object now, so we end up
+  // Xraying those temporary objects, and filtering access to |it.value|
+  // based on whether or not it's Xrayable and/or callable, which breaks
+  // the for/of iteration.
+  //
+  // This code is designed to handle untrusted objects, so we can safely
+  // waive Xrays on the iterable, and relying on the Debugger machinery to
+  // make sure we handle the resulting objects carefully.
+  let raw = objectActor.obj.unsafeDereference();
+  let keys = Cu.waiveXrays(ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(raw));
+
+  return {
+    size: keys.length,
+    [Symbol.iterator]: function*() {
+      for (let key of keys) {
+        let value = WeakMap.prototype.get.call(raw, key);
+        yield [ key, value ].map(val => gripFromEntry(objectActor, val));
+      }
+    }
+  };
+}
+
+function enumSetEntries(objectActor) {
+  // We currently lack XrayWrappers for Set, so when we iterate over
+  // the values, the temporary iterator objects get created in the target
+  // compartment. However, we _do_ have Xrays to Object now, so we end up
+  // Xraying those temporary objects, and filtering access to |it.value|
+  // based on whether or not it's Xrayable and/or callable, which breaks
+  // the for/of iteration.
+  //
+  // This code is designed to handle untrusted objects, so we can safely
+  // waive Xrays on the iterable, and relying on the Debugger machinery to
+  // make sure we handle the resulting objects carefully.
+  let raw = objectActor.obj.unsafeDereference();
+
+  return {
+    [Symbol.iterator]: function*() {
+      for (let item of Cu.waiveXrays(Set.prototype.values.call(raw))) {
+        yield gripFromEntry(objectActor, item);
+      }
+    }
+  };
+}
+
+function enumWeakSetEntries(objectActor) {
+  // We currently lack XrayWrappers for WeakSet, so when we iterate over
+  // the values, the temporary iterator objects get created in the target
+  // compartment. However, we _do_ have Xrays to Object now, so we end up
+  // Xraying those temporary objects, and filtering access to |it.value|
+  // based on whether or not it's Xrayable and/or callable, which breaks
+  // the for/of iteration.
+  //
+  // This code is designed to handle untrusted objects, so we can safely
+  // waive Xrays on the iterable, and relying on the Debugger machinery to
+  // make sure we handle the resulting objects carefully.
+  let raw = objectActor.obj.unsafeDereference();
+  let keys = Cu.waiveXrays(ThreadSafeChromeUtils.nondeterministicGetWeakSetKeys(raw));
+
+  return {
+    size: keys.length,
+    [Symbol.iterator]: function*() {
+      for (let item of keys) {
+        yield gripFromEntry(objectActor, item);
+      }
+    }
+  };
+}
+
+/**
  * 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 two
  * arguments:
  *   - the ObjectActor instance and its hooks to make a preview for,
  *   - the grip object being prepared for the client,
@@ -979,168 +1157,108 @@ DebuggerServer.ObjectActorPreviewers = {
       if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
         break;
       }
     }
 
     return true;
   }],
 
-  Set: [function({obj, hooks}, grip) {
-    let size = DevToolsUtils.getProperty(obj, "size");
+  Set: [function(objectActor, grip) {
+    let size = DevToolsUtils.getProperty(objectActor.obj, "size");
     if (typeof size != "number") {
       return false;
     }
 
     grip.preview = {
       kind: "ArrayLike",
       length: size,
     };
 
     // Avoid recursive object grips.
-    if (hooks.getGripDepth() > 1) {
+    if (objectActor.hooks.getGripDepth() > 1) {
       return true;
     }
 
-    let raw = obj.unsafeDereference();
     let items = grip.preview.items = [];
-    // We currently lack XrayWrappers for Set, so when we iterate over
-    // the values, the temporary iterator objects get created in the target
-    // compartment. However, we _do_ have Xrays to Object now, so we end up
-    // Xraying those temporary objects, and filtering access to |it.value|
-    // based on whether or not it's Xrayable and/or callable, which breaks
-    // the for/of iteration.
-    //
-    // This code is designed to handle untrusted objects, so we can safely
-    // waive Xrays on the iterable, and relying on the Debugger machinery to
-    // make sure we handle the resulting objects carefully.
-    for (let item of Cu.waiveXrays(Set.prototype.values.call(raw))) {
-      item = Cu.unwaiveXrays(item);
-      item = makeDebuggeeValueIfNeeded(obj, item);
-      items.push(hooks.createValueGrip(item));
+    for (let item of enumSetEntries(objectActor)) {
+      items.push(item);
       if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
         break;
       }
     }
 
     return true;
   }],
 
-  WeakSet: [function({obj, hooks}, grip) {
-    let raw = obj.unsafeDereference();
+  WeakSet: [function(objectActor, grip) {
+    let enumEntries = enumWeakSetEntries(objectActor);
 
-    // We currently lack XrayWrappers for WeakSet, so when we iterate over
-    // the values, the temporary iterator objects get created in the target
-    // compartment. However, we _do_ have Xrays to Object now, so we end up
-    // Xraying those temporary objects, and filtering access to |it.value|
-    // based on whether or not it's Xrayable and/or callable, which breaks
-    // the for/of iteration.
-    //
-    // This code is designed to handle untrusted objects, so we can safely
-    // waive Xrays on the iterable, and relying on the Debugger machinery to
-    // make sure we handle the resulting objects carefully.
-    let keys = Cu.waiveXrays(ThreadSafeChromeUtils.nondeterministicGetWeakSetKeys(raw));
     grip.preview = {
       kind: "ArrayLike",
-      length: keys.length,
+      length: enumEntries.size
     };
 
     // Avoid recursive object grips.
-    if (hooks.getGripDepth() > 1) {
+    if (objectActor.hooks.getGripDepth() > 1) {
       return true;
     }
 
     let items = grip.preview.items = [];
-    for (let item of keys) {
-      item = Cu.unwaiveXrays(item);
-      item = makeDebuggeeValueIfNeeded(obj, item);
-      items.push(hooks.createValueGrip(item));
+    for (let item of enumEntries) {
+      items.push(item);
       if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
         break;
       }
     }
 
     return true;
   }],
 
-  Map: [function({obj, hooks}, grip) {
-    let size = DevToolsUtils.getProperty(obj, "size");
+  Map: [function(objectActor, grip) {
+    let size = DevToolsUtils.getProperty(objectActor.obj, "size");
     if (typeof size != "number") {
       return false;
     }
 
     grip.preview = {
       kind: "MapLike",
       size: size,
     };
 
-    if (hooks.getGripDepth() > 1) {
+    if (objectActor.hooks.getGripDepth() > 1) {
       return true;
     }
 
-    let raw = obj.unsafeDereference();
     let entries = grip.preview.entries = [];
-    // Iterating over a Map via .entries goes through various intermediate
-    // objects - an Iterator object, then a 2-element Array object, then the
-    // actual values we care about. We don't have Xrays to Iterator objects,
-    // so we get Opaque wrappers for them. And even though we have Xrays to
-    // Arrays, the semantics often deny access to the entires based on the
-    // nature of the values. So we need waive Xrays for the iterator object
-    // and the tupes, and then re-apply them on the underlying values until
-    // we fix bug 1023984.
-    //
-    // Even then though, we might want to continue waiving Xrays here for the
-    // same reason we do so for Arrays above - this filtering behavior is likely
-    // to be more confusing than beneficial in the case of Object previews.
-    for (let keyValuePair of Cu.waiveXrays(Map.prototype.entries.call(raw))) {
-      let key = Cu.unwaiveXrays(keyValuePair[0]);
-      let value = Cu.unwaiveXrays(keyValuePair[1]);
-      key = makeDebuggeeValueIfNeeded(obj, key);
-      value = makeDebuggeeValueIfNeeded(obj, value);
-      entries.push([hooks.createValueGrip(key),
-                    hooks.createValueGrip(value)]);
+    for (let entry of enumMapEntries(objectActor)) {
+      entries.push(entry);
       if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
         break;
       }
     }
 
     return true;
   }],
 
-  WeakMap: [function({obj, hooks}, grip) {
-    let raw = obj.unsafeDereference();
-    // We currently lack XrayWrappers for WeakMap, so when we iterate over
-    // the values, the temporary iterator objects get created in the target
-    // compartment. However, we _do_ have Xrays to Object now, so we end up
-    // Xraying those temporary objects, and filtering access to |it.value|
-    // based on whether or not it's Xrayable and/or callable, which breaks
-    // the for/of iteration.
-    //
-    // This code is designed to handle untrusted objects, so we can safely
-    // waive Xrays on the iterable, and relying on the Debugger machinery to
-    // make sure we handle the resulting objects carefully.
-    let rawEntries = Cu.waiveXrays(ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(raw));
+  WeakMap: [function(objectActor, grip) {
+    let enumEntries = enumWeakMapEntries(objectActor);
 
     grip.preview = {
       kind: "MapLike",
-      size: rawEntries.length,
+      size: enumEntries.size
     };
 
-    if (hooks.getGripDepth() > 1) {
+    if (objectActor.hooks.getGripDepth() > 1) {
       return true;
     }
 
     let entries = grip.preview.entries = [];
-    for (let key of rawEntries) {
-      let value = Cu.unwaiveXrays(WeakMap.prototype.get.call(raw, key));
-      key = Cu.unwaiveXrays(key);
-      key = makeDebuggeeValueIfNeeded(obj, key);
-      value = makeDebuggeeValueIfNeeded(obj, value);
-      entries.push([hooks.createValueGrip(key),
-                    hooks.createValueGrip(value)]);
+    for (let entry of enumEntries) {
+      entries.push(entry);
       if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
         break;
       }
     }
 
     return true;
   }],
 
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -2483,16 +2483,41 @@ ObjectClient.prototype = {
         return { iterator: new PropertyIteratorClient(this._client, aResponse.iterator) };
       }
       return aResponse;
     },
     telemetry: "ENUMPROPERTIES"
   }),
 
   /**
+   * Request a PropertyIteratorClient instance to enumerate entries in a
+   * Map/Set-like object.
+   *
+   * @param aOnResponse function Called with the request's response.
+   */
+  enumEntries: DebuggerClient.requester({
+    type: "enumEntries"
+  }, {
+    before: function(packet) {
+      if (!["Map", "WeakMap", "Set", "WeakSet"].includes(this._grip.class)) {
+        throw new Error("enumEntries is only valid for Map/Set-like grips.");
+      }
+      return packet;
+    },
+    after: function(response) {
+      if (response.iterator) {
+        return {
+          iterator: new PropertyIteratorClient(this._client, response.iterator)
+        };
+      }
+      return response;
+    }
+  }),
+
+  /**
    * 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)