diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js')
-rw-r--r-- | devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js new file mode 100644 index 0000000000..6085a37f61 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js @@ -0,0 +1,550 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/* exported Cr, CC, Match, Census, Task, DevToolsUtils, HeapAnalysesClient, + assertThrows, getFilePath, saveHeapSnapshotAndTakeCensus, + saveHeapSnapshotAndComputeDominatorTree, compareCensusViewData, assertDiff, + assertLabelAndShallowSize, makeTestDominatorTreeNode, + assertDominatorTreeNodeInsertion, assertDeduplicatedPaths, + assertCountToBucketBreakdown, pathEntry */ + +var CC = Components.Constructor; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { Match } = ChromeUtils.importESModule("resource://test/Match.sys.mjs"); +const { Census } = ChromeUtils.importESModule("resource://test/Census.sys.mjs"); +const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" +); + +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +const HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js"); +const { + censusReportToCensusTreeNode, +} = require("resource://devtools/shared/heapsnapshot/census-tree-node.js"); +const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js"); +const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js"); +const { + deduplicatePaths, +} = require("resource://devtools/shared/heapsnapshot/shortest-paths.js"); +const { LabelAndShallowSizeVisitor } = DominatorTreeNode; + +// Always log packets when running tests. runxpcshelltests.py will throw +// the output away anyway, unless you give it the --verbose flag. +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { + Services.prefs.setBoolPref("devtools.debugger.log", true); + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.debugger.log"); + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); +} + +const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal +); + +function dumpn(msg) { + dump("HEAPSNAPSHOT-TEST: " + msg + "\n"); +} + +function addTestingFunctionsToGlobal(global) { + global.eval( + ` + const testingFunctions = Components.utils.getJSTestingFunctions(); + for (let k in testingFunctions) { + this[k] = testingFunctions[k]; + } + ` + ); + if (!global.print) { + global.print = info; + } + if (!global.newGlobal) { + global.newGlobal = newGlobal; + } + if (!global.Debugger) { + addDebuggerToGlobal(global); + } +} + +addTestingFunctionsToGlobal(this); + +/** + * Create a new global, with all the JS shell testing functions. Similar to the + * newGlobal function exposed to JS shells, and useful for porting JS shell + * tests to xpcshell tests. + */ +function newGlobal() { + const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true }); + addTestingFunctionsToGlobal(global); + return global; +} + +function assertThrows(f, val, msg) { + let fullmsg; + try { + f(); + } catch (exc) { + if (exc === val && (val !== 0 || 1 / exc === 1 / val)) { + return; + } else if (exc instanceof Error && exc.message === val) { + return; + } + fullmsg = "Assertion failed: expected exception " + val + ", got " + exc; + } + if (fullmsg === undefined) { + fullmsg = + "Assertion failed: expected exception " + val + ", no exception thrown"; + } + if (msg !== undefined) { + fullmsg += " - " + msg; + } + throw new Error(fullmsg); +} + +/** + * Returns the full path of the file with the specified name in a + * platform-independent and URL-like form. + */ +function getFilePath( + name, + allowMissing = false, + usePlatformPathSeparator = false +) { + const file = do_get_file(name, allowMissing); + let path = Services.io.newFileURI(file).spec; + let filePrePath = "file://"; + if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) { + filePrePath += "/"; + } + + path = path.slice(filePrePath.length); + + if (usePlatformPathSeparator && path.match(/^\w:/)) { + path = path.replace(/\//g, "\\"); + } + + return path; +} + +function saveNewHeapSnapshot(opts = { runtime: true }) { + const filePath = ChromeUtils.saveHeapSnapshot(opts); + ok(filePath, "Should get a file path to save the core dump to."); + ok(true, "Saved a heap snapshot to " + filePath); + return filePath; +} + +function readHeapSnapshot(filePath) { + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should have read a heap snapshot back from " + filePath); + ok( + HeapSnapshot.isInstance(snapshot), + "snapshot should be an instance of HeapSnapshot" + ); + return snapshot; +} + +/** + * Save a heap snapshot to the file with the given name in the current + * directory, read it back as a HeapSnapshot instance, and then take a census of + * the heap snapshot's serialized heap graph with the provided census options. + * + * @param {Object|undefined} censusOptions + * Options that should be passed through to the takeCensus method. See + * js/src/doc/Debugger/Debugger.Memory.md for details. + * + * @param {Debugger|null} dbg + * If a Debugger object is given, only serialize the subgraph covered by + * the Debugger's debuggees. If null, serialize the whole heap graph. + * + * @param {String} fileName + * The file name to save the heap snapshot's core dump file to, within + * the current directory. + * + * @returns Census + */ +function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) { + const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true }; + const filePath = saveNewHeapSnapshot(snapshotOptions); + const snapshot = readHeapSnapshot(filePath); + + equal( + typeof snapshot.takeCensus, + "function", + "snapshot should have a takeCensus method" + ); + + return snapshot.takeCensus(censusOptions); +} + +/** + * Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and + * then compute its dominator tree. + * + * @param {Debugger|null} dbg + * If a Debugger object is given, only serialize the subgraph covered by + * the Debugger's debuggees. If null, serialize the whole heap graph. + * + * @returns {DominatorTree} + */ +function saveHeapSnapshotAndComputeDominatorTree(dbg = null) { + const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true }; + const filePath = saveNewHeapSnapshot(snapshotOptions); + const snapshot = readHeapSnapshot(filePath); + + equal( + typeof snapshot.computeDominatorTree, + "function", + "snapshot should have a `computeDominatorTree` method" + ); + + const dominatorTree = snapshot.computeDominatorTree(); + + ok(dominatorTree, "Should be able to compute a dominator tree"); + ok( + DominatorTree.isInstance(dominatorTree), + "Should be an instance of DominatorTree" + ); + + return dominatorTree; +} + +function isSavedFrame(obj) { + return Object.prototype.toString.call(obj) === "[object SavedFrame]"; +} + +function savedFrameReplacer(key, val) { + if (isSavedFrame(val)) { + return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`; + } + return val; +} + +/** + * Assert that creating a CensusTreeNode from the given `report` with the + * specified `breakdown` creates the given `expected` CensusTreeNode. + * + * @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, 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, 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") { + if (actual === expected) { + equal(actual, expected, "actual and expected are the same"); + return; + } + + equal(typeof actual, typeof expected, `${path}: typeof should be the same`); + + if (actual && typeof actual === "object") { + const actualProtoString = Object.prototype.toString.call(actual); + const expectedProtoString = Object.prototype.toString.call(expected); + equal( + actualProtoString, + expectedProtoString, + `${path}: Object.prototype.toString.call() should be the same` + ); + + if (actualProtoString === "[object Map]") { + const expectedKeys = new Set([...expected.keys()]); + + for (const key of actual.keys()) { + ok( + expectedKeys.has(key), + `${path}: every key in actual is expected: ${String(key).slice( + 0, + 10 + )}` + ); + expectedKeys.delete(key); + + assertStructurallyEquivalent( + actual.get(key), + expected.get(key), + path + ".get(" + String(key).slice(0, 20) + ")" + ); + } + + equal( + expectedKeys.size, + 0, + `${path}: every key in expected should also exist in actual,\ + did not see ${[...expectedKeys]}` + ); + } else if (actualProtoString === "[object Set]") { + const expectedItems = new Set([...expected]); + + for (const item of actual) { + ok( + expectedItems.has(item), + `${path}: every set item in actual should exist in expected: ${item}` + ); + expectedItems.delete(item); + } + + equal( + expectedItems.size, + 0, + `${path}: every set item in expected should also exist in actual,\ + did not see ${[...expectedItems]}` + ); + } else { + const expectedKeys = new Set(Object.keys(expected)); + + for (const key of Object.keys(actual)) { + ok( + expectedKeys.has(key), + `${path}: every key in actual should exist in expected: ${key}` + ); + expectedKeys.delete(key); + + assertStructurallyEquivalent( + actual[key], + expected[key], + path + "." + key + ); + } + + equal( + expectedKeys.size, + 0, + `${path}: every key in expected should also exist in actual,\ + did not see ${[...expectedKeys]}` + ); + } + } else { + equal(actual, expected, `${path}: primitives should be equal`); + } +} + +/** + * Assert that creating a diff of the `first` and `second` census reports + * creates the `expected` delta-report. + * + * @param {Object} breakdown + * The census breakdown. + * + * @param {Object} first + * The first census report. + * + * @param {Object} second + * The second census report. + * + * @param {Object} expected + * The expected delta-report. + */ +function assertDiff(breakdown, first, second, expected) { + dumpn("Diffing census reports:"); + dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4)); + dumpn("First census report: " + JSON.stringify(first, null, 4)); + dumpn("Second census report: " + JSON.stringify(second, null, 4)); + dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4)); + + const actual = CensusUtils.diff(breakdown, first, second); + dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4)); + + assertStructurallyEquivalent(actual, expected); +} + +/** + * Assert that creating a label and getting a shallow size from the given node + * description with the specified breakdown is as expected. + * + * @param {Object} breakdown + * @param {Object} givenDescription + * @param {Number} expectedShallowSize + * @param {Object} expectedLabel + */ +function assertLabelAndShallowSize( + breakdown, + givenDescription, + expectedShallowSize, + expectedLabel +) { + dumpn("Computing label and shallow size from node description:"); + dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4)); + dumpn("Given description: " + JSON.stringify(givenDescription, null, 4)); + + const visitor = new LabelAndShallowSizeVisitor(); + CensusUtils.walk(breakdown, givenDescription, visitor); + + dumpn("Expected shallow size: " + expectedShallowSize); + dumpn("Actual shallow size: " + visitor.shallowSize()); + equal( + visitor.shallowSize(), + expectedShallowSize, + "Shallow size should be correct" + ); + + dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4)); + dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4)); + assertStructurallyEquivalent(visitor.label(), expectedLabel); +} + +// Counter for mock DominatorTreeNode ids. +let TEST_NODE_ID_COUNTER = 0; + +/** + * Create a mock DominatorTreeNode for testing, with sane defaults. Override any + * property by providing it on `opts`. Optionally pass child nodes as well. + * + * @param {Object} opts + * @param {Array<DominatorTreeNode>?} children + * + * @returns {DominatorTreeNode} + */ +function makeTestDominatorTreeNode(opts, children) { + const nodeId = TEST_NODE_ID_COUNTER++; + + const node = Object.assign( + { + nodeId, + label: undefined, + shallowSize: 1, + retainedSize: (children || []).reduce( + (size, c) => size + c.retainedSize, + 1 + ), + parentId: undefined, + children, + moreChildrenAvailable: true, + }, + opts + ); + + if (children && children.length) { + children.map(c => (c.parentId = node.nodeId)); + } + + return node; +} + +/** + * Insert `newChildren` into the given dominator `tree` as specified by the + * `path` from the root to the node the `newChildren` should be inserted + * beneath. Assert that the resulting tree matches `expected`. + */ +function assertDominatorTreeNodeInsertion( + tree, + path, + newChildren, + moreChildrenAvailable, + expected +) { + dumpn("Inserting new children into a dominator tree:"); + dumpn("Dominator tree: " + JSON.stringify(tree, null, 2)); + dumpn("Path: " + JSON.stringify(path, null, 2)); + dumpn("New children: " + JSON.stringify(newChildren, null, 2)); + dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2)); + + const actual = DominatorTreeNode.insert( + tree, + path, + newChildren, + moreChildrenAvailable + ); + dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(actual, expected); +} + +function assertDeduplicatedPaths({ + target, + paths, + expectedNodes, + expectedEdges, +}) { + dumpn("Deduplicating paths:"); + dumpn("target = " + target); + dumpn("paths = " + JSON.stringify(paths, null, 2)); + dumpn("expectedNodes = " + expectedNodes); + dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2)); + + const { nodes, edges } = deduplicatePaths(target, paths); + + dumpn("Actual nodes = " + nodes); + dumpn("Actual edges = " + JSON.stringify(edges, null, 2)); + + equal( + nodes.length, + expectedNodes.length, + "actual number of nodes is equal to the expected number of nodes" + ); + + equal( + edges.length, + expectedEdges.length, + "actual number of edges is equal to the expected number of edges" + ); + + const expectedNodeSet = new Set(expectedNodes); + const nodeSet = new Set(nodes); + ok(nodeSet.size === nodes.length, "each returned node should be unique"); + + for (const node of nodes) { + ok(expectedNodeSet.has(node), `the ${node} node was expected`); + } + + for (const expectedEdge of expectedEdges) { + let count = 0; + for (const edge of edges) { + if ( + edge.from === expectedEdge.from && + edge.to === expectedEdge.to && + edge.name === expectedEdge.name + ) { + count++; + } + } + equal( + count, + 1, + "should have exactly one matching edge for the expected edge = " + + JSON.stringify(expectedEdge) + ); + } +} + +function assertCountToBucketBreakdown(breakdown, expected) { + dumpn("count => bucket breakdown"); + dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2)); + dumpn("Expected results = ", JSON.stringify(expected, null, 2)); + + const actual = CensusUtils.countToBucketBreakdown(breakdown); + dumpn("Actual results = ", JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(actual, expected); +} + +/** + * Create a mock path entry for the given predecessor and edge. + */ +function pathEntry(predecessor, edge) { + return { predecessor, edge }; +} |