Bug 1217248 - Add the ability to invert CensusTreeNode trees; r=jsantell
authorNick Fitzgerald <fitzgen@gmail.com>
Thu, 22 Oct 2015 09:12:36 -0700
changeset 269032 e472a823244c69201a5acab853fdd87c59f26b68
parent 269031 d61a6f4432d61393c47649afe20843599c05dfac
child 269033 0e104df4b5e921276668c761b3d8922e8dd5d614
push id29568
push userkwierso@gmail.com
push dateThu, 22 Oct 2015 23:45:51 +0000
treeherdermozilla-central@1f03a14106e5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1217248
milestone44.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 1217248 - Add the ability to invert CensusTreeNode trees; r=jsantell
devtools/shared/heapsnapshot/census-tree-node.js
devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js
devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-06.js
devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-07.js
devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
--- a/devtools/shared/heapsnapshot/census-tree-node.js
+++ b/devtools/shared/heapsnapshot/census-tree-node.js
@@ -19,78 +19,127 @@ const { Visitor, walk } = require("resou
  * @param {any} obj
  * @returns {Boolean}
  */
 function isSavedFrame(obj) {
   return Object.prototype.toString.call(obj) === "[object SavedFrame]";
 }
 
 /**
- * A FrameCache maps from SavedFrames to CensusTreeNodes. It is used when
+ * A CensusTreeNodeCache maps from SavedFrames to CensusTreeNodes. It is used when
  * aggregating multiple SavedFrame allocation stack keys into a tree of many
  * CensusTreeNodes. Each stack may share older frames, and we want to preserve
  * this sharing when converting to CensusTreeNode, so before creating a new
- * CensusTreeNode, we look for an existing one in one of our FrameCaches.
+ * CensusTreeNode, we look for an existing one in one of our CensusTreeNodeCaches.
  */
-function FrameCache() {}
-FrameCache.prototype = null;
+function CensusTreeNodeCache() {}
+CensusTreeNodeCache.prototype = null;
 
 /**
- * The value of a single entry stored in a FrameCache. It is a pair of the
- * CensusTreeNode for this frame, and the subsequent FrameCache for this node's
- * children.
+ * The value of a single entry stored in a CensusTreeNodeCache. It is a pair of
+ * the CensusTreeNode for this cache value, and the subsequent
+ * CensusTreeNodeCache for this node's children.
  *
  * @param {SavedFrame} frame
  *        The frame being cached.
  */
-function FrameCacheValue(frame) {
-  // The CensusTreeNode for this frame.
-  this.node = new CensusTreeNode(frame);
-  // The FrameCache for this frame's children.
+function CensusTreeNodeCacheValue() {
+  // The CensusTreeNode for this cache value.
+  this.node = undefined;
+  // The CensusTreeNodeCache for this frame's children.
   this.children = undefined;
 }
 
-FrameCacheValue.prototype = null;
+CensusTreeNodeCacheValue.prototype = null;
 
 /**
  * Create a unique string for the given SavedFrame (ignoring the frame's parent
- * chain) that can be used as a hash to key this frame within a FrameCache.
+ * chain) that can be used as a hash to key this frame within a CensusTreeNodeCache.
+ *
+ * NB: We manually hash rather than using an ES6 Map because we are purposely
+ * ignoring the parent chain and wish to consider frames with everything the
+ * same except their parents as the same.
  *
  * @param {SavedFrame} frame
  *        The SavedFrame object we would like to lookup in or insert into a
- *        FrameCache.
+ *        CensusTreeNodeCache.
  *
  * @returns {String}
- *          The unique string that can be used as a key in a FrameCache.
+ *          The unique string that can be used as a key in a CensusTreeNodeCache.
  */
-FrameCache.hash = function (frame) {
-  return `${frame.functionDisplayName},${frame.source},${frame.line},${frame.column},${frame.asyncCause}`;
+CensusTreeNodeCache.hashFrame = function (frame) {
+  return `FRAME,${frame.functionDisplayName},${frame.source},${frame.line},${frame.column},${frame.asyncCause}`;
 };
 
 /**
- * Associate `frame` with `value` in the given `cache`.
+ * Create a unique string for the given CensusTreeNode **with regards to
+ * siblings at the current depth of the tree, not within the whole tree.** It
+ * can be used as a hash to key this node within a CensusTreeNodeCache.
+ *
+ * @param {CensusTreeNode} node
+ *        The node we would like to lookup in or insert into a cache.
+ *
+ * @returns {String}
+ *          The unique string that can be used as a key in a CensusTreeNodeCache.
+ */
+CensusTreeNodeCache.hashNode = function (node) {
+  return isSavedFrame(node.name)
+    ? CensusTreeNodeCache.hashFrame(node.name)
+    : `NODE,${node.name}`;
+};
+
+/**
+ * Insert the given CensusTreeNodeCacheValue whose node.name is a SavedFrame
+ * object in the given cache.
  *
- * @param {FrameCache} cache
- * @param {SavedFrame} frame
- * @param {FrameCacheValue} value
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNodeCacheValue} value
  */
-FrameCache.insert = function (cache, frame, value) {
-  cache[FrameCache.hash(frame)] = value;
+CensusTreeNodeCache.insertFrame = function (cache, value) {
+  cache[CensusTreeNodeCache.hashFrame(value.node.name)] = value;
+};
+
+/**
+ * Insert the given value in the cache.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNodeCacheValue} value
+ */
+CensusTreeNodeCache.insertNode = function (cache, value) {
+  if (isSavedFrame(value.node.name)) {
+    CensusTreeNodeCache.insertFrame(cache, value);
+  } else {
+    cache[CensusTreeNodeCache.hashNode(value.node)] = value;
+  }
 };
 
 /**
  * Lookup `frame` in `cache` and return its value if it exists.
  *
- * @param {FrameCache} cache
+ * @param {CensusTreeNodeCache} cache
  * @param {SavedFrame} frame
  *
- * @returns {undefined|FrameCacheValue}
+ * @returns {undefined|CensusTreeNodeCacheValue}
  */
-FrameCache.lookup = function (cache, frame) {
-  return cache[FrameCache.hash(frame)];
+CensusTreeNodeCache.lookupFrame = function (cache, frame) {
+  return cache[CensusTreeNodeCache.hashFrame(frame)];
+};
+
+/**
+ * Lookup `node` in `cache` and return its value if it exists.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNode} node
+ *
+ * @returns {undefined|CensusTreeNodeCacheValue}
+ */
+CensusTreeNodeCache.lookupNode = function (cache, node) {
+  return isSavedFrame(node.name)
+    ? CensusTreeNodeCache.lookupFrame(cache, node.name)
+    : cache[CensusTreeNodeCache.hashNode(node)];
 };
 
 /**
  * Add `child` to `parent`'s set of children.
  *
  * @param {CensusTreeNode} parent
  * @param {CensusTreeNode} child
  */
@@ -126,64 +175,65 @@ function getArrayOfFrames(stack) {
  *        The breakdown specifying the structure of the given report.
  *
  * @param {Object} report
  *        The census report.
  *
  * @param {null|String|SavedFrame} edge
  *        The edge leading to this report from the parent report.
  *
- * @param {FrameCache} frameCache
+ * @param {CensusTreeNodeCache} cache
  *        The cache of CensusTreeNodes we have already made for the siblings of
  *        the node being created. The existing nodes are reused when possible.
  *
  * @param {Object} outParams
  *        The return values are attached to this object after this function
  *        returns. Because we create a CensusTreeNode for each frame in a
  *        SavedFrame stack edge, there may multiple nodes per sub-report.
  *
  *          - top: The deepest node in the CensusTreeNode subtree created.
  *
  *          - bottom: The shallowest node in the CensusTreeNode subtree created.
  *                    This is null if the shallowest node in the subtree was
- *                    found in the `frameCache` and reused.
+ *                    found in the `cache` and reused.
  *
  *        Note that top and bottom are not necessarily different. In the case
  *        where there is a 1:1 correspondence between an edge in the report and
  *        a CensusTreeNode, top and bottom refer to the same node.
  */
-function makeCensusTreeNodeSubTree(breakdown, report, edge, frameCache, outParams) {
+function makeCensusTreeNodeSubTree(breakdown, report, edge, cache, outParams) {
   if (!isSavedFrame(edge)) {
     const node = new CensusTreeNode(edge);
     outParams.top = outParams.bottom = node;
     return;
   }
 
   // Loop through each frame in the stack and get or create a CensusTreeNode for
   // the frame.
 
   const frames = getArrayOfFrames(edge);
-  let cache = frameCache;
+  let currentCache = cache;
   let prevNode;
   for (let i = 0, length = frames.length; i < length; i++) {
     const frame = frames[i];
 
-    // Get or create the FrameCacheValue for this frame. If we already have a
-    // FrameCacheValue (and hence a CensusTreeNode) for this frame, we don't
-    // need to add the node to the previous node's children as we have already
-    // done that. If we don't have a FrameCacheValue and CensusTreeNode for
-    // this frame, then create one and make sure to hook it up as a child of
-    // the previous node.
+    // Get or create the CensusTreeNodeCacheValue for this frame. If we already
+    // have a CensusTreeNodeCacheValue (and hence a CensusTreeNode) for this
+    // frame, we don't need to add the node to the previous node's children as
+    // we have already done that. If we don't have a CensusTreeNodeCacheValue
+    // and CensusTreeNode for this frame, then create one and make sure to hook
+    // it up as a child of the previous node.
     let isNewNode = false;
-    let val = FrameCache.lookup(cache, frame);
+    let val = CensusTreeNodeCache.lookupFrame(currentCache, frame);
     if (!val) {
       isNewNode = true;
-      val = new FrameCacheValue(frame);
+      val = new CensusTreeNodeCacheValue();
+      val.node = new CensusTreeNode(frame);
 
-      FrameCache.insert(cache, frame, val);
+      CensusTreeNodeCache.insertFrame(currentCache, val);
       if (prevNode) {
         addChild(prevNode, val.node);
       }
     }
 
     if (i === 0) {
       outParams.bottom = isNewNode ? val.node : null;
     }
@@ -191,20 +241,20 @@ function makeCensusTreeNodeSubTree(break
       outParams.top = val.node;
     }
 
     prevNode = val.node;
 
     if (i !== length - 1 && !val.children) {
       // This is not the last frame and therefore this node will have
       // children, which we must cache.
-      val.children = new FrameCache();
+      val.children = new CensusTreeNodeCache();
     }
 
-    cache = val.children;
+    currentCache = val.children;
   }
 }
 
 /**
  * A Visitor that walks a census report and creates the corresponding
  * CensusTreeNode tree.
  */
 function CensusTreeNodeVisitor() {
@@ -217,43 +267,43 @@ function CensusTreeNodeVisitor() {
 
   // To avoid unnecessary allocations, we reuse the same out parameter object
   // passed to `makeCensusTreeNodeSubTree` every time we call it.
   this._outParams = {
     top: null,
     bottom: null,
   };
 
-  // The stack of `FrameCache`s that we use to aggregate many SavedFrame stacks
+  // The stack of `CensusTreeNodeCache`s that we use to aggregate many SavedFrame stacks
   // into a single CensusTreeNode tree.
-  this._frameCacheStack = [new FrameCache()];
+  this._cacheStack = [new CensusTreeNodeCache()];
 }
 
 CensusTreeNodeVisitor.prototype = Object.create(Visitor);
 
 /**
  * Create the CensusTreeNode subtree for this sub-report and link it to the
  * parent CensusTreeNode.
  *
  * @overrides Visitor.prototype.enter
  */
 CensusTreeNodeVisitor.prototype.enter = function (breakdown, report, edge) {
-  const cache = this._frameCacheStack[this._frameCacheStack.length - 1];
+  const cache = this._cacheStack[this._cacheStack.length - 1];
   makeCensusTreeNodeSubTree(breakdown, report, edge, cache, this._outParams);
   const { top, bottom } = this._outParams;
 
   if (!this._root) {
     this._root = bottom;
   } else {
     if (bottom) {
       addChild(this._nodeStack[this._nodeStack.length - 1], bottom);
     }
   }
 
-  this._frameCacheStack.push(new FrameCache);
+  this._cacheStack.push(new CensusTreeNodeCache);
   this._nodeStack.push(top);
 };
 
 function values(cache) {
   return Object.keys(cache).map(k => cache[k]);
 }
 
 /**
@@ -276,27 +326,27 @@ CensusTreeNodeVisitor.prototype.exit = f
         dfs(childValues[i].node, childValues[i].children);
       }
     }
 
     node.totalCount = node.count;
     node.totalBytes = node.bytes;
 
     if (node.children) {
-      node.children.sort(compareByTotalBytes);
+      node.children.sort(compareByTotal);
 
       for (let i = 0, length = node.children.length; i < length; i++) {
         node.totalCount += node.children[i].totalCount;
         node.totalBytes += node.children[i].totalBytes;
       }
     }
   }
 
   const top = this._nodeStack.pop();
-  const cache = this._frameCacheStack.pop();
+  const cache = this._cacheStack.pop();
   dfs(top, cache);
 };
 
 /**
  * @overrides Visitor.prototype.count
  */
 CensusTreeNodeVisitor.prototype.count = function (breakdown, report, edge) {
   const node = this._nodeStack[this._nodeStack.length - 1];
@@ -340,29 +390,129 @@ function CensusTreeNode (name) {
   this.totalCount = 0;
   this.children = undefined;
 }
 
 CensusTreeNode.prototype = null;
 
 /**
  * Compare the given nodes by their `totalBytes` properties, and breaking ties
- * with the `bytes`, `totalCount`, and `count` properties (in that order).
+ * with the `totalCount`, `bytes`, and `count` properties (in that order).
+ *
+ * @param {CensusTreeNode} node1
+ * @param {CensusTreeNode} node2
+ *
+ * @returns {Number}
+ *          A number suitable for using with Array.prototype.sort.
+ */
+function compareByTotal(node1, node2) {
+  return node2.totalBytes - node1.totalBytes
+      || node2.totalCount - node1.totalCount
+      || node2.bytes      - node1.bytes
+      || node2.count      - node1.count;
+}
+
+/**
+ * Compare the given nodes by their `bytes` properties, and breaking ties with
+ * the `count`, `totalBytes`, and `totalCount` properties (in that order).
  *
  * @param {CensusTreeNode} node1
  * @param {CensusTreeNode} node2
  *
  * @returns {Number}
  *          A number suitable for using with Array.prototype.sort.
  */
-function compareByTotalBytes (node1, node2) {
-  return node2.totalBytes - node1.totalBytes
-      || node2.bytes      - node1.bytes
-      || node2.totalCount - node1.totalCount
-      || node2.count      - node1.count;
+function compareBySelf(node1, node2) {
+  return node2.bytes      - node1.bytes
+      || node2.count      - node1.count
+      || node2.totalBytes - node1.totalBytes
+      || node2.totalCount - node1.totalCount;
+}
+
+/**
+ * Given an un-inverted CensusTreeNode tree, return the corresponding inverted
+ * CensusTreeNode tree. The input tree is not modified. The resulting inverted
+ * tree is sorted by self bytes rather than by total bytes.
+ *
+ * @param {CensusTreeNode} tree
+ *        The un-inverted tree.
+ *
+ * @returns {CensusTreeNode}
+ *          The corresponding inverted tree.
+ */
+function invert(tree) {
+  const inverted = new CensusTreeNodeCacheValue();
+  inverted.node = new CensusTreeNode(null);
+
+  // Do a depth-first search of the un-inverted tree. As we reach each leaf,
+  // take the path from the old root to the leaf, reverse that path, and add it
+  // to the new, inverted tree's root.
+
+  const path = [];
+  (function addInvertedPaths(node) {
+    path.push(node);
+
+    if (node.children) {
+      for (let i = 0, length = node.children.length; i < length; i++) {
+        addInvertedPaths(node.children[i]);
+      }
+    } else {
+      // We found a leaf node, add the reverse path to the inverted tree.
+
+      let current = inverted;
+      for (let i = path.length - 1; i >= 0; i--) {
+        const node = path[i];
+
+        if (!current.children) {
+          current.children = new CensusTreeNodeCache();
+        }
+
+        // If we already have a corresponding node in the inverted tree, merge
+        // this node's counts with it. Otherwise, create the corresponding node
+        // in the inverted tree, add it to the parent's children cache, and
+        // create the parent->child edge.
+        let val = CensusTreeNodeCache.lookupNode(current.children, node);
+        if (val) {
+          val.node.count += node.count;
+          val.node.totalCount += node.totalCount;
+          val.node.bytes += node.bytes;
+          val.node.totalBytes += node.totalBytes;
+        } else {
+          val = new CensusTreeNodeCacheValue();
+
+          val.node = new CensusTreeNode(node.name);
+          val.node.count = node.count;
+          val.node.totalCount = node.totalCount;
+          val.node.bytes = node.bytes;
+          val.node.totalBytes = node.totalBytes;
+
+          addChild(current.node, val.node);
+          CensusTreeNodeCache.insertNode(current.children, val);
+        }
+
+        current = val;
+      }
+    }
+
+    path.pop();
+  }(tree));
+
+  // Next, do a depth-first search of the inverted tree and ensure that siblings
+  // are sorted by their self bytes/count.
+
+  (function ensureSorted(node) {
+    if (node.children) {
+      node.children.sort(compareBySelf);
+      for (let i = 0, length = node.children.length; i < length; i++) {
+        ensureSorted(node.children[i]);
+      }
+    }
+  }(inverted.node));
+
+  return inverted.node;
 }
 
 /**
  * Takes a report from a census (`dbg.memory.takeCensus()`) and the breakdown
  * used to generate the census and returns a structure used to render
  * a tree to display the data.
  *
  * Returns a recursive "CensusTreeNode" object, looking like:
@@ -376,15 +526,22 @@ function compareByTotalBytes (node1, nod
  * }
  *
  * @param {Object} breakdown
  *        The breakdown used to generate the census report.
  *
  * @param {Object} report
  *        The census report generated with the specified breakdown.
  *
+ * @param {Object} options
+ *        Configuration options.
+ *          - invert: Whether to invert the resulting tree or not. Defaults to
+ *                    false, ie uninverted.
+ *
  * @returns {CensusTreeNode}
  */
-exports.censusReportToCensusTreeNode = function (breakdown, report) {
+exports.censusReportToCensusTreeNode = function (breakdown, report,
+                                                 options = { invert: false }) {
   const visitor = new CensusTreeNodeVisitor();
   walk(breakdown, report, visitor);
-  return visitor.root();
+  const root = visitor.root();
+  return options.invert ? invert(root) : root;
 };
--- a/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js
+++ b/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js
@@ -169,24 +169,27 @@ function savedFrameReplacer(key, val) {
  * @param {Object} breakdown
  *        The census breakdown.
  *
  * @param {Object} report
  *        The census report.
  *
  * @param {Object} expected
  *        The expected CensusTreeNode result.
+ *
+ * @param {Object} options
+ *        The options to pass through to `censusReportToCensusTreeNode`.
  */
-function compareCensusViewData (breakdown, report, expected) {
+function compareCensusViewData (breakdown, report, expected, options) {
   dumpn("Generating CensusTreeNode from report:");
   dumpn("breakdown: " + JSON.stringify(breakdown, null, 4));
   dumpn("report: " + JSON.stringify(report, null, 4));
   dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4));
 
-  const actual = censusReportToCensusTreeNode(breakdown, report);
+  const actual = censusReportToCensusTreeNode(breakdown, report, options);
   dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4));
 
   assertStructurallyEquivalent(actual, expected);
 }
 
 // Deep structural equivalence that can handle Map objects in addition to plain
 // objects.
 function assertStructurallyEquivalent(actual, expected, path="root") {
new file mode 100644
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-06.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test inverting CensusTreeNode with a by alloaction stack breakdown.
+ */
+
+function run_test() {
+  const BREAKDOWN = {
+    by: "allocationStack",
+    then: { by: "count", count: true, bytes: true },
+    noStack: { by: "count", count: true, bytes: true },
+  };
+
+  let stack1, stack2, stack3, stack4;
+
+  function a(n) {
+    return b(n);
+  }
+  function b(n) {
+    return c(n);
+  }
+  function c(n) {
+    return saveStack(n);
+  }
+  function d(n) {
+    return b(n);
+  }
+  function e(n) {
+    return c(n);
+  }
+
+  const abc_Stack = a(3);
+  const  bc_Stack = b(2);
+  const   c_Stack = c(1);
+  const dbc_Stack = d(3);
+  const  ec_Stack = e(2);
+
+  const REPORT = new Map([
+    [abc_Stack, { bytes: 10, count: 1 }],
+    [ bc_Stack, { bytes: 10, count: 1 }],
+    [  c_Stack, { bytes: 10, count: 1 }],
+    [dbc_Stack, { bytes: 10, count: 1 }],
+    [ ec_Stack, { bytes: 10, count: 1 }],
+    ["noStack", { bytes: 50, count: 5 }],
+  ]);
+
+  const EXPECTED = {
+    name: null,
+    bytes: 0,
+    totalBytes: 0,
+    count: 0,
+    totalCount: 0,
+    children: [
+      {
+        name: "noStack",
+        bytes: 50,
+        totalBytes: 50,
+        count: 5,
+        totalCount: 5,
+        children: [
+          {
+            name: null,
+            bytes: 0,
+            totalBytes: 100,
+            count: 0,
+            totalCount: 10,
+            children: undefined
+          }
+        ]
+      },
+      {
+        name: abc_Stack,
+        bytes: 50,
+        totalBytes: 50,
+        count: 5,
+        totalCount: 5,
+        children: [
+          {
+            name: null,
+            bytes: 0,
+            totalBytes: 100,
+            count: 0,
+            totalCount: 10,
+            children: undefined
+          },
+          {
+            name: abc_Stack.parent,
+            bytes: 0,
+            totalBytes: 30,
+            count: 0,
+            totalCount: 3,
+            children: [
+              {
+                name: null,
+                bytes: 0,
+                totalBytes: 100,
+                count: 0,
+                totalCount: 10,
+                children: undefined
+              },
+              {
+                name: abc_Stack.parent.parent,
+                bytes: 0,
+                totalBytes: 10,
+                count: 0,
+                totalCount: 1,
+                children: [
+                  {
+                    name: null,
+                    bytes: 0,
+                    totalBytes: 100,
+                    count: 0,
+                    totalCount: 10,
+                    children: undefined
+                  }
+                ]
+              },
+              {
+                name: dbc_Stack.parent.parent,
+                bytes: 0,
+                totalBytes: 10,
+                count: 0,
+                totalCount: 1,
+                children: [
+                  {
+                    name: null,
+                    bytes: 0,
+                    totalBytes: 100,
+                    count: 0,
+                    totalCount: 10,
+                    children: undefined
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            name: ec_Stack.parent,
+            bytes: 0,
+            totalBytes: 10,
+            count: 0,
+            totalCount: 1,
+            children: [
+              {
+                name: null,
+                bytes: 0,
+                totalBytes: 100,
+                count: 0,
+                totalCount: 10,
+                children: undefined
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  };
+
+  compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-07.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test inverting CensusTreeNode with a non-allocation stack breakdown.
+ */
+
+function run_test() {
+  const BREAKDOWN = {
+    by: "coarseType",
+    objects: {
+      by: "objectClass",
+      then: { by: "count", count: true, bytes: true },
+      other: { by: "count", count: true, bytes: true },
+    },
+    scripts: {
+      by: "internalType",
+      then: { by: "count", count: true, bytes: true },
+    },
+    strings: {
+      by: "internalType",
+      then: { by: "count", count: true, bytes: true },
+    },
+    other:{
+      by: "internalType",
+      then: { by: "count", count: true, bytes: true },
+    },
+  };
+
+  const REPORT = {
+    objects: {
+      Array: { bytes: 50, count: 5 },
+      other: { bytes: 0, count: 0 },
+    },
+    scripts: {
+      "js::jit::JitScript": { bytes: 30, count: 3 },
+    },
+    strings: {
+      JSAtom: { bytes: 60, count: 6 },
+    },
+    other: {
+      "js::Shape": { bytes: 80, count: 8 },
+    }
+  };
+
+  const EXPECTED = {
+    name: null,
+    bytes: 0,
+    totalBytes: 0,
+    count: 0,
+    totalCount: 0,
+    children: [
+      {
+        name: "js::Shape",
+        bytes: 80,
+        totalBytes: 80,
+        count: 8,
+        totalCount: 8,
+        children: [
+          {
+            name: "other",
+            bytes: 0,
+            totalBytes: 80,
+            count: 0,
+            totalCount: 8,
+            children: [
+              {
+                name: null,
+                bytes: 0,
+                totalBytes: 220,
+                count: 0,
+                totalCount: 22,
+                children: undefined
+              }
+            ]
+          }
+        ]
+      },
+      {
+        name: "JSAtom",
+        bytes: 60,
+        totalBytes: 60,
+        count: 6,
+        totalCount: 6,
+        children: [
+          {
+            name: "strings",
+            bytes: 0,
+            totalBytes: 60,
+            count: 0,
+            totalCount: 6,
+            children: [
+              {
+                name: null,
+                bytes: 0,
+                totalBytes: 220,
+                count: 0,
+                totalCount: 22,
+                children: undefined
+              }
+            ]
+          }
+        ]
+      },
+      {
+        name: "Array",
+        bytes: 50,
+        totalBytes: 50,
+        count: 5,
+        totalCount: 5,
+        children: [
+          {
+            name: "objects",
+            bytes: 0,
+            totalBytes: 50,
+            count: 0,
+            totalCount: 5,
+            children: [
+              {
+                name: null,
+                bytes: 0,
+                totalBytes: 220,
+                count: 0,
+                totalCount: 22,
+                children: undefined
+              }
+            ]
+          }
+        ]
+      },
+      {
+        name: "js::jit::JitScript",
+        bytes: 30,
+        totalBytes: 30,
+        count: 3,
+        totalCount: 3,
+        children: [
+          {
+            name: "scripts",
+            bytes: 0,
+            totalBytes: 30,
+            count: 0,
+            totalCount: 3,
+            children: [
+              {
+                name: null,
+                bytes: 0,
+                totalBytes: 220,
+                count: 0,
+                totalCount: 22,
+                children: undefined
+              }
+            ]
+          }
+        ]
+      },
+      {
+        name: "other",
+        bytes: 0,
+        totalBytes: 0,
+        count: 0,
+        totalCount: 0,
+        children: [
+          {
+            name: "objects",
+            bytes: 0,
+            totalBytes: 50,
+            count: 0,
+            totalCount: 5,
+            children: [
+              {
+                name: null,
+                bytes: 0,
+                totalBytes: 220,
+                count: 0,
+                totalCount: 22,
+                children: undefined
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  };
+
+  compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true });
+}
--- a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
+++ b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
@@ -16,16 +16,18 @@ support-files =
 [test_census_diff_04.js]
 [test_census_diff_05.js]
 [test_census_diff_06.js]
 [test_census-tree-node-01.js]
 [test_census-tree-node-02.js]
 [test_census-tree-node-03.js]
 [test_census-tree-node-04.js]
 [test_census-tree-node-05.js]
+[test_census-tree-node-06.js]
+[test_census-tree-node-07.js]
 [test_HeapAnalyses_readHeapSnapshot_01.js]
 [test_HeapAnalyses_takeCensusDiff_01.js]
 [test_HeapAnalyses_takeCensus_01.js]
 [test_HeapAnalyses_takeCensus_02.js]
 [test_HeapAnalyses_takeCensus_03.js]
 [test_HeapAnalyses_takeCensus_04.js]
 [test_HeapAnalyses_takeCensus_05.js]
 [test_HeapAnalyses_takeCensus_06.js]