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 329104 50c354c8516f0cf1ea64b216ca09f5de69c5968c
parent 328908 bccb11375f2af838cda714d42fd8cef78f5c7bf1
child 329105 6ecf26c604a3f6a80e6757b9107a2deb1a689937
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof, tromey, kwierso
bugs1172920
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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)