449 lines
15 KiB
JavaScript
449 lines
15 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
var Cc = Components.classes;
|
|
var Ci = Components.interfaces;
|
|
var Cu = Components.utils;
|
|
var Cr = Components.results;
|
|
var CC = Components.Constructor;
|
|
|
|
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
|
const { Match } = Cu.import("resource://test/Match.jsm", {});
|
|
const { Census } = Cu.import("resource://test/Census.jsm", {});
|
|
const { addDebuggerToGlobal } =
|
|
Cu.import("resource://gre/modules/jsdebugger.jsm", {});
|
|
const { Task } = require("devtools/shared/task");
|
|
|
|
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
|
const flags = require("devtools/shared/flags");
|
|
const HeapAnalysesClient =
|
|
require("devtools/shared/heapsnapshot/HeapAnalysesClient");
|
|
const Services = require("Services");
|
|
const { censusReportToCensusTreeNode } = require("devtools/shared/heapsnapshot/census-tree-node");
|
|
const CensusUtils = require("devtools/shared/heapsnapshot/CensusUtils");
|
|
const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
|
|
const { deduplicatePaths } = require("devtools/shared/heapsnapshot/shortest-paths");
|
|
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);
|
|
}
|
|
flags.wantLogging = true;
|
|
|
|
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 = do_print;
|
|
}
|
|
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 assertThrowsValue(f, val, msg) {
|
|
var fullmsg;
|
|
try {
|
|
f();
|
|
} catch (exc) {
|
|
if ((exc === val) === (val === val) && (val !== 0 || 1 / exc === 1 / 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(aName, aAllowMissing = false, aUsePlatformPathSeparator = false)
|
|
{
|
|
let file = do_get_file(aName, aAllowMissing);
|
|
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 (aUsePlatformPathSeparator && 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(snapshot instanceof HeapSnapshot, "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 instanceof 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()}'>`;
|
|
} else {
|
|
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 (let key of actual.keys()) {
|
|
ok(expectedKeys.has(key),
|
|
`${path}: every key in actual should exist in 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 (let 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 (let 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, description, 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 (let node of nodes) {
|
|
ok(expectedNodeSet.has(node), `the ${node} node was expected`);
|
|
}
|
|
|
|
for (let expectedEdge of expectedEdges) {
|
|
let count = 0;
|
|
for (let 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(edge));
|
|
}
|
|
}
|
|
|
|
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 };
|
|
}
|