blob: 49353ce1d47632d10e364f70ed967e2ec5452a5d [file] [log] [blame] [edit]
// DOM Helpers
class DOMUtils
{
static removeAllChildren(node)
{
while (node.lastChild)
node.removeChild(node.lastChild);
}
static createDetails(summaryText)
{
let summary = document.createElement('summary');
summary.textContent = summaryText;
let details = document.createElement('details');
details.appendChild(summary);
details.addEventListener('keydown', function(event) {
const rightArrowKeyCode = 39;
const leftArrowKeyCode = 37;
if (event.keyCode == rightArrowKeyCode)
this.open = true;
else if (event.keyCode == leftArrowKeyCode)
this.open = false;
}, false);
return details;
}
// Create a details widget with the specified `summaryText` as the collapsed content.
// The `contentBuilder` function is called (with `this` unbound) when the widget is
// expanded. The DOM node returned by the builder is installed as the expanded
// content. The content is built only once when the widget is first expanded.
static createLazyDetails(summaryText, contentBuilder)
{
let details = document.createElement('details');
let summary = document.createElement('summary');
summary.textContent = summaryText;
details.appendChild(summary);
let content = document.createElement('div');
let loading = document.createElement('p');
loading.textContent = 'Loading...';
content.appendChild(loading);
details.appendChild(content);
let contentLoaded = false;
details.addEventListener('toggle', () => {
if (details.open && !contentLoaded) {
let loadedContent;
try {
loadedContent = contentBuilder();
} catch (e) {
loadedContent = document.createElement('p');
loadedContent.textContent = e;
};
content.firstChild.replaceWith(loadedContent);
contentLoaded = true;
}
});
return details;
}
// Place the domNode into a container div with a 'close' button.
// The result can be added as a child to the Details section.
static detailsPanelWith(domNode)
{
let panel = document.createElement('div');
panel.className = 'panel';
const closeButton = document.createElement('button');
closeButton.textContent = 'x';
closeButton.className = 'close-button';
closeButton.addEventListener('click', function() {
panel.parentElement.removeChild(panel);
});
panel.appendChild(closeButton)
panel.appendChild(domNode);
return panel;
}
static scrollIntoViewIfNeeded(element)
{
const rect = element.getBoundingClientRect();
const isVisible = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
if (!isVisible) {
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
}
};
// Heap Inspector Helpers
class HeapInspectorUtils
{
static humanReadableSize(sizeInBytes)
{
var i = -1;
if (sizeInBytes < 512)
return sizeInBytes + 'B';
var byteUnits = ['KB', 'MB', 'GB'];
do {
sizeInBytes = sizeInBytes / 1024;
i++;
} while (sizeInBytes > 1024);
return Math.max(sizeInBytes, 0.1).toFixed(1) + byteUnits[i];
}
static addressForNode(node)
{
return node.wrappedAddress ? node.wrappedAddress : node.address;
}
static nodeName(snapshot, node)
{
if (node.type == "Internal")
return 'Internal node';
let result = node.className + ' @' + node.id + ' (' + HeapInspectorUtils.addressForNode(node) + ' ' + node.label + ')';
if (node.gcRoot || node.markedRoot)
result += ' (GC root—' + snapshot.reasonNamesForRoot(node.id).join(', ') + ')';
return result;
}
static nodeNameAndIdOnly(node)
{
let nodeSpan = document.createElement('span');
nodeSpan.innerHTML = node.className + `<a class="node-id" href="#">${node.id}</a>`
let anchor = nodeSpan.querySelector('a');
anchor.onclick = function(event) {
event.preventDefault();
inspector.exploreNodeId(node.id);
};
return nodeSpan;
}
static spanForNode(inspector, node, showPathButton)
{
let nodeSpan = document.createElement('span');
if (node.type == "Internal") {
nodeSpan.textContent = 'Internal node';
return nodeSpan;
}
let wrappedAddressString = node.wrappedAddress ? `wrapped ${node.wrappedAddress}` : '';
let nodeHTML = node.className + `<a class="node-id" href="#">${node.id}</a> <span class="node-address">cell ${node.address} ${wrappedAddressString}</span> <span class="node-size retained-size">(retains ${HeapInspectorUtils.humanReadableSize(node.retainedSize)})</span>`;
if (node.label.length)
nodeHTML += ` <span class="node-label">“${node.label}”</span>`;
nodeSpan.innerHTML = nodeHTML;
let anchor = nodeSpan.querySelector('a');
anchor.onclick = function(event) {
event.preventDefault();
inspector.exploreNodeId(node.id);
};
if (node.gcRoot || node.markedRoot) {
let gcRootSpan = document.createElement('span');
gcRootSpan.className = 'node-gc-root';
gcRootSpan.textContent = ' (GC root—' + inspector.snapshot.reasonNamesForRoot(node.id).join(', ') + ')';
nodeSpan.appendChild(gcRootSpan);
} else if (showPathButton) {
let showAllPathsAnchor = document.createElement('button');
showAllPathsAnchor.className = 'node-show-all-paths';
showAllPathsAnchor.textContent = 'Show all paths';
showAllPathsAnchor.addEventListener('click', (e) => {
inspector.showAllPathsToNode(node);
}, false);
nodeSpan.appendChild(showAllPathsAnchor);
}
let dominators = this.dominatorIdsOfNode(node, inspector.snapshot);
if (dominators.length > 0) {
let mySpan = document.createElement('span');
mySpan.textContent = ' dominators: [' + dominators.join(', ') + ']';
nodeSpan.appendChild(mySpan);
}
return nodeSpan;
}
static dominatorIdsOfNode(node, snapshot)
{
let result = [];
let here = node;
let isFirst = true;
while (here.id != 0) {
if (!isFirst) result.push(here.id);
isFirst = false;
let dominatorId = here.dominatorNodeIdentifier;
here = snapshot.nodeWithIdentifier(Number(dominatorId));
}
return result;
}
static spanForEdge(snapshot, edge)
{
let edgeSpan = document.createElement('span');
edgeSpan.className = 'edge';
edgeSpan.innerHTML = '<span class="edge-type">' + edge.type + '</span> <span class="edge-data">' + edge.data + '</span>';
return edgeSpan;
}
static summarySpanForPath(inspector, path)
{
let pathSpan = document.createElement('span');
pathSpan.className = 'path-summary';
if (path.length > 0) {
let pathLength = (path.length - 1) / 2;
pathSpan.textContent = pathLength + ' step' + (pathLength > 1 ? 's' : '') + ' from ';
pathSpan.appendChild(HeapInspectorUtils.spanForNode(inspector, path[0]), true);
}
return pathSpan;
}
static edgeName(edge)
{
return '⇒ ' + edge.type + ' ' + edge.data + ' ⇒';
}
};
// Manages a list of heap snapshot nodes that can dynamically build the contents of an HTMLListElement.
class InstanceList
{
constructor(listElement, snapshot, listGeneratorFunc)
{
this.listElement = listElement;
this.snapshot = snapshot;
this.nodeList = listGeneratorFunc(this.snapshot);
this.entriesAdded = 0;
this.initialEntries = 100;
this.entriesQuantum = 50;
this.showMoreItem = undefined;
}
buildList(inspector)
{
DOMUtils.removeAllChildren(this.listElement);
if (this.nodeList.length == 0)
return;
let maxIndex = Math.min(this.nodeList.length, this.initialEntries);
this.appendItemsForRange(inspector, 0, maxIndex);
if (maxIndex < this.nodeList.length) {
this.showMoreItem = this.makeShowMoreItem(inspector);
this.listElement.appendChild(this.showMoreItem);
}
this.entriesAdded = maxIndex;
}
appendItemsForRange(inspector, startIndex, endIndex)
{
for (let index = startIndex; index < endIndex; ++index) {
let instance = this.nodeList[index];
let listItem = document.createElement('li');
listItem.appendChild(HeapInspectorUtils.spanForNode(inspector, instance, true));
this.listElement.appendChild(listItem);
}
this.entriesAdded = endIndex;
}
appendMoreEntries(inspector)
{
let numRemaining = this.nodeList.length - this.entriesAdded;
if (numRemaining == 0)
return;
this.showMoreItem.remove();
let fromIndex = this.entriesAdded;
let toIndex = Math.min(this.nodeList.length, fromIndex + this.entriesQuantum);
this.appendItemsForRange(inspector, fromIndex, toIndex);
if (toIndex < this.nodeList.length) {
this.showMoreItem = this.makeShowMoreItem(inspector);
this.listElement.appendChild(this.showMoreItem);
}
}
makeShowMoreItem(inspector)
{
let numberRemaining = this.nodeList.length - this.entriesAdded;
let listItem = document.createElement('li');
listItem.className = 'show-more';
listItem.appendChild(document.createTextNode(`${numberRemaining} more `));
let moreButton = document.createElement('button');
moreButton.textContent = `Show ${Math.min(this.entriesQuantum, numberRemaining)} more`;
let thisList = this;
moreButton.addEventListener('click', function(e) {
thisList.appendMoreEntries(inspector, 10);
}, false);
listItem.appendChild(moreButton);
return listItem;
}
};
class HeapSnapshotInspector
{
constructor(containerElement, heapJSONData, filename)
{
this.containerElement = containerElement;
this.resetUI();
this.snapshot = new HeapSnapshot(1, heapJSONData, filename);
this.buildRoots();
this.buildAllObjectsByType();
this.buildPathsToRootsOfType('Window');
this.buildPathsToRootsOfType('HTMLDocument');
}
resetUI()
{
DOMUtils.removeAllChildren(this.containerElement);
// All objects by type
this.objectByTypeContainer = document.createElement('section');
this.objectByTypeContainer.id = 'all-objects-by-type';
let header = document.createElement('h1');
header.textContent = 'All Objects by Type'
this.objectByTypeContainer.appendChild(header);
// Roots
this.rootsContainer = document.createElement('section');
this.rootsContainer.id = 'roots';
header = document.createElement('h1');
header.textContent = 'Roots'
this.rootsContainer.appendChild(header);
// Important objects
this.pathsToRootsContainer = document.createElement('section');
this.pathsToRootsContainer.id = 'paths-to-roots';
header = document.createElement('h1');
header.textContent = 'Important objects'
this.pathsToRootsContainer.appendChild(header);
// Details
this.details = document.createElement('section');
this.details.id = 'details';
header = document.createElement('h1');
header.textContent = 'Details'
this.details.appendChild(header);
let clearAllButton = document.createElement('button');
clearAllButton.id = 'details-clear-all';
clearAllButton.innerText = 'Clear All';
clearAllButton.addEventListener("click", () => {
let header = this.details.childNodes[0];
DOMUtils.removeAllChildren(this.details);
this.details.appendChild(header);
});
header.appendChild(clearAllButton);
this.containerElement.appendChild(this.details);
this.containerElement.appendChild(this.pathsToRootsContainer);
this.containerElement.appendChild(this.rootsContainer);
this.containerElement.appendChild(this.objectByTypeContainer);
// Cache for precomputed paths in the All Paths To... section.
// Enables on-demand DOM node creation to cut down on memory use for
// objects with a huge number of paths.
this.nodePathDetailsWeakMap = new WeakMap()
}
dominatorInfo(className)
{
let dominators = {};
let functions = HeapSnapshot.instancesWithClassName(this.snapshot, className);
for (var node of functions) {
let dominatorId = node.dominatorNodeIdentifier;
let count = dominators[dominatorId];
count = count === undefined ? 1 : count + 1;
dominators[dominatorId] = count;
}
return dominators;
}
dominatorSummary(className)
{
let summary = document.createElement('section');
summary.className = 'dominator-summary';
let dominators = this.dominatorInfo(className);
let entries = Object.entries(dominators).sort(([,a], [,b]) => b - a); // Sort by value (descending)
let i = 0;
for (const [id, count] of entries) {
let node = this.snapshot.nodeWithIdentifier(Number(id));
let entry = document.createElement('p');
entry.textContent = `${count} dominated by `;
entry.appendChild(HeapInspectorUtils.nodeNameAndIdOnly(node));
summary.appendChild(entry);
i++;
if (i > 50) {
let ellipsis = document.createElement('p');
ellipsis.textContent = '...';
summary.appendChild(ellipsis);
break;
}
}
return summary;
}
widgetWithDominatorSummary(className)
{
let self = this;
let count = this.snapshot._categories[className]?.count ?? 0;
return DOMUtils.createLazyDetails(`Dominator summary of ${className} (${count} instances)`, function () {
return self.dominatorSummary(className);
});
}
// Produce a DOM node expandable into the shortest path from GC roots to the node with the given ID.
widgetWithPathToNodeWithId(id)
{
let node = this.snapshot.nodeWithIdentifier(id);
let shortestPath = this.snapshot.shortestGCRootPath(id).reverse();
let details = DOMUtils.createDetails('Shortest path to ');
let summary = details.firstChild;
summary.appendChild(HeapInspectorUtils.spanForNode(this, node, true));
summary.appendChild(document.createTextNode('—'));
summary.appendChild(HeapInspectorUtils.summarySpanForPath(this, shortestPath));
let pathList = document.createElement('ul');
pathList.className = 'path';
let isNode = true;
let currItem = undefined;
for (let item of shortestPath) {
if (isNode) {
currItem = document.createElement('li');
currItem.appendChild(HeapInspectorUtils.spanForNode(this, item));
pathList.appendChild(currItem);
} else {
currItem.appendChild(HeapInspectorUtils.spanForEdge(this.snapshot, item));
currItem = undefined;
}
isNode = !isNode;
}
details.appendChild(pathList);
return details;
}
widgetExpandableToImmedidatelyRetained(id, prefixText='')
{
let node = this.snapshot.nodeWithIdentifier(id);
let self = this;
let details = DOMUtils.createLazyDetails(prefixText, function() {
let retained = self.snapshot.retainedNodes(id);
let list = document.createElement('div');
for (let each of retained.retainedNodes) {
list.appendChild(self.widgetExpandableToImmedidatelyRetained(each.id));
}
return list;
});
let summary = details.firstChild;
summary.appendChild(HeapInspectorUtils.spanForNode(this, node));
return details;
}
widgetExpandableToImmedidateOwners(id, prefixText='')
{
let node = this.snapshot.nodeWithIdentifier(id);
let self = this;
let details = DOMUtils.createLazyDetails(prefixText, function() {
let owners = self.snapshot.retainers(id);
let list = document.createElement('div');
for (let each of owners.retainers) {
list.appendChild(self.widgetExpandableToImmedidateOwners(each.id));
}
return list;
});
let summary = details.firstChild;
summary.appendChild(HeapInspectorUtils.spanForNode(this, node));
return details;
}
// Add to the details area an explorer panel for a node with the given ID.
exploreNodeId(id)
{
let retained = this.widgetWithPathToNodeWithId(id);
let explorer = DOMUtils.detailsPanelWith(retained);
explorer.appendChild(this.widgetExpandableToImmedidateOwners(id, 'Owners of '));
explorer.appendChild(this.widgetExpandableToImmedidatelyRetained(id, 'Owned by '));
this.details.appendChild(explorer);
DOMUtils.scrollIntoViewIfNeeded(explorer);
}
// Add to the details area a widget with a summary of immediate dominators of instances of the given type.
showDominatorsOfType(typeName)
{
let widget = this.widgetWithDominatorSummary(typeName);
let panel = DOMUtils.detailsPanelWith(widget);
this.details.appendChild(panel);
DOMUtils.scrollIntoViewIfNeeded(panel);
}
buildAllObjectsByType()
{
let categories = this.snapshot._categories;
let categoryNames = Object.keys(categories).sort();
for (let categoryName of categoryNames) {
let category = categories[categoryName];
let details = DOMUtils.createDetails(`${category.className} (${category.count})`);
let summaryElement = details.firstChild;
let sizeElement = summaryElement.appendChild(document.createElement('span'));
sizeElement.className = 'retained-size';
sizeElement.textContent = ' ' + HeapInspectorUtils.humanReadableSize(category.retainedSize);
let dominatorsButton = document.createElement('button');
dominatorsButton.className = 'type-show-dominators';
dominatorsButton.textContent = 'Dominators';
dominatorsButton.addEventListener('click', (e) => {
e.preventDefault();
inspector.showDominatorsOfType(categoryName);
}, false);
summaryElement.appendChild(dominatorsButton);
let instanceListElement = document.createElement('ul');
instanceListElement.className = 'instance-list';
let instanceList = new InstanceList(instanceListElement, this.snapshot, function(snapshot) {
return HeapSnapshot.instancesWithClassName(snapshot, categoryName);
});
instanceList.buildList(this);
details.appendChild(instanceListElement);
this.objectByTypeContainer.appendChild(details);
}
}
buildRoots()
{
let roots = this.snapshot.rootNodes();
if (roots.length == 0)
return;
let groupings = roots.reduce(function(accumulator, node) {
var key = node.className;
if (!accumulator[key]) {
accumulator[key] = [];
}
accumulator[key].push(node);
return accumulator;
}, {});
let rootNames = Object.keys(groupings).sort();
for (var rootClassName of rootNames) {
let rootsOfType = groupings[rootClassName];
rootsOfType.sort(function(a, b) {
let addressA = HeapInspectorUtils.addressForNode(a);
let addressB = HeapInspectorUtils.addressForNode(b);
return (addressA < addressB) ? -1 : (addressA > addressB) ? 1 : 0;
})
let details = DOMUtils.createDetails(`${rootClassName} (${rootsOfType.length})`);
let summaryElement = details.firstChild;
let retainedSize = rootsOfType.reduce((accumulator, node) => accumulator + node.retainedSize, 0);
let sizeElement = summaryElement.appendChild(document.createElement('span'));
sizeElement.className = 'retained-size';
sizeElement.textContent = ' ' + HeapInspectorUtils.humanReadableSize(retainedSize);
let rootsTypeList = document.createElement('ul')
rootsTypeList.className = 'instance-list';
for (let root of rootsOfType) {
let rootListItem = document.createElement('li');
rootListItem.appendChild(HeapInspectorUtils.spanForNode(this, root, true));
rootsTypeList.appendChild(rootListItem);
}
details.appendChild(rootsTypeList);
this.rootsContainer.appendChild(details);
}
}
buildPathsToRootsOfType(type)
{
let instances = HeapSnapshot.instancesWithClassName(this.snapshot, type);
if (instances.length == 0)
return;
let detailsContainer = document.createElement('section')
detailsContainer.className = 'path';
for (var instance of instances) {
let shortestPath = this.snapshot.shortestGCRootPath(instance.id).reverse();
let details = DOMUtils.createDetails('');
let summary = details.firstChild;
summary.appendChild(HeapInspectorUtils.spanForNode(this, instance, true));
summary.appendChild(document.createTextNode('—'));
summary.appendChild(HeapInspectorUtils.summarySpanForPath(this, shortestPath));
let pathList = document.createElement('ul');
pathList.className = 'path';
let isNode = true;
let currItem = undefined;
for (let item of shortestPath) {
if (isNode) {
currItem = document.createElement('li');
currItem.appendChild(HeapInspectorUtils.spanForNode(this, item));
pathList.appendChild(currItem);
} else {
currItem.appendChild(HeapInspectorUtils.spanForEdge(this.snapshot, item));
currItem = undefined;
}
isNode = !isNode;
}
details.appendChild(pathList);
detailsContainer.appendChild(details);
}
this.pathsToRootsContainer.appendChild(detailsContainer);
}
populatePathDetailsOnDemand(pathDetails)
{
let pathNodes = this.nodePathDetailsWeakMap.get(pathDetails);
if (!pathNodes) {
pathDetails.appendChild(document.createTextNode('Error loading path: Path not cached in HeapSnapshotInspector.nodePathDetailsWeakMap'));
return;
}
let pathList = document.createElement('ul');
pathList.className = 'path';
let isNode = true;
let currItem = undefined;
for (let item of pathNodes) {
if (isNode) {
currItem = document.createElement('li');
currItem.appendChild(HeapInspectorUtils.spanForNode(this, item));
pathList.appendChild(currItem);
} else {
currItem.appendChild(HeapInspectorUtils.spanForEdge(this.snapshot, item));
currItem = undefined;
}
isNode = !isNode;
}
pathDetails.appendChild(pathList);
}
showAllPathsToNode(node)
{
let paths = this.snapshot._gcRootPaths(node.id);
let details = DOMUtils.createDetails('');
let summary = details.firstChild;
summary.appendChild(document.createTextNode(`${paths.length} path${paths.length > 1 ? 's' : ''} to `));
summary.appendChild(HeapInspectorUtils.spanForNode(this, node, false));
let detailsContainer = document.createElement('section')
detailsContainer.className = 'path';
for (let path of paths) {
let pathNodes = path.map((component) => {
if (component.node)
return this.snapshot.serializeNode(component.node);
return this.snapshot.serializeEdge(component.edge);
}).reverse();
let pathDetails = DOMUtils.createDetails('');
let pathSummary = pathDetails.firstChild;
pathSummary.appendChild(HeapInspectorUtils.summarySpanForPath(this, pathNodes));
if (!this.nodePathDetailsWeakMap.get(pathDetails))
this.nodePathDetailsWeakMap.set(pathDetails, pathNodes);
pathDetails.addEventListener('toggle', (event) => {
if (event.target.open)
return this.populatePathDetailsOnDemand(event.target);
let pathSummary = pathDetails.firstChild;
DOMUtils.removeAllChildren(event.target);
pathDetails.appendChild(pathSummary);
});
detailsContainer.appendChild(pathDetails);
}
details.appendChild(detailsContainer);
let panel = DOMUtils.detailsPanelWith(details);
this.details.appendChild(panel);
DOMUtils.scrollIntoViewIfNeeded(panel);
}
};
function loadResults(dataString, filename)
{
let inspectorContainer = document.getElementById('uiContainer');
inspector = new HeapSnapshotInspector(inspectorContainer, dataString, filename)
}
function filenameForPath(filepath)
{
var matched = filepath.match(/([^\/]+)(?=\.\w+$)/);
if (matched)
return matched[0];
return filepath;
}
function hideDescription()
{
document.getElementById('description').classList.add('hidden');
}
var inspector;
function setupInterface()
{
// See if we have a file to load specified in the query string.
var query_parameters = {};
var pairs = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
var filename = "test-heap.json";
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
query_parameters[pair[0]] = decodeURIComponent(pair[1]);
}
if ("filename" in query_parameters)
filename = query_parameters["filename"];
fetch(filename)
.then(function(response) {
if (response.ok)
return response.text();
throw new Error('Failed to load data file ' + filename);
})
.then(function(dataString) {
loadResults(dataString, filenameForPath(filename));
hideDescription();
document.getElementById('uiContainer').style.display = 'block';
});
var drop_target = document.getElementById("dropTarget");
drop_target.addEventListener("dragenter", function (e) {
drop_target.className = "dragOver";
e.stopPropagation();
e.preventDefault();
}, false);
drop_target.addEventListener("dragover", function (e) {
e.stopPropagation();
e.preventDefault();
}, false);
drop_target.addEventListener("dragleave", function (e) {
drop_target.className = "";
e.stopPropagation();
e.preventDefault();
}, false);
drop_target.addEventListener("drop", function (e) {
drop_target.className = "";
e.stopPropagation();
e.preventDefault();
for (var i = 0; i < e.dataTransfer.files.length; ++i) {
var file = e.dataTransfer.files[i];
var reader = new FileReader();
reader.filename = file.name;
reader.onload = function(e) {
loadResults(e.target.result, filenameForPath(this.filename));
hideDescription();
document.getElementById('uiContainer').style.display = 'block';
};
reader.readAsText(file);
document.title = "GC Heap: " + reader.filename;
}
}, false);
}
window.addEventListener('load', setupInterface, false);