// 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; } }; // 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 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 + ` ${node.id} cell ${node.address} ${wrappedAddressString} (retains ${HeapInspectorUtils.humanReadableSize(node.retainedSize)})`; if (node.label.length) nodeHTML += ` “${node.label}”`; nodeSpan.innerHTML = nodeHTML; 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); } return nodeSpan; } static spanForEdge(snapshot, edge) { let edgeSpan = document.createElement('span'); edgeSpan.className = 'edge'; edgeSpan.innerHTML = '' + edge.type + ' ' + edge.data + ''; 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); 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); this.rootsContainer = document.createElement('section'); this.rootsContainer.id = 'roots'; header = document.createElement('h1'); header.textContent = 'Roots' this.rootsContainer.appendChild(header); this.pathsToRootsContainer = document.createElement('section'); this.pathsToRootsContainer.id = 'paths-to-roots'; header = document.createElement('h1'); header.textContent = 'Paths to roots' this.pathsToRootsContainer.appendChild(header); this.allPathsContainer = document.createElement('section'); this.allPathsContainer.id = 'all-paths'; header = document.createElement('h1'); header.textContent = 'All paths to…' this.allPathsContainer.appendChild(header); this.containerElement.appendChild(this.pathsToRootsContainer); this.containerElement.appendChild(this.allPathsContainer); this.containerElement.appendChild(this.rootsContainer); this.containerElement.appendChild(this.objectByTypeContainer); } buildAllObjectsByType() { let categories = this.snapshot._categories; let categoryNames = Object.keys(categories).sort(); for (var 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 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 header = document.createElement('h2'); header.textContent = 'Shortest paths to all ' + type + 's'; 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(header); this.pathsToRootsContainer.appendChild(detailsContainer); } 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)); 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); detailsContainer.appendChild(pathDetails); } details.appendChild(detailsContainer); this.allPathsContainer.appendChild(details); } }; 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);