CensusUtils.js (13941B)
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 flatten, 8 } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js"); 9 10 /*** Visitor ****************************************************************/ 11 12 /** 13 * A Visitor visits each node and edge of a census report tree as the census 14 * report is being traversed by `walk`. 15 */ 16 class Visitor { 17 /** 18 * The `enter` method is called when a new sub-report is entered in traversal. 19 * 20 * @param {object} breakdown 21 * The breakdown for the sub-report that is being entered by traversal. 22 * 23 * @param {object} report 24 * The report generated by the given breakdown. 25 * 26 * @param {any} edge 27 * The edge leading to this sub-report. The edge is null if (but not iff! 28 * eg, null allocation stack edges) we are entering the root report. 29 */ 30 enter() {} 31 /** 32 * The `exit` method is called when traversal of a sub-report has finished. 33 * 34 * @param {object} breakdown 35 * The breakdown for the sub-report whose traversal has finished. 36 * 37 * @param {object} report 38 * The report generated by the given breakdown. 39 * 40 * @param {any} edge 41 * The edge leading to this sub-report. The edge is null if (but not iff! 42 * eg, null allocation stack edges) we are entering the root report. 43 */ 44 exit() {} 45 /** 46 * The `count` method is called when leaf nodes (reports whose breakdown is 47 * by: "count") in the report tree are encountered. 48 * 49 * @param {object} breakdown 50 * The count breakdown for this report. 51 * 52 * @param {object} report 53 * The report generated by a breakdown by "count". 54 * 55 * @param {any|null} edge 56 * The edge leading to this count report. The edge is null if we are 57 * entering the root report. 58 */ 59 count() {} 60 } 61 exports.Visitor = Visitor; 62 63 /*** getReportEdges *********************************************************/ 64 65 const EDGES = Object.create(null); 66 67 EDGES.count = function () { 68 return []; 69 }; 70 71 EDGES.bucket = function () { 72 return []; 73 }; 74 75 EDGES.internalType = function (breakdown, report) { 76 return Object.keys(report).map(key => ({ 77 edge: key, 78 referent: report[key], 79 breakdown: breakdown.then, 80 })); 81 }; 82 83 EDGES.descriptiveType = function (breakdown, report) { 84 return Object.keys(report).map(key => ({ 85 edge: key, 86 referent: report[key], 87 breakdown: breakdown.then, 88 })); 89 }; 90 91 EDGES.objectClass = function (breakdown, report) { 92 return Object.keys(report).map(key => ({ 93 edge: key, 94 referent: report[key], 95 breakdown: key === "other" ? breakdown.other : breakdown.then, 96 })); 97 }; 98 99 EDGES.coarseType = function (breakdown, report) { 100 return [ 101 { edge: "objects", referent: report.objects, breakdown: breakdown.objects }, 102 { edge: "scripts", referent: report.scripts, breakdown: breakdown.scripts }, 103 { edge: "strings", referent: report.strings, breakdown: breakdown.strings }, 104 { edge: "other", referent: report.other, breakdown: breakdown.other }, 105 { edge: "domNode", referent: report.domNode, breakdown: breakdown.domNode }, 106 ]; 107 }; 108 109 EDGES.allocationStack = function (breakdown, report) { 110 const edges = []; 111 report.forEach((value, key) => { 112 edges.push({ 113 edge: key, 114 referent: value, 115 breakdown: key === "noStack" ? breakdown.noStack : breakdown.then, 116 }); 117 }); 118 return edges; 119 }; 120 121 EDGES.filename = function (breakdown, report) { 122 return Object.keys(report).map(key => ({ 123 edge: key, 124 referent: report[key], 125 breakdown: key === "noFilename" ? breakdown.noFilename : breakdown.then, 126 })); 127 }; 128 129 /** 130 * Get the set of outgoing edges from `report` as specified by the given 131 * breakdown. 132 * 133 * @param {object} breakdown 134 * The census breakdown. 135 * 136 * @param {object} report 137 * The census report. 138 */ 139 function getReportEdges(breakdown, report) { 140 return EDGES[breakdown.by](breakdown, report); 141 } 142 exports.getReportEdges = getReportEdges; 143 144 /*** walk *******************************************************************/ 145 146 function recursiveWalk(breakdown, edge, report, visitor) { 147 if (breakdown.by === "count") { 148 visitor.enter(breakdown, report, edge); 149 visitor.count(breakdown, report, edge); 150 visitor.exit(breakdown, report, edge); 151 } else { 152 visitor.enter(breakdown, report, edge); 153 for (const { 154 edge: ed, 155 referent, 156 breakdown: subBreakdown, 157 } of getReportEdges(breakdown, report)) { 158 recursiveWalk(subBreakdown, ed, referent, visitor); 159 } 160 visitor.exit(breakdown, report, edge); 161 } 162 } 163 164 /** 165 * Walk the given `report` that was generated by taking a census with the 166 * specified `breakdown`. 167 * 168 * @param {object} breakdown 169 * The census breakdown. 170 * 171 * @param {object} report 172 * The census report. 173 * 174 * @param {Visitor} visitor 175 * The Visitor instance to call into while traversing. 176 */ 177 function walk(breakdown, report, visitor) { 178 recursiveWalk(breakdown, null, report, visitor); 179 } 180 exports.walk = walk; 181 182 /*** diff *******************************************************************/ 183 184 /** 185 * Return true if the object is a Map, false otherwise. Works with Map objects 186 * from other globals, unlike `instanceof`. 187 * 188 * @returns {boolean} 189 */ 190 function isMap(obj) { 191 return Object.prototype.toString.call(obj) === "[object Map]"; 192 } 193 194 const basisTotalBytes = (exports.basisTotalBytes = Symbol("basisTotalBytes")); 195 const basisTotalCount = (exports.basisTotalCount = Symbol("basisTotalCount")); 196 197 /** 198 * A Visitor for computing the difference between the census report being 199 * traversed and the given other census. 200 */ 201 class DiffVisitor extends Visitor { 202 /** 203 * @param {object} otherCensus 204 * The other census report. 205 */ 206 constructor(otherCensus) { 207 super(); 208 209 // The other census we are comparing against. 210 this._otherCensus = otherCensus; 211 212 // The total bytes and count of the basis census we are traversing. 213 this._totalBytes = 0; 214 this._totalCount = 0; 215 216 // Stack maintaining the current corresponding sub-report for the other 217 // census we are comparing against. 218 this._otherCensusStack = []; 219 220 // Stack maintaining the set of edges visited at each sub-report. 221 this._edgesVisited = [new Set()]; 222 223 // The final delta census. Valid only after traversal. 224 this._results = null; 225 226 // Stack maintaining the results corresponding to each sub-report we are 227 // currently traversing. 228 this._resultsStack = []; 229 } 230 /** 231 * Given a report and an outgoing edge, get the edge's referent. 232 */ 233 _get(report, edge) { 234 if (!report) { 235 return undefined; 236 } 237 return isMap(report) ? report.get(edge) : report[edge]; 238 } 239 /** 240 * Given a report, an outgoing edge, and a value, set the edge's referent to 241 * the given value. 242 */ 243 _set(report, edge, val) { 244 if (isMap(report)) { 245 report.set(edge, val); 246 } else { 247 report[edge] = val; 248 } 249 } 250 /** 251 * @override 252 */ 253 enter(breakdown, report, edge) { 254 const newResults = breakdown.by === "allocationStack" ? new Map() : {}; 255 let newOther; 256 257 if (!this._results) { 258 // This is the first time we have entered a sub-report. 259 this._results = newResults; 260 newOther = this._otherCensus; 261 } else { 262 const topResults = this._resultsStack[this._resultsStack.length - 1]; 263 this._set(topResults, edge, newResults); 264 265 const topOther = 266 this._otherCensusStack[this._otherCensusStack.length - 1]; 267 newOther = this._get(topOther, edge); 268 } 269 270 this._resultsStack.push(newResults); 271 this._otherCensusStack.push(newOther); 272 273 const visited = this._edgesVisited[this._edgesVisited.length - 1]; 274 visited.add(edge); 275 this._edgesVisited.push(new Set()); 276 } 277 /** 278 * @override 279 */ 280 exit(breakdown) { 281 // Find all the edges in the other census report that were not traversed and 282 // add them to the results directly. 283 const other = this._otherCensusStack[this._otherCensusStack.length - 1]; 284 if (other) { 285 const visited = this._edgesVisited[this._edgesVisited.length - 1]; 286 const unvisited = getReportEdges(breakdown, other) 287 .map(e => e.edge) 288 .filter(e => !visited.has(e)); 289 const results = this._resultsStack[this._resultsStack.length - 1]; 290 for (const edg of unvisited) { 291 this._set(results, edg, this._get(other, edg)); 292 } 293 } 294 295 this._otherCensusStack.pop(); 296 this._resultsStack.pop(); 297 this._edgesVisited.pop(); 298 } 299 /** 300 * @override 301 */ 302 count(breakdown, report) { 303 const other = this._otherCensusStack[this._otherCensusStack.length - 1]; 304 const results = this._resultsStack[this._resultsStack.length - 1]; 305 306 if (breakdown.count) { 307 this._totalCount += report.count; 308 } 309 if (breakdown.bytes) { 310 this._totalBytes += report.bytes; 311 } 312 313 if (other) { 314 if (breakdown.count) { 315 results.count = other.count - report.count; 316 } 317 if (breakdown.bytes) { 318 results.bytes = other.bytes - report.bytes; 319 } 320 } else { 321 if (breakdown.count) { 322 results.count = -report.count; 323 } 324 if (breakdown.bytes) { 325 results.bytes = -report.bytes; 326 } 327 } 328 } 329 330 /** 331 * Get the resulting report of the difference between the traversed census 332 * report and the other census report. 333 * 334 * @returns {object} 335 * The delta census report. 336 */ 337 results() { 338 if (!this._results) { 339 throw new Error("Attempt to get results before computing diff!"); 340 } 341 342 if (this._resultsStack.length) { 343 throw new Error("Attempt to get results while still computing diff!"); 344 } 345 346 this._results[basisTotalBytes] = this._totalBytes; 347 this._results[basisTotalCount] = this._totalCount; 348 349 return this._results; 350 } 351 } 352 353 /** 354 * Take the difference between two censuses. The resulting delta report 355 * contains the number/size of things that are in the `endCensus` that are not 356 * in the `startCensus`. 357 * 358 * @param {object} breakdown 359 * The breakdown used to generate both census reports. 360 * 361 * @param {object} startCensus 362 * The first census report. 363 * 364 * @param {object} endCensus 365 * The second census report. 366 * 367 * @returns {object} 368 * A delta report mirroring the structure of the two census reports (as 369 * specified by the given breakdown). Has two additional properties: 370 * - {Number} basisTotalBytes: the total number of bytes in the start 371 * census. 372 * - {Number} basisTotalCount: the total count in the start census. 373 */ 374 function diff(breakdown, startCensus, endCensus) { 375 const visitor = new DiffVisitor(endCensus); 376 walk(breakdown, startCensus, visitor); 377 return visitor.results(); 378 } 379 exports.diff = diff; 380 381 /** 382 * Creates a hash map mapping node IDs to its parent node. 383 * 384 * @param {CensusTreeNode} node 385 * @param {{[key: number]: TreeNode}} aggregator 386 * 387 * @return {{[key: number]: TreeNode}} 388 */ 389 const createParentMap = function (node, getId = n => n.id, aggregator = {}) { 390 if (node.children) { 391 for (let i = 0, length = node.children.length; i < length; i++) { 392 const child = node.children[i]; 393 aggregator[getId(child)] = node; 394 createParentMap(child, getId, aggregator); 395 } 396 } 397 return aggregator; 398 }; 399 exports.createParentMap = createParentMap; 400 401 const BUCKET = Object.freeze({ by: "bucket" }); 402 403 /** 404 * Convert a breakdown whose leaves are { by: "count" } to an identical 405 * breakdown, except with { by: "bucket" } leaves. 406 * 407 * @param {object} breakdown 408 * @returns {object} 409 */ 410 exports.countToBucketBreakdown = function (breakdown) { 411 if (typeof breakdown !== "object" || !breakdown) { 412 return breakdown; 413 } 414 415 if (breakdown.by === "count") { 416 return BUCKET; 417 } 418 419 const keys = Object.keys(breakdown); 420 const vals = keys.reduce((vs, k) => { 421 vs.push(exports.countToBucketBreakdown(breakdown[k])); 422 return vs; 423 }, []); 424 425 const result = {}; 426 for (let i = 0, length = keys.length; i < length; i++) { 427 result[keys[i]] = vals[i]; 428 } 429 430 return Object.freeze(result); 431 }; 432 433 /** 434 * A Visitor for finding report leaves by their DFS index. 435 */ 436 class GetLeavesVisitor extends Visitor { 437 constructor(targetIndices) { 438 super(); 439 440 this._index = -1; 441 this._targetIndices = targetIndices; 442 this._leaves = []; 443 } 444 /** 445 * @override 446 */ 447 enter(breakdown, report) { 448 this._index++; 449 if (this._targetIndices.has(this._index)) { 450 this._leaves.push(report); 451 } 452 } 453 /** 454 * Get the accumulated report leaves after traversal. 455 */ 456 leaves() { 457 if (this._index === -1) { 458 throw new Error("Attempt to call `leaves` before traversing report!"); 459 } 460 return this._leaves; 461 } 462 } 463 464 /** 465 * Given a set of indices of leaves in a pre-order depth-first traversal of the 466 * given census report, return the leaves. 467 * 468 * @param {Set<number>} indices 469 * @param {object} breakdown 470 * @param {object} report 471 * 472 * @returns {Array<object>} 473 */ 474 exports.getReportLeaves = function (indices, breakdown, report) { 475 const visitor = new GetLeavesVisitor(indices); 476 walk(breakdown, report, visitor); 477 return visitor.leaves(); 478 }; 479 480 /** 481 * Get a list of the individual node IDs that belong to the census report leaves 482 * of the given indices. 483 * 484 * @param {Set<number>} indices 485 * @param {object} breakdown 486 * @param {HeapSnapshot} snapshot 487 * 488 * @returns {Array<NodeId>} 489 */ 490 exports.getCensusIndividuals = function (indices, countBreakdown, snapshot) { 491 const bucketBreakdown = exports.countToBucketBreakdown(countBreakdown); 492 const bucketReport = snapshot.takeCensus({ breakdown: bucketBreakdown }); 493 const buckets = exports.getReportLeaves( 494 indices, 495 bucketBreakdown, 496 bucketReport 497 ); 498 return flatten(buckets); 499 };