Bug 1248085 - Compute shortest paths in the HeapAnalysesWorker; r=jimb

This commit is contained in:
Nick Fitzgerald 2016-02-12 15:23:00 +01:00
Родитель e23e94e333
Коммит 2d9d0a66d0
12 изменённых файлов: 491 добавлений и 8 удалений

Просмотреть файл

@ -3,11 +3,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Visitor, walk } = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
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.
@ -34,6 +36,10 @@ function DominatorTreeNode(nodeId, label, shallowSize, retainedSize) {
// 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.
//
@ -289,3 +295,42 @@ DominatorTreeNode.getNodeByIdAlongPath = function (id, tree, path) {
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;
}
};

Просмотреть файл

@ -226,6 +226,8 @@ HeapAnalysesClient.prototype.getDominatorTree = function (opts) {
* 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:

Просмотреть файл

@ -157,7 +157,8 @@ workerHelper.createTask(self, "getDominatorTree", request => {
dominatorTreeId,
breakdown,
maxDepth,
maxSiblings
maxSiblings,
maxRetainingPaths,
} = request;
if (!(0 <= dominatorTreeId && dominatorTreeId < dominatorTrees.length)) {
@ -168,11 +169,29 @@ workerHelper.createTask(self, "getDominatorTree", request => {
const dominatorTree = dominatorTrees[dominatorTreeId];
const snapshot = dominatorTreeSnapshots[dominatorTreeId];
return DominatorTreeNode.partialTraversal(dominatorTree,
snapshot,
breakdown,
maxDepth,
maxSiblings);
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;
});
/**
@ -184,7 +203,8 @@ workerHelper.createTask(self, "getImmediatelyDominated", request => {
nodeId,
breakdown,
startIndex,
maxCount
maxCount,
maxRetainingPaths,
} = request;
if (!(0 <= dominatorTreeId && dominatorTreeId < dominatorTrees.length)) {
@ -228,5 +248,11 @@ workerHelper.createTask(self, "getImmediatelyDominated", request => {
const moreChildrenAvailable = childIds.length > end;
DominatorTreeNode.attachShortestPaths(snapshot,
breakdown,
dominatorTree.root,
nodes,
maxRetainingPaths);
return { nodes, moreChildrenAvailable, path };
});

Просмотреть файл

@ -55,4 +55,5 @@ DevToolsModules(
'HeapAnalysesClient.js',
'HeapAnalysesWorker.js',
'HeapSnapshotFileUtils.js',
'shortest-paths.js',
)

Просмотреть файл

@ -0,0 +1,79 @@
/* 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);
}
for (let path of paths) {
const pathLength = path.length;
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 };
};

Просмотреть файл

@ -23,6 +23,7 @@ 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;
@ -375,3 +376,51 @@ function assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenA
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));
}
}
/**
* Create a mock path entry for the given predecessor and edge.
*/
function pathEntry(predecessor, edge) {
return { predecessor, edge };
}

Просмотреть файл

@ -0,0 +1,132 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the DominatorTreeNode.attachShortestPaths function can correctly
// attach the deduplicated shortest retaining paths for each node it is given.
const startNodeId = 9999;
const maxNumPaths = 2;
// Mock data mapping node id to shortest paths to that node id.
const shortestPaths = new Map([
[1000, [
[pathEntry(1100, "a"), pathEntry(1200, "b")],
[pathEntry(1100, "c"), pathEntry(1300, "d")],
]],
[2000, [
[pathEntry(2100, "e"), pathEntry(2200, "f"), pathEntry(2300, "g")]
]],
[3000, [
[pathEntry(3100, "h")],
[pathEntry(3100, "i")],
[pathEntry(3100, "j")],
[pathEntry(3200, "k")],
[pathEntry(3300, "l")],
[pathEntry(3400, "m")],
]],
]);
const actual = [
makeTestDominatorTreeNode({ nodeId: 1000 }),
makeTestDominatorTreeNode({ nodeId: 2000 }),
makeTestDominatorTreeNode({ nodeId: 3000 }),
];
const expected = [
makeTestDominatorTreeNode({
nodeId: 1000,
shortestPaths: {
nodes: [
{ id: 1000, label: ["SomeType-1000"] },
{ id: 1100, label: ["SomeType-1100"] },
{ id: 1200, label: ["SomeType-1200"] },
{ id: 1300, label: ["SomeType-1300"] },
],
edges: [
{ from: 1100, to: 1200, name: "a" },
{ from: 1100, to: 1300, name: "c" },
{ from: 1200, to: 1000, name: "b" },
{ from: 1300, to: 1000, name: "d" },
]
}
}),
makeTestDominatorTreeNode({
nodeId: 2000,
shortestPaths: {
nodes: [
{ id: 2000, label: ["SomeType-2000"] },
{ id: 2100, label: ["SomeType-2100"] },
{ id: 2200, label: ["SomeType-2200"] },
{ id: 2300, label: ["SomeType-2300"] },
],
edges: [
{ from: 2100, to: 2200, name: "e" },
{ from: 2200, to: 2300, name: "f" },
{ from: 2300, to: 2000, name: "g" },
]
}
}),
makeTestDominatorTreeNode({ nodeId: 3000,
shortestPaths: {
nodes: [
{ id: 3000, label: ["SomeType-3000"] },
{ id: 3100, label: ["SomeType-3100"] },
{ id: 3200, label: ["SomeType-3200"] },
{ id: 3300, label: ["SomeType-3300"] },
{ id: 3400, label: ["SomeType-3400"] },
],
edges: [
{ from: 3100, to: 3000, name: "h" },
{ from: 3100, to: 3000, name: "i" },
{ from: 3100, to: 3000, name: "j" },
{ from: 3200, to: 3000, name: "k" },
{ from: 3300, to: 3000, name: "l" },
{ from: 3400, to: 3000, name: "m" },
]
}
}),
];
const breakdown = {
by: "internalType",
then: { by: "count", count: true, bytes: true }
};
const mockSnapshot = {
computeShortestPaths: (start, nodeIds, max) => {
equal(start, startNodeId);
equal(max, maxNumPaths);
return new Map(nodeIds.map(nodeId => {
const paths = shortestPaths.get(nodeId);
ok(paths, "Expected computeShortestPaths call for node id = " + nodeId);
return [nodeId, paths];
}));
},
describeNode: (bd, nodeId) => {
equal(bd, breakdown);
return {
["SomeType-" + nodeId]: {
count: 1,
bytes: 10,
}
};
},
};
function run_test() {
DominatorTreeNode.attachShortestPaths(mockSnapshot,
breakdown,
startNodeId,
actual,
maxNumPaths);
dumpn("Expected = " + JSON.stringify(expected, null, 2));
dumpn("Actual = " + JSON.stringify(actual, null, 2));
assertStructurallyEquivalent(expected, actual);
}

Просмотреть файл

@ -60,6 +60,7 @@ const expected = {
],
shallowSize: 10,
retainedSize: 10,
shortestPaths: undefined,
children: [
{
nodeId: 200,
@ -70,6 +71,7 @@ const expected = {
shallowSize: 10,
retainedSize: 10,
parentId: 100,
shortestPaths: undefined,
children: [
{
nodeId: 500,
@ -81,6 +83,7 @@ const expected = {
retainedSize: 10,
parentId: 200,
moreChildrenAvailable: false,
shortestPaths: undefined,
children: undefined
},
{
@ -93,6 +96,7 @@ const expected = {
retainedSize: 10,
parentId: 200,
moreChildrenAvailable: false,
shortestPaths: undefined,
children: undefined
}
],
@ -107,6 +111,7 @@ const expected = {
shallowSize: 10,
retainedSize: 10,
parentId: 100,
shortestPaths: undefined,
children: [
{
nodeId: 800,
@ -118,6 +123,7 @@ const expected = {
retainedSize: 10,
parentId: 300,
moreChildrenAvailable: false,
shortestPaths: undefined,
children: undefined
},
{
@ -130,6 +136,7 @@ const expected = {
retainedSize: 10,
parentId: 300,
moreChildrenAvailable: false,
shortestPaths: undefined,
children: undefined
}
],

Просмотреть файл

@ -51,6 +51,13 @@ add_task(function* () {
equal(typeof node.moreChildrenAvailable, "boolean",
"each node should indicate if there are more children available or not");
equal(typeof node.shortestPaths, "object",
"Should have shortest paths");
equal(typeof node.shortestPaths.nodes, "object",
"Should have shortest paths' nodes");
equal(typeof node.shortestPaths.edges, "object",
"Should have shortest paths' edges");
if (node.children) {
node.children.forEach(checkTree);
}

Просмотреть файл

@ -44,6 +44,15 @@ add_task(function* () {
equal(response.path.length, 1);
equal(response.path[0], partialTree.nodeId);
for (let node of response.nodes) {
equal(typeof node.shortestPaths, "object",
"Should have shortest paths");
equal(typeof node.shortestPaths.nodes, "object",
"Should have shortest paths' nodes");
equal(typeof node.shortestPaths.edges, "object",
"Should have shortest paths' edges");
}
// Next, test getting a subset of children available.
const secondResponse = yield client.getImmediatelyDominated({
dominatorTreeId,
@ -59,5 +68,14 @@ add_task(function* () {
equal(secondResponse.path.length, 1);
equal(secondResponse.path[0], partialTree.nodeId);
for (let node of secondResponse.nodes) {
equal(typeof node.shortestPaths, "object",
"Should have shortest paths");
equal(typeof node.shortestPaths.nodes, "object",
"Should have shortest paths' nodes");
equal(typeof node.shortestPaths.edges, "object",
"Should have shortest paths' edges");
}
client.destroy();
});

Просмотреть файл

@ -0,0 +1,115 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the behavior of the deduplicatePaths utility function.
function edge(from, to, name) {
return { from, to, name };
}
function run_test() {
const a = 1;
const b = 2;
const c = 3;
const d = 4;
const e = 5;
const f = 6;
const g = 7;
dumpn("Single long path");
assertDeduplicatedPaths({
target: g,
paths: [
[
pathEntry(a, "e1"),
pathEntry(b, "e2"),
pathEntry(c, "e3"),
pathEntry(d, "e4"),
pathEntry(e, "e5"),
pathEntry(f, "e6"),
]
],
expectedNodes: [a, b, c, d, e, f, g],
expectedEdges: [
edge(a, b, "e1"),
edge(b, c, "e2"),
edge(c, d, "e3"),
edge(d, e, "e4"),
edge(e, f, "e5"),
edge(f, g, "e6"),
]
});
dumpn("Multiple edges from and to the same nodes");
assertDeduplicatedPaths({
target: a,
paths: [
[pathEntry(b, "x")],
[pathEntry(b, "y")],
[pathEntry(b, "z")],
],
expectedNodes: [a, b],
expectedEdges: [
edge(b, a, "x"),
edge(b, a, "y"),
edge(b, a, "z"),
]
});
dumpn("Multiple paths sharing some nodes and edges");
assertDeduplicatedPaths({
target: g,
paths: [
[
pathEntry(a, "a->b"),
pathEntry(b, "b->c"),
pathEntry(c, "foo"),
],
[
pathEntry(a, "a->b"),
pathEntry(b, "b->d"),
pathEntry(d, "bar"),
],
[
pathEntry(a, "a->b"),
pathEntry(b, "b->e"),
pathEntry(e, "baz"),
],
],
expectedNodes: [a, b, c, d, e, g],
expectedEdges: [
edge(a, b, "a->b"),
edge(b, c, "b->c"),
edge(b, d, "b->d"),
edge(b, e, "b->e"),
edge(c, g, "foo"),
edge(d, g, "bar"),
edge(e, g, "baz"),
]
});
dumpn("Second shortest path contains target itself");
assertDeduplicatedPaths({
target: g,
paths: [
[
pathEntry(a, "a->b"),
pathEntry(b, "b->g"),
],
[
pathEntry(a, "a->b"),
pathEntry(b, "b->g"),
pathEntry(g, "g->f"),
pathEntry(f, "f->g"),
],
],
expectedNodes: [a, b, f, g],
expectedEdges: [
edge(a, b, "a->b"),
edge(b, g, "b->g"),
edge(g, f, "g->f"),
edge(f, g, "f->g"),
]
});
}

Просмотреть файл

@ -29,12 +29,14 @@ support-files =
[test_census-tree-node-06.js]
[test_census-tree-node-07.js]
[test_census-tree-node-08.js]
[test_deduplicatePaths_01.js]
[test_DominatorTree_01.js]
[test_DominatorTree_02.js]
[test_DominatorTree_03.js]
[test_DominatorTree_04.js]
[test_DominatorTree_05.js]
[test_DominatorTree_06.js]
[test_DominatorTreeNode_attachShortestPaths_01.js]
[test_DominatorTreeNode_getNodeByIdAlongPath_01.js]
[test_DominatorTreeNode_insert_01.js]
[test_DominatorTreeNode_insert_02.js]