UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

1,214 lines 103 kB
/* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* eslint-disable rulesdir/use_private_class_members */ import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as HeapSnapshotModel from '../../models/heap_snapshot_model/heap_snapshot_model.js'; import { AllocationProfile } from './AllocationProfile.js'; export class HeapSnapshotEdge { snapshot; edges; edgeIndex; constructor(snapshot, edgeIndex) { this.snapshot = snapshot; this.edges = snapshot.containmentEdges; this.edgeIndex = edgeIndex || 0; } clone() { return new HeapSnapshotEdge(this.snapshot, this.edgeIndex); } hasStringName() { throw new Error('Not implemented'); } name() { throw new Error('Not implemented'); } node() { return this.snapshot.createNode(this.nodeIndex()); } nodeIndex() { if (typeof this.snapshot.edgeToNodeOffset === 'undefined') { throw new Error('edgeToNodeOffset is undefined'); } return this.edges[this.edgeIndex + this.snapshot.edgeToNodeOffset]; } toString() { return 'HeapSnapshotEdge: ' + this.name(); } type() { return this.snapshot.edgeTypes[this.rawType()]; } itemIndex() { return this.edgeIndex; } serialize() { return new HeapSnapshotModel.HeapSnapshotModel.Edge(this.name(), this.node().serialize(), this.type(), this.edgeIndex); } rawType() { if (typeof this.snapshot.edgeTypeOffset === 'undefined') { throw new Error('edgeTypeOffset is undefined'); } return this.edges[this.edgeIndex + this.snapshot.edgeTypeOffset]; } isInvisible() { throw new Error('Not implemented'); } isWeak() { throw new Error('Not implemented'); } } export class HeapSnapshotNodeIndexProvider { #node; constructor(snapshot) { this.#node = snapshot.createNode(); } itemForIndex(index) { this.#node.nodeIndex = index; return this.#node; } } export class HeapSnapshotEdgeIndexProvider { #edge; constructor(snapshot) { this.#edge = snapshot.createEdge(0); } itemForIndex(index) { this.#edge.edgeIndex = index; return this.#edge; } } export class HeapSnapshotRetainerEdgeIndexProvider { #retainerEdge; constructor(snapshot) { this.#retainerEdge = snapshot.createRetainingEdge(0); } itemForIndex(index) { this.#retainerEdge.setRetainerIndex(index); return this.#retainerEdge; } } export class HeapSnapshotEdgeIterator { #sourceNode; edge; constructor(node) { this.#sourceNode = node; this.edge = node.snapshot.createEdge(node.edgeIndexesStart()); } hasNext() { return this.edge.edgeIndex < this.#sourceNode.edgeIndexesEnd(); } item() { return this.edge; } next() { if (typeof this.edge.snapshot.edgeFieldsCount === 'undefined') { throw new Error('edgeFieldsCount is undefined'); } this.edge.edgeIndex += this.edge.snapshot.edgeFieldsCount; } } export class HeapSnapshotRetainerEdge { snapshot; #retainerIndexInternal; #globalEdgeIndex; #retainingNodeIndex; #edgeInstance; #nodeInstance; constructor(snapshot, retainerIndex) { this.snapshot = snapshot; this.setRetainerIndex(retainerIndex); } clone() { return new HeapSnapshotRetainerEdge(this.snapshot, this.retainerIndex()); } hasStringName() { return this.edge().hasStringName(); } name() { return this.edge().name(); } node() { return this.nodeInternal(); } nodeIndex() { if (typeof this.#retainingNodeIndex === 'undefined') { throw new Error('retainingNodeIndex is undefined'); } return this.#retainingNodeIndex; } retainerIndex() { return this.#retainerIndexInternal; } setRetainerIndex(retainerIndex) { if (retainerIndex === this.#retainerIndexInternal) { return; } if (!this.snapshot.retainingEdges || !this.snapshot.retainingNodes) { throw new Error('Snapshot does not contain retaining edges or retaining nodes'); } this.#retainerIndexInternal = retainerIndex; this.#globalEdgeIndex = this.snapshot.retainingEdges[retainerIndex]; this.#retainingNodeIndex = this.snapshot.retainingNodes[retainerIndex]; this.#edgeInstance = null; this.#nodeInstance = null; } set edgeIndex(edgeIndex) { this.setRetainerIndex(edgeIndex); } nodeInternal() { if (!this.#nodeInstance) { this.#nodeInstance = this.snapshot.createNode(this.#retainingNodeIndex); } return this.#nodeInstance; } edge() { if (!this.#edgeInstance) { this.#edgeInstance = this.snapshot.createEdge(this.#globalEdgeIndex); } return this.#edgeInstance; } toString() { return this.edge().toString(); } itemIndex() { return this.#retainerIndexInternal; } serialize() { return new HeapSnapshotModel.HeapSnapshotModel.Edge(this.name(), this.node().serialize(), this.type(), this.#globalEdgeIndex); } type() { return this.edge().type(); } } export class HeapSnapshotRetainerEdgeIterator { #retainersEnd; retainer; constructor(retainedNode) { const snapshot = retainedNode.snapshot; const retainedNodeOrdinal = retainedNode.ordinal(); if (!snapshot.firstRetainerIndex) { throw new Error('Snapshot does not contain firstRetainerIndex'); } const retainerIndex = snapshot.firstRetainerIndex[retainedNodeOrdinal]; this.#retainersEnd = snapshot.firstRetainerIndex[retainedNodeOrdinal + 1]; this.retainer = snapshot.createRetainingEdge(retainerIndex); } hasNext() { return this.retainer.retainerIndex() < this.#retainersEnd; } item() { return this.retainer; } next() { this.retainer.setRetainerIndex(this.retainer.retainerIndex() + 1); } } export class HeapSnapshotNode { snapshot; nodeIndex; constructor(snapshot, nodeIndex) { this.snapshot = snapshot; this.nodeIndex = nodeIndex || 0; } distance() { return this.snapshot.nodeDistances[this.nodeIndex / this.snapshot.nodeFieldCount]; } className() { throw new Error('Not implemented'); } classIndex() { throw new Error('Not implemented'); } dominatorIndex() { const nodeFieldCount = this.snapshot.nodeFieldCount; return this.snapshot.dominatorsTree[this.nodeIndex / this.snapshot.nodeFieldCount] * nodeFieldCount; } edges() { return new HeapSnapshotEdgeIterator(this); } edgesCount() { return (this.edgeIndexesEnd() - this.edgeIndexesStart()) / this.snapshot.edgeFieldsCount; } id() { throw new Error('Not implemented'); } rawName() { throw new Error('Not implemented'); } isRoot() { return this.nodeIndex === this.snapshot.rootNodeIndex; } isUserRoot() { throw new Error('Not implemented'); } isHidden() { throw new Error('Not implemented'); } isArray() { throw new Error('Not implemented'); } isDocumentDOMTreesRoot() { throw new Error('Not implemented'); } name() { return this.snapshot.strings[this.nameInternal()]; } retainedSize() { return this.snapshot.retainedSizes[this.ordinal()]; } retainers() { return new HeapSnapshotRetainerEdgeIterator(this); } retainersCount() { const snapshot = this.snapshot; const ordinal = this.ordinal(); return snapshot.firstRetainerIndex[ordinal + 1] - snapshot.firstRetainerIndex[ordinal]; } selfSize() { const snapshot = this.snapshot; return snapshot.nodes[this.nodeIndex + snapshot.nodeSelfSizeOffset]; } type() { return this.snapshot.nodeTypes[this.rawType()]; } traceNodeId() { const snapshot = this.snapshot; return snapshot.nodes[this.nodeIndex + snapshot.nodeTraceNodeIdOffset]; } itemIndex() { return this.nodeIndex; } serialize() { return new HeapSnapshotModel.HeapSnapshotModel.Node(this.id(), this.name(), this.distance(), this.nodeIndex, this.retainedSize(), this.selfSize(), this.type()); } nameInternal() { const snapshot = this.snapshot; return snapshot.nodes[this.nodeIndex + snapshot.nodeNameOffset]; } edgeIndexesStart() { return this.snapshot.firstEdgeIndexes[this.ordinal()]; } edgeIndexesEnd() { return this.snapshot.firstEdgeIndexes[this.ordinal() + 1]; } ordinal() { return this.nodeIndex / this.snapshot.nodeFieldCount; } nextNodeIndex() { return this.nodeIndex + this.snapshot.nodeFieldCount; } rawType() { const snapshot = this.snapshot; return snapshot.nodes[this.nodeIndex + snapshot.nodeTypeOffset]; } } export class HeapSnapshotNodeIterator { node; #nodesLength; constructor(node) { this.node = node; this.#nodesLength = node.snapshot.nodes.length; } hasNext() { return this.node.nodeIndex < this.#nodesLength; } item() { return this.node; } next() { this.node.nodeIndex = this.node.nextNodeIndex(); } } export class HeapSnapshotIndexRangeIterator { #itemProvider; #indexes; #position; constructor(itemProvider, indexes) { this.#itemProvider = itemProvider; this.#indexes = indexes; this.#position = 0; } hasNext() { return this.#position < this.#indexes.length; } item() { const index = this.#indexes[this.#position]; return this.#itemProvider.itemForIndex(index); } next() { ++this.#position; } } export class HeapSnapshotFilteredIterator { #iterator; #filter; constructor(iterator, filter) { this.#iterator = iterator; this.#filter = filter; this.skipFilteredItems(); } hasNext() { return this.#iterator.hasNext(); } item() { return this.#iterator.item(); } next() { this.#iterator.next(); this.skipFilteredItems(); } skipFilteredItems() { while (this.#iterator.hasNext() && this.#filter && !this.#filter(this.#iterator.item())) { this.#iterator.next(); } } } export class HeapSnapshotProgress { #dispatcher; constructor(dispatcher) { this.#dispatcher = dispatcher; } updateStatus(status) { this.sendUpdateEvent(i18n.i18n.serializeUIString(status)); } updateProgress(title, value, total) { const percentValue = ((total ? (value / total) : 0) * 100).toFixed(0); this.sendUpdateEvent(i18n.i18n.serializeUIString(title, { PH1: percentValue })); } reportProblem(error) { // May be undefined in tests. if (this.#dispatcher) { this.#dispatcher.sendEvent(HeapSnapshotModel.HeapSnapshotModel.HeapSnapshotProgressEvent.BrokenSnapshot, error); } } sendUpdateEvent(serializedText) { // May be undefined in tests. if (this.#dispatcher) { this.#dispatcher.sendEvent(HeapSnapshotModel.HeapSnapshotModel.HeapSnapshotProgressEvent.Update, serializedText); } } } export class HeapSnapshotProblemReport { #errors; constructor(title) { this.#errors = [title]; } addError(error) { if (this.#errors.length > 100) { return; } this.#errors.push(error); } toString() { return this.#errors.join('\n '); } } export class HeapSnapshot { nodes; containmentEdges; #metaNode; #rawSamples; #samples; strings; #locations; #progress; #noDistance; rootNodeIndexInternal; #snapshotDiffs; #aggregatesForDiffInternal; #aggregates; #aggregatesSortedFlags; #profile; nodeTypeOffset; nodeNameOffset; nodeIdOffset; nodeSelfSizeOffset; #nodeEdgeCountOffset; nodeTraceNodeIdOffset; nodeFieldCount; nodeTypes; nodeArrayType; nodeHiddenType; nodeObjectType; nodeNativeType; nodeConsStringType; nodeSlicedStringType; nodeCodeType; nodeSyntheticType; edgeFieldsCount; edgeTypeOffset; edgeNameOffset; edgeToNodeOffset; edgeTypes; edgeElementType; edgeHiddenType; edgeInternalType; edgeShortcutType; edgeWeakType; edgeInvisibleType; #locationIndexOffset; #locationScriptIdOffset; #locationLineOffset; #locationColumnOffset; #locationFieldCount; nodeCount; #edgeCount; retainedSizes; firstEdgeIndexes; retainingNodes; retainingEdges; firstRetainerIndex; nodeDistances; firstDominatedNodeIndex; dominatedNodes; dominatorsTree; #allocationProfile; #nodeDetachednessOffset; #locationMap; lazyStringCache; constructor(profile, progress) { this.nodes = profile.nodes; this.containmentEdges = profile.edges; this.#metaNode = profile.snapshot.meta; this.#rawSamples = profile.samples; this.#samples = null; this.strings = profile.strings; this.#locations = profile.locations; this.#progress = progress; this.#noDistance = -5; this.rootNodeIndexInternal = 0; if (profile.snapshot.root_index) { this.rootNodeIndexInternal = profile.snapshot.root_index; } this.#snapshotDiffs = {}; this.#aggregates = {}; this.#aggregatesSortedFlags = {}; this.#profile = profile; } initialize() { const meta = this.#metaNode; this.nodeTypeOffset = meta.node_fields.indexOf('type'); this.nodeNameOffset = meta.node_fields.indexOf('name'); this.nodeIdOffset = meta.node_fields.indexOf('id'); this.nodeSelfSizeOffset = meta.node_fields.indexOf('self_size'); this.#nodeEdgeCountOffset = meta.node_fields.indexOf('edge_count'); this.nodeTraceNodeIdOffset = meta.node_fields.indexOf('trace_node_id'); this.#nodeDetachednessOffset = meta.node_fields.indexOf('detachedness'); this.nodeFieldCount = meta.node_fields.length; this.nodeTypes = meta.node_types[this.nodeTypeOffset]; this.nodeArrayType = this.nodeTypes.indexOf('array'); this.nodeHiddenType = this.nodeTypes.indexOf('hidden'); this.nodeObjectType = this.nodeTypes.indexOf('object'); this.nodeNativeType = this.nodeTypes.indexOf('native'); this.nodeConsStringType = this.nodeTypes.indexOf('concatenated string'); this.nodeSlicedStringType = this.nodeTypes.indexOf('sliced string'); this.nodeCodeType = this.nodeTypes.indexOf('code'); this.nodeSyntheticType = this.nodeTypes.indexOf('synthetic'); this.edgeFieldsCount = meta.edge_fields.length; this.edgeTypeOffset = meta.edge_fields.indexOf('type'); this.edgeNameOffset = meta.edge_fields.indexOf('name_or_index'); this.edgeToNodeOffset = meta.edge_fields.indexOf('to_node'); this.edgeTypes = meta.edge_types[this.edgeTypeOffset]; this.edgeTypes.push('invisible'); this.edgeElementType = this.edgeTypes.indexOf('element'); this.edgeHiddenType = this.edgeTypes.indexOf('hidden'); this.edgeInternalType = this.edgeTypes.indexOf('internal'); this.edgeShortcutType = this.edgeTypes.indexOf('shortcut'); this.edgeWeakType = this.edgeTypes.indexOf('weak'); this.edgeInvisibleType = this.edgeTypes.indexOf('invisible'); const locationFields = meta.location_fields || []; this.#locationIndexOffset = locationFields.indexOf('object_index'); this.#locationScriptIdOffset = locationFields.indexOf('script_id'); this.#locationLineOffset = locationFields.indexOf('line'); this.#locationColumnOffset = locationFields.indexOf('column'); this.#locationFieldCount = locationFields.length; this.nodeCount = this.nodes.length / this.nodeFieldCount; this.#edgeCount = this.containmentEdges.length / this.edgeFieldsCount; this.retainedSizes = new Float64Array(this.nodeCount); this.firstEdgeIndexes = new Uint32Array(this.nodeCount + 1); this.retainingNodes = new Uint32Array(this.#edgeCount); this.retainingEdges = new Uint32Array(this.#edgeCount); this.firstRetainerIndex = new Uint32Array(this.nodeCount + 1); this.nodeDistances = new Int32Array(this.nodeCount); this.firstDominatedNodeIndex = new Uint32Array(this.nodeCount + 1); this.dominatedNodes = new Uint32Array(this.nodeCount - 1); this.#progress.updateStatus('Building edge indexes…'); this.buildEdgeIndexes(); this.#progress.updateStatus('Building retainers…'); this.buildRetainers(); this.#progress.updateStatus('Propagating DOM state…'); this.propagateDOMState(); this.#progress.updateStatus('Calculating node flags…'); this.calculateFlags(); this.#progress.updateStatus('Calculating distances…'); this.calculateDistances(); this.#progress.updateStatus('Building postorder index…'); const result = this.buildPostOrderIndex(); // Actually it is array that maps node ordinal number to dominator node ordinal number. this.#progress.updateStatus('Building dominator tree…'); this.dominatorsTree = this.buildDominatorTree(result.postOrderIndex2NodeOrdinal, result.nodeOrdinal2PostOrderIndex); this.#progress.updateStatus('Calculating retained sizes…'); this.calculateRetainedSizes(result.postOrderIndex2NodeOrdinal); this.#progress.updateStatus('Building dominated nodes…'); this.buildDominatedNodes(); this.#progress.updateStatus('Calculating statistics…'); this.calculateStatistics(); this.#progress.updateStatus('Calculating samples…'); this.buildSamples(); this.#progress.updateStatus('Building locations…'); this.buildLocationMap(); this.#progress.updateStatus('Finished processing.'); if (this.#profile.snapshot.trace_function_count) { this.#progress.updateStatus('Building allocation statistics…'); const nodes = this.nodes; const nodesLength = nodes.length; const nodeFieldCount = this.nodeFieldCount; const node = this.rootNode(); const liveObjects = {}; for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { node.nodeIndex = nodeIndex; const traceNodeId = node.traceNodeId(); let stats = liveObjects[traceNodeId]; if (!stats) { liveObjects[traceNodeId] = stats = { count: 0, size: 0, ids: [] }; } stats.count++; stats.size += node.selfSize(); stats.ids.push(node.id()); } this.#allocationProfile = new AllocationProfile(this.#profile, liveObjects); this.#progress.updateStatus('done'); } } buildEdgeIndexes() { const nodes = this.nodes; const nodeCount = this.nodeCount; const firstEdgeIndexes = this.firstEdgeIndexes; const nodeFieldCount = this.nodeFieldCount; const edgeFieldsCount = this.edgeFieldsCount; const nodeEdgeCountOffset = this.#nodeEdgeCountOffset; firstEdgeIndexes[nodeCount] = this.containmentEdges.length; for (let nodeOrdinal = 0, edgeIndex = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) { firstEdgeIndexes[nodeOrdinal] = edgeIndex; edgeIndex += nodes[nodeOrdinal * nodeFieldCount + nodeEdgeCountOffset] * edgeFieldsCount; } } buildRetainers() { const retainingNodes = this.retainingNodes; const retainingEdges = this.retainingEdges; // Index of the first retainer in the retainingNodes and retainingEdges // arrays. Addressed by retained node index. const firstRetainerIndex = this.firstRetainerIndex; const containmentEdges = this.containmentEdges; const edgeFieldsCount = this.edgeFieldsCount; const nodeFieldCount = this.nodeFieldCount; const edgeToNodeOffset = this.edgeToNodeOffset; const firstEdgeIndexes = this.firstEdgeIndexes; const nodeCount = this.nodeCount; for (let toNodeFieldIndex = edgeToNodeOffset, l = containmentEdges.length; toNodeFieldIndex < l; toNodeFieldIndex += edgeFieldsCount) { const toNodeIndex = containmentEdges[toNodeFieldIndex]; if (toNodeIndex % nodeFieldCount) { throw new Error('Invalid toNodeIndex ' + toNodeIndex); } ++firstRetainerIndex[toNodeIndex / nodeFieldCount]; } for (let i = 0, firstUnusedRetainerSlot = 0; i < nodeCount; i++) { const retainersCount = firstRetainerIndex[i]; firstRetainerIndex[i] = firstUnusedRetainerSlot; retainingNodes[firstUnusedRetainerSlot] = retainersCount; firstUnusedRetainerSlot += retainersCount; } firstRetainerIndex[nodeCount] = retainingNodes.length; let nextNodeFirstEdgeIndex = firstEdgeIndexes[0]; for (let srcNodeOrdinal = 0; srcNodeOrdinal < nodeCount; ++srcNodeOrdinal) { const firstEdgeIndex = nextNodeFirstEdgeIndex; nextNodeFirstEdgeIndex = firstEdgeIndexes[srcNodeOrdinal + 1]; const srcNodeIndex = srcNodeOrdinal * nodeFieldCount; for (let edgeIndex = firstEdgeIndex; edgeIndex < nextNodeFirstEdgeIndex; edgeIndex += edgeFieldsCount) { const toNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset]; if (toNodeIndex % nodeFieldCount) { throw new Error('Invalid toNodeIndex ' + toNodeIndex); } const firstRetainerSlotIndex = firstRetainerIndex[toNodeIndex / nodeFieldCount]; const nextUnusedRetainerSlotIndex = firstRetainerSlotIndex + (--retainingNodes[firstRetainerSlotIndex]); retainingNodes[nextUnusedRetainerSlotIndex] = srcNodeIndex; retainingEdges[nextUnusedRetainerSlotIndex] = edgeIndex; } } } allNodes() { return new HeapSnapshotNodeIterator(this.rootNode()); } rootNode() { return this.createNode(this.rootNodeIndexInternal); } get rootNodeIndex() { return this.rootNodeIndexInternal; } get totalSize() { return this.rootNode().retainedSize(); } getDominatedIndex(nodeIndex) { if (nodeIndex % this.nodeFieldCount) { throw new Error('Invalid nodeIndex: ' + nodeIndex); } return this.firstDominatedNodeIndex[nodeIndex / this.nodeFieldCount]; } createFilter(nodeFilter) { const minNodeId = nodeFilter.minNodeId; const maxNodeId = nodeFilter.maxNodeId; const allocationNodeId = nodeFilter.allocationNodeId; let filter; if (typeof allocationNodeId === 'number') { filter = this.createAllocationStackFilter(allocationNodeId); if (!filter) { throw new Error('Unable to create filter'); } // @ts-ignore key can be added as a static property filter.key = 'AllocationNodeId: ' + allocationNodeId; } else if (typeof minNodeId === 'number' && typeof maxNodeId === 'number') { filter = this.createNodeIdFilter(minNodeId, maxNodeId); // @ts-ignore key can be added as a static property filter.key = 'NodeIdRange: ' + minNodeId + '..' + maxNodeId; } return filter; } search(searchConfig, nodeFilter) { const query = searchConfig.query; function filterString(matchedStringIndexes, string, index) { if (string.indexOf(query) !== -1) { matchedStringIndexes.add(index); } return matchedStringIndexes; } const regexp = searchConfig.isRegex ? new RegExp(query) : Platform.StringUtilities.createPlainTextSearchRegex(query, 'i'); function filterRegexp(matchedStringIndexes, string, index) { if (regexp.test(string)) { matchedStringIndexes.add(index); } return matchedStringIndexes; } const stringFilter = (searchConfig.isRegex || !searchConfig.caseSensitive) ? filterRegexp : filterString; const stringIndexes = this.strings.reduce(stringFilter, new Set()); if (!stringIndexes.size) { return []; } const filter = this.createFilter(nodeFilter); const nodeIds = []; const nodesLength = this.nodes.length; const nodes = this.nodes; const nodeNameOffset = this.nodeNameOffset; const nodeIdOffset = this.nodeIdOffset; const nodeFieldCount = this.nodeFieldCount; const node = this.rootNode(); for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { node.nodeIndex = nodeIndex; if (filter && !filter(node)) { continue; } if (stringIndexes.has(nodes[nodeIndex + nodeNameOffset])) { nodeIds.push(nodes[nodeIndex + nodeIdOffset]); } } return nodeIds; } aggregatesWithFilter(nodeFilter) { const filter = this.createFilter(nodeFilter); // @ts-ignore key is added in createFilter const key = filter ? filter.key : 'allObjects'; return this.getAggregatesByClassName(false, key, filter); } createNodeIdFilter(minNodeId, maxNodeId) { function nodeIdFilter(node) { const id = node.id(); return id > minNodeId && id <= maxNodeId; } return nodeIdFilter; } createAllocationStackFilter(bottomUpAllocationNodeId) { if (!this.#allocationProfile) { throw new Error('No Allocation Profile provided'); } const traceIds = this.#allocationProfile.traceIds(bottomUpAllocationNodeId); if (!traceIds.length) { return undefined; } const set = {}; for (let i = 0; i < traceIds.length; i++) { set[traceIds[i]] = true; } function traceIdFilter(node) { return Boolean(set[node.traceNodeId()]); } return traceIdFilter; } getAggregatesByClassName(sortedIndexes, key, filter) { const aggregates = this.buildAggregates(filter); let aggregatesByClassName; if (key && this.#aggregates[key]) { aggregatesByClassName = this.#aggregates[key]; } else { this.calculateClassesRetainedSize(aggregates.aggregatesByClassIndex, filter); aggregatesByClassName = aggregates.aggregatesByClassName; if (key) { this.#aggregates[key] = aggregatesByClassName; } } if (sortedIndexes && (!key || !this.#aggregatesSortedFlags[key])) { this.sortAggregateIndexes(aggregatesByClassName); if (key) { this.#aggregatesSortedFlags[key] = sortedIndexes; } } return aggregatesByClassName; } allocationTracesTops() { return this.#allocationProfile.serializeTraceTops(); } allocationNodeCallers(nodeId) { return this.#allocationProfile.serializeCallers(nodeId); } allocationStack(nodeIndex) { const node = this.createNode(nodeIndex); const allocationNodeId = node.traceNodeId(); if (!allocationNodeId) { return null; } return this.#allocationProfile.serializeAllocationStack(allocationNodeId); } aggregatesForDiff() { if (this.#aggregatesForDiffInternal) { return this.#aggregatesForDiffInternal; } const aggregatesByClassName = this.getAggregatesByClassName(true, 'allObjects'); this.#aggregatesForDiffInternal = {}; const node = this.createNode(); for (const className in aggregatesByClassName) { const aggregate = aggregatesByClassName[className]; const indexes = aggregate.idxs; const ids = new Array(indexes.length); const selfSizes = new Array(indexes.length); for (let i = 0; i < indexes.length; i++) { node.nodeIndex = indexes[i]; ids[i] = node.id(); selfSizes[i] = node.selfSize(); } this.#aggregatesForDiffInternal[className] = { indexes: indexes, ids: ids, selfSizes: selfSizes }; } return this.#aggregatesForDiffInternal; } isUserRoot(_node) { return true; } calculateDistances(filter) { const nodeCount = this.nodeCount; const distances = this.nodeDistances; const noDistance = this.#noDistance; for (let i = 0; i < nodeCount; ++i) { distances[i] = noDistance; } const nodesToVisit = new Uint32Array(this.nodeCount); let nodesToVisitLength = 0; // BFS for user root objects. for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) { const node = iter.edge.node(); if (this.isUserRoot(node)) { distances[node.ordinal()] = 1; nodesToVisit[nodesToVisitLength++] = node.nodeIndex; } } this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); // BFS for objects not reached from user roots. distances[this.rootNode().ordinal()] = nodesToVisitLength > 0 ? HeapSnapshotModel.HeapSnapshotModel.baseSystemDistance : 0; nodesToVisit[0] = this.rootNode().nodeIndex; nodesToVisitLength = 1; this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); } bfs(nodesToVisit, nodesToVisitLength, distances, filter) { // Preload fields into local variables for better performance. const edgeFieldsCount = this.edgeFieldsCount; const nodeFieldCount = this.nodeFieldCount; const containmentEdges = this.containmentEdges; const firstEdgeIndexes = this.firstEdgeIndexes; const edgeToNodeOffset = this.edgeToNodeOffset; const edgeTypeOffset = this.edgeTypeOffset; const nodeCount = this.nodeCount; const edgeWeakType = this.edgeWeakType; const noDistance = this.#noDistance; let index = 0; const edge = this.createEdge(0); const node = this.createNode(0); while (index < nodesToVisitLength) { const nodeIndex = nodesToVisit[index++]; // shift generates too much garbage. const nodeOrdinal = nodeIndex / nodeFieldCount; const distance = distances[nodeOrdinal] + 1; const firstEdgeIndex = firstEdgeIndexes[nodeOrdinal]; const edgesEnd = firstEdgeIndexes[nodeOrdinal + 1]; node.nodeIndex = nodeIndex; for (let edgeIndex = firstEdgeIndex; edgeIndex < edgesEnd; edgeIndex += edgeFieldsCount) { const edgeType = containmentEdges[edgeIndex + edgeTypeOffset]; if (edgeType === edgeWeakType) { continue; } const childNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset]; const childNodeOrdinal = childNodeIndex / nodeFieldCount; if (distances[childNodeOrdinal] !== noDistance) { continue; } edge.edgeIndex = edgeIndex; if (filter && !filter(node, edge)) { continue; } distances[childNodeOrdinal] = distance; nodesToVisit[nodesToVisitLength++] = childNodeIndex; } } if (nodesToVisitLength > nodeCount) { throw new Error('BFS failed. Nodes to visit (' + nodesToVisitLength + ') is more than nodes count (' + nodeCount + ')'); } } buildAggregates(filter) { const aggregates = {}; const aggregatesByClassName = {}; const classIndexes = []; const nodes = this.nodes; const nodesLength = nodes.length; const nodeNativeType = this.nodeNativeType; const nodeFieldCount = this.nodeFieldCount; const selfSizeOffset = this.nodeSelfSizeOffset; const nodeTypeOffset = this.nodeTypeOffset; const node = this.rootNode(); const nodeDistances = this.nodeDistances; for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { node.nodeIndex = nodeIndex; if (filter && !filter(node)) { continue; } const selfSize = nodes[nodeIndex + selfSizeOffset]; if (!selfSize && nodes[nodeIndex + nodeTypeOffset] !== nodeNativeType) { continue; } const classIndex = node.classIndex(); const nodeOrdinal = nodeIndex / nodeFieldCount; const distance = nodeDistances[nodeOrdinal]; if (!(classIndex in aggregates)) { const nodeType = node.type(); const nameMatters = nodeType === 'object' || nodeType === 'native'; const value = { count: 1, distance: distance, self: selfSize, maxRet: 0, type: nodeType, name: nameMatters ? node.name() : null, idxs: [nodeIndex], }; aggregates[classIndex] = value; classIndexes.push(classIndex); aggregatesByClassName[node.className()] = value; } else { const clss = aggregates[classIndex]; if (!clss) { continue; } clss.distance = Math.min(clss.distance, distance); ++clss.count; clss.self += selfSize; clss.idxs.push(nodeIndex); } } // Shave off provisionally allocated space. for (let i = 0, l = classIndexes.length; i < l; ++i) { const classIndex = classIndexes[i]; const classIndexValues = aggregates[classIndex]; if (!classIndexValues) { continue; } classIndexValues.idxs = classIndexValues.idxs.slice(); } return { aggregatesByClassName: aggregatesByClassName, aggregatesByClassIndex: aggregates }; } calculateClassesRetainedSize(aggregates, filter) { const rootNodeIndex = this.rootNodeIndexInternal; const node = this.createNode(rootNodeIndex); const list = [rootNodeIndex]; const sizes = [-1]; const classes = []; const seenClassNameIndexes = new Map(); const nodeFieldCount = this.nodeFieldCount; const nodeTypeOffset = this.nodeTypeOffset; const nodeNativeType = this.nodeNativeType; const dominatedNodes = this.dominatedNodes; const nodes = this.nodes; const firstDominatedNodeIndex = this.firstDominatedNodeIndex; while (list.length) { const nodeIndex = list.pop(); node.nodeIndex = nodeIndex; let classIndex = node.classIndex(); const seen = Boolean(seenClassNameIndexes.get(classIndex)); const nodeOrdinal = nodeIndex / nodeFieldCount; const dominatedIndexFrom = firstDominatedNodeIndex[nodeOrdinal]; const dominatedIndexTo = firstDominatedNodeIndex[nodeOrdinal + 1]; if (!seen && (!filter || filter(node)) && (node.selfSize() || nodes[nodeIndex + nodeTypeOffset] === nodeNativeType)) { aggregates[classIndex].maxRet += node.retainedSize(); if (dominatedIndexFrom !== dominatedIndexTo) { seenClassNameIndexes.set(classIndex, true); sizes.push(list.length); classes.push(classIndex); } } for (let i = dominatedIndexFrom; i < dominatedIndexTo; i++) { list.push(dominatedNodes[i]); } const l = list.length; while (sizes[sizes.length - 1] === l) { sizes.pop(); classIndex = classes.pop(); seenClassNameIndexes.set(classIndex, false); } } } sortAggregateIndexes(aggregates) { const nodeA = this.createNode(); const nodeB = this.createNode(); for (const clss in aggregates) { aggregates[clss].idxs.sort((idxA, idxB) => { nodeA.nodeIndex = idxA; nodeB.nodeIndex = idxB; return nodeA.id() < nodeB.id() ? -1 : 1; }); } } /** * The function checks is the edge should be considered during building * postorder iterator and dominator tree. */ isEssentialEdge(nodeIndex, edgeType) { // Shortcuts at the root node have special meaning of marking user global objects. return edgeType !== this.edgeWeakType && (edgeType !== this.edgeShortcutType || nodeIndex === this.rootNodeIndexInternal); } buildPostOrderIndex() { const nodeFieldCount = this.nodeFieldCount; const nodeCount = this.nodeCount; const rootNodeOrdinal = this.rootNodeIndexInternal / nodeFieldCount; const edgeFieldsCount = this.edgeFieldsCount; const edgeTypeOffset = this.edgeTypeOffset; const edgeToNodeOffset = this.edgeToNodeOffset; const firstEdgeIndexes = this.firstEdgeIndexes; const containmentEdges = this.containmentEdges; const mapAndFlag = this.userObjectsMapAndFlag(); const flags = mapAndFlag ? mapAndFlag.map : null; const flag = mapAndFlag ? mapAndFlag.flag : 0; const stackNodes = new Uint32Array(nodeCount); const stackCurrentEdge = new Uint32Array(nodeCount); const postOrderIndex2NodeOrdinal = new Uint32Array(nodeCount); const nodeOrdinal2PostOrderIndex = new Uint32Array(nodeCount); const visited = new Uint8Array(nodeCount); let postOrderIndex = 0; let stackTop = 0; stackNodes[0] = rootNodeOrdinal; stackCurrentEdge[0] = firstEdgeIndexes[rootNodeOrdinal]; visited[rootNodeOrdinal] = 1; let iteration = 0; while (true) { ++iteration; while (stackTop >= 0) { const nodeOrdinal = stackNodes[stackTop]; const edgeIndex = stackCurrentEdge[stackTop]; const edgesEnd = firstEdgeIndexes[nodeOrdinal + 1]; if (edgeIndex < edgesEnd) { stackCurrentEdge[stackTop] += edgeFieldsCount; const edgeType = containmentEdges[edgeIndex + edgeTypeOffset]; if (!this.isEssentialEdge(nodeOrdinal * nodeFieldCount, edgeType)) { continue; } const childNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset]; const childNodeOrdinal = childNodeIndex / nodeFieldCount; if (visited[childNodeOrdinal]) { continue; } const nodeFlag = !flags || (flags[nodeOrdinal] & flag); const childNodeFlag = !flags || (flags[childNodeOrdinal] & flag); // We are skipping the edges from non-page-owned nodes to page-owned nodes. // Otherwise the dominators for the objects that also were retained by debugger would be affected. if (nodeOrdinal !== rootNodeOrdinal && childNodeFlag && !nodeFlag) { continue; } ++stackTop; stackNodes[stackTop] = childNodeOrdinal; stackCurrentEdge[stackTop] = firstEdgeIndexes[childNodeOrdinal]; visited[childNodeOrdinal] = 1; } else { // Done with all the node children nodeOrdinal2PostOrderIndex[nodeOrdinal] = postOrderIndex; postOrderIndex2NodeOrdinal[postOrderIndex++] = nodeOrdinal; --stackTop; } } if (postOrderIndex === nodeCount || iteration > 1) { break; } const errors = new HeapSnapshotProblemReport(`Heap snapshot: ${nodeCount - postOrderIndex} nodes are unreachable from the root. Following nodes have only weak retainers:`); const dumpNode = this.rootNode(); // Remove root from the result (last node in the array) and put it at the bottom of the stack so that it is // visited after all orphan nodes and their subgraphs. --postOrderIndex; stackTop = 0; stackNodes[0] = rootNodeOrdinal; stackCurrentEdge[0] = firstEdgeIndexes[rootNodeOrdinal + 1]; // no need to reiterate its edges for (let i = 0; i < nodeCount; ++i) { if (visited[i] || !this.hasOnlyWeakRetainers(i)) { continue; } // Add all nodes that have only weak retainers to traverse their subgraphs. stackNodes[++stackTop] = i; stackCurrentEdge[stackTop] = firstEdgeIndexes[i]; visited[i] = 1; dumpNode.nodeIndex = i * nodeFieldCount; const retainers = []; for (let it = dumpNode.retainers(); it.hasNext(); it.next()) { retainers.push(`${it.item().node().name()}@${it.item().node().id()}.${it.item().name()}`); } errors.addError(`${dumpNode.name()} @${dumpNode.id()} weak retainers: ${retainers.join(', ')}`); } console.warn(errors.toString()); } // If we already processed all orphan nodes that have only weak retainers and still have some orphans... if (postOrderIndex !== nodeCount) { const errors = new HeapSnapshotProblemReport('Still found ' + (nodeCount - postOrderIndex) + ' unreachable nodes in heap snapshot:'); const dumpNode = this.rootNode(); // Remove root from the result (last node in the array) and put it at the bottom of the stack so that it is // visited after all orphan nodes and their subgraphs. --postOrderIndex; for (let i = 0; i < nodeCount; ++i) { if (visited[i]) { continue; } dumpNode.nodeIndex = i * nodeFieldCount; errors.addError(dumpNode.name() + ' @' + dumpNode.id()); // Fix it by giving the node a postorder index anyway. nodeOrdinal2PostOrderIndex[i] = postOrderIndex; postOrderIndex2NodeOrdinal[postOrderIndex++] = i; } nodeOrdinal2PostOrderIndex[rootNodeOrdinal] = postOrderIndex; postOrderIndex2NodeOrdinal[postOrderIndex++] = rootNodeOrdinal; console.warn(errors.toString()); } return { postOrderIndex2NodeOrdinal: postOrderIndex2NodeOrdinal, nodeOrdinal2PostOrderIndex: nodeOrdinal2PostOrderIndex, }; } hasOnlyWeakRetainers(nodeOrdinal) { const edgeTypeOffset = this.edgeTypeOffset; const edgeWeakType = this.edgeWeakType; const edgeShortcutType = this.edgeShortcutType; const containmentEdges = this.containmentEdges; const retainingEdges = this.retainingEdges; const beginRetainerIndex = this.firstRetainerIndex[nodeOrdinal]; const endRetainerIndex = this.firstRetainerIndex[nodeOrdinal + 1]; for (let retainerIndex = beginRetainerIndex; retainerIndex < endRetainerIndex; ++retainerIndex) { const retainerEdgeIndex = retainingEdges[retainerIndex]; const retainerEdgeType = containmentEdges[retainerEdgeIndex + edgeTypeOffset]; if (retainerEdgeType !== edgeWeakType && retainerEdgeType !== edgeShortcutType) { return false; } } return true; } // The algorithm is based on the article: // K. Cooper, T. Harvey and K. Kennedy "A Simple, Fast Dominance Algorithm" // Softw. Pract. Exper. 4 (2001), pp. 1-10. buildDominatorTree(postOrderIndex2NodeOrdinal, nodeOrdinal2PostOrderIndex) { const nodeFieldCount = this.nodeFieldCount; const firstRetainerIndex = this.firstRetainerIndex; const retainingNodes = this.retainingNodes; const retainingEdges = this.retainingEdges; const edgeFieldsCount = this.edgeFieldsCount; const edgeTypeOffset = this.edgeTypeOffset; const edgeToNodeOffset = this.edgeToNodeOffset; const firstEdgeIndexes = this.firstEdgeIndexes; const containmentEdges = this.containmentEdges; const rootNodeIndex = this.rootNodeIndexInternal; const mapAndFlag = this.userObjectsMapAndFlag(); const flags = mapAndFlag ? mapAndFlag.map : null; const flag = mapAndFlag ? mapAndFlag.flag : 0; const nodesCount = postOrderIndex2NodeOrdinal.length; const rootPostOrderedIndex = nodesCount - 1; const noEntry = nodesCount; const dominators = new Uint32Array(nodesCount); for (let i = 0; i < rootPostOrderedIndex; ++i) { dominators[i] = noEntry; } dominators[rootPostOrderedIndex] = rootPostOrderedIndex; // The affected array is used to mark entries which dominators // have to be recalculated because of changes in their retainers. const affected = new Uint8Array(nodesCount); let nodeOrdinal; { // Mark the root direct children as affected. nodeOrdinal = this.rootNodeIndexInternal / nodeFieldCount; const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1]; for (let edgeIndex = firstEdgeIndexes[nodeOrdinal]; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) { const edgeType = containmentEdges[edgeIndex + edgeTypeOffset]; if (!this.isEssentialEdge(this.rootNodeIndexInternal, edgeType)) { continue; } const childNodeOrdinal = containmentEdges[edgeIndex + edgeToNodeOffset] / nodeFieldCount; affected[nodeOrdinal2PostOrderIndex[childNodeOrdinal]] = 1; } } let changed = true; while (changed) { changed = false; for (let postOrderIndex = rootPostOrderedIndex - 1; postOrderIndex >= 0; --postOrderIndex) { if (affected[postOrderIndex] === 0) { continue; } affected[postOrderIndex] = 0; // If dominator of the entry has already been set to root, // then it can't propagate any further. if (dominators[postOrderIndex] === rootPostOrderedIndex) { continue; } nodeOrdinal = postOrderIndex2NodeOrdinal[postOrderIndex]; const nodeFlag = !flags || (flags[nodeOrdinal] & flag); let newDominatorIndex = noEntry; const beginRetainerIndex = firstRetainerIndex[nodeOrdinal]; const endRetainerIndex = firstRetainerIndex[nodeOrdinal + 1]; let orphanNode = true; for (let retainerIndex = beginRetainerIndex; retainerIndex < endRetainerIndex; ++retainerIndex) { const retainerEdgeIndex = retainingEdges[