DominatorTreeNode.js (10260B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 "use strict"; 5 6 const { 7 immutableUpdate, 8 } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js"); 9 const { 10 Visitor, 11 walk, 12 } = require("resource://devtools/shared/heapsnapshot/CensusUtils.js"); 13 const { 14 deduplicatePaths, 15 } = require("resource://devtools/shared/heapsnapshot/shortest-paths.js"); 16 17 const DEFAULT_MAX_DEPTH = 4; 18 const DEFAULT_MAX_SIBLINGS = 15; 19 const DEFAULT_MAX_NUM_PATHS = 5; 20 21 /** 22 * A Visitor that is used to generate a label for a node in the heap snapshot 23 * and get its shallow size as well while we are at it. 24 */ 25 class LabelAndShallowSizeVisitor extends Visitor { 26 constructor() { 27 super(); 28 29 // As we walk the description, we accumulate edges in this array. 30 this._labelPieces = []; 31 32 // Once we reach the non-zero count leaf node in the description, we move the 33 // labelPieces here to signify that we no longer need to accumulate edges. 34 this._label = undefined; 35 36 // Once we reach the non-zero count leaf node in the description, we grab the 37 // shallow size and place it here. 38 this._shallowSize = 0; 39 } 40 /** 41 * @override 42 */ 43 enter(breakdown, report, edge) { 44 if (this._labelPieces && edge) { 45 this._labelPieces.push(edge); 46 } 47 } 48 /** 49 * @override 50 */ 51 exit(breakdown, report, edge) { 52 if (this._labelPieces && edge) { 53 this._labelPieces.pop(); 54 } 55 } 56 /** 57 * @override 58 */ 59 count(breakdown, report) { 60 if (report.count === 0) { 61 return; 62 } 63 64 this._label = this._labelPieces; 65 this._labelPieces = undefined; 66 67 this._shallowSize = report.bytes; 68 } 69 /** 70 * Get the generated label structure accumulated by this visitor. 71 * 72 * @returns {object} 73 */ 74 label() { 75 return this._label; 76 } 77 /** 78 * Get the shallow size of the node this visitor visited. 79 * 80 * @returns {number} 81 */ 82 shallowSize() { 83 return this._shallowSize; 84 } 85 } 86 87 /** 88 * A single node in a dominator tree. 89 */ 90 class DominatorTreeNode { 91 /** 92 * @param {NodeId} nodeId 93 * @param {NodeSize} retainedSize 94 */ 95 constructor(nodeId, label, shallowSize, retainedSize) { 96 // The id of this node. 97 this.nodeId = nodeId; 98 99 // The label structure generated by describing the given node. 100 this.label = label; 101 102 // The shallow size of this node. 103 this.shallowSize = shallowSize; 104 105 // The retained size of this node. 106 this.retainedSize = retainedSize; 107 108 // The id of this node's parent or undefined if this node is the root. 109 this.parentId = undefined; 110 111 // An array of immediately dominated child `DominatorTreeNode`s, or undefined. 112 this.children = undefined; 113 114 // An object of the form returned by `deduplicatePaths`, encoding the set of 115 // the N shortest retaining paths for this node as a graph. 116 this.shortestPaths = undefined; 117 118 // True iff the `children` property does not contain every immediately 119 // dominated node. 120 // 121 // * If children is an array and this property is true: the array does not 122 // contain the complete set of immediately dominated children. 123 // * If children is an array and this property is false: the array contains 124 // the complete set of immediately dominated children. 125 // * If children is undefined and this property is true: there exist 126 // immediately dominated children for this node, but they have not been 127 // loaded yet. 128 // * If children is undefined and this property is false: this node does not 129 // dominate any others and therefore has no children. 130 this.moreChildrenAvailable = true; 131 } 132 133 /** 134 * Add `child` to the `parent`'s set of children. 135 * 136 * @param {DominatorTreeNode} parent 137 * @param {DominatorTreeNode} child 138 */ 139 static addChild(parent, child) { 140 if (parent.children === undefined) { 141 parent.children = []; 142 } 143 144 parent.children.push(child); 145 child.parentId = parent.nodeId; 146 } 147 148 /** 149 * Generate a label structure for the node with the given id and grab its 150 * shallow size. 151 * 152 * What is a "label" structure? HeapSnapshot.describeNode essentially takes a 153 * census of a single node rather than the whole heap graph. The resulting 154 * report has only one count leaf that is non-zero. The label structure is the 155 * path in this report from the root to the non-zero count leaf. 156 * 157 * @param {number} nodeId 158 * @param {HeapSnapshot} snapshot 159 * @param {object} breakdown 160 * 161 * @returns {object} 162 * An object with the following properties: 163 * - {Number} shallowSize 164 * - {Object} label 165 */ 166 static getLabelAndShallowSize(nodeId, snapshot, breakdown) { 167 const description = snapshot.describeNode(breakdown, nodeId); 168 169 const visitor = new LabelAndShallowSizeVisitor(); 170 walk(breakdown, description, visitor); 171 172 return { 173 label: visitor.label(), 174 shallowSize: visitor.shallowSize(), 175 }; 176 } 177 178 /** 179 * Do a partial traversal of the given dominator tree and convert it into a tree 180 * of `DominatorTreeNode`s. Dominator trees have a node for every node in the 181 * snapshot's heap graph, so we must not allocate a JS object for every node. It 182 * would be way too many and the node count is effectively unbounded. 183 * 184 * Go no deeper down the tree than `maxDepth` and only consider at most 185 * `maxSiblings` within any single node's children. 186 * 187 * @param {DominatorTree} dominatorTree 188 * @param {HeapSnapshot} snapshot 189 * @param {object} breakdown 190 * @param {number} maxDepth 191 * @param {number} maxSiblings 192 * 193 * @returns {DominatorTreeNode} 194 */ 195 static partialTraversal( 196 dominatorTree, 197 snapshot, 198 breakdown, 199 maxDepth = DEFAULT_MAX_DEPTH, 200 maxSiblings = DEFAULT_MAX_SIBLINGS 201 ) { 202 function dfs(nodeId, depth) { 203 const { label, shallowSize } = DominatorTreeNode.getLabelAndShallowSize( 204 nodeId, 205 snapshot, 206 breakdown 207 ); 208 const retainedSize = dominatorTree.getRetainedSize(nodeId); 209 const node = new DominatorTreeNode( 210 nodeId, 211 label, 212 shallowSize, 213 retainedSize 214 ); 215 const childNodeIds = dominatorTree.getImmediatelyDominated(nodeId); 216 217 const newDepth = depth + 1; 218 if (newDepth < maxDepth) { 219 const endIdx = Math.min(childNodeIds.length, maxSiblings); 220 for (let i = 0; i < endIdx; i++) { 221 DominatorTreeNode.addChild(node, dfs(childNodeIds[i], newDepth)); 222 } 223 node.moreChildrenAvailable = endIdx < childNodeIds.length; 224 } else { 225 node.moreChildrenAvailable = !!childNodeIds.length; 226 } 227 228 return node; 229 } 230 231 return dfs(dominatorTree.root, 0); 232 } 233 234 /** 235 * Insert more children into the given (partially complete) dominator tree. 236 * 237 * The tree is updated in an immutable and persistent manner: a new tree is 238 * returned, but all unmodified subtrees (which is most) are shared with the 239 * original tree. Only the modified nodes are re-allocated. 240 * 241 * @param {DominatorTreeNode} tree 242 * @param {Array<NodeId>} path 243 * @param {Array<DominatorTreeNode>} newChildren 244 * @param {boolean} moreChildrenAvailable 245 * 246 * @returns {DominatorTreeNode} 247 */ 248 static insert(nodeTree, path, newChildren, moreChildrenAvailable) { 249 function insert(tree, i) { 250 if (tree.nodeId !== path[i]) { 251 return tree; 252 } 253 254 if (i == path.length - 1) { 255 return immutableUpdate(tree, { 256 children: (tree.children || []).concat(newChildren), 257 moreChildrenAvailable, 258 }); 259 } 260 261 return tree.children 262 ? immutableUpdate(tree, { 263 children: tree.children.map(c => insert(c, i + 1)), 264 }) 265 : tree; 266 } 267 268 return insert(nodeTree, 0); 269 } 270 271 /** 272 * Get the new canonical node with the given `id` in `tree` that exists along 273 * `path`. If there is no such node along `path`, return null. 274 * 275 * This is useful if we have a reference to a now-outdated DominatorTreeNode due 276 * to a recent call to DominatorTreeNode.insert and want to get the up-to-date 277 * version. We don't have to walk the whole tree: if there is an updated version 278 * of the node then it *must* be along the path. 279 * 280 * @param {NodeId} id 281 * @param {DominatorTreeNode} tree 282 * @param {Array<NodeId>} path 283 * 284 * @returns {DominatorTreeNode|null} 285 */ 286 static getNodeByIdAlongPath(id, tree, path) { 287 function find(node, i) { 288 if (!node || node.nodeId !== path[i]) { 289 return null; 290 } 291 292 if (node.nodeId === id) { 293 return node; 294 } 295 296 if (i === path.length - 1 || !node.children) { 297 return null; 298 } 299 300 const nextId = path[i + 1]; 301 return find( 302 node.children.find(c => c.nodeId === nextId), 303 i + 1 304 ); 305 } 306 307 return find(tree, 0); 308 } 309 310 /** 311 * Find the shortest retaining paths for the given set of DominatorTreeNodes, 312 * and populate each node's `shortestPaths` property with them in place. 313 * 314 * @param {HeapSnapshot} snapshot 315 * @param {object} breakdown 316 * @param {NodeId} start 317 * @param {Array<DominatorTreeNode>} treeNodes 318 * @param {number} maxNumPaths 319 */ 320 static attachShortestPaths( 321 snapshot, 322 breakdown, 323 start, 324 treeNodes, 325 maxNumPaths = DEFAULT_MAX_NUM_PATHS 326 ) { 327 const idToTreeNode = new Map(); 328 const targets = []; 329 for (const node of treeNodes) { 330 const id = node.nodeId; 331 idToTreeNode.set(id, node); 332 targets.push(id); 333 } 334 335 const shortestPaths = snapshot.computeShortestPaths( 336 start, 337 targets, 338 maxNumPaths 339 ); 340 341 for (const [target, paths] of shortestPaths) { 342 const deduped = deduplicatePaths(target, paths); 343 deduped.nodes = deduped.nodes.map(id => { 344 const { label } = DominatorTreeNode.getLabelAndShallowSize( 345 id, 346 snapshot, 347 breakdown 348 ); 349 return { id, label }; 350 }); 351 352 idToTreeNode.get(target).shortestPaths = deduped; 353 } 354 } 355 356 static LabelAndShallowSizeVisitor = LabelAndShallowSizeVisitor; 357 } 358 359 module.exports = DominatorTreeNode;