mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 07:48:38 +09:00
Follow-up to 4e2e9be6a - Move HeapSnapshot DevTools-only Modules back to DevTools
I am so done with this. Resolves #316
This commit is contained in:
parent
f1182eaec9
commit
021bff6bf8
10 changed files with 16 additions and 11 deletions
489
devtools/shared/heapsnapshot/CensusUtils.js
Normal file
489
devtools/shared/heapsnapshot/CensusUtils.js
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { flatten } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
|
||||
|
||||
/** * Visitor ****************************************************************/
|
||||
|
||||
/**
|
||||
* A Visitor visits each node and edge of a census report tree as the census
|
||||
* report is being traversed by `walk`.
|
||||
*/
|
||||
function Visitor() { }
|
||||
exports.Visitor = Visitor;
|
||||
|
||||
/**
|
||||
* The `enter` method is called when a new sub-report is entered in traversal.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The breakdown for the sub-report that is being entered by traversal.
|
||||
*
|
||||
* @param {Object} report
|
||||
* The report generated by the given breakdown.
|
||||
*
|
||||
* @param {any} edge
|
||||
* The edge leading to this sub-report. The edge is null if (but not iff!
|
||||
* eg, null allocation stack edges) we are entering the root report.
|
||||
*/
|
||||
Visitor.prototype.enter = function (breakdown, report, edge) { };
|
||||
|
||||
/**
|
||||
* The `exit` method is called when traversal of a sub-report has finished.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The breakdown for the sub-report whose traversal has finished.
|
||||
*
|
||||
* @param {Object} report
|
||||
* The report generated by the given breakdown.
|
||||
*
|
||||
* @param {any} edge
|
||||
* The edge leading to this sub-report. The edge is null if (but not iff!
|
||||
* eg, null allocation stack edges) we are entering the root report.
|
||||
*/
|
||||
Visitor.prototype.exit = function (breakdown, report, edge) { };
|
||||
|
||||
/**
|
||||
* The `count` method is called when leaf nodes (reports whose breakdown is
|
||||
* by: "count") in the report tree are encountered.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The count breakdown for this report.
|
||||
*
|
||||
* @param {Object} report
|
||||
* The report generated by a breakdown by "count".
|
||||
*
|
||||
* @param {any|null} edge
|
||||
* The edge leading to this count report. The edge is null if we are
|
||||
* entering the root report.
|
||||
*/
|
||||
Visitor.prototype.count = function (breakdown, report, edge) { };
|
||||
|
||||
/** * getReportEdges *********************************************************/
|
||||
|
||||
const EDGES = Object.create(null);
|
||||
|
||||
EDGES.count = function (breakdown, report) {
|
||||
return [];
|
||||
};
|
||||
|
||||
EDGES.bucket = function (breakdown, report) {
|
||||
return [];
|
||||
};
|
||||
|
||||
EDGES.internalType = function (breakdown, report) {
|
||||
return Object.keys(report).map(key => ({
|
||||
edge: key,
|
||||
referent: report[key],
|
||||
breakdown: breakdown.then
|
||||
}));
|
||||
};
|
||||
|
||||
EDGES.objectClass = function (breakdown, report) {
|
||||
return Object.keys(report).map(key => ({
|
||||
edge: key,
|
||||
referent: report[key],
|
||||
breakdown: key === "other" ? breakdown.other : breakdown.then
|
||||
}));
|
||||
};
|
||||
|
||||
EDGES.coarseType = function (breakdown, report) {
|
||||
return [
|
||||
{ edge: "objects", referent: report.objects, breakdown: breakdown.objects },
|
||||
{ edge: "scripts", referent: report.scripts, breakdown: breakdown.scripts },
|
||||
{ edge: "strings", referent: report.strings, breakdown: breakdown.strings },
|
||||
{ edge: "other", referent: report.other, breakdown: breakdown.other },
|
||||
];
|
||||
};
|
||||
|
||||
EDGES.allocationStack = function (breakdown, report) {
|
||||
const edges = [];
|
||||
report.forEach((value, key) => {
|
||||
edges.push({
|
||||
edge: key,
|
||||
referent: value,
|
||||
breakdown: key === "noStack" ? breakdown.noStack : breakdown.then
|
||||
});
|
||||
});
|
||||
return edges;
|
||||
};
|
||||
|
||||
EDGES.filename = function (breakdown, report) {
|
||||
return Object.keys(report).map(key => ({
|
||||
edge: key,
|
||||
referent: report[key],
|
||||
breakdown: key === "noFilename" ? breakdown.noFilename : breakdown.then
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the set of outgoing edges from `report` as specified by the given
|
||||
* breakdown.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The census breakdown.
|
||||
*
|
||||
* @param {Object} report
|
||||
* The census report.
|
||||
*/
|
||||
function getReportEdges(breakdown, report) {
|
||||
return EDGES[breakdown.by](breakdown, report);
|
||||
}
|
||||
exports.getReportEdges = getReportEdges;
|
||||
|
||||
/** * walk *******************************************************************/
|
||||
|
||||
function recursiveWalk(breakdown, edge, report, visitor) {
|
||||
if (breakdown.by === "count") {
|
||||
visitor.enter(breakdown, report, edge);
|
||||
visitor.count(breakdown, report, edge);
|
||||
visitor.exit(breakdown, report, edge);
|
||||
} else {
|
||||
visitor.enter(breakdown, report, edge);
|
||||
for (let { edge, referent, breakdown: subBreakdown } of getReportEdges(breakdown, report)) {
|
||||
recursiveWalk(subBreakdown, edge, referent, visitor);
|
||||
}
|
||||
visitor.exit(breakdown, report, edge);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the given `report` that was generated by taking a census with the
|
||||
* specified `breakdown`.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The census breakdown.
|
||||
*
|
||||
* @param {Object} report
|
||||
* The census report.
|
||||
*
|
||||
* @param {Visitor} visitor
|
||||
* The Visitor instance to call into while traversing.
|
||||
*/
|
||||
function walk(breakdown, report, visitor) {
|
||||
recursiveWalk(breakdown, null, report, visitor);
|
||||
}
|
||||
exports.walk = walk;
|
||||
|
||||
/** * diff *******************************************************************/
|
||||
|
||||
/**
|
||||
* Return true if the object is a Map, false otherwise. Works with Map objects
|
||||
* from other globals, unlike `instanceof`.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function isMap(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Map]";
|
||||
}
|
||||
|
||||
/**
|
||||
* A Visitor for computing the difference between the census report being
|
||||
* traversed and the given other census.
|
||||
*
|
||||
* @param {Object} otherCensus
|
||||
* The other census report.
|
||||
*/
|
||||
function DiffVisitor(otherCensus) {
|
||||
// The other census we are comparing against.
|
||||
this._otherCensus = otherCensus;
|
||||
|
||||
// The total bytes and count of the basis census we are traversing.
|
||||
this._totalBytes = 0;
|
||||
this._totalCount = 0;
|
||||
|
||||
// Stack maintaining the current corresponding sub-report for the other
|
||||
// census we are comparing against.
|
||||
this._otherCensusStack = [];
|
||||
|
||||
// Stack maintaining the set of edges visited at each sub-report.
|
||||
this._edgesVisited = [new Set()];
|
||||
|
||||
// The final delta census. Valid only after traversal.
|
||||
this._results = null;
|
||||
|
||||
// Stack maintaining the results corresponding to each sub-report we are
|
||||
// currently traversing.
|
||||
this._resultsStack = [];
|
||||
}
|
||||
|
||||
DiffVisitor.prototype = Object.create(Visitor.prototype);
|
||||
|
||||
/**
|
||||
* Given a report and an outgoing edge, get the edge's referent.
|
||||
*/
|
||||
DiffVisitor.prototype._get = function (report, edge) {
|
||||
if (!report) {
|
||||
return undefined;
|
||||
}
|
||||
return isMap(report) ? report.get(edge) : report[edge];
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a report, an outgoing edge, and a value, set the edge's referent to
|
||||
* the given value.
|
||||
*/
|
||||
DiffVisitor.prototype._set = function (report, edge, val) {
|
||||
if (isMap(report)) {
|
||||
report.set(edge, val);
|
||||
} else {
|
||||
report[edge] = val;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @overrides Visitor.prototype.enter
|
||||
*/
|
||||
DiffVisitor.prototype.enter = function (breakdown, report, edge) {
|
||||
const isFirstTimeEntering = this._results === null;
|
||||
|
||||
const newResults = breakdown.by === "allocationStack" ? new Map() : {};
|
||||
let newOther;
|
||||
|
||||
if (!this._results) {
|
||||
// This is the first time we have entered a sub-report.
|
||||
this._results = newResults;
|
||||
newOther = this._otherCensus;
|
||||
} else {
|
||||
const topResults = this._resultsStack[this._resultsStack.length - 1];
|
||||
this._set(topResults, edge, newResults);
|
||||
|
||||
const topOther = this._otherCensusStack[this._otherCensusStack.length - 1];
|
||||
newOther = this._get(topOther, edge);
|
||||
}
|
||||
|
||||
this._resultsStack.push(newResults);
|
||||
this._otherCensusStack.push(newOther);
|
||||
|
||||
const visited = this._edgesVisited[this._edgesVisited.length - 1];
|
||||
visited.add(edge);
|
||||
this._edgesVisited.push(new Set());
|
||||
};
|
||||
|
||||
/**
|
||||
* @overrides Visitor.prototype.exit
|
||||
*/
|
||||
DiffVisitor.prototype.exit = function (breakdown, report, edge) {
|
||||
// Find all the edges in the other census report that were not traversed and
|
||||
// add them to the results directly.
|
||||
const other = this._otherCensusStack[this._otherCensusStack.length - 1];
|
||||
if (other) {
|
||||
const visited = this._edgesVisited[this._edgesVisited.length - 1];
|
||||
const unvisited = getReportEdges(breakdown, other)
|
||||
.map(e => e.edge)
|
||||
.filter(e => !visited.has(e));
|
||||
const results = this._resultsStack[this._resultsStack.length - 1];
|
||||
for (let edge of unvisited) {
|
||||
this._set(results, edge, this._get(other, edge));
|
||||
}
|
||||
}
|
||||
|
||||
this._otherCensusStack.pop();
|
||||
this._resultsStack.pop();
|
||||
this._edgesVisited.pop();
|
||||
};
|
||||
|
||||
/**
|
||||
* @overrides Visitor.prototype.count
|
||||
*/
|
||||
DiffVisitor.prototype.count = function (breakdown, report, edge) {
|
||||
const other = this._otherCensusStack[this._otherCensusStack.length - 1];
|
||||
const results = this._resultsStack[this._resultsStack.length - 1];
|
||||
|
||||
if (breakdown.count) {
|
||||
this._totalCount += report.count;
|
||||
}
|
||||
if (breakdown.bytes) {
|
||||
this._totalBytes += report.bytes;
|
||||
}
|
||||
|
||||
if (other) {
|
||||
if (breakdown.count) {
|
||||
results.count = other.count - report.count;
|
||||
}
|
||||
if (breakdown.bytes) {
|
||||
results.bytes = other.bytes - report.bytes;
|
||||
}
|
||||
} else {
|
||||
if (breakdown.count) {
|
||||
results.count = -report.count;
|
||||
}
|
||||
if (breakdown.bytes) {
|
||||
results.bytes = -report.bytes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const basisTotalBytes = exports.basisTotalBytes = Symbol("basisTotalBytes");
|
||||
const basisTotalCount = exports.basisTotalCount = Symbol("basisTotalCount");
|
||||
|
||||
/**
|
||||
* Get the resulting report of the difference between the traversed census
|
||||
* report and the other census report.
|
||||
*
|
||||
* @returns {Object}
|
||||
* The delta census report.
|
||||
*/
|
||||
DiffVisitor.prototype.results = function () {
|
||||
if (!this._results) {
|
||||
throw new Error("Attempt to get results before computing diff!");
|
||||
}
|
||||
|
||||
if (this._resultsStack.length) {
|
||||
throw new Error("Attempt to get results while still computing diff!");
|
||||
}
|
||||
|
||||
this._results[basisTotalBytes] = this._totalBytes;
|
||||
this._results[basisTotalCount] = this._totalCount;
|
||||
|
||||
return this._results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Take the difference between two censuses. The resulting delta report
|
||||
* contains the number/size of things that are in the `endCensus` that are not
|
||||
* in the `startCensus`.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The breakdown used to generate both census reports.
|
||||
*
|
||||
* @param {Object} startCensus
|
||||
* The first census report.
|
||||
*
|
||||
* @param {Object} endCensus
|
||||
* The second census report.
|
||||
*
|
||||
* @returns {Object}
|
||||
* A delta report mirroring the structure of the two census reports (as
|
||||
* specified by the given breakdown). Has two additional properties:
|
||||
* - {Number} basisTotalBytes: the total number of bytes in the start
|
||||
* census.
|
||||
* - {Number} basisTotalCount: the total count in the start census.
|
||||
*/
|
||||
function diff(breakdown, startCensus, endCensus) {
|
||||
const visitor = new DiffVisitor(endCensus);
|
||||
walk(breakdown, startCensus, visitor);
|
||||
return visitor.results();
|
||||
}
|
||||
exports.diff = diff;
|
||||
|
||||
/**
|
||||
* Creates a hash map mapping node IDs to its parent node.
|
||||
*
|
||||
* @param {CensusTreeNode} node
|
||||
* @param {Object<number, TreeNode>} aggregator
|
||||
*
|
||||
* @return {Object<number, TreeNode>}
|
||||
*/
|
||||
const createParentMap = exports.createParentMap = function (node,
|
||||
getId = node => node.id,
|
||||
aggregator = Object.create(null)) {
|
||||
if (node.children) {
|
||||
for (let i = 0, length = node.children.length; i < length; i++) {
|
||||
const child = node.children[i];
|
||||
aggregator[getId(child)] = node;
|
||||
createParentMap(child, getId, aggregator);
|
||||
}
|
||||
}
|
||||
|
||||
return aggregator;
|
||||
};
|
||||
|
||||
const BUCKET = Object.freeze({ by: "bucket" });
|
||||
|
||||
/**
|
||||
* Convert a breakdown whose leaves are { by: "count" } to an identical
|
||||
* breakdown, except with { by: "bucket" } leaves.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* @returns {Object}
|
||||
*/
|
||||
exports.countToBucketBreakdown = function (breakdown) {
|
||||
if (typeof breakdown !== "object" || !breakdown) {
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
if (breakdown.by === "count") {
|
||||
return BUCKET;
|
||||
}
|
||||
|
||||
const keys = Object.keys(breakdown);
|
||||
const vals = keys.reduce((vs, k) => {
|
||||
vs.push(exports.countToBucketBreakdown(breakdown[k]));
|
||||
return vs;
|
||||
}, []);
|
||||
|
||||
const result = {};
|
||||
for (let i = 0, length = keys.length; i < length; i++) {
|
||||
result[keys[i]] = vals[i];
|
||||
}
|
||||
|
||||
return Object.freeze(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* A Visitor for finding report leaves by their DFS index.
|
||||
*/
|
||||
function GetLeavesVisitor(targetIndices) {
|
||||
this._index = -1;
|
||||
this._targetIndices = targetIndices;
|
||||
this._leaves = [];
|
||||
}
|
||||
|
||||
GetLeavesVisitor.prototype = Object.create(Visitor.prototype);
|
||||
|
||||
/**
|
||||
* @overrides Visitor.prototype.enter
|
||||
*/
|
||||
GetLeavesVisitor.prototype.enter = function (breakdown, report, edge) {
|
||||
this._index++;
|
||||
if (this._targetIndices.has(this._index)) {
|
||||
this._leaves.push(report);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the accumulated report leaves after traversal.
|
||||
*/
|
||||
GetLeavesVisitor.prototype.leaves = function () {
|
||||
if (this._index === -1) {
|
||||
throw new Error("Attempt to call `leaves` before traversing report!");
|
||||
}
|
||||
return this._leaves;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a set of indices of leaves in a pre-order depth-first traversal of the
|
||||
* given census report, return the leaves.
|
||||
*
|
||||
* @param {Set<Number>} indices
|
||||
* @param {Object} breakdown
|
||||
* @param {Object} report
|
||||
*
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
exports.getReportLeaves = function (indices, breakdown, report) {
|
||||
const visitor = new GetLeavesVisitor(indices);
|
||||
walk(breakdown, report, visitor);
|
||||
return visitor.leaves();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of the individual node IDs that belong to the census report leaves
|
||||
* of the given indices.
|
||||
*
|
||||
* @param {Set<Number>} indices
|
||||
* @param {Object} breakdown
|
||||
* @param {HeapSnapshot} snapshot
|
||||
*
|
||||
* @returns {Array<NodeId>}
|
||||
*/
|
||||
exports.getCensusIndividuals = function (indices, countBreakdown, snapshot) {
|
||||
const bucketBreakdown = exports.countToBucketBreakdown(countBreakdown);
|
||||
const bucketReport = snapshot.takeCensus({ breakdown: bucketBreakdown });
|
||||
const buckets = exports.getReportLeaves(indices,
|
||||
bucketBreakdown,
|
||||
bucketReport);
|
||||
return flatten(buckets);
|
||||
};
|
||||
336
devtools/shared/heapsnapshot/DominatorTreeNode.js
Normal file
336
devtools/shared/heapsnapshot/DominatorTreeNode.js
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { immutableUpdate } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
|
||||
const { Visitor, walk } = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
|
||||
const { deduplicatePaths } = require("resource://devtools/shared/heapsnapshot/shortest-paths");
|
||||
|
||||
const DEFAULT_MAX_DEPTH = 4;
|
||||
const DEFAULT_MAX_SIBLINGS = 15;
|
||||
const DEFAULT_MAX_NUM_PATHS = 5;
|
||||
|
||||
/**
|
||||
* A single node in a dominator tree.
|
||||
*
|
||||
* @param {NodeId} nodeId
|
||||
* @param {NodeSize} retainedSize
|
||||
*/
|
||||
function DominatorTreeNode(nodeId, label, shallowSize, retainedSize) {
|
||||
// The id of this node.
|
||||
this.nodeId = nodeId;
|
||||
|
||||
// The label structure generated by describing the given node.
|
||||
this.label = label;
|
||||
|
||||
// The shallow size of this node.
|
||||
this.shallowSize = shallowSize;
|
||||
|
||||
// The retained size of this node.
|
||||
this.retainedSize = retainedSize;
|
||||
|
||||
// The id of this node's parent or undefined if this node is the root.
|
||||
this.parentId = undefined;
|
||||
|
||||
// An array of immediately dominated child `DominatorTreeNode`s, or undefined.
|
||||
this.children = undefined;
|
||||
|
||||
// An object of the form returned by `deduplicatePaths`, encoding the set of
|
||||
// the N shortest retaining paths for this node as a graph.
|
||||
this.shortestPaths = undefined;
|
||||
|
||||
// True iff the `children` property does not contain every immediately
|
||||
// dominated node.
|
||||
//
|
||||
// * If children is an array and this property is true: the array does not
|
||||
// contain the complete set of immediately dominated children.
|
||||
// * If children is an array and this property is false: the array contains
|
||||
// the complete set of immediately dominated children.
|
||||
// * If children is undefined and this property is true: there exist
|
||||
// immediately dominated children for this node, but they have not been
|
||||
// loaded yet.
|
||||
// * If children is undefined and this property is false: this node does not
|
||||
// dominate any others and therefore has no children.
|
||||
this.moreChildrenAvailable = true;
|
||||
}
|
||||
|
||||
DominatorTreeNode.prototype = null;
|
||||
|
||||
module.exports = DominatorTreeNode;
|
||||
|
||||
/**
|
||||
* Add `child` to the `parent`'s set of children.
|
||||
*
|
||||
* @param {DominatorTreeNode} parent
|
||||
* @param {DominatorTreeNode} child
|
||||
*/
|
||||
DominatorTreeNode.addChild = function (parent, child) {
|
||||
if (parent.children === undefined) {
|
||||
parent.children = [];
|
||||
}
|
||||
|
||||
parent.children.push(child);
|
||||
child.parentId = parent.nodeId;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Visitor that is used to generate a label for a node in the heap snapshot
|
||||
* and get its shallow size as well while we are at it.
|
||||
*/
|
||||
function LabelAndShallowSizeVisitor() {
|
||||
// As we walk the description, we accumulate edges in this array.
|
||||
this._labelPieces = [];
|
||||
|
||||
// Once we reach the non-zero count leaf node in the description, we move the
|
||||
// labelPieces here to signify that we no longer need to accumulate edges.
|
||||
this._label = undefined;
|
||||
|
||||
// Once we reach the non-zero count leaf node in the description, we grab the
|
||||
// shallow size and place it here.
|
||||
this._shallowSize = 0;
|
||||
}
|
||||
|
||||
DominatorTreeNode.LabelAndShallowSizeVisitor = LabelAndShallowSizeVisitor;
|
||||
|
||||
LabelAndShallowSizeVisitor.prototype = Object.create(Visitor);
|
||||
|
||||
/**
|
||||
* @overrides Visitor.prototype.enter
|
||||
*/
|
||||
LabelAndShallowSizeVisitor.prototype.enter = function (breakdown, report, edge) {
|
||||
if (this._labelPieces && edge) {
|
||||
this._labelPieces.push(edge);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @overrides Visitor.prototype.exit
|
||||
*/
|
||||
LabelAndShallowSizeVisitor.prototype.exit = function (breakdown, report, edge) {
|
||||
if (this._labelPieces && edge) {
|
||||
this._labelPieces.pop();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @overrides Visitor.prototype.count
|
||||
*/
|
||||
LabelAndShallowSizeVisitor.prototype.count = function (breakdown, report, edge) {
|
||||
if (report.count === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._label = this._labelPieces;
|
||||
this._labelPieces = undefined;
|
||||
|
||||
this._shallowSize = report.bytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the generated label structure accumulated by this visitor.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
LabelAndShallowSizeVisitor.prototype.label = function () {
|
||||
return this._label;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the shallow size of the node this visitor visited.
|
||||
*
|
||||
* @returns {Number}
|
||||
*/
|
||||
LabelAndShallowSizeVisitor.prototype.shallowSize = function () {
|
||||
return this._shallowSize;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a label structure for the node with the given id and grab its
|
||||
* shallow size.
|
||||
*
|
||||
* What is a "label" structure? HeapSnapshot.describeNode essentially takes a
|
||||
* census of a single node rather than the whole heap graph. The resulting
|
||||
* report has only one count leaf that is non-zero. The label structure is the
|
||||
* path in this report from the root to the non-zero count leaf.
|
||||
*
|
||||
* @param {Number} nodeId
|
||||
* @param {HeapSnapshot} snapshot
|
||||
* @param {Object} breakdown
|
||||
*
|
||||
* @returns {Object}
|
||||
* An object with the following properties:
|
||||
* - {Number} shallowSize
|
||||
* - {Object} label
|
||||
*/
|
||||
DominatorTreeNode.getLabelAndShallowSize = function (nodeId,
|
||||
snapshot,
|
||||
breakdown) {
|
||||
const description = snapshot.describeNode(breakdown, nodeId);
|
||||
|
||||
const visitor = new LabelAndShallowSizeVisitor();
|
||||
walk(breakdown, description, visitor);
|
||||
|
||||
return {
|
||||
label: visitor.label(),
|
||||
shallowSize: visitor.shallowSize(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Do a partial traversal of the given dominator tree and convert it into a tree
|
||||
* of `DominatorTreeNode`s. Dominator trees have a node for every node in the
|
||||
* snapshot's heap graph, so we must not allocate a JS object for every node. It
|
||||
* would be way too many and the node count is effectively unbounded.
|
||||
*
|
||||
* Go no deeper down the tree than `maxDepth` and only consider at most
|
||||
* `maxSiblings` within any single node's children.
|
||||
*
|
||||
* @param {DominatorTree} dominatorTree
|
||||
* @param {HeapSnapshot} snapshot
|
||||
* @param {Object} breakdown
|
||||
* @param {Number} maxDepth
|
||||
* @param {Number} maxSiblings
|
||||
*
|
||||
* @returns {DominatorTreeNode}
|
||||
*/
|
||||
DominatorTreeNode.partialTraversal = function (dominatorTree,
|
||||
snapshot,
|
||||
breakdown,
|
||||
maxDepth = DEFAULT_MAX_DEPTH,
|
||||
maxSiblings = DEFAULT_MAX_SIBLINGS) {
|
||||
function dfs(nodeId, depth) {
|
||||
const { label, shallowSize } =
|
||||
DominatorTreeNode.getLabelAndShallowSize(nodeId, snapshot, breakdown);
|
||||
const retainedSize = dominatorTree.getRetainedSize(nodeId);
|
||||
const node = new DominatorTreeNode(nodeId, label, shallowSize, retainedSize);
|
||||
const childNodeIds = dominatorTree.getImmediatelyDominated(nodeId);
|
||||
|
||||
const newDepth = depth + 1;
|
||||
if (newDepth < maxDepth) {
|
||||
const endIdx = Math.min(childNodeIds.length, maxSiblings);
|
||||
for (let i = 0; i < endIdx; i++) {
|
||||
DominatorTreeNode.addChild(node, dfs(childNodeIds[i], newDepth));
|
||||
}
|
||||
node.moreChildrenAvailable = endIdx < childNodeIds.length;
|
||||
} else {
|
||||
node.moreChildrenAvailable = childNodeIds.length > 0;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
return dfs(dominatorTree.root, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert more children into the given (partially complete) dominator tree.
|
||||
*
|
||||
* The tree is updated in an immutable and persistent manner: a new tree is
|
||||
* returned, but all unmodified subtrees (which is most) are shared with the
|
||||
* original tree. Only the modified nodes are re-allocated.
|
||||
*
|
||||
* @param {DominatorTreeNode} tree
|
||||
* @param {Array<NodeId>} path
|
||||
* @param {Array<DominatorTreeNode>} newChildren
|
||||
* @param {Boolean} moreChildrenAvailable
|
||||
*
|
||||
* @returns {DominatorTreeNode}
|
||||
*/
|
||||
DominatorTreeNode.insert = function (tree, path, newChildren, moreChildrenAvailable) {
|
||||
function insert(tree, i) {
|
||||
if (tree.nodeId !== path[i]) {
|
||||
return tree;
|
||||
}
|
||||
|
||||
if (i == path.length - 1) {
|
||||
return immutableUpdate(tree, {
|
||||
children: (tree.children || []).concat(newChildren),
|
||||
moreChildrenAvailable,
|
||||
});
|
||||
}
|
||||
|
||||
return tree.children
|
||||
? immutableUpdate(tree, {
|
||||
children: tree.children.map(c => insert(c, i + 1))
|
||||
})
|
||||
: tree;
|
||||
}
|
||||
|
||||
return insert(tree, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the new canonical node with the given `id` in `tree` that exists along
|
||||
* `path`. If there is no such node along `path`, return null.
|
||||
*
|
||||
* This is useful if we have a reference to a now-outdated DominatorTreeNode due
|
||||
* to a recent call to DominatorTreeNode.insert and want to get the up-to-date
|
||||
* version. We don't have to walk the whole tree: if there is an updated version
|
||||
* of the node then it *must* be along the path.
|
||||
*
|
||||
* @param {NodeId} id
|
||||
* @param {DominatorTreeNode} tree
|
||||
* @param {Array<NodeId>} path
|
||||
*
|
||||
* @returns {DominatorTreeNode|null}
|
||||
*/
|
||||
DominatorTreeNode.getNodeByIdAlongPath = function (id, tree, path) {
|
||||
function find(node, i) {
|
||||
if (!node || node.nodeId !== path[i]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.nodeId === id) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (i === path.length - 1 || !node.children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextId = path[i + 1];
|
||||
return find(node.children.find(c => c.nodeId === nextId), i + 1);
|
||||
}
|
||||
|
||||
return find(tree, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the shortest retaining paths for the given set of DominatorTreeNodes,
|
||||
* and populate each node's `shortestPaths` property with them in place.
|
||||
*
|
||||
* @param {HeapSnapshot} snapshot
|
||||
* @param {Object} breakdown
|
||||
* @param {NodeId} start
|
||||
* @param {Array<DominatorTreeNode>} treeNodes
|
||||
* @param {Number} maxNumPaths
|
||||
*/
|
||||
DominatorTreeNode.attachShortestPaths = function (snapshot,
|
||||
breakdown,
|
||||
start,
|
||||
treeNodes,
|
||||
maxNumPaths = DEFAULT_MAX_NUM_PATHS) {
|
||||
const idToTreeNode = new Map();
|
||||
const targets = [];
|
||||
for (let node of treeNodes) {
|
||||
const id = node.nodeId;
|
||||
idToTreeNode.set(id, node);
|
||||
targets.push(id);
|
||||
}
|
||||
|
||||
const shortestPaths = snapshot.computeShortestPaths(start,
|
||||
targets,
|
||||
maxNumPaths);
|
||||
|
||||
for (let [target, paths] of shortestPaths) {
|
||||
const deduped = deduplicatePaths(target, paths);
|
||||
deduped.nodes = deduped.nodes.map(id => {
|
||||
const { label } =
|
||||
DominatorTreeNode.getLabelAndShallowSize(id, snapshot, breakdown);
|
||||
return { id, label };
|
||||
});
|
||||
|
||||
idToTreeNode.get(target).shortestPaths = deduped;
|
||||
}
|
||||
};
|
||||
277
devtools/shared/heapsnapshot/HeapAnalysesClient.js
Normal file
277
devtools/shared/heapsnapshot/HeapAnalysesClient.js
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
const { DevToolsWorker } = require("devtools/shared/worker/worker");
|
||||
|
||||
const WORKER_URL =
|
||||
"resource://devtools/shared/heapsnapshot/HeapAnalysesWorker.js";
|
||||
var workerCounter = 0;
|
||||
|
||||
/**
|
||||
* A HeapAnalysesClient instance provides a developer-friendly interface for
|
||||
* interacting with a HeapAnalysesWorker. This enables users to be ignorant of
|
||||
* the message passing protocol used to communicate with the worker. The
|
||||
* HeapAnalysesClient owns the worker, and terminating the worker is done by
|
||||
* terminating the client (see the `destroy` method).
|
||||
*/
|
||||
const HeapAnalysesClient = module.exports = function () {
|
||||
this._worker = new DevToolsWorker(WORKER_URL, {
|
||||
name: `HeapAnalyses-${workerCounter++}`,
|
||||
verbose: DevToolsUtils.dumpv.wantVerbose
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy the worker, causing it to release its resources (such as heap
|
||||
* snapshots it has deserialized and read into memory). The client is no longer
|
||||
* usable after calling this method.
|
||||
*/
|
||||
HeapAnalysesClient.prototype.destroy = function () {
|
||||
this._worker.destroy();
|
||||
this._worker = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tell the worker to read into memory the heap snapshot at the given file
|
||||
* path. This is a prerequisite for asking the worker to perform various
|
||||
* analyses on a heap snapshot.
|
||||
*
|
||||
* @param {String} snapshotFilePath
|
||||
*
|
||||
* @returns Promise
|
||||
* The promise is fulfilled if the heap snapshot is successfully
|
||||
* deserialized and read into memory. The promise is rejected if that
|
||||
* does not happen, eg due to a bad file path or malformed heap
|
||||
* snapshot file.
|
||||
*/
|
||||
HeapAnalysesClient.prototype.readHeapSnapshot = function (snapshotFilePath) {
|
||||
return this._worker.performTask("readHeapSnapshot", { snapshotFilePath });
|
||||
};
|
||||
|
||||
/**
|
||||
* Tell the worker to delete all references to the snapshot and dominator trees
|
||||
* linked to the provided snapshot file path.
|
||||
*
|
||||
* @param {String} snapshotFilePath
|
||||
* @return Promise<undefined>
|
||||
*/
|
||||
HeapAnalysesClient.prototype.deleteHeapSnapshot = function (snapshotFilePath) {
|
||||
return this._worker.performTask("deleteHeapSnapshot", { snapshotFilePath });
|
||||
};
|
||||
|
||||
/**
|
||||
* Request the creation time given a snapshot file path. Returns `null`
|
||||
* if snapshot does not exist.
|
||||
*
|
||||
* @param {String} snapshotFilePath
|
||||
* The path to the snapshot.
|
||||
* @return {Number?}
|
||||
* The unix timestamp of the creation time of the snapshot, or null if
|
||||
* snapshot does not exist.
|
||||
*/
|
||||
HeapAnalysesClient.prototype.getCreationTime = function (snapshotFilePath) {
|
||||
return this._worker.performTask("getCreationTime", snapshotFilePath);
|
||||
};
|
||||
|
||||
/** * Censuses *****************************************************************/
|
||||
|
||||
/**
|
||||
* Ask the worker to perform a census analysis on the heap snapshot with the
|
||||
* given path. The heap snapshot at the given path must have already been read
|
||||
* into memory by the worker (see `readHeapSnapshot`).
|
||||
*
|
||||
* @param {String} snapshotFilePath
|
||||
*
|
||||
* @param {Object} censusOptions
|
||||
* A structured-cloneable object specifying the requested census's
|
||||
* breakdown. See the "takeCensus" section of
|
||||
* `js/src/doc/Debugger/Debugger.Memory.md` for detailed documentation.
|
||||
*
|
||||
* @param {Object} requestOptions
|
||||
* An object specifying options of this worker request.
|
||||
* - {Boolean} asTreeNode
|
||||
* Whether or not the census is returned as a CensusTreeNode,
|
||||
* or just a breakdown report. Defaults to false.
|
||||
* @see `devtools/shared/heapsnapshot/census-tree-node.js`
|
||||
* - {Boolean} asInvertedTreeNode
|
||||
* Whether or not the census is returned as an inverted
|
||||
* CensusTreeNode. Defaults to false.
|
||||
* - {String} filter
|
||||
* A filter string to prune the resulting tree with. Only applies if
|
||||
* either asTreeNode or asInvertedTreeNode is true.
|
||||
*
|
||||
* @returns Promise<Object>
|
||||
* An object with the following properties:
|
||||
* - report:
|
||||
* The report generated by the given census breakdown, or a
|
||||
* CensusTreeNode generated by the given census breakdown if
|
||||
* `asTreeNode` is true.
|
||||
* - parentMap:
|
||||
* The result of calling CensusUtils.createParentMap on the generated
|
||||
* report. Only exists if asTreeNode or asInvertedTreeNode are set.
|
||||
*/
|
||||
HeapAnalysesClient.prototype.takeCensus = function (snapshotFilePath,
|
||||
censusOptions,
|
||||
requestOptions = {}) {
|
||||
return this._worker.performTask("takeCensus", {
|
||||
snapshotFilePath,
|
||||
censusOptions,
|
||||
requestOptions,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the individual nodes that correspond to the given census report leaf
|
||||
* indices.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* An object with the following properties:
|
||||
* - {DominatorTreeId} dominatorTreeId: The id of the dominator tree.
|
||||
* - {Set<Number>} indices: The indices of the census report leaves we
|
||||
* would like to get the individuals for.
|
||||
* - {Object} censusBreakdown: The breakdown used to generate the census.
|
||||
* - {Object} labelBreakdown: The breakdown we would like to use when
|
||||
* labeling the resulting nodes.
|
||||
* - {Number} maxRetainingPaths: The maximum number of retaining paths to
|
||||
* compute for each node.
|
||||
* - {Number} maxIndividuals: The maximum number of individual nodes to
|
||||
* return.
|
||||
*
|
||||
* @returns {Promise<Object>}
|
||||
* A promise of an object with the following properties:
|
||||
* - {Array<DominatorTreeNode>} nodes: An array of `DominatorTreeNode`s
|
||||
* with their shortest paths attached, and without any dominator tree
|
||||
* child/parent information attached. The results are sorted by
|
||||
* retained size.
|
||||
*
|
||||
*/
|
||||
HeapAnalysesClient.prototype.getCensusIndividuals = function (opts) {
|
||||
return this._worker.performTask("getCensusIndividuals", opts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Request that the worker take a census on the heap snapshots with the given
|
||||
* paths and then return the difference between them. Both heap snapshots must
|
||||
* have already been read into memory by the worker (see `readHeapSnapshot`).
|
||||
*
|
||||
* @param {String} firstSnapshotFilePath
|
||||
* The first snapshot file path.
|
||||
*
|
||||
* @param {String} secondSnapshotFilePath
|
||||
* The second snapshot file path.
|
||||
*
|
||||
* @param {Object} censusOptions
|
||||
* A structured-cloneable object specifying the requested census's
|
||||
* breakdown. See the "takeCensus" section of
|
||||
* `js/src/doc/Debugger/Debugger.Memory.md` for detailed documentation.
|
||||
*
|
||||
* @param {Object} requestOptions
|
||||
* An object specifying options for this request.
|
||||
* - {Boolean} asTreeNode
|
||||
* Whether the resulting delta report should be converted to a census
|
||||
* tree node before returned. Defaults to false.
|
||||
* - {Boolean} asInvertedTreeNode
|
||||
* Whether or not the census is returned as an inverted
|
||||
* CensusTreeNode. Defaults to false.
|
||||
* - {String} filter
|
||||
* A filter string to prune the resulting tree with. Only applies if
|
||||
* either asTreeNode or asInvertedTreeNode is true.
|
||||
*
|
||||
* @returns Promise<Object>
|
||||
* - delta:
|
||||
* The delta report generated by diffing the two census reports, or a
|
||||
* CensusTreeNode generated from the delta report if
|
||||
* `requestOptions.asTreeNode` was true.
|
||||
* - parentMap:
|
||||
* The result of calling CensusUtils.createParentMap on the generated
|
||||
* delta. Only exists if asTreeNode or asInvertedTreeNode are set.
|
||||
*/
|
||||
HeapAnalysesClient.prototype.takeCensusDiff = function (firstSnapshotFilePath,
|
||||
secondSnapshotFilePath,
|
||||
censusOptions,
|
||||
requestOptions = {}) {
|
||||
return this._worker.performTask("takeCensusDiff", {
|
||||
firstSnapshotFilePath,
|
||||
secondSnapshotFilePath,
|
||||
censusOptions,
|
||||
requestOptions
|
||||
});
|
||||
};
|
||||
|
||||
/** * Dominator Trees **********************************************************/
|
||||
|
||||
/**
|
||||
* Compute the dominator tree of the heap snapshot loaded from the given file
|
||||
* path. Returns the id of the computed dominator tree.
|
||||
*
|
||||
* @param {String} snapshotFilePath
|
||||
*
|
||||
* @returns {Promise<DominatorTreeId>}
|
||||
*/
|
||||
HeapAnalysesClient.prototype.computeDominatorTree = function (snapshotFilePath) {
|
||||
return this._worker.performTask("computeDominatorTree", snapshotFilePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the initial, partial view of the dominator tree with the given id.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* An object specifying options for this request.
|
||||
* - {DominatorTreeId} dominatorTreeId
|
||||
* The id of the dominator tree.
|
||||
* - {Object} breakdown
|
||||
* The breakdown used to generate node labels.
|
||||
* - {Number} maxDepth
|
||||
* The maximum depth to traverse down the tree to create this initial
|
||||
* view.
|
||||
* - {Number} maxSiblings
|
||||
* The maximum number of siblings to visit within each traversed node's
|
||||
* children.
|
||||
* - {Number} maxRetainingPaths
|
||||
* The maximum number of retaining paths to find for each node.
|
||||
*
|
||||
* @returns {Promise<DominatorTreeNode>}
|
||||
*/
|
||||
HeapAnalysesClient.prototype.getDominatorTree = function (opts) {
|
||||
return this._worker.performTask("getDominatorTree", opts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a subset of a nodes children in the dominator tree.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* An object specifying options for this request.
|
||||
* - {DominatorTreeId} dominatorTreeId
|
||||
* The id of the dominator tree.
|
||||
* - {NodeId} nodeId
|
||||
* The id of the node whose children are being found.
|
||||
* - {Object} breakdown
|
||||
* The breakdown used to generate node labels.
|
||||
* - {Number} startIndex
|
||||
* The starting index within the full set of immediately dominated
|
||||
* children of the children being requested. Children are always sorted
|
||||
* by greatest to least retained size.
|
||||
* - {Number} maxCount
|
||||
* The maximum number of children to return.
|
||||
* - {Number} maxRetainingPaths
|
||||
* The maximum number of retaining paths to find for each node.
|
||||
*
|
||||
* @returns {Promise<Object>}
|
||||
* A promise of an object with the following properties:
|
||||
* - {Array<DominatorTreeNode>} nodes
|
||||
* The requested nodes that are immediately dominated by the node
|
||||
* identified by `opts.nodeId`.
|
||||
* - {Boolean} moreChildrenAvailable
|
||||
* True iff there are more children available after the returned
|
||||
* nodes.
|
||||
* - {Array<NodeId>} path
|
||||
* The path through the tree from the root to these node's parent, eg
|
||||
* [root's id, child of root's id, child of child of root's id, ..., `nodeId`].
|
||||
*/
|
||||
HeapAnalysesClient.prototype.getImmediatelyDominated = function (opts) {
|
||||
return this._worker.performTask("getImmediatelyDominated", opts);
|
||||
};
|
||||
303
devtools/shared/heapsnapshot/HeapAnalysesWorker.js
Normal file
303
devtools/shared/heapsnapshot/HeapAnalysesWorker.js
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
/* global ThreadSafeChromeUtils*/
|
||||
|
||||
// This is a worker which reads offline heap snapshots into memory and performs
|
||||
// heavyweight analyses on them without blocking the main thread. A
|
||||
// HeapAnalysesWorker is owned and communicated with by a HeapAnalysesClient
|
||||
// instance. See HeapAnalysesClient.js.
|
||||
|
||||
"use strict";
|
||||
|
||||
importScripts("resource://gre/modules/workers/require.js");
|
||||
importScripts("resource://devtools/shared/worker/helper.js");
|
||||
const { censusReportToCensusTreeNode } = require("resource://devtools/shared/heapsnapshot/census-tree-node.js");
|
||||
const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");
|
||||
const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
|
||||
|
||||
const DEFAULT_START_INDEX = 0;
|
||||
const DEFAULT_MAX_COUNT = 50;
|
||||
|
||||
/**
|
||||
* The set of HeapSnapshot instances this worker has read into memory. Keyed by
|
||||
* snapshot file path.
|
||||
*/
|
||||
const snapshots = Object.create(null);
|
||||
|
||||
/**
|
||||
* The set of `DominatorTree`s that have been computed, mapped by their id (aka
|
||||
* the index into this array).
|
||||
*
|
||||
* @see /dom/webidl/DominatorTree.webidl
|
||||
*/
|
||||
const dominatorTrees = [];
|
||||
|
||||
/**
|
||||
* The i^th HeapSnapshot in this array is the snapshot used to generate the i^th
|
||||
* dominator tree in `dominatorTrees` above. This lets us map from a dominator
|
||||
* tree id to the snapshot it came from.
|
||||
*/
|
||||
const dominatorTreeSnapshots = [];
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.readHeapSnapshot
|
||||
*/
|
||||
workerHelper.createTask(self, "readHeapSnapshot", ({ snapshotFilePath }) => {
|
||||
snapshots[snapshotFilePath] =
|
||||
ThreadSafeChromeUtils.readHeapSnapshot(snapshotFilePath);
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.deleteHeapSnapshot
|
||||
*/
|
||||
workerHelper.createTask(self, "deleteHeapSnapshot", ({ snapshotFilePath }) => {
|
||||
let snapshot = snapshots[snapshotFilePath];
|
||||
if (!snapshot) {
|
||||
throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
|
||||
}
|
||||
|
||||
snapshots[snapshotFilePath] = undefined;
|
||||
|
||||
let dominatorTreeId = dominatorTreeSnapshots.indexOf(snapshot);
|
||||
if (dominatorTreeId != -1) {
|
||||
dominatorTreeSnapshots[dominatorTreeId] = undefined;
|
||||
dominatorTrees[dominatorTreeId] = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.takeCensus
|
||||
*/
|
||||
workerHelper.createTask(self, "takeCensus", ({ snapshotFilePath, censusOptions, requestOptions }) => {
|
||||
if (!snapshots[snapshotFilePath]) {
|
||||
throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
|
||||
}
|
||||
|
||||
let report = snapshots[snapshotFilePath].takeCensus(censusOptions);
|
||||
let parentMap;
|
||||
|
||||
if (requestOptions.asTreeNode || requestOptions.asInvertedTreeNode) {
|
||||
const opts = { filter: requestOptions.filter || null };
|
||||
if (requestOptions.asInvertedTreeNode) {
|
||||
opts.invert = true;
|
||||
}
|
||||
report = censusReportToCensusTreeNode(censusOptions.breakdown, report, opts);
|
||||
parentMap = CensusUtils.createParentMap(report);
|
||||
}
|
||||
|
||||
return { report, parentMap };
|
||||
});
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.getCensusIndividuals
|
||||
*/
|
||||
workerHelper.createTask(self, "getCensusIndividuals", request => {
|
||||
const {
|
||||
dominatorTreeId,
|
||||
indices,
|
||||
censusBreakdown,
|
||||
labelBreakdown,
|
||||
maxRetainingPaths,
|
||||
maxIndividuals,
|
||||
} = request;
|
||||
|
||||
const dominatorTree = dominatorTrees[dominatorTreeId];
|
||||
if (!dominatorTree) {
|
||||
throw new Error(
|
||||
`There does not exist a DominatorTree with the id ${dominatorTreeId}`);
|
||||
}
|
||||
|
||||
const snapshot = dominatorTreeSnapshots[dominatorTreeId];
|
||||
const nodeIds = CensusUtils.getCensusIndividuals(indices, censusBreakdown, snapshot);
|
||||
|
||||
const nodes = nodeIds
|
||||
.sort((a, b) => dominatorTree.getRetainedSize(b) - dominatorTree.getRetainedSize(a))
|
||||
.slice(0, maxIndividuals)
|
||||
.map(id => {
|
||||
const { label, shallowSize } =
|
||||
DominatorTreeNode.getLabelAndShallowSize(id, snapshot, labelBreakdown);
|
||||
const retainedSize = dominatorTree.getRetainedSize(id);
|
||||
const node = new DominatorTreeNode(id, label, shallowSize, retainedSize);
|
||||
node.moreChildrenAvailable = false;
|
||||
return node;
|
||||
});
|
||||
|
||||
DominatorTreeNode.attachShortestPaths(snapshot,
|
||||
labelBreakdown,
|
||||
dominatorTree.root,
|
||||
nodes,
|
||||
maxRetainingPaths);
|
||||
|
||||
return { nodes };
|
||||
});
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.takeCensusDiff
|
||||
*/
|
||||
workerHelper.createTask(self, "takeCensusDiff", request => {
|
||||
const {
|
||||
firstSnapshotFilePath,
|
||||
secondSnapshotFilePath,
|
||||
censusOptions,
|
||||
requestOptions
|
||||
} = request;
|
||||
|
||||
if (!snapshots[firstSnapshotFilePath]) {
|
||||
throw new Error(`No known heap snapshot for '${firstSnapshotFilePath}'`);
|
||||
}
|
||||
|
||||
if (!snapshots[secondSnapshotFilePath]) {
|
||||
throw new Error(`No known heap snapshot for '${secondSnapshotFilePath}'`);
|
||||
}
|
||||
|
||||
const first = snapshots[firstSnapshotFilePath].takeCensus(censusOptions);
|
||||
const second = snapshots[secondSnapshotFilePath].takeCensus(censusOptions);
|
||||
let delta = CensusUtils.diff(censusOptions.breakdown, first, second);
|
||||
let parentMap;
|
||||
|
||||
if (requestOptions.asTreeNode || requestOptions.asInvertedTreeNode) {
|
||||
const opts = { filter: requestOptions.filter || null };
|
||||
if (requestOptions.asInvertedTreeNode) {
|
||||
opts.invert = true;
|
||||
}
|
||||
delta = censusReportToCensusTreeNode(censusOptions.breakdown, delta, opts);
|
||||
parentMap = CensusUtils.createParentMap(delta);
|
||||
}
|
||||
|
||||
return { delta, parentMap };
|
||||
});
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.getCreationTime
|
||||
*/
|
||||
workerHelper.createTask(self, "getCreationTime", snapshotFilePath => {
|
||||
if (!snapshots[snapshotFilePath]) {
|
||||
throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
|
||||
}
|
||||
return snapshots[snapshotFilePath].creationTime;
|
||||
});
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.computeDominatorTree
|
||||
*/
|
||||
workerHelper.createTask(self, "computeDominatorTree", snapshotFilePath => {
|
||||
const snapshot = snapshots[snapshotFilePath];
|
||||
if (!snapshot) {
|
||||
throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
|
||||
}
|
||||
|
||||
const id = dominatorTrees.length;
|
||||
dominatorTrees.push(snapshot.computeDominatorTree());
|
||||
dominatorTreeSnapshots.push(snapshot);
|
||||
return id;
|
||||
});
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.getDominatorTree
|
||||
*/
|
||||
workerHelper.createTask(self, "getDominatorTree", request => {
|
||||
const {
|
||||
dominatorTreeId,
|
||||
breakdown,
|
||||
maxDepth,
|
||||
maxSiblings,
|
||||
maxRetainingPaths,
|
||||
} = request;
|
||||
|
||||
if (!(0 <= dominatorTreeId && dominatorTreeId < dominatorTrees.length)) {
|
||||
throw new Error(
|
||||
`There does not exist a DominatorTree with the id ${dominatorTreeId}`);
|
||||
}
|
||||
|
||||
const dominatorTree = dominatorTrees[dominatorTreeId];
|
||||
const snapshot = dominatorTreeSnapshots[dominatorTreeId];
|
||||
|
||||
const tree = DominatorTreeNode.partialTraversal(dominatorTree,
|
||||
snapshot,
|
||||
breakdown,
|
||||
maxDepth,
|
||||
maxSiblings);
|
||||
|
||||
const nodes = [];
|
||||
(function getNodes(node) {
|
||||
nodes.push(node);
|
||||
if (node.children) {
|
||||
for (let i = 0, length = node.children.length; i < length; i++) {
|
||||
getNodes(node.children[i]);
|
||||
}
|
||||
}
|
||||
}(tree));
|
||||
|
||||
DominatorTreeNode.attachShortestPaths(snapshot,
|
||||
breakdown,
|
||||
dominatorTree.root,
|
||||
nodes,
|
||||
maxRetainingPaths);
|
||||
|
||||
return tree;
|
||||
});
|
||||
|
||||
/**
|
||||
* @see HeapAnalysesClient.prototype.getImmediatelyDominated
|
||||
*/
|
||||
workerHelper.createTask(self, "getImmediatelyDominated", request => {
|
||||
const {
|
||||
dominatorTreeId,
|
||||
nodeId,
|
||||
breakdown,
|
||||
startIndex,
|
||||
maxCount,
|
||||
maxRetainingPaths,
|
||||
} = request;
|
||||
|
||||
if (!(0 <= dominatorTreeId && dominatorTreeId < dominatorTrees.length)) {
|
||||
throw new Error(
|
||||
`There does not exist a DominatorTree with the id ${dominatorTreeId}`);
|
||||
}
|
||||
|
||||
const dominatorTree = dominatorTrees[dominatorTreeId];
|
||||
const snapshot = dominatorTreeSnapshots[dominatorTreeId];
|
||||
|
||||
const childIds = dominatorTree.getImmediatelyDominated(nodeId);
|
||||
if (!childIds) {
|
||||
throw new Error(`${nodeId} is not a node id in the dominator tree`);
|
||||
}
|
||||
|
||||
const start = startIndex || DEFAULT_START_INDEX;
|
||||
const count = maxCount || DEFAULT_MAX_COUNT;
|
||||
const end = start + count;
|
||||
|
||||
const nodes = childIds
|
||||
.slice(start, end)
|
||||
.map(id => {
|
||||
const { label, shallowSize } =
|
||||
DominatorTreeNode.getLabelAndShallowSize(id, snapshot, breakdown);
|
||||
const retainedSize = dominatorTree.getRetainedSize(id);
|
||||
const node = new DominatorTreeNode(id, label, shallowSize, retainedSize);
|
||||
node.parentId = nodeId;
|
||||
// DominatorTree.getImmediatelyDominated will always return non-null here
|
||||
// because we got the id directly from the dominator tree.
|
||||
node.moreChildrenAvailable = dominatorTree.getImmediatelyDominated(id).length > 0;
|
||||
return node;
|
||||
});
|
||||
|
||||
const path = [];
|
||||
let id = nodeId;
|
||||
do {
|
||||
path.push(id);
|
||||
id = dominatorTree.getImmediateDominator(id);
|
||||
} while (id !== null);
|
||||
path.reverse();
|
||||
|
||||
const moreChildrenAvailable = childIds.length > end;
|
||||
|
||||
DominatorTreeNode.attachShortestPaths(snapshot,
|
||||
breakdown,
|
||||
dominatorTree.root,
|
||||
nodes,
|
||||
maxRetainingPaths);
|
||||
|
||||
return { nodes, moreChildrenAvailable, path };
|
||||
});
|
||||
95
devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js
Normal file
95
devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Heap snapshots are always saved in the temp directory, and have a regular
|
||||
// naming convention. This module provides helpers for working with heap
|
||||
// snapshot files in a safe manner. Because we attempt to avoid unnecessary
|
||||
// copies of the heap snapshot files by checking the local filesystem for a heap
|
||||
// snapshot file with the given snapshot id, we want to ensure that we are only
|
||||
// attempting to open heap snapshot files and not `~/.ssh/id_rsa`, for
|
||||
// example. Therefore, the RDP only talks about snapshot ids, or transfering the
|
||||
// bulk file data. A file path can be recovered from a snapshot id, which allows
|
||||
// one to check for the presence of the heap snapshot file on the local file
|
||||
// system, but we don't have to worry about opening arbitrary files.
|
||||
//
|
||||
// The heap snapshot file path conventions permits the following forms:
|
||||
//
|
||||
// $TEMP_DIRECTORY/XXXXXXXXXX.fxsnapshot
|
||||
// $TEMP_DIRECTORY/XXXXXXXXXX-XXXXX.fxsnapshot
|
||||
//
|
||||
// Where the strings of "X" are zero or more digits.
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Ci } = require("chrome");
|
||||
loader.lazyRequireGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm", true);
|
||||
loader.lazyRequireGetter(this, "OS", "resource://gre/modules/osfile.jsm", true);
|
||||
|
||||
function getHeapSnapshotFileTemplate() {
|
||||
return OS.Path.join(OS.Constants.Path.tmpDir, `${Date.now()}.fxsnapshot`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unique temp file path for a new heap snapshot. The file is guaranteed
|
||||
* not to exist before this call.
|
||||
*
|
||||
* @returns String
|
||||
*/
|
||||
exports.getNewUniqueHeapSnapshotTempFilePath = function () {
|
||||
let file = new FileUtils.File(getHeapSnapshotFileTemplate());
|
||||
// The call to createUnique will append "-N" after the leaf name (but before
|
||||
// the extension) until a new file is found and create it. This guarantees we
|
||||
// won't accidentally choose the same file twice.
|
||||
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
|
||||
return file.path;
|
||||
};
|
||||
|
||||
function isValidSnapshotFileId(snapshotId) {
|
||||
return /^\d+(\-\d+)?$/.test(snapshotId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path for the given snapshot id.
|
||||
*
|
||||
* @param {String} snapshotId
|
||||
*
|
||||
* @returns String | null
|
||||
*/
|
||||
exports.getHeapSnapshotTempFilePath = function (snapshotId) {
|
||||
// Don't want anyone sneaking "../../../.." strings into the snapshot id and
|
||||
// trying to make us open arbitrary files.
|
||||
if (!isValidSnapshotFileId(snapshotId)) {
|
||||
return null;
|
||||
}
|
||||
return OS.Path.join(OS.Constants.Path.tmpDir, snapshotId + ".fxsnapshot");
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if we have the heap snapshot file for the given snapshot id on
|
||||
* the local file system. False is returned otherwise.
|
||||
*
|
||||
* @returns Promise<Boolean>
|
||||
*/
|
||||
exports.haveHeapSnapshotTempFile = function (snapshotId) {
|
||||
const path = exports.getHeapSnapshotTempFilePath(snapshotId);
|
||||
if (!path) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return OS.File.stat(path).then(() => true,
|
||||
() => false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a heap snapshot's file path, extricate the snapshot id.
|
||||
*
|
||||
* @param {String} path
|
||||
*
|
||||
* @returns String
|
||||
*/
|
||||
exports.getSnapshotIdFromPath = function (path) {
|
||||
return path.slice(OS.Constants.Path.tmpDir.length + 1,
|
||||
path.length - ".fxsnapshot".length);
|
||||
};
|
||||
748
devtools/shared/heapsnapshot/census-tree-node.js
Normal file
748
devtools/shared/heapsnapshot/census-tree-node.js
Normal file
|
|
@ -0,0 +1,748 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
// CensusTreeNode is an intermediate representation of a census report that
|
||||
// exists between after a report is generated by taking a census and before the
|
||||
// report is rendered in the DOM. It must be dead simple to render, with no
|
||||
// further data processing or massaging needed before rendering DOM nodes. Our
|
||||
// goal is to do the census report to CensusTreeNode transformation in the
|
||||
// HeapAnalysesWorker, and ensure that the **only** work that the main thread
|
||||
// has to do is strictly DOM rendering work.
|
||||
|
||||
const {
|
||||
Visitor,
|
||||
walk,
|
||||
basisTotalBytes,
|
||||
basisTotalCount,
|
||||
} = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
|
||||
|
||||
// Monotonically increasing integer for CensusTreeNode `id`s.
|
||||
let censusTreeNodeIdCounter = 0;
|
||||
|
||||
/**
|
||||
* Return true if the given object is a SavedFrame stack object, false otherwise.
|
||||
*
|
||||
* @param {any} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function isSavedFrame(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object SavedFrame]";
|
||||
}
|
||||
|
||||
/**
|
||||
* A CensusTreeNodeCache maps from SavedFrames to CensusTreeNodes. It is used when
|
||||
* aggregating multiple SavedFrame allocation stack keys into a tree of many
|
||||
* CensusTreeNodes. Each stack may share older frames, and we want to preserve
|
||||
* this sharing when converting to CensusTreeNode, so before creating a new
|
||||
* CensusTreeNode, we look for an existing one in one of our CensusTreeNodeCaches.
|
||||
*/
|
||||
function CensusTreeNodeCache() {}
|
||||
CensusTreeNodeCache.prototype = null;
|
||||
|
||||
/**
|
||||
* The value of a single entry stored in a CensusTreeNodeCache. It is a pair of
|
||||
* the CensusTreeNode for this cache value, and the subsequent
|
||||
* CensusTreeNodeCache for this node's children.
|
||||
*
|
||||
* @param {SavedFrame} frame
|
||||
* The frame being cached.
|
||||
*/
|
||||
function CensusTreeNodeCacheValue() {
|
||||
// The CensusTreeNode for this cache value.
|
||||
this.node = undefined;
|
||||
// The CensusTreeNodeCache for this frame's children.
|
||||
this.children = undefined;
|
||||
}
|
||||
|
||||
CensusTreeNodeCacheValue.prototype = null;
|
||||
|
||||
/**
|
||||
* Create a unique string for the given SavedFrame (ignoring the frame's parent
|
||||
* chain) that can be used as a hash to key this frame within a CensusTreeNodeCache.
|
||||
*
|
||||
* NB: We manually hash rather than using an ES6 Map because we are purposely
|
||||
* ignoring the parent chain and wish to consider frames with everything the
|
||||
* same except their parents as the same.
|
||||
*
|
||||
* @param {SavedFrame} frame
|
||||
* The SavedFrame object we would like to lookup in or insert into a
|
||||
* CensusTreeNodeCache.
|
||||
*
|
||||
* @returns {String}
|
||||
* The unique string that can be used as a key in a CensusTreeNodeCache.
|
||||
*/
|
||||
CensusTreeNodeCache.hashFrame = function (frame) {
|
||||
return `FRAME,${frame.functionDisplayName},${frame.source},${frame.line},${frame.column},${frame.asyncCause}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a unique string for the given CensusTreeNode **with regards to
|
||||
* siblings at the current depth of the tree, not within the whole tree.** It
|
||||
* can be used as a hash to key this node within a CensusTreeNodeCache.
|
||||
*
|
||||
* @param {CensusTreeNode} node
|
||||
* The node we would like to lookup in or insert into a cache.
|
||||
*
|
||||
* @returns {String}
|
||||
* The unique string that can be used as a key in a CensusTreeNodeCache.
|
||||
*/
|
||||
CensusTreeNodeCache.hashNode = function (node) {
|
||||
return isSavedFrame(node.name)
|
||||
? CensusTreeNodeCache.hashFrame(node.name)
|
||||
: `NODE,${node.name}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert the given CensusTreeNodeCacheValue whose node.name is a SavedFrame
|
||||
* object in the given cache.
|
||||
*
|
||||
* @param {CensusTreeNodeCache} cache
|
||||
* @param {CensusTreeNodeCacheValue} value
|
||||
*/
|
||||
CensusTreeNodeCache.insertFrame = function (cache, value) {
|
||||
cache[CensusTreeNodeCache.hashFrame(value.node.name)] = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert the given value in the cache.
|
||||
*
|
||||
* @param {CensusTreeNodeCache} cache
|
||||
* @param {CensusTreeNodeCacheValue} value
|
||||
*/
|
||||
CensusTreeNodeCache.insertNode = function (cache, value) {
|
||||
if (isSavedFrame(value.node.name)) {
|
||||
CensusTreeNodeCache.insertFrame(cache, value);
|
||||
} else {
|
||||
cache[CensusTreeNodeCache.hashNode(value.node)] = value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Lookup `frame` in `cache` and return its value if it exists.
|
||||
*
|
||||
* @param {CensusTreeNodeCache} cache
|
||||
* @param {SavedFrame} frame
|
||||
*
|
||||
* @returns {undefined|CensusTreeNodeCacheValue}
|
||||
*/
|
||||
CensusTreeNodeCache.lookupFrame = function (cache, frame) {
|
||||
return cache[CensusTreeNodeCache.hashFrame(frame)];
|
||||
};
|
||||
|
||||
/**
|
||||
* Lookup `node` in `cache` and return its value if it exists.
|
||||
*
|
||||
* @param {CensusTreeNodeCache} cache
|
||||
* @param {CensusTreeNode} node
|
||||
*
|
||||
* @returns {undefined|CensusTreeNodeCacheValue}
|
||||
*/
|
||||
CensusTreeNodeCache.lookupNode = function (cache, node) {
|
||||
return isSavedFrame(node.name)
|
||||
? CensusTreeNodeCache.lookupFrame(cache, node.name)
|
||||
: cache[CensusTreeNodeCache.hashNode(node)];
|
||||
};
|
||||
|
||||
/**
|
||||
* Add `child` to `parent`'s set of children and store the parent ID
|
||||
* on the child.
|
||||
*
|
||||
* @param {CensusTreeNode} parent
|
||||
* @param {CensusTreeNode} child
|
||||
*/
|
||||
function addChild(parent, child) {
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
child.parent = parent.id;
|
||||
parent.children.push(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of each frame in the provided stack.
|
||||
*
|
||||
* @param {SavedFrame} stack
|
||||
* @returns {Array<SavedFrame>}
|
||||
*/
|
||||
function getArrayOfFrames(stack) {
|
||||
const frames = [];
|
||||
let frame = stack;
|
||||
while (frame) {
|
||||
frames.push(frame);
|
||||
frame = frame.parent;
|
||||
}
|
||||
frames.reverse();
|
||||
return frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an `edge` to a sub-`report` whose structure is described by
|
||||
* `breakdown`, create a CensusTreeNode tree.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The breakdown specifying the structure of the given report.
|
||||
*
|
||||
* @param {Object} report
|
||||
* The census report.
|
||||
*
|
||||
* @param {null|String|SavedFrame} edge
|
||||
* The edge leading to this report from the parent report.
|
||||
*
|
||||
* @param {CensusTreeNodeCache} cache
|
||||
* The cache of CensusTreeNodes we have already made for the siblings of
|
||||
* the node being created. The existing nodes are reused when possible.
|
||||
*
|
||||
* @param {Object} outParams
|
||||
* The return values are attached to this object after this function
|
||||
* returns. Because we create a CensusTreeNode for each frame in a
|
||||
* SavedFrame stack edge, there may multiple nodes per sub-report.
|
||||
*
|
||||
* - top: The deepest node in the CensusTreeNode subtree created.
|
||||
*
|
||||
* - bottom: The shallowest node in the CensusTreeNode subtree created.
|
||||
* This is null if the shallowest node in the subtree was
|
||||
* found in the `cache` and reused.
|
||||
*
|
||||
* Note that top and bottom are not necessarily different. In the case
|
||||
* where there is a 1:1 correspondence between an edge in the report and
|
||||
* a CensusTreeNode, top and bottom refer to the same node.
|
||||
*/
|
||||
function makeCensusTreeNodeSubTree(breakdown, report, edge, cache, outParams) {
|
||||
if (!isSavedFrame(edge)) {
|
||||
const node = new CensusTreeNode(edge);
|
||||
outParams.top = outParams.bottom = node;
|
||||
return;
|
||||
}
|
||||
|
||||
const frames = getArrayOfFrames(edge);
|
||||
let currentCache = cache;
|
||||
let prevNode;
|
||||
for (let i = 0, length = frames.length; i < length; i++) {
|
||||
const frame = frames[i];
|
||||
|
||||
// Get or create the CensusTreeNodeCacheValue for this frame. If we already
|
||||
// have a CensusTreeNodeCacheValue (and hence a CensusTreeNode) for this
|
||||
// frame, we don't need to add the node to the previous node's children as
|
||||
// we have already done that. If we don't have a CensusTreeNodeCacheValue
|
||||
// and CensusTreeNode for this frame, then create one and make sure to hook
|
||||
// it up as a child of the previous node.
|
||||
let isNewNode = false;
|
||||
let val = CensusTreeNodeCache.lookupFrame(currentCache, frame);
|
||||
if (!val) {
|
||||
isNewNode = true;
|
||||
val = new CensusTreeNodeCacheValue();
|
||||
val.node = new CensusTreeNode(frame);
|
||||
|
||||
CensusTreeNodeCache.insertFrame(currentCache, val);
|
||||
if (prevNode) {
|
||||
addChild(prevNode, val.node);
|
||||
}
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
outParams.bottom = isNewNode ? val.node : null;
|
||||
}
|
||||
if (i === length - 1) {
|
||||
outParams.top = val.node;
|
||||
}
|
||||
|
||||
prevNode = val.node;
|
||||
|
||||
if (i !== length - 1 && !val.children) {
|
||||
// This is not the last frame and therefore this node will have
|
||||
// children, which we must cache.
|
||||
val.children = new CensusTreeNodeCache();
|
||||
}
|
||||
|
||||
currentCache = val.children;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Visitor that walks a census report and creates the corresponding
|
||||
* CensusTreeNode tree.
|
||||
*/
|
||||
function CensusTreeNodeVisitor() {
|
||||
// The root of the resulting CensusTreeNode tree.
|
||||
this._root = null;
|
||||
|
||||
// The stack of CensusTreeNodes that we are in the process of building while
|
||||
// walking the census report.
|
||||
this._nodeStack = [];
|
||||
|
||||
// To avoid unnecessary allocations, we reuse the same out parameter object
|
||||
// passed to `makeCensusTreeNodeSubTree` every time we call it.
|
||||
this._outParams = {
|
||||
top: null,
|
||||
bottom: null,
|
||||
};
|
||||
|
||||
// The stack of `CensusTreeNodeCache`s that we use to aggregate many
|
||||
// SavedFrame stacks into a single CensusTreeNode tree.
|
||||
this._cacheStack = [new CensusTreeNodeCache()];
|
||||
|
||||
// The current index in the DFS of the census report tree.
|
||||
this._index = -1;
|
||||
}
|
||||
|
||||
CensusTreeNodeVisitor.prototype = Object.create(Visitor);
|
||||
|
||||
/**
|
||||
* Create the CensusTreeNode subtree for this sub-report and link it to the
|
||||
* parent CensusTreeNode.
|
||||
*
|
||||
* @overrides Visitor.prototype.enter
|
||||
*/
|
||||
CensusTreeNodeVisitor.prototype.enter = function (breakdown, report, edge) {
|
||||
this._index++;
|
||||
|
||||
const cache = this._cacheStack[this._cacheStack.length - 1];
|
||||
makeCensusTreeNodeSubTree(breakdown, report, edge, cache, this._outParams);
|
||||
const { top, bottom } = this._outParams;
|
||||
|
||||
if (!this._root) {
|
||||
this._root = bottom;
|
||||
} else if (bottom) {
|
||||
addChild(this._nodeStack[this._nodeStack.length - 1], bottom);
|
||||
}
|
||||
|
||||
this._cacheStack.push(new CensusTreeNodeCache());
|
||||
this._nodeStack.push(top);
|
||||
};
|
||||
|
||||
function values(cache) {
|
||||
return Object.keys(cache).map(k => cache[k]);
|
||||
}
|
||||
|
||||
function isNonEmpty(node) {
|
||||
return (node.children !== undefined && node.children.length)
|
||||
|| node.bytes !== 0
|
||||
|| node.count !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* We have finished adding children to the CensusTreeNode subtree for the
|
||||
* current sub-report. Make sure that the children are sorted for every node in
|
||||
* the subtree.
|
||||
*
|
||||
* @overrides Visitor.prototype.exit
|
||||
*/
|
||||
CensusTreeNodeVisitor.prototype.exit = function (breakdown, report, edge) {
|
||||
// Ensure all children are sorted and have their counts/bytes aggregated. We
|
||||
// only need to consider cache children here, because other children
|
||||
// correspond to other sub-reports and we already fixed them up in an earlier
|
||||
// invocation of `exit`.
|
||||
|
||||
function dfs(node, childrenCache) {
|
||||
if (childrenCache) {
|
||||
const childValues = values(childrenCache);
|
||||
for (let i = 0, length = childValues.length; i < length; i++) {
|
||||
dfs(childValues[i].node, childValues[i].children);
|
||||
}
|
||||
}
|
||||
|
||||
node.totalCount = node.count;
|
||||
node.totalBytes = node.bytes;
|
||||
|
||||
if (node.children) {
|
||||
// Prune empty leaves.
|
||||
node.children = node.children.filter(isNonEmpty);
|
||||
|
||||
node.children.sort(compareByTotal);
|
||||
|
||||
for (let i = 0, length = node.children.length; i < length; i++) {
|
||||
node.totalCount += node.children[i].totalCount;
|
||||
node.totalBytes += node.children[i].totalBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const top = this._nodeStack.pop();
|
||||
const cache = this._cacheStack.pop();
|
||||
dfs(top, cache);
|
||||
};
|
||||
|
||||
/**
|
||||
* @overrides Visitor.prototype.count
|
||||
*/
|
||||
CensusTreeNodeVisitor.prototype.count = function (breakdown, report, edge) {
|
||||
const node = this._nodeStack[this._nodeStack.length - 1];
|
||||
node.reportLeafIndex = this._index;
|
||||
|
||||
if (breakdown.count) {
|
||||
node.count = report.count;
|
||||
}
|
||||
|
||||
if (breakdown.bytes) {
|
||||
node.bytes = report.bytes;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the root of the resulting CensusTreeNode tree.
|
||||
*
|
||||
* @returns {CensusTreeNode}
|
||||
*/
|
||||
CensusTreeNodeVisitor.prototype.root = function () {
|
||||
if (!this._root) {
|
||||
throw new Error("Attempt to get the root before walking the census report!");
|
||||
}
|
||||
|
||||
if (this._nodeStack.length) {
|
||||
throw new Error("Attempt to get the root while walking the census report!");
|
||||
}
|
||||
|
||||
return this._root;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a single, uninitialized CensusTreeNode.
|
||||
*
|
||||
* @param {null|String|SavedFrame} name
|
||||
*/
|
||||
function CensusTreeNode(name) {
|
||||
// Display name for this CensusTreeNode. Either null, a string, or a
|
||||
// SavedFrame.
|
||||
this.name = name;
|
||||
|
||||
// The number of bytes occupied by matching things in the heap snapshot.
|
||||
this.bytes = 0;
|
||||
|
||||
// The sum of `this.bytes` and `child.totalBytes` for each child in
|
||||
// `this.children`.
|
||||
this.totalBytes = 0;
|
||||
|
||||
// The number of things in the heap snapshot that match this node in the
|
||||
// census tree.
|
||||
this.count = 0;
|
||||
|
||||
// The sum of `this.count` and `child.totalCount` for each child in
|
||||
// `this.children`.
|
||||
this.totalCount = 0;
|
||||
|
||||
// An array of this node's children, or undefined if it has no children.
|
||||
this.children = undefined;
|
||||
|
||||
// The unique ID of this node.
|
||||
this.id = ++censusTreeNodeIdCounter;
|
||||
|
||||
// If present, the unique ID of this node's parent. If this node does not have
|
||||
// a parent, then undefined.
|
||||
this.parent = undefined;
|
||||
|
||||
// The `reportLeafIndex` property allows mapping a CensusTreeNode node back to
|
||||
// a leaf in the census report it was generated from. It is always one of the
|
||||
// following variants:
|
||||
//
|
||||
// * A `Number` index pointing a leaf report in a pre-order DFS traversal of
|
||||
// this CensusTreeNode's census report.
|
||||
//
|
||||
// * A `Set` object containing such indices, when this is part of an inverted
|
||||
// CensusTreeNode tree and multiple leaves in the report map onto this node.
|
||||
//
|
||||
// * Finally, `undefined` when no leaves in the census report correspond with
|
||||
// this node.
|
||||
//
|
||||
// The first and third cases are the common cases. The second case is rather
|
||||
// uncommon, and to avoid doubling the number of allocations when creating
|
||||
// CensusTreeNode trees, and objects that get structured cloned when sending
|
||||
// such trees from the HeapAnalysesWorker to the main thread, we only allocate
|
||||
// a Set object once a node actually does have multiple leaves it corresponds
|
||||
// to.
|
||||
this.reportLeafIndex = undefined;
|
||||
}
|
||||
|
||||
CensusTreeNode.prototype = null;
|
||||
|
||||
/**
|
||||
* Compare the given nodes by their `totalBytes` properties, and breaking ties
|
||||
* with the `totalCount`, `bytes`, and `count` properties (in that order).
|
||||
*
|
||||
* @param {CensusTreeNode} node1
|
||||
* @param {CensusTreeNode} node2
|
||||
*
|
||||
* @returns {Number}
|
||||
* A number suitable for using with Array.prototype.sort.
|
||||
*/
|
||||
function compareByTotal(node1, node2) {
|
||||
return Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes)
|
||||
|| Math.abs(node2.totalCount) - Math.abs(node1.totalCount)
|
||||
|| Math.abs(node2.bytes) - Math.abs(node1.bytes)
|
||||
|| Math.abs(node2.count) - Math.abs(node1.count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the given nodes by their `bytes` properties, and breaking ties with
|
||||
* the `count`, `totalBytes`, and `totalCount` properties (in that order).
|
||||
*
|
||||
* @param {CensusTreeNode} node1
|
||||
* @param {CensusTreeNode} node2
|
||||
*
|
||||
* @returns {Number}
|
||||
* A number suitable for using with Array.prototype.sort.
|
||||
*/
|
||||
function compareBySelf(node1, node2) {
|
||||
return Math.abs(node2.bytes) - Math.abs(node1.bytes)
|
||||
|| Math.abs(node2.count) - Math.abs(node1.count)
|
||||
|| Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes)
|
||||
|| Math.abs(node2.totalCount) - Math.abs(node1.totalCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a parent cache value from a tree we are building and a child node from
|
||||
* a tree we are basing the new tree off of, if we already have a corresponding
|
||||
* node in the parent's children cache, merge this node's counts with
|
||||
* it. Otherwise, create the corresponding node, add it to the parent's children
|
||||
* cache, and create the parent->child edge.
|
||||
*
|
||||
* @param {CensusTreeNodeCacheValue} parentCachevalue
|
||||
* @param {CensusTreeNode} node
|
||||
*
|
||||
* @returns {CensusTreeNodeCacheValue}
|
||||
* The new or extant child node's corresponding cache value.
|
||||
*/
|
||||
function insertOrMergeNode(parentCacheValue, node) {
|
||||
if (!parentCacheValue.children) {
|
||||
parentCacheValue.children = new CensusTreeNodeCache();
|
||||
}
|
||||
|
||||
let val = CensusTreeNodeCache.lookupNode(parentCacheValue.children, node);
|
||||
|
||||
if (val) {
|
||||
// When inverting, it is possible that multiple leaves in the census report
|
||||
// get merged into a single CensusTreeNode node. When this occurs, switch
|
||||
// from a single index to a set of indices.
|
||||
if (val.node.reportLeafIndex !== undefined &&
|
||||
val.node.reportLeafIndex !== node.reportLeafIndex) {
|
||||
if (typeof val.node.reportLeafIndex === "number") {
|
||||
const oldIndex = val.node.reportLeafIndex;
|
||||
val.node.reportLeafIndex = new Set();
|
||||
val.node.reportLeafIndex.add(oldIndex);
|
||||
val.node.reportLeafIndex.add(node.reportLeafIndex);
|
||||
} else {
|
||||
val.node.reportLeafIndex.add(node.reportLeafIndex);
|
||||
}
|
||||
}
|
||||
|
||||
val.node.count += node.count;
|
||||
val.node.bytes += node.bytes;
|
||||
} else {
|
||||
val = new CensusTreeNodeCacheValue();
|
||||
|
||||
val.node = new CensusTreeNode(node.name);
|
||||
val.node.reportLeafIndex = node.reportLeafIndex;
|
||||
val.node.count = node.count;
|
||||
val.node.totalCount = node.totalCount;
|
||||
val.node.bytes = node.bytes;
|
||||
val.node.totalBytes = node.totalBytes;
|
||||
|
||||
addChild(parentCacheValue.node, val.node);
|
||||
CensusTreeNodeCache.insertNode(parentCacheValue.children, val);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an un-inverted CensusTreeNode tree, return the corresponding inverted
|
||||
* CensusTreeNode tree. The input tree is not modified. The resulting inverted
|
||||
* tree is sorted by self bytes rather than by total bytes.
|
||||
*
|
||||
* @param {CensusTreeNode} tree
|
||||
* The un-inverted tree.
|
||||
*
|
||||
* @returns {CensusTreeNode}
|
||||
* The corresponding inverted tree.
|
||||
*/
|
||||
function invert(tree) {
|
||||
const inverted = new CensusTreeNodeCacheValue();
|
||||
inverted.node = new CensusTreeNode(null);
|
||||
|
||||
// Do a depth-first search of the un-inverted tree. As we reach each leaf,
|
||||
// take the path from the old root to the leaf, reverse that path, and add it
|
||||
// to the new, inverted tree's root.
|
||||
|
||||
const path = [];
|
||||
(function addInvertedPaths(node) {
|
||||
path.push(node);
|
||||
|
||||
if (node.children) {
|
||||
for (let i = 0, length = node.children.length; i < length; i++) {
|
||||
addInvertedPaths(node.children[i]);
|
||||
}
|
||||
} else {
|
||||
// We found a leaf node, add the reverse path to the inverted tree.
|
||||
let currentCacheValue = inverted;
|
||||
for (let i = path.length - 1; i >= 0; i--) {
|
||||
currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
|
||||
}
|
||||
}
|
||||
|
||||
path.pop();
|
||||
}(tree));
|
||||
|
||||
// Ensure that the root node always has the totals.
|
||||
inverted.node.totalBytes = tree.totalBytes;
|
||||
inverted.node.totalCount = tree.totalCount;
|
||||
|
||||
return inverted.node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a CensusTreeNode tree and predicate function, create the tree
|
||||
* containing only the nodes in any path `(node_0, node_1, ..., node_n-1)` in
|
||||
* the given tree where `predicate(node_j)` is true for `0 <= j < n`, `node_0`
|
||||
* is the given tree's root, and `node_n-1` is a leaf in the given tree. The
|
||||
* given tree is left unmodified.
|
||||
*
|
||||
* @param {CensusTreeNode} tree
|
||||
* @param {Function} predicate
|
||||
*
|
||||
* @returns {CensusTreeNode}
|
||||
*/
|
||||
function filter(tree, predicate) {
|
||||
const filtered = new CensusTreeNodeCacheValue();
|
||||
filtered.node = new CensusTreeNode(null);
|
||||
|
||||
// Do a DFS over the given tree. If the predicate returns true for any node,
|
||||
// add that node and its whole subtree to the filtered tree.
|
||||
|
||||
const path = [];
|
||||
let match = false;
|
||||
|
||||
function addMatchingNodes(node) {
|
||||
path.push(node);
|
||||
|
||||
let oldMatch = match;
|
||||
if (!match && predicate(node)) {
|
||||
match = true;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (let i = 0, length = node.children.length; i < length; i++) {
|
||||
addMatchingNodes(node.children[i]);
|
||||
}
|
||||
} else if (match) {
|
||||
// We found a matching leaf node, add it to the filtered tree.
|
||||
let currentCacheValue = filtered;
|
||||
for (let i = 0, length = path.length; i < length; i++) {
|
||||
currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
|
||||
}
|
||||
}
|
||||
|
||||
match = oldMatch;
|
||||
path.pop();
|
||||
}
|
||||
|
||||
if (tree.children) {
|
||||
for (let i = 0, length = tree.children.length; i < length; i++) {
|
||||
addMatchingNodes(tree.children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
filtered.node.count = tree.count;
|
||||
filtered.node.totalCount = tree.totalCount;
|
||||
filtered.node.bytes = tree.bytes;
|
||||
filtered.node.totalBytes = tree.totalBytes;
|
||||
|
||||
return filtered.node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a filter string, return a predicate function that takes a node and
|
||||
* returns true iff the node matches the filter.
|
||||
*
|
||||
* @param {String} filterString
|
||||
* @returns {Function}
|
||||
*/
|
||||
function makeFilterPredicate(filterString) {
|
||||
return function (node) {
|
||||
if (!node.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSavedFrame(node.name)) {
|
||||
return node.name.source.includes(filterString)
|
||||
|| (node.name.functionDisplayName || "").includes(filterString)
|
||||
|| (node.name.asyncCause || "").includes(filterString);
|
||||
}
|
||||
|
||||
return String(node.name).includes(filterString);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a report from a census (`dbg.memory.takeCensus()`) and the breakdown
|
||||
* used to generate the census and returns a structure used to render
|
||||
* a tree to display the data.
|
||||
*
|
||||
* Returns a recursive "CensusTreeNode" object, looking like:
|
||||
*
|
||||
* CensusTreeNode = {
|
||||
* // `children` if it exists, is sorted by `bytes`, if they are leaf nodes.
|
||||
* children: ?[<CensusTreeNode...>],
|
||||
* name: <?String>
|
||||
* count: <?Number>
|
||||
* bytes: <?Number>
|
||||
* id: <?Number>
|
||||
* parent: <?Number>
|
||||
* }
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The breakdown used to generate the census report.
|
||||
*
|
||||
* @param {Object} report
|
||||
* The census report generated with the specified breakdown.
|
||||
*
|
||||
* @param {Object} options
|
||||
* Configuration options.
|
||||
* - invert: Whether to invert the resulting tree or not. Defaults to
|
||||
* false, ie uninverted.
|
||||
*
|
||||
* @returns {CensusTreeNode}
|
||||
*/
|
||||
exports.censusReportToCensusTreeNode = function (breakdown, report,
|
||||
options = {
|
||||
invert: false,
|
||||
filter: null
|
||||
}) {
|
||||
// Reset the counter so that turning the same census report into a
|
||||
// CensusTreeNode tree repeatedly is idempotent.
|
||||
censusTreeNodeIdCounter = 0;
|
||||
|
||||
const visitor = new CensusTreeNodeVisitor();
|
||||
walk(breakdown, report, visitor);
|
||||
let result = visitor.root();
|
||||
|
||||
if (options.invert) {
|
||||
result = invert(result);
|
||||
}
|
||||
|
||||
if (typeof options.filter === "string") {
|
||||
result = filter(result, makeFilterPredicate(options.filter));
|
||||
}
|
||||
|
||||
// If the report is a delta report that was generated by diffing two other
|
||||
// reports, make sure to use the basis totals rather than the totals of the
|
||||
// difference.
|
||||
if (typeof report[basisTotalBytes] === "number") {
|
||||
result.totalBytes = report[basisTotalBytes];
|
||||
result.totalCount = report[basisTotalCount];
|
||||
}
|
||||
|
||||
// Inverting and filtering could have messed up the sort order, so do a
|
||||
// depth-first search of the tree and ensure that siblings are sorted.
|
||||
const comparator = options.invert ? compareBySelf : compareByTotal;
|
||||
(function ensureSorted(node) {
|
||||
if (node.children) {
|
||||
node.children.sort(comparator);
|
||||
for (let i = 0, length = node.children.length; i < length; i++) {
|
||||
ensureSorted(node.children[i]);
|
||||
}
|
||||
}
|
||||
}(result));
|
||||
|
||||
return result;
|
||||
};
|
||||
15
devtools/shared/heapsnapshot/moz.build
Normal file
15
devtools/shared/heapsnapshot/moz.build
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'census-tree-node.js',
|
||||
'CensusUtils.js',
|
||||
'DominatorTreeNode.js',
|
||||
'HeapAnalysesClient.js',
|
||||
'HeapAnalysesWorker.js',
|
||||
'HeapSnapshotFileUtils.js',
|
||||
'shortest-paths.js',
|
||||
)
|
||||
91
devtools/shared/heapsnapshot/shortest-paths.js
Normal file
91
devtools/shared/heapsnapshot/shortest-paths.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Compress a set of paths leading to `target` into a single graph, returned as
|
||||
* a set of nodes and a set of edges.
|
||||
*
|
||||
* @param {NodeId} target
|
||||
* The target node passed to `HeapSnapshot.computeShortestPaths`.
|
||||
*
|
||||
* @param {Array<Path>} paths
|
||||
* An array of paths to `target`, as returned by
|
||||
* `HeapSnapshot.computeShortestPaths`.
|
||||
*
|
||||
* @returns {Object}
|
||||
* An object with two properties:
|
||||
* - edges: An array of unique objects of the form:
|
||||
* {
|
||||
* from: <node ID>,
|
||||
* to: <node ID>,
|
||||
* name: <string or null>
|
||||
* }
|
||||
* - nodes: An array of unique node IDs. Every `from` and `to` id is
|
||||
* guaranteed to be in this array exactly once.
|
||||
*/
|
||||
exports.deduplicatePaths = function (target, paths) {
|
||||
// Use this structure to de-duplicate edges among many retaining paths from
|
||||
// start to target.
|
||||
//
|
||||
// Map<FromNodeId, Map<ToNodeId, Set<EdgeName>>>
|
||||
const deduped = new Map();
|
||||
|
||||
function insert(from, to, name) {
|
||||
let toMap = deduped.get(from);
|
||||
if (!toMap) {
|
||||
toMap = new Map();
|
||||
deduped.set(from, toMap);
|
||||
}
|
||||
|
||||
let nameSet = toMap.get(to);
|
||||
if (!nameSet) {
|
||||
nameSet = new Set();
|
||||
toMap.set(to, nameSet);
|
||||
}
|
||||
|
||||
nameSet.add(name);
|
||||
}
|
||||
|
||||
outer: for (let path of paths) {
|
||||
const pathLength = path.length;
|
||||
|
||||
// Check for duplicate predecessors in the path, and skip paths that contain
|
||||
// them.
|
||||
const predecessorsSeen = new Set();
|
||||
predecessorsSeen.add(target);
|
||||
for (let i = 0; i < pathLength; i++) {
|
||||
if (predecessorsSeen.has(path[i].predecessor)) {
|
||||
continue outer;
|
||||
}
|
||||
predecessorsSeen.add(path[i].predecessor);
|
||||
}
|
||||
|
||||
for (let i = 0; i < pathLength - 1; i++) {
|
||||
insert(path[i].predecessor, path[i + 1].predecessor, path[i].edge);
|
||||
}
|
||||
|
||||
insert(path[pathLength - 1].predecessor, target, path[pathLength - 1].edge);
|
||||
}
|
||||
|
||||
const nodes = [target];
|
||||
const edges = [];
|
||||
|
||||
for (let [from, toMap] of deduped) {
|
||||
// If the second/third/etc shortest path contains the `target` anywhere
|
||||
// other than the very last node, we could accidentally put the `target` in
|
||||
// `nodes` more than once.
|
||||
if (from !== target) {
|
||||
nodes.push(from);
|
||||
}
|
||||
|
||||
for (let [to, edgeNameSet] of toMap) {
|
||||
for (let name of edgeNameSet) {
|
||||
edges.push({ from, to, name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ DIRS += [
|
|||
'discovery',
|
||||
'fronts',
|
||||
'gcli',
|
||||
'heapsnapshot',
|
||||
'inspector',
|
||||
'jsbeautify',
|
||||
'layout',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue