diff --git a/devtools/shared/heapsnapshot/census-tree-node.js b/devtools/shared/heapsnapshot/census-tree-node.js index 1b3e99382e6d..9eda307b0bc4 100644 --- a/devtools/shared/heapsnapshot/census-tree-node.js +++ b/devtools/shared/heapsnapshot/census-tree-node.js @@ -24,68 +24,117 @@ function isSavedFrame(obj) { } /** - * 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 {FrameCache} cache - * @param {SavedFrame} frame - * @param {FrameCacheValue} value + * @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. */ -FrameCache.insert = function (cache, frame, value) { - cache[FrameCache.hash(frame)] = value; +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 {CensusTreeNodeCache} cache + * @param {CensusTreeNodeCacheValue} 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)]; }; /** @@ -131,7 +180,7 @@ function getArrayOfFrames(stack) { * @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. * @@ -144,13 +193,13 @@ function getArrayOfFrames(stack) { * * - 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; @@ -161,24 +210,25 @@ function makeCensusTreeNodeSubTree(breakdown, report, edge, frameCache, outParam // 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); } @@ -196,10 +246,10 @@ function makeCensusTreeNodeSubTree(breakdown, report, edge, frameCache, outParam 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; } } @@ -222,9 +272,9 @@ function CensusTreeNodeVisitor() { 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); @@ -236,7 +286,7 @@ CensusTreeNodeVisitor.prototype = Object.create(Visitor); * @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; @@ -248,7 +298,7 @@ CensusTreeNodeVisitor.prototype.enter = function (breakdown, report, edge) { } } - this._frameCacheStack.push(new FrameCache); + this._cacheStack.push(new CensusTreeNodeCache); this._nodeStack.push(top); }; @@ -281,7 +331,7 @@ CensusTreeNodeVisitor.prototype.exit = function (breakdown, report, edge) { 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; @@ -291,7 +341,7 @@ CensusTreeNodeVisitor.prototype.exit = function (breakdown, report, edge) { } const top = this._nodeStack.pop(); - const cache = this._frameCacheStack.pop(); + const cache = this._cacheStack.pop(); dfs(top, cache); }; @@ -345,7 +395,7 @@ 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 @@ -353,13 +403,113 @@ CensusTreeNode.prototype = null; * @returns {Number} * A number suitable for using with Array.prototype.sort. */ -function compareByTotalBytes (node1, node2) { +function compareByTotal(node1, node2) { return node2.totalBytes - node1.totalBytes - || node2.bytes - node1.bytes || 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 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 @@ -381,10 +531,17 @@ function compareByTotalBytes (node1, node2) { * @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; }; diff --git a/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js b/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js index a30b263f1b89..d8710d64c55a 100644 --- a/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js +++ b/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js @@ -174,14 +174,17 @@ function savedFrameReplacer(key, val) { * * @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); diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-06.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-06.js new file mode 100644 index 000000000000..0470247645e8 --- /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 }); +} diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-07.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-07.js new file mode 100644 index 000000000000..95a2e12dd0b0 --- /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 }); +} diff --git a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini index f09d62929fc4..9614985cf5b4 100644 --- a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini +++ b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini @@ -21,6 +21,8 @@ support-files = [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]