Bug 1392760 - Avoid exponential behavior when inspecting nested proxies. r=nchevobbe
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Tue, 14 May 2019 06:46:30 +0000
changeset 532657 1b3234069aba6667a34988719cca18bc18402fbb
parent 532656 f94b6f3a7fff9783dded2b404b84c0500c4182f2
child 532658 304607e12723d4f602602b4eba9232f27f4b6d64
push id11270
push userrgurzau@mozilla.com
push dateWed, 15 May 2019 15:07:19 +0000
treeherdermozilla-beta@571bc76da583 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1392760
milestone68.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 1392760 - Avoid exponential behavior when inspecting nested proxies. r=nchevobbe Differential Revision: https://phabricator.services.mozilla.com/D28214
devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/expand.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/proxy.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/get-children.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-indexed-properties.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-non-indexed-properties.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-prototype.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-symbols.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/types.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/client.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/load-properties.js
devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js
devtools/client/debugger/packages/devtools-reps/src/reps/stubs/grip.js
devtools/client/debugger/packages/devtools-reps/src/reps/tests/grip.js
devtools/client/shared/components/reps/reps.js
devtools/client/shared/widgets/VariablesViewController.jsm
devtools/client/webconsole/test/mochitest/browser.ini
devtools/client/webconsole/test/mochitest/browser_webconsole_object_inspector_nested_proxy.js
devtools/server/actors/object.js
devtools/server/actors/object/previewers.js
devtools/server/tests/unit/test_objectgrips-17.js
devtools/server/tests/unit/test_objectgrips-nested-proxy.js
devtools/server/tests/unit/xpcshell.ini
devtools/shared/client/debugger-client.js
devtools/shared/client/object-client.js
devtools/shared/specs/object.js
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/expand.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/expand.js
@@ -49,17 +49,18 @@ function generateDefaults(overrides) {
   };
 }
 const LongStringClientMock = require("../__mocks__/long-string-client");
 
 function mount(props, { initialState } = {}) {
   const client = {
     createObjectClient: grip =>
       ObjectClient(grip, {
-        getPrototype: () => Promise.resolve(protoStub)
+        getPrototype: () => Promise.resolve(protoStub),
+        getProxySlots: () => Promise.resolve(gripRepStubs.get("testProxySlots"))
       }),
 
     createLongStringClient: grip =>
       LongStringClientMock(grip, {
         substring: function(initiaLength, length, cb) {
           cb({
             substring: "<<<<"
           });
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/proxy.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/component/proxy.js
@@ -1,17 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 /* global jest */
 const { mountObjectInspector } = require("../test-utils");
 
 const { MODE } = require("../../../reps/constants");
-const stub = require("../../../reps/stubs/grip").get("testProxy");
+const gripStubs = require("../../../reps/stubs/grip");
+const stub = gripStubs.get("testProxy");
+const proxySlots = gripStubs.get("testProxySlots");
 const { formatObjectInspector } = require("../test-utils");
 
 const ObjectClient = require("../__mocks__/object-client");
 function generateDefaults(overrides) {
   return {
     roots: [
       {
         path: "root",
@@ -27,63 +29,72 @@ function generateDefaults(overrides) {
 function getEnumPropertiesMock() {
   return jest.fn(() => ({
     iterator: {
       slice: () => ({})
     }
   }));
 }
 
+function getProxySlotsMock() {
+  return jest.fn(() => proxySlots);
+}
+
 function mount(props, { initialState } = {}) {
   const enumProperties = getEnumPropertiesMock();
+  const getProxySlots = getProxySlotsMock();
 
   const client = {
-    createObjectClient: grip => ObjectClient(grip, { enumProperties })
+    createObjectClient: grip =>
+      ObjectClient(grip, { enumProperties, getProxySlots })
   };
 
   const obj = mountObjectInspector({
     client,
     props: generateDefaults(props),
     initialState
   });
 
-  return { ...obj, enumProperties };
+  return { ...obj, enumProperties, getProxySlots };
 }
 
 describe("ObjectInspector - Proxy", () => {
   it("renders Proxy as expected", () => {
-    const { wrapper, enumProperties } = mount(
+    const { wrapper, enumProperties, getProxySlots } = mount(
       {},
       {
         initialState: {
           objectInspector: {
             // Have the prototype already loaded so the component does not call
             // enumProperties for the root's properties.
-            loadedProperties: new Map([["root", { prototype: {} }]]),
+            loadedProperties: new Map([["root", proxySlots]]),
             evaluations: new Map()
           }
         }
       }
     );
 
     expect(formatObjectInspector(wrapper)).toMatchSnapshot();
 
     // enumProperties should not have been called.
     expect(enumProperties.mock.calls).toHaveLength(0);
+
+    // getProxySlots should not have been called.
+    expect(getProxySlots.mock.calls).toHaveLength(0);
   });
 
   it("calls enumProperties on <target> and <handler> clicks", () => {
     const { wrapper, enumProperties } = mount(
       {},
       {
         initialState: {
           objectInspector: {
             // Have the prototype already loaded so the component does not call
             // enumProperties for the root's properties.
-            loadedProperties: new Map([["root", { prototype: {} }]]),
+            loadedProperties: new Map([["root", proxySlots]]),
             evaluations: new Map()
           }
         }
       }
     );
 
     const nodes = wrapper.find(".node");
 
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/get-children.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/get-children.js
@@ -70,24 +70,25 @@ describe("getChildren", () => {
     expect(paths).toEqual([
       "Symbol(rootpath/x)",
       "Symbol(rootpath/<get x()>)",
       "Symbol(rootpath/<set x()>)"
     ]);
   });
 
   it("returns the expected nodes for Proxy", () => {
-    const nodes = getChildren({
-      item: createNode({
-        name: "root",
-        path: "rootpath",
-        contents: { value: gripStubs.get("testProxy") }
-      })
+    const proxyNode = createNode({
+      name: "root",
+      path: "rootpath",
+      contents: { value: gripStubs.get("testProxy") }
     });
-
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const nodes = getChildren({ item: proxyNode, loadedProperties });
     const names = nodes.map(n => n.name);
     const paths = nodes.map(n => n.path.toString());
 
     expect(names).toEqual(["<target>", "<handler>"]);
     expect(paths).toEqual([
       "Symbol(rootpath/<target>)",
       "Symbol(rootpath/<handler>)"
     ]);
@@ -243,17 +244,17 @@ describe("getChildren", () => {
     const cachedNodes = new Map();
     const node = createNode({
       name: "root",
       contents: { value: gripStubs.get("testProxy") }
     });
     const children = getChildren({
       cachedNodes,
       item: node,
-      loadedProperties: new Map([[node.path, { prototype: {} }]])
+      loadedProperties: new Map([[node.path, gripStubs.get("testProxySlots")]])
     });
     expect(cachedNodes.get(node.path)).toBe(children);
   });
 
   it("doesn't cache children on node with buckets and no loaded props", () => {
     const cachedNodes = new Map();
     const node = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-indexed-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-indexed-properties.js
@@ -205,17 +205,20 @@ describe("shouldLoadItemIndexedPropertie
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemIndexedProperties(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-non-indexed-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-non-indexed-properties.js
@@ -168,17 +168,20 @@ describe("shouldLoadItemNonIndexedProper
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemNonIndexedProperties(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-prototype.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-prototype.js
@@ -147,34 +147,37 @@ describe("shouldLoadItemPrototype", () =
     expect(shouldLoadItemPrototype(defaultPropertiesNode)).toBeFalsy();
   });
 
   it("returns false for a MapEntry node", () => {
     const node = GripMapEntryRep.createGripMapEntry("key", "value");
     expect(shouldLoadItemPrototype(node)).toBeFalsy();
   });
 
-  it("returns true for a Proxy node", () => {
+  it("returns false for a Proxy node", () => {
     const node = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    expect(shouldLoadItemPrototype(node)).toBeTruthy();
+    expect(shouldLoadItemPrototype(node)).toBeFalsy();
   });
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemPrototype(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-symbols.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/tests/utils/should-load-item-symbols.js
@@ -164,17 +164,20 @@ describe("shouldLoadItemSymbols", () => 
 
   it("returns true for a Proxy target node", () => {
     const proxyNode = createNode({
       name: "root",
       contents: {
         value: gripStubs.get("testProxy")
       }
     });
-    const [targetNode] = getChildren({ item: proxyNode });
+    const loadedProperties = new Map([
+      [proxyNode.path, gripStubs.get("testProxySlots")]
+    ]);
+    const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
     // Make sure we have the target node.
     expect(targetNode.name).toBe("<target>");
     expect(shouldLoadItemSymbols(targetNode)).toBeTruthy();
   });
 
   it("returns false for an accessor node", () => {
     const accessorNode = createNode({
       name: "root",
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/types.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/types.js
@@ -68,17 +68,18 @@ export type PropertiesIterator = {
   count: number,
   slice: (start: number, count: number) => Promise<GripProperties>
 };
 
 export type ObjectClient = {
   enumEntries: () => Promise<PropertiesIterator>,
   enumProperties: (options: Object) => Promise<PropertiesIterator>,
   enumSymbols: () => Promise<PropertiesIterator>,
-  getPrototype: () => Promise<{ prototype: Object }>
+  getPrototype: () => Promise<{ prototype: Object }>,
+  getProxySlots: () => Promise<{ proxyTarget: Object, proxyHandler: Object }>
 };
 
 export type LongStringClient = {
   substring: (
     start: number,
     end: number,
     response: {
       substring?: string,
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/client.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/client.js
@@ -112,16 +112,22 @@ async function getFullText(
 
       resolve({
         fullText: initial + response.substring
       });
     });
   });
 }
 
+async function getProxySlots(
+  objectClient: ObjectClient
+): Promise<{ proxyTarget?: Object, proxyHandler?: Object }> {
+  return objectClient.getProxySlots();
+}
+
 function iteratorSlice(
   iterator: PropertiesIterator,
   start: ?number,
   end: ?number
 ): Promise<GripProperties> {
   start = start || 0;
   const count = end ? end - start + 1 : iterator.count;
 
@@ -132,10 +138,11 @@ function iteratorSlice(
 }
 
 module.exports = {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   enumSymbols,
   getPrototype,
-  getFullText
+  getFullText,
+  getProxySlots
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/load-properties.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/load-properties.js
@@ -3,17 +3,18 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   getPrototype,
   enumSymbols,
-  getFullText
+  getFullText,
+  getProxySlots
 } = require("./client");
 
 const {
   getClosestGripNode,
   getClosestNonBucketNode,
   getValue,
   nodeHasAccessors,
   nodeHasAllEntriesInPreview,
@@ -72,16 +73,20 @@ function loadItemProperties(
   if (shouldLoadItemSymbols(item, loadedProperties)) {
     promises.push(enumSymbols(getObjectClient(), start, end));
   }
 
   if (shouldLoadItemFullText(item, loadedProperties)) {
     promises.push(getFullText(createLongStringClient(value), item));
   }
 
+  if (shouldLoadItemProxySlots(item, loadedProperties)) {
+    promises.push(getProxySlots(getObjectClient()));
+  }
+
   return Promise.all(promises).then(mergeResponses);
 }
 
 function mergeResponses(responses: Array<Object>): Object {
   const data = {};
 
   for (const response of responses) {
     if (response.hasOwnProperty("ownProperties")) {
@@ -94,16 +99,21 @@ function mergeResponses(responses: Array
 
     if (response.prototype) {
       data.prototype = response.prototype;
     }
 
     if (response.fullText) {
       data.fullText = response.fullText;
     }
+
+    if (response.proxyTarget && response.proxyHandler) {
+      data.proxyTarget = response.proxyTarget;
+      data.proxyHandler = response.proxyHandler;
+    }
   }
 
   return data;
 }
 
 function shouldLoadItemIndexedProperties(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
@@ -168,17 +178,18 @@ function shouldLoadItemPrototype(
     value &&
     !loadedProperties.has(item.path) &&
     !nodeIsBucket(item) &&
     !nodeIsMapEntry(item) &&
     !nodeIsEntries(item) &&
     !nodeIsDefaultProperties(item) &&
     !nodeHasAccessors(item) &&
     !nodeIsPrimitive(item) &&
-    !nodeIsLongString(item)
+    !nodeIsLongString(item) &&
+    !nodeIsProxy(item)
   );
 }
 
 function shouldLoadItemSymbols(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
 ): boolean {
   const value = getValue(item);
@@ -199,18 +210,26 @@ function shouldLoadItemSymbols(
 
 function shouldLoadItemFullText(
   item: Node,
   loadedProperties: LoadedProperties = new Map()
 ) {
   return !loadedProperties.has(item.path) && nodeIsLongString(item);
 }
 
+function shouldLoadItemProxySlots(
+  item: Node,
+  loadedProperties: LoadedProperties = new Map()
+): boolean {
+  return !loadedProperties.has(item.path) && nodeIsProxy(item);
+}
+
 module.exports = {
   loadItemProperties,
   mergeResponses,
   shouldLoadItemEntries,
   shouldLoadItemIndexedProperties,
   shouldLoadItemNonIndexedProperties,
   shouldLoadItemPrototype,
   shouldLoadItemSymbols,
-  shouldLoadItemFullText
+  shouldLoadItemFullText,
+  shouldLoadItemProxySlots
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/utils/node.js
@@ -341,18 +341,21 @@ function makeNodesForPromiseProperties(i
         type: NODE_TYPES.PROMISE_VALUE
       })
     );
   }
 
   return properties;
 }
 
-function makeNodesForProxyProperties(item: Node): Array<Node> {
-  const { proxyHandler, proxyTarget } = getValue(item);
+function makeNodesForProxyProperties(
+  loadedProps: GripProperties,
+  item: Node
+): Array<Node> {
+  const { proxyHandler, proxyTarget } = loadedProps;
 
   return [
     createNode({
       parent: item,
       name: "<target>",
       contents: { value: proxyTarget },
       type: NODE_TYPES.PROXY_TARGET
     }),
@@ -787,18 +790,18 @@ function getChildren(options: {
   if (nodeHasChildren(item)) {
     return addToCache(item.contents);
   }
 
   if (nodeIsMapEntry(item)) {
     return addToCache(makeNodesForMapEntry(item));
   }
 
-  if (nodeIsProxy(item)) {
-    return addToCache(makeNodesForProxyProperties(item));
+  if (nodeIsProxy(item) && hasLoadedProps) {
+    return addToCache(makeNodesForProxyProperties(loadedProps, item));
   }
 
   if (nodeIsLongString(item) && hasLoadedProps) {
     // Set longString object's fullText to fetched one.
     return addToCache(setNodeFullText(loadedProps, item));
   }
 
   if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/stubs/grip.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/stubs/grip.js
@@ -298,32 +298,16 @@ stubs.set("testStringObject", {
     safeGetterValues: {},
     wrappedValue: "foo"
   }
 });
 stubs.set("testProxy", {
   type: "object",
   actor: "server1.conn1.child1/obj47",
   class: "Proxy",
-  proxyTarget: {
-    type: "object",
-    actor: "server1.conn1.child1/obj48",
-    class: "Object",
-    ownPropertyLength: 1
-  },
-  proxyHandler: {
-    type: "object",
-    actor: "server1.conn1.child1/obj49",
-    class: "Array",
-    ownPropertyLength: 4,
-    preview: {
-      kind: "ArrayLike",
-      length: 3
-    }
-  },
   preview: {
     kind: "Object",
     ownProperties: {
       "<target>": {
         value: {
           type: "object",
           actor: "server1.conn1.child1/obj48",
           class: "Object",
@@ -341,16 +325,34 @@ stubs.set("testProxy", {
             length: 3
           }
         }
       }
     },
     ownPropertiesLength: 2
   }
 });
+stubs.set("testProxySlots", {
+  proxyTarget: {
+    type: "object",
+    actor: "server1.conn1.child1/obj48",
+    class: "Object",
+    ownPropertyLength: 1
+  },
+  proxyHandler: {
+    type: "object",
+    actor: "server1.conn1.child1/obj49",
+    class: "Array",
+    ownPropertyLength: 4,
+    preview: {
+      kind: "ArrayLike",
+      length: 3
+    }
+  }
+});
 stubs.set("testArrayBuffer", {
   type: "object",
   actor: "server1.conn1.child1/obj170",
   class: "ArrayBuffer",
   extensible: true,
   frozen: false,
   sealed: false,
   ownPropertyLength: 0,
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/tests/grip.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/tests/grip.js
@@ -119,17 +119,18 @@ describe("Grip - Proxy", () => {
   const object = stubs.get("testProxy");
 
   it("correctly selects Grip Rep", () => {
     expect(getRep(object)).toBe(Grip.rep);
   });
 
   it("renders as expected", () => {
     const renderRep = props => shallowRenderRep(object, props);
-    const handlerLength = getGripLengthBubbleText(object.proxyHandler, {
+    const handler = object.preview.ownProperties["<handler>"].value;
+    const handlerLength = getGripLengthBubbleText(handler, {
       mode: MODE.TINY
     });
     const out = `Proxy { <target>: {…}, <handler>: ${handlerLength} […] }`;
 
     expect(renderRep({ mode: undefined }).text()).toBe(out);
     expect(renderRep({ mode: MODE.TINY }).text()).toBe("Proxy");
     expect(renderRep({ mode: MODE.SHORT }).text()).toBe(out);
     expect(renderRep({ mode: MODE.LONG }).text()).toBe(out);
--- a/devtools/client/shared/components/reps/reps.js
+++ b/devtools/client/shared/components/reps/reps.js
@@ -1707,18 +1707,18 @@ function makeNodesForPromiseProperties(i
       contents: { value: value },
       type: NODE_TYPES.PROMISE_VALUE
     }));
   }
 
   return properties;
 }
 
-function makeNodesForProxyProperties(item) {
-  const { proxyHandler, proxyTarget } = getValue(item);
+function makeNodesForProxyProperties(loadedProps, item) {
+  const { proxyHandler, proxyTarget } = loadedProps;
 
   return [createNode({
     parent: item,
     name: "<target>",
     contents: { value: proxyTarget },
     type: NODE_TYPES.PROXY_TARGET
   }), createNode({
     parent: item,
@@ -2101,18 +2101,18 @@ function getChildren(options) {
   if (nodeHasChildren(item)) {
     return addToCache(item.contents);
   }
 
   if (nodeIsMapEntry(item)) {
     return addToCache(makeNodesForMapEntry(item));
   }
 
-  if (nodeIsProxy(item)) {
-    return addToCache(makeNodesForProxyProperties(item));
+  if (nodeIsProxy(item) && hasLoadedProps) {
+    return addToCache(makeNodesForProxyProperties(loadedProps, item));
   }
 
   if (nodeIsLongString(item) && hasLoadedProps) {
     // Set longString object's fullText to fetched one.
     return addToCache(setNodeFullText(loadedProps, item));
   }
 
   if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
@@ -3476,17 +3476,18 @@ module.exports = {
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   getPrototype,
   enumSymbols,
-  getFullText
+  getFullText,
+  getProxySlots
 } = __webpack_require__(197);
 
 const {
   getClosestGripNode,
   getClosestNonBucketNode,
   getValue,
   nodeHasAccessors,
   nodeHasAllEntriesInPreview,
@@ -3530,16 +3531,20 @@ function loadItemProperties(item, create
   if (shouldLoadItemSymbols(item, loadedProperties)) {
     promises.push(enumSymbols(getObjectClient(), start, end));
   }
 
   if (shouldLoadItemFullText(item, loadedProperties)) {
     promises.push(getFullText(createLongStringClient(value), item));
   }
 
+  if (shouldLoadItemProxySlots(item, loadedProperties)) {
+    promises.push(getProxySlots(getObjectClient()));
+  }
+
   return Promise.all(promises).then(mergeResponses);
 }
 
 function mergeResponses(responses) {
   const data = {};
 
   for (const response of responses) {
     if (response.hasOwnProperty("ownProperties")) {
@@ -3552,16 +3557,21 @@ function mergeResponses(responses) {
 
     if (response.prototype) {
       data.prototype = response.prototype;
     }
 
     if (response.fullText) {
       data.fullText = response.fullText;
     }
+
+    if (response.proxyTarget && response.proxyHandler) {
+      data.proxyTarget = response.proxyTarget;
+      data.proxyHandler = response.proxyHandler;
+    }
   }
 
   return data;
 }
 
 function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) {
   const gripItem = getClosestGripNode(item);
   const value = getValue(gripItem);
@@ -3585,38 +3595,43 @@ function shouldLoadItemEntries(item, loa
   const value = getValue(gripItem);
 
   return value && nodeIsEntries(getClosestNonBucketNode(item)) && !nodeHasAllEntriesInPreview(gripItem) && !loadedProperties.has(item.path) && !nodeNeedsNumericalBuckets(item);
 }
 
 function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
   const value = getValue(item);
 
-  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item);
+  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
 }
 
 function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
   const value = getValue(item);
 
   return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
 }
 
 function shouldLoadItemFullText(item, loadedProperties = new Map()) {
   return !loadedProperties.has(item.path) && nodeIsLongString(item);
 }
 
+function shouldLoadItemProxySlots(item, loadedProperties = new Map()) {
+  return !loadedProperties.has(item.path) && nodeIsProxy(item);
+}
+
 module.exports = {
   loadItemProperties,
   mergeResponses,
   shouldLoadItemEntries,
   shouldLoadItemIndexedProperties,
   shouldLoadItemNonIndexedProperties,
   shouldLoadItemPrototype,
   shouldLoadItemSymbols,
-  shouldLoadItemFullText
+  shouldLoadItemFullText,
+  shouldLoadItemProxySlots
 };
 
 /***/ }),
 
 /***/ 197:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
@@ -3701,33 +3716,38 @@ async function getFullText(longStringCli
 
       resolve({
         fullText: initial + response.substring
       });
     });
   });
 }
 
+async function getProxySlots(objectClient) {
+  return objectClient.getProxySlots();
+}
+
 function iteratorSlice(iterator, start, end) {
   start = start || 0;
   const count = end ? end - start + 1 : iterator.count;
 
   if (count === 0) {
     return Promise.resolve({});
   }
   return iterator.slice(start, count);
 }
 
 module.exports = {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   enumSymbols,
   getPrototype,
-  getFullText
+  getFullText,
+  getProxySlots
 };
 
 /***/ }),
 
 /***/ 2:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
--- a/devtools/client/shared/widgets/VariablesViewController.jsm
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -313,26 +313,28 @@ VariablesViewController.prototype = {
    *
    * @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 === "Proxy") {
-      this.addExpander(
-        aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }),
-        aGrip.proxyTarget);
-      this.addExpander(
-        aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }),
-        aGrip.proxyHandler);
-
-      // Refuse to play the proxy's stupid game and return immediately
+      // Refuse to play the proxy's stupid game and just expose the target and handler.
       const deferred = defer();
-      deferred.resolve();
+      const objectClient = this._getObjectClient(aGrip);
+      objectClient.getProxySlots(aResponse => {
+        const target = aTarget.addItem("<target>", { value: aResponse.proxyTarget },
+          { internalItem: true });
+        this.addExpander(target, aResponse.proxyTarget);
+        const handler = aTarget.addItem("<handler>", { value: aResponse.proxyHandler },
+          { internalItem: true });
+        this.addExpander(handler, aResponse.proxyHandler);
+        deferred.resolve();
+      });
       return deferred.promise;
     }
 
     if (aGrip.class === "Promise" && aGrip.promiseState) {
       const { state, value, reason } = aGrip.promiseState;
       aTarget.addItem("<state>", { value: state }, { internalItem: true });
       if (state === "fulfilled") {
         this.addExpander(
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -353,16 +353,17 @@ skip-if = true  # Bug 1438979
 [browser_webconsole_object_inspector.js]
 [browser_webconsole_object_inspector__proto__.js]
 [browser_webconsole_object_inspector_entries.js]
 [browser_webconsole_object_inspector_getters.js]
 [browser_webconsole_object_inspector_getters_prototype.js]
 [browser_webconsole_object_inspector_getters_shadowed.js]
 [browser_webconsole_object_inspector_key_sorting.js]
 [browser_webconsole_object_inspector_local_session_storage.js]
+[browser_webconsole_object_inspector_nested_proxy.js]
 [browser_webconsole_object_inspector_selected_text.js]
 [browser_webconsole_object_inspector_scroll.js]
 [browser_webconsole_object_inspector_while_debugging_and_inspecting.js]
 [browser_webconsole_observer_notifications.js]
 [browser_webconsole_optimized_out_vars.js]
 [browser_webconsole_output_copy.js]
 tags = clipboard
 [browser_webconsole_output_copy_newlines.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_object_inspector_nested_proxy.js
@@ -0,0 +1,50 @@
+/* -*- 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/ */
+
+"use strict";
+
+// Check evaluating and expanding getters in the console.
+const TEST_URI = "data:text/html;charset=utf8,"
+ + "<h1>Object Inspector on deeply nested proxies</h1>";
+
+add_task(async function() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    let proxy = new Proxy({}, {});
+    for (let i = 0; i < 1e5; ++i) {
+      proxy = new Proxy(proxy, proxy);
+    }
+    content.wrappedJSObject.console.log("oi-test", proxy);
+  });
+
+  const node = await waitFor(() => findMessage(hud, "oi-test"));
+  const oi = node.querySelector(".tree");
+  const [proxyNode] = getObjectInspectorNodes(oi);
+
+  expandObjectInspectorNode(proxyNode);
+  await waitFor(() => getObjectInspectorNodes(oi).length > 1);
+  checkChildren(proxyNode, [`<target>`, `<handler>`]);
+
+  const targetNode = findObjectInspectorNode(oi, "<target>");
+  expandObjectInspectorNode(targetNode);
+  await waitFor(() => getObjectInspectorChildrenNodes(targetNode).length > 0);
+  checkChildren(targetNode, [`<target>`, `<handler>`]);
+
+  const handlerNode = findObjectInspectorNode(oi, "<handler>");
+  expandObjectInspectorNode(handlerNode);
+  await waitFor(() => getObjectInspectorChildrenNodes(handlerNode).length > 0);
+  checkChildren(handlerNode, [`<target>`, `<handler>`]);
+});
+
+function checkChildren(node, expectedChildren) {
+  const children = getObjectInspectorChildrenNodes(node);
+  is(children.length, expectedChildren.length,
+    "There is the expected number of children");
+  children.forEach((child, index) => {
+    ok(child.textContent.includes(expectedChildren[index]),
+      `Expected "${expectedChildren[index]}" child`);
+  });
+}
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -861,16 +861,34 @@ const proto = {
       source,
       line: stack.line,
       column: stack.column,
       functionDisplayName: stack.functionDisplayName,
     };
   },
 
   /**
+   * Handle a protocol request to get the target and handler internal slots of a proxy.
+   */
+  proxySlots: function() {
+    // There could be transparent security wrappers, unwrap to check if it's a proxy.
+    // However, retrieve proxyTarget and proxyHandler from `this.obj` to avoid exposing
+    // the unwrapped target and handler.
+    const unwrapped = DevToolsUtils.unwrap(this.obj);
+    if (!unwrapped || !unwrapped.isProxy) {
+      return this.throwError("objectNotProxy",
+        "'proxySlots' request is only valid for grips with a 'Proxy' class.");
+    }
+    return {
+      proxyTarget: this.hooks.createValueGrip(this.obj.proxyTarget),
+      proxyHandler: this.hooks.createValueGrip(this.obj.proxyHandler),
+    };
+  },
+
+  /**
    * Release the actor, when it isn't needed anymore.
    * Protocol.js uses this release method to call the destroy method.
    */
   release: function() {},
 };
 
 exports.ObjectActor = protocol.ActorClassWithSpec(objectSpec, proto);
 exports.ObjectActorProto = proto;
--- a/devtools/server/actors/object/previewers.js
+++ b/devtools/server/actors/object/previewers.js
@@ -283,37 +283,37 @@ const previewers = {
         break;
       }
     }
 
     return true;
   }],
 
   Proxy: [function({obj, hooks}, grip, rawObj) {
+    // Only preview top-level proxies, avoiding recursion. Otherwise, since both the
+    // target and handler can also be proxies, we could get an exponential behavior.
+    if (hooks.getGripDepth() > 1) {
+      return true;
+    }
+
     // The `isProxy` getter of the debuggee object only detects proxies without
     // security wrappers. If false, the target and handler are not available.
     const hasTargetAndHandler = obj.isProxy;
-    if (hasTargetAndHandler) {
-      grip.proxyTarget = hooks.createValueGrip(obj.proxyTarget);
-      grip.proxyHandler = hooks.createValueGrip(obj.proxyHandler);
-    }
 
     grip.preview = {
       kind: "Object",
       ownProperties: Object.create(null),
       ownPropertiesLength: 2 * hasTargetAndHandler,
     };
 
-    if (hooks.getGripDepth() > 1) {
-      return true;
-    }
-
     if (hasTargetAndHandler) {
-      grip.preview.ownProperties["<target>"] = {value: grip.proxyTarget};
-      grip.preview.ownProperties["<handler>"] = {value: grip.proxyHandler};
+      Object.assign(grip.preview.ownProperties, {
+        "<target>": {value: hooks.createValueGrip(obj.proxyTarget)},
+        "<handler>": {value: hooks.createValueGrip(obj.proxyHandler)},
+      });
     }
 
     return true;
   }],
 };
 
 /**
  * Generic previewer for classes wrapping primitives, like String,
--- a/devtools/server/tests/unit/test_objectgrips-17.js
+++ b/devtools/server/tests/unit/test_objectgrips-17.js
@@ -52,18 +52,22 @@ function test({ threadClient, debuggee }
   return new Promise(function(resolve) {
     threadClient.addOneTimeListener("paused", async function(event, packet) {
       // Get the grips.
       const [proxyGrip, inheritsProxyGrip, inheritsProxy2Grip] = packet.frame.arguments;
 
       // Check the grip of the proxy object.
       check_proxy_grip(debuggee, testOptions, proxyGrip);
 
+      // Check the target and handler slots of the proxy object.
+      const proxyClient = threadClient.pauseGrip(proxyGrip);
+      const proxySlots = await proxyClient.getProxySlots();
+      check_proxy_slots(debuggee, testOptions, proxyGrip, proxySlots);
+
       // Check the prototype and properties of the proxy object.
-      const proxyClient = threadClient.pauseGrip(proxyGrip);
       const proxyResponse = await proxyClient.getPrototypeAndProperties();
       check_properties(testOptions, proxyResponse.ownProperties, true, false);
       check_prototype(debuggee, testOptions, proxyResponse.prototype, true, false);
 
       // Check the prototype and properties of the object which inherits from the proxy.
       const inheritsProxyClient = threadClient.pauseGrip(inheritsProxyGrip);
       const inheritsProxyResponse = await inheritsProxyClient.getPrototypeAndProperties();
       check_properties(testOptions, inheritsProxyResponse.ownProperties, false, false);
@@ -112,47 +116,58 @@ function test({ threadClient, debuggee }
 
 function check_proxy_grip(debuggee, testOptions, grip) {
   const { global, isOpaque, subsumes, globalIsInvisible } = testOptions;
   const {preview} = grip;
 
   if (global === debuggee) {
     // The proxy has no security wrappers.
     strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
-    ok(grip.proxyTarget, "There is a [[ProxyTarget]] grip.");
-    ok(grip.proxyHandler, "There is a [[ProxyHandler]] grip.");
     strictEqual(preview.ownPropertiesLength, 2, "The preview has 2 properties.");
-    const target = preview.ownProperties["<target>"].value;
-    strictEqual(target, grip.proxyTarget, "<target> contains the [[ProxyTarget]].");
-    const handler = preview.ownProperties["<handler>"].value;
-    strictEqual(handler, grip.proxyHandler, "<handler> contains the [[ProxyHandler]].");
+    const props = preview.ownProperties;
+    ok(props["<target>"].value, "<target> contains the [[ProxyTarget]].");
+    ok(props["<handler>"].value, "<handler> contains the [[ProxyHandler]].");
   } else if (isOpaque) {
     // The proxy has opaque security wrappers.
     strictEqual(grip.class, "Opaque", "The grip has an Opaque class.");
     strictEqual(grip.ownPropertyLength, 0, "The grip has no properties.");
   } else if (!subsumes) {
     // The proxy belongs to compartment not subsumed by the debuggee.
-    strictEqual(grip.class, "Restricted", "The grip has an Restricted class.");
+    strictEqual(grip.class, "Restricted", "The grip has a Restricted class.");
     ok(!("ownPropertyLength" in grip), "The grip doesn't know the number of properties.");
   } else if (globalIsInvisible) {
     // The proxy belongs to an invisible-to-debugger compartment.
     strictEqual(grip.class, "InvisibleToDebugger: Object",
                 "The grip has an InvisibleToDebugger class.");
     ok(!("ownPropertyLength" in grip), "The grip doesn't know the number of properties.");
   } else {
     // The proxy has non-opaque security wrappers.
     strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
-    ok(!("proxyTarget" in grip), "There is no [[ProxyTarget]] grip.");
-    ok(!("proxyHandler" in grip), "There is no [[ProxyHandler]] grip.");
     strictEqual(preview.ownPropertiesLength, 0, "The preview has no properties.");
     ok(!("<target>" in preview), "The preview has no <target> property.");
     ok(!("<handler>" in preview), "The preview has no <handler> property.");
   }
 }
 
+function check_proxy_slots(debuggee, testOptions, grip, proxySlots) {
+  const { global } = testOptions;
+
+  if (grip.class !== "Proxy") {
+    strictEqual(proxySlots, undefined, "Slots can only be retrived for Proxy grips.");
+  } else if (global === debuggee) {
+    const { proxyTarget, proxyHandler } = proxySlots;
+    strictEqual(proxyTarget.type, "object", "There is a [[ProxyTarget]] grip.");
+    strictEqual(proxyHandler.type, "object", "There is a [[ProxyHandler]] grip.");
+  } else {
+    const { proxyTarget, proxyHandler } = proxySlots;
+    strictEqual(proxyTarget.type, "undefined", "There is no [[ProxyTarget]] grip.");
+    strictEqual(proxyHandler.type, "undefined", "There is no [[ProxyHandler]] grip.");
+  }
+}
+
 function check_properties(testOptions, props, isProxy, createdInDebuggee) {
   const { subsumes, globalIsInvisible } = testOptions;
   const ownPropertiesLength = Reflect.ownKeys(props).length;
 
   if (createdInDebuggee || !isProxy && subsumes && !globalIsInvisible) {
     // The debuggee can access the properties.
     strictEqual(ownPropertiesLength, 1, "1 own property was retrieved.");
     strictEqual(props.x.value, 1, "The property has the right value.");
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_objectgrips-nested-proxy.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+add_task(threadClientTest(async ({ threadClient, debuggee, client }) => {
+  await new Promise(function(resolve) {
+    threadClient.addOneTimeListener("paused", async function(event, packet) {
+      const [grip] = packet.frame.arguments;
+      const objClient = threadClient.pauseGrip(grip);
+      const {proxyTarget, proxyHandler} = await objClient.getProxySlots();
+
+      strictEqual(grip.class, "Proxy", "Its a proxy grip.");
+      strictEqual(proxyTarget.class, "Proxy", "The target is also a proxy.");
+      strictEqual(proxyHandler.class, "Proxy", "The handler is also a proxy.");
+
+      await threadClient.resume();
+      resolve();
+    });
+    debuggee.eval(function stopMe(arg) {
+      debugger;
+    }.toString());
+    debuggee.eval(`
+      var proxy = new Proxy({}, {});
+      for (let i = 0; i < 1e5; ++i)
+        proxy = new Proxy(proxy, proxy);
+      stopMe(proxy);
+    `);
+  });
+}));
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -165,16 +165,17 @@ skip-if = true # breakpoint sliding is n
 [test_objectgrips-22.js]
 [test_objectgrips-array-like-object.js]
 [test_objectgrips-property-value-01.js]
 [test_objectgrips-property-value-02.js]
 [test_objectgrips-property-value-03.js]
 [test_objectgrips-fn-apply-01.js]
 [test_objectgrips-fn-apply-02.js]
 [test_objectgrips-fn-apply-03.js]
+[test_objectgrips-nested-proxy.js]
 [test_promise_state-01.js]
 [test_promise_state-02.js]
 [test_promise_state-03.js]
 [test_interrupt.js]
 [test_stepping-01.js]
 [test_stepping-02.js]
 [test_stepping-03.js]
 [test_stepping-04.js]
--- a/devtools/shared/client/debugger-client.js
+++ b/devtools/shared/client/debugger-client.js
@@ -354,20 +354,21 @@ DebuggerClient.prototype = {
     request.format = "json";
     request.stack = getStack();
 
     // Implement a Promise like API on the returned object
     // that resolves/rejects on request response
     const deferred = promise.defer();
     function listenerJson(resp) {
       removeRequestListeners();
+      resp = safeOnResponse(resp);
       if (resp.error) {
-        deferred.reject(safeOnResponse(resp));
+        deferred.reject(resp);
       } else {
-        deferred.resolve(safeOnResponse(resp));
+        deferred.resolve(resp);
       }
     }
     function listenerBulk(resp) {
       removeRequestListeners();
       deferred.resolve(safeOnResponse(resp));
     }
 
     const removeRequestListeners = () => {
--- a/devtools/shared/client/object-client.js
+++ b/devtools/shared/client/object-client.js
@@ -298,11 +298,34 @@ ObjectClient.prototype = {
     before: function(packet) {
       if (this._grip.class !== "Promise") {
         throw new Error("getPromiseRejectionStack is only valid for " +
           "promise grips.");
       }
       return packet;
     },
   }),
+
+  /**
+   * Request the target and handler internal slots of a proxy.
+   */
+  getProxySlots: DebuggerClient.requester({
+    type: "proxySlots",
+  }, {
+    before: function(packet) {
+      if (this._grip.class !== "Proxy") {
+        throw new Error("getProxySlots is only valid for proxy grips.");
+      }
+      return packet;
+    },
+    after: function(response) {
+      // Before Firefox 68 (bug 1392760), the proxySlots request didn't exist.
+      // The proxy target and handler were directly included in the grip.
+      if (response.error === "unrecognizedPacketType") {
+        const {proxyTarget, proxyHandler} = this._grip;
+        return {proxyTarget, proxyHandler};
+      }
+      return response;
+    },
+  }),
 };
 
 module.exports = ObjectClient;
--- a/devtools/shared/specs/object.js
+++ b/devtools/shared/specs/object.js
@@ -97,16 +97,21 @@ types.addDictType("object.dependentPromi
 
 types.addDictType("object.originalSourceLocation", {
   source: "source",
   line: "number",
   column: "number",
   functionDisplayName: "string",
 });
 
+types.addDictType("object.proxySlots", {
+  proxyTarget: "object.descriptor",
+  proxyHandler: "object.descriptor",
+});
+
 const objectSpec = generateActorSpec({
   typeName: "obj",
 
   methods: {
     allocationStack: {
       request: {},
       response: {
         allocationStack: RetVal("array:object.originalSourceLocation"),
@@ -193,16 +198,20 @@ const objectSpec = generateActorSpec({
       response: RetVal("object.apply"),
     },
     rejectionStack: {
       request: {},
       response: {
         rejectionStack: RetVal("array:object.originalSourceLocation"),
       },
     },
+    proxySlots: {
+      request: {},
+      response: RetVal("object.proxySlots"),
+    },
     release: { release: true },
     scope: {
       request: {},
       response: RetVal("object.scope"),
     },
     // Needed for the PauseScopedObjectActor which extends the ObjectActor.
     threadGrip: {
       request: {},