summaryrefslogtreecommitdiffstats
path: root/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js')
-rw-r--r--devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js554
1 files changed, 554 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..d51decb104
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js
@@ -0,0 +1,554 @@
+/* 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);
+ Assert.strictEqual(
+ 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 };
+}