head_heapsnapshot.js (16543B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 /* exported Cr, CC, Match, Census, Task, DevToolsUtils, HeapAnalysesClient, 6 assertThrows, getFilePath, saveHeapSnapshotAndTakeCensus, 7 saveHeapSnapshotAndComputeDominatorTree, compareCensusViewData, assertDiff, 8 assertLabelAndShallowSize, makeTestDominatorTreeNode, 9 assertDominatorTreeNodeInsertion, assertDeduplicatedPaths, 10 assertCountToBucketBreakdown, pathEntry */ 11 12 var CC = Components.Constructor; 13 14 const { require } = ChromeUtils.importESModule( 15 "resource://devtools/shared/loader/Loader.sys.mjs" 16 ); 17 const { Match } = ChromeUtils.importESModule("resource://test/Match.sys.mjs"); 18 const { Census } = ChromeUtils.importESModule("resource://test/Census.sys.mjs"); 19 const { addDebuggerToGlobal } = ChromeUtils.importESModule( 20 "resource://gre/modules/jsdebugger.sys.mjs" 21 ); 22 23 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 24 const HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js"); 25 const { 26 censusReportToCensusTreeNode, 27 } = require("resource://devtools/shared/heapsnapshot/census-tree-node.js"); 28 const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js"); 29 const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js"); 30 const { 31 deduplicatePaths, 32 } = require("resource://devtools/shared/heapsnapshot/shortest-paths.js"); 33 const { LabelAndShallowSizeVisitor } = DominatorTreeNode; 34 35 // Always log packets when running tests. runxpcshelltests.py will throw 36 // the output away anyway, unless you give it the --verbose flag. 37 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { 38 Services.prefs.setBoolPref("devtools.debugger.log", true); 39 Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); 40 registerCleanupFunction(() => { 41 Services.prefs.clearUserPref("devtools.debugger.log"); 42 Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); 43 }); 44 } 45 46 const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance( 47 Ci.nsIPrincipal 48 ); 49 50 function dumpn(msg) { 51 dump("HEAPSNAPSHOT-TEST: " + msg + "\n"); 52 } 53 54 function addTestingFunctionsToGlobal(global) { 55 global.eval( 56 ` 57 const testingFunctions = Components.utils.getJSTestingFunctions(); 58 for (let k in testingFunctions) { 59 this[k] = testingFunctions[k]; 60 } 61 ` 62 ); 63 if (!global.print) { 64 global.print = info; 65 } 66 if (!global.newGlobal) { 67 global.newGlobal = newGlobal; 68 } 69 if (!global.Debugger) { 70 addDebuggerToGlobal(global); 71 } 72 } 73 74 addTestingFunctionsToGlobal(this); 75 76 /** 77 * Create a new global, with all the JS shell testing functions. Similar to the 78 * newGlobal function exposed to JS shells, and useful for porting JS shell 79 * tests to xpcshell tests. 80 */ 81 function newGlobal() { 82 const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true }); 83 addTestingFunctionsToGlobal(global); 84 return global; 85 } 86 87 function assertThrows(f, val, msg) { 88 let fullmsg; 89 try { 90 f(); 91 } catch (exc) { 92 if (exc === val && (val !== 0 || 1 / exc === 1 / val)) { 93 return; 94 } else if (exc instanceof Error && exc.message === val) { 95 return; 96 } 97 fullmsg = "Assertion failed: expected exception " + val + ", got " + exc; 98 } 99 if (fullmsg === undefined) { 100 fullmsg = 101 "Assertion failed: expected exception " + val + ", no exception thrown"; 102 } 103 if (msg !== undefined) { 104 fullmsg += " - " + msg; 105 } 106 throw new Error(fullmsg); 107 } 108 109 /** 110 * Returns the full path of the file with the specified name in a 111 * platform-independent and URL-like form. 112 */ 113 function getFilePath( 114 name, 115 allowMissing = false, 116 usePlatformPathSeparator = false 117 ) { 118 const file = do_get_file(name, allowMissing); 119 let path = Services.io.newFileURI(file).spec; 120 let filePrePath = "file://"; 121 if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) { 122 filePrePath += "/"; 123 } 124 125 path = path.slice(filePrePath.length); 126 127 if (usePlatformPathSeparator && path.match(/^\w:/)) { 128 path = path.replace(/\//g, "\\"); 129 } 130 131 return path; 132 } 133 134 function saveNewHeapSnapshot(opts = { runtime: true }) { 135 const filePath = ChromeUtils.saveHeapSnapshot(opts); 136 ok(filePath, "Should get a file path to save the core dump to."); 137 ok(true, "Saved a heap snapshot to " + filePath); 138 return filePath; 139 } 140 141 function readHeapSnapshot(filePath) { 142 const snapshot = ChromeUtils.readHeapSnapshot(filePath); 143 ok(snapshot, "Should have read a heap snapshot back from " + filePath); 144 ok( 145 HeapSnapshot.isInstance(snapshot), 146 "snapshot should be an instance of HeapSnapshot" 147 ); 148 return snapshot; 149 } 150 151 /** 152 * Save a heap snapshot to the file with the given name in the current 153 * directory, read it back as a HeapSnapshot instance, and then take a census of 154 * the heap snapshot's serialized heap graph with the provided census options. 155 * 156 * @param {object | undefined} censusOptions 157 * Options that should be passed through to the takeCensus method. See 158 * js/src/doc/Debugger/Debugger.Memory.md for details. 159 * 160 * @param {Debugger|null} dbg 161 * If a Debugger object is given, only serialize the subgraph covered by 162 * the Debugger's debuggees. If null, serialize the whole heap graph. 163 * 164 * @param {string} fileName 165 * The file name to save the heap snapshot's core dump file to, within 166 * the current directory. 167 * 168 * @returns Census 169 */ 170 function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) { 171 const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true }; 172 const filePath = saveNewHeapSnapshot(snapshotOptions); 173 const snapshot = readHeapSnapshot(filePath); 174 175 equal( 176 typeof snapshot.takeCensus, 177 "function", 178 "snapshot should have a takeCensus method" 179 ); 180 181 return snapshot.takeCensus(censusOptions); 182 } 183 184 /** 185 * Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and 186 * then compute its dominator tree. 187 * 188 * @param {Debugger|null} dbg 189 * If a Debugger object is given, only serialize the subgraph covered by 190 * the Debugger's debuggees. If null, serialize the whole heap graph. 191 * 192 * @returns {DominatorTree} 193 */ 194 function saveHeapSnapshotAndComputeDominatorTree(dbg = null) { 195 const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true }; 196 const filePath = saveNewHeapSnapshot(snapshotOptions); 197 const snapshot = readHeapSnapshot(filePath); 198 199 equal( 200 typeof snapshot.computeDominatorTree, 201 "function", 202 "snapshot should have a `computeDominatorTree` method" 203 ); 204 205 const dominatorTree = snapshot.computeDominatorTree(); 206 207 ok(dominatorTree, "Should be able to compute a dominator tree"); 208 ok( 209 DominatorTree.isInstance(dominatorTree), 210 "Should be an instance of DominatorTree" 211 ); 212 213 return dominatorTree; 214 } 215 216 function isSavedFrame(obj) { 217 return Object.prototype.toString.call(obj) === "[object SavedFrame]"; 218 } 219 220 function savedFrameReplacer(key, val) { 221 if (isSavedFrame(val)) { 222 return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`; 223 } 224 return val; 225 } 226 227 /** 228 * Assert that creating a CensusTreeNode from the given `report` with the 229 * specified `breakdown` creates the given `expected` CensusTreeNode. 230 * 231 * @param {object} breakdown 232 * The census breakdown. 233 * 234 * @param {object} report 235 * The census report. 236 * 237 * @param {object} expected 238 * The expected CensusTreeNode result. 239 * 240 * @param {object} options 241 * The options to pass through to `censusReportToCensusTreeNode`. 242 */ 243 function compareCensusViewData(breakdown, report, expected, options) { 244 dumpn("Generating CensusTreeNode from report:"); 245 dumpn("breakdown: " + JSON.stringify(breakdown, null, 4)); 246 dumpn("report: " + JSON.stringify(report, null, 4)); 247 dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4)); 248 249 const actual = censusReportToCensusTreeNode(breakdown, report, options); 250 dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4)); 251 252 assertStructurallyEquivalent(actual, expected); 253 } 254 255 // Deep structural equivalence that can handle Map objects in addition to plain 256 // objects. 257 function assertStructurallyEquivalent(actual, expected, path = "root") { 258 if (actual === expected) { 259 equal(actual, expected, "actual and expected are the same"); 260 return; 261 } 262 263 equal(typeof actual, typeof expected, `${path}: typeof should be the same`); 264 265 if (actual && typeof actual === "object") { 266 const actualProtoString = Object.prototype.toString.call(actual); 267 const expectedProtoString = Object.prototype.toString.call(expected); 268 equal( 269 actualProtoString, 270 expectedProtoString, 271 `${path}: Object.prototype.toString.call() should be the same` 272 ); 273 274 if (actualProtoString === "[object Map]") { 275 const expectedKeys = new Set([...expected.keys()]); 276 277 for (const key of actual.keys()) { 278 ok( 279 expectedKeys.has(key), 280 `${path}: every key in actual is expected: ${String(key).slice( 281 0, 282 10 283 )}` 284 ); 285 expectedKeys.delete(key); 286 287 assertStructurallyEquivalent( 288 actual.get(key), 289 expected.get(key), 290 path + ".get(" + String(key).slice(0, 20) + ")" 291 ); 292 } 293 294 equal( 295 expectedKeys.size, 296 0, 297 `${path}: every key in expected should also exist in actual,\ 298 did not see ${[...expectedKeys]}` 299 ); 300 } else if (actualProtoString === "[object Set]") { 301 const expectedItems = new Set([...expected]); 302 303 for (const item of actual) { 304 ok( 305 expectedItems.has(item), 306 `${path}: every set item in actual should exist in expected: ${item}` 307 ); 308 expectedItems.delete(item); 309 } 310 311 equal( 312 expectedItems.size, 313 0, 314 `${path}: every set item in expected should also exist in actual,\ 315 did not see ${[...expectedItems]}` 316 ); 317 } else { 318 const expectedKeys = new Set(Object.keys(expected)); 319 320 for (const key of Object.keys(actual)) { 321 ok( 322 expectedKeys.has(key), 323 `${path}: every key in actual should exist in expected: ${key}` 324 ); 325 expectedKeys.delete(key); 326 327 assertStructurallyEquivalent( 328 actual[key], 329 expected[key], 330 path + "." + key 331 ); 332 } 333 334 equal( 335 expectedKeys.size, 336 0, 337 `${path}: every key in expected should also exist in actual,\ 338 did not see ${[...expectedKeys]}` 339 ); 340 } 341 } else { 342 equal(actual, expected, `${path}: primitives should be equal`); 343 } 344 } 345 346 /** 347 * Assert that creating a diff of the `first` and `second` census reports 348 * creates the `expected` delta-report. 349 * 350 * @param {object} breakdown 351 * The census breakdown. 352 * 353 * @param {object} first 354 * The first census report. 355 * 356 * @param {object} second 357 * The second census report. 358 * 359 * @param {object} expected 360 * The expected delta-report. 361 */ 362 function assertDiff(breakdown, first, second, expected) { 363 dumpn("Diffing census reports:"); 364 dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4)); 365 dumpn("First census report: " + JSON.stringify(first, null, 4)); 366 dumpn("Second census report: " + JSON.stringify(second, null, 4)); 367 dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4)); 368 369 const actual = CensusUtils.diff(breakdown, first, second); 370 dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4)); 371 372 assertStructurallyEquivalent(actual, expected); 373 } 374 375 /** 376 * Assert that creating a label and getting a shallow size from the given node 377 * description with the specified breakdown is as expected. 378 * 379 * @param {object} breakdown 380 * @param {object} givenDescription 381 * @param {number} expectedShallowSize 382 * @param {object} expectedLabel 383 */ 384 function assertLabelAndShallowSize( 385 breakdown, 386 givenDescription, 387 expectedShallowSize, 388 expectedLabel 389 ) { 390 dumpn("Computing label and shallow size from node description:"); 391 dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4)); 392 dumpn("Given description: " + JSON.stringify(givenDescription, null, 4)); 393 394 const visitor = new LabelAndShallowSizeVisitor(); 395 CensusUtils.walk(breakdown, givenDescription, visitor); 396 397 dumpn("Expected shallow size: " + expectedShallowSize); 398 dumpn("Actual shallow size: " + visitor.shallowSize()); 399 equal( 400 visitor.shallowSize(), 401 expectedShallowSize, 402 "Shallow size should be correct" 403 ); 404 405 dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4)); 406 dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4)); 407 assertStructurallyEquivalent(visitor.label(), expectedLabel); 408 } 409 410 // Counter for mock DominatorTreeNode ids. 411 let TEST_NODE_ID_COUNTER = 0; 412 413 /** 414 * Create a mock DominatorTreeNode for testing, with sane defaults. Override any 415 * property by providing it on `opts`. Optionally pass child nodes as well. 416 * 417 * @param {object} opts 418 * @param {Array<DominatorTreeNode>?} children 419 * 420 * @returns {DominatorTreeNode} 421 */ 422 function makeTestDominatorTreeNode(opts, children) { 423 const nodeId = TEST_NODE_ID_COUNTER++; 424 425 const node = Object.assign( 426 { 427 nodeId, 428 label: undefined, 429 shallowSize: 1, 430 retainedSize: (children || []).reduce( 431 (size, c) => size + c.retainedSize, 432 1 433 ), 434 parentId: undefined, 435 children, 436 moreChildrenAvailable: true, 437 }, 438 opts 439 ); 440 441 if (children && children.length) { 442 children.map(c => (c.parentId = node.nodeId)); 443 } 444 445 return node; 446 } 447 448 /** 449 * Insert `newChildren` into the given dominator `tree` as specified by the 450 * `path` from the root to the node the `newChildren` should be inserted 451 * beneath. Assert that the resulting tree matches `expected`. 452 */ 453 function assertDominatorTreeNodeInsertion( 454 tree, 455 path, 456 newChildren, 457 moreChildrenAvailable, 458 expected 459 ) { 460 dumpn("Inserting new children into a dominator tree:"); 461 dumpn("Dominator tree: " + JSON.stringify(tree, null, 2)); 462 dumpn("Path: " + JSON.stringify(path, null, 2)); 463 dumpn("New children: " + JSON.stringify(newChildren, null, 2)); 464 dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2)); 465 466 const actual = DominatorTreeNode.insert( 467 tree, 468 path, 469 newChildren, 470 moreChildrenAvailable 471 ); 472 dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2)); 473 474 assertStructurallyEquivalent(actual, expected); 475 } 476 477 function assertDeduplicatedPaths({ 478 target, 479 paths, 480 expectedNodes, 481 expectedEdges, 482 }) { 483 dumpn("Deduplicating paths:"); 484 dumpn("target = " + target); 485 dumpn("paths = " + JSON.stringify(paths, null, 2)); 486 dumpn("expectedNodes = " + expectedNodes); 487 dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2)); 488 489 const { nodes, edges } = deduplicatePaths(target, paths); 490 491 dumpn("Actual nodes = " + nodes); 492 dumpn("Actual edges = " + JSON.stringify(edges, null, 2)); 493 494 equal( 495 nodes.length, 496 expectedNodes.length, 497 "actual number of nodes is equal to the expected number of nodes" 498 ); 499 500 equal( 501 edges.length, 502 expectedEdges.length, 503 "actual number of edges is equal to the expected number of edges" 504 ); 505 506 const expectedNodeSet = new Set(expectedNodes); 507 const nodeSet = new Set(nodes); 508 Assert.strictEqual( 509 nodeSet.size, 510 nodes.length, 511 "each returned node should be unique" 512 ); 513 514 for (const node of nodes) { 515 ok(expectedNodeSet.has(node), `the ${node} node was expected`); 516 } 517 518 for (const expectedEdge of expectedEdges) { 519 let count = 0; 520 for (const edge of edges) { 521 if ( 522 edge.from === expectedEdge.from && 523 edge.to === expectedEdge.to && 524 edge.name === expectedEdge.name 525 ) { 526 count++; 527 } 528 } 529 equal( 530 count, 531 1, 532 "should have exactly one matching edge for the expected edge = " + 533 JSON.stringify(expectedEdge) 534 ); 535 } 536 } 537 538 function assertCountToBucketBreakdown(breakdown, expected) { 539 dumpn("count => bucket breakdown"); 540 dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2)); 541 dumpn("Expected results = ", JSON.stringify(expected, null, 2)); 542 543 const actual = CensusUtils.countToBucketBreakdown(breakdown); 544 dumpn("Actual results = ", JSON.stringify(actual, null, 2)); 545 546 assertStructurallyEquivalent(actual, expected); 547 } 548 549 /** 550 * Create a mock path entry for the given predecessor and edge. 551 */ 552 function pathEntry(predecessor, edge) { 553 return { predecessor, edge }; 554 }