| /* |
| * Copyright (C) 2011 Google Inc. All rights reserved. |
| * Copyright (C) 2016 Apple 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. |
| */ |
| |
| // nodes |
| // [<0:id>, <1:size>, <2:classNameTableIndex>, <3:flags>] |
| const nodeFieldCount = 4; |
| const nodeIdOffset = 0; |
| const nodeSizeOffset = 1; |
| const nodeClassNameOffset = 2; |
| const nodeFlagsOffset = 3; |
| const gcDebuggingNodeFieldCount = 7; |
| |
| // node flags |
| const internalFlagsMask = (1 << 0); |
| const objectTypeMask = (1 << 1); |
| |
| // edges |
| // [<0:fromId>, <1:toId>, <2:typeTableIndex>, <3:edgeDataIndexOrEdgeNameIndex>] |
| const edgeFieldCount = 4; |
| const edgeFromIdOffset = 0; |
| const edgeToIdOffset = 1; |
| const edgeTypeOffset = 2; |
| const edgeDataOffset = 3; |
| |
| // Other constants. |
| const rootNodeIndex = 0; |
| const rootNodeOrdinal = 0; |
| const rootNodeIdentifier = 0; |
| |
| // Version Differences: |
| // - In Version 1, node[3] now named <flags> was the value 0 or 1 indicating not-internal or internal. |
| // - In Version 2, this became a bitmask so multiple flags could be included without modifying the size. |
| // |
| // Terminology: |
| // - `nodeIndex` is an index into the `nodes` list. |
| // - `nodeOrdinal` is the order of the node in the `nodes` list. (nodeIndex / nodeFieldCount). |
| // - `nodeIdentifier` is the node's id value. (nodes[nodeIndex + nodeIdOffset]). |
| // - `edgeIndex` is an index into the `edges` list. |
| // |
| // Lists: |
| // - _nodeOrdinalToFirstOutgoingEdge - `nodeOrdinal` to `edgeIndex` in `edges`. |
| // Iterate edges by walking `edges` (edgeFieldCount) and checking if fromIdentifier is current. |
| // - _nodeOrdinalToFirstIncomingEdge - `nodeOrdinal` to `incomingEdgeIndex` in `incomingEdges`. |
| // Iterate edges by walking `incomingEdges` until `nodeOrdinal+1`'s first incoming edge index. |
| // - _nodeOrdinalToDominatorNodeOrdinal - `nodeOrdinal` to `nodeOrdinal` of dominator. |
| // - _nodeOrdinalToRetainedSizes - `nodeOrdinal` to retain size value. |
| // - _nodeOrdinalIsDead - `nodeOrdinal` is dead or alive. |
| // |
| // Temporary Lists: |
| // - nodeOrdinalToPostOrderIndex - `nodeOrdinal` to a `postOrderIndex`. |
| // - postOrderIndexToNodeOrdinal - `postOrderIndex` to a `nodeOrdinal`. |
| |
| let nextSnapshotIdentifier = 1; |
| |
| HeapSnapshot = class HeapSnapshot |
| { |
| constructor(objectId, snapshotDataString, title = null, imported = false) |
| { |
| this._identifier = nextSnapshotIdentifier++; |
| this._objectId = objectId; |
| this._title = title; |
| this._imported = imported; |
| |
| let json = JSON.parse(snapshotDataString); |
| snapshotDataString = null; |
| |
| let {version, type, nodes, nodeClassNames, edges, edgeTypes, edgeNames} = json; |
| console.assert(version === 1 || version === 2, "Expect JavaScriptCore Heap Snapshot version 1 or 2"); |
| console.assert(!type || (type === "Inspector" || type === "GCDebugging"), "Expect an Inspector / GCDebugging Heap Snapshot"); |
| |
| this._nodeFieldCount = type === "GCDebugging" ? gcDebuggingNodeFieldCount : nodeFieldCount; |
| |
| this._nodes = nodes; |
| this._nodeCount = nodes.length / this._nodeFieldCount; |
| |
| this._edges = edges; |
| this._edgeCount = edges.length / edgeFieldCount; |
| |
| this._edgeTypesTable = edgeTypes; |
| this._edgeNamesTable = edgeNames; |
| this._nodeClassNamesTable = nodeClassNames; |
| |
| this._totalSize = 0; |
| this._nodeIdentifierToOrdinal = new Map; // <node identifier> => nodeOrdinal |
| this._lastNodeIdentifier = 0; |
| for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += this._nodeFieldCount) { |
| let nodeOrdinal = nodeIndex / this._nodeFieldCount; |
| let nodeIdentifier = nodes[nodeIndex + nodeIdOffset]; |
| this._nodeIdentifierToOrdinal.set(nodeIdentifier, nodeOrdinal); |
| this._totalSize += nodes[nodeIndex + nodeSizeOffset]; |
| if (nodeIdentifier > this._lastNodeIdentifier) |
| this._lastNodeIdentifier = nodeIdentifier; |
| } |
| |
| // FIXME: Replace toIdentifier and fromIdentifier in edges with nodeIndex to reduce hash lookups? |
| |
| this._nodeOrdinalToFirstOutgoingEdge = new Uint32Array(this._nodeCount); // nodeOrdinal => edgeIndex |
| this._buildOutgoingEdges(); |
| |
| this._nodeOrdinalToFirstIncomingEdge = new Uint32Array(this._nodeCount + 1); // nodeOrdinal => incomingNodes/incomingEdges index |
| this._incomingNodes = new Uint32Array(this._edgeCount); // from nodeOrdinals. |
| this._incomingEdges = new Uint32Array(this._edgeCount); // edgeIndex. |
| this._buildIncomingEdges(); |
| |
| let {nodeOrdinalToPostOrderIndex, postOrderIndexToNodeOrdinal} = this._buildPostOrderIndexes(); |
| |
| this._nodeOrdinalToDominatorNodeOrdinal = new Uint32Array(this._nodeCount); |
| this._nodeOrdinalIsGCRoot = new Uint8Array(this._nodeCount); |
| this._buildDominatorIndexes(nodeOrdinalToPostOrderIndex, postOrderIndexToNodeOrdinal); |
| |
| nodeOrdinalToPostOrderIndex = null; |
| |
| this._nodeOrdinalToRetainedSizes = new Uint32Array(this._nodeCount); |
| this._buildRetainedSizes(postOrderIndexToNodeOrdinal); |
| |
| postOrderIndexToNodeOrdinal = null; |
| |
| this._nodeOrdinalIsDead = new Uint8Array(this._nodeCount); |
| |
| let {liveSize, categories} = HeapSnapshot.updateCategoriesAndMetadata(this); |
| this._liveSize = liveSize; |
| this._categories = categories; |
| } |
| |
| // Static |
| |
| static updateCategoriesAndMetadata(snapshot, allowNodeIdentifierCallback) |
| { |
| let liveSize = 0; |
| let categories = {}; |
| |
| let nodes = snapshot._nodes; |
| let nodeClassNamesTable = snapshot._nodeClassNamesTable; |
| let nodeOrdinalToRetainedSizes = snapshot._nodeOrdinalToRetainedSizes; |
| let nodeOrdinalIsDead = snapshot._nodeOrdinalIsDead; |
| |
| // Skip the <root> node. |
| let firstNodeIndex = snapshot._nodeFieldCount; |
| let firstNodeOrdinal = 1; |
| for (let nodeIndex = firstNodeIndex, nodeOrdinal = firstNodeOrdinal; nodeIndex < nodes.length; nodeIndex += snapshot._nodeFieldCount, nodeOrdinal++) { |
| if (allowNodeIdentifierCallback && !allowNodeIdentifierCallback(nodes[nodeIndex + nodeIdOffset])) |
| continue; |
| |
| let classNameTableIndex = nodes[nodeIndex + nodeClassNameOffset]; |
| let className = nodeClassNamesTable[classNameTableIndex]; |
| let size = nodes[nodeIndex + nodeSizeOffset]; |
| let retainedSize = nodeOrdinalToRetainedSizes[nodeOrdinal]; |
| let flags = nodes[nodeIndex + nodeFlagsOffset]; |
| let dead = nodeOrdinalIsDead[nodeOrdinal] ? true : false; |
| |
| let category = categories[className]; |
| if (!category) |
| category = categories[className] = {className, size: 0, retainedSize: 0, count: 0, internalCount: 0, deadCount: 0, objectCount: 0}; |
| |
| category.size += size; |
| category.retainedSize += retainedSize; |
| category.count += 1; |
| if (flags & internalFlagsMask) |
| category.internalCount += 1; |
| if (flags & objectTypeMask) |
| category.objectCount += 1; |
| if (dead) |
| category.deadCount += 1; |
| else |
| liveSize += size; |
| } |
| |
| return {liveSize, categories}; |
| } |
| |
| static allocationBucketCounts(snapshot, bucketSizes, allowNodeIdentifierCallback) |
| { |
| let counts = new Array(bucketSizes.length + 1); |
| let remainderBucket = counts.length - 1; |
| counts.fill(0); |
| |
| let nodes = snapshot._nodes; |
| |
| // Skip the <root> node. |
| let firstNodeIndex = snapshot._nodeFieldCount; |
| |
| outer: |
| for (let nodeIndex = firstNodeIndex; nodeIndex < nodes.length; nodeIndex += snapshot._nodeFieldCount) { |
| if (allowNodeIdentifierCallback && !allowNodeIdentifierCallback(nodes[nodeIndex + nodeIdOffset])) |
| continue; |
| |
| let size = nodes[nodeIndex + nodeSizeOffset]; |
| for (let i = 0; i < bucketSizes.length; ++i) { |
| if (size < bucketSizes[i]) { |
| counts[i]++; |
| continue outer; |
| } |
| } |
| counts[remainderBucket]++; |
| } |
| |
| return counts; |
| } |
| |
| static instancesWithClassName(snapshot, className, allowNodeIdentifierCallback) |
| { |
| let instances = []; |
| |
| let nodes = snapshot._nodes; |
| let nodeClassNamesTable = snapshot._nodeClassNamesTable; |
| |
| // Skip the <root> node. |
| let firstNodeIndex = snapshot._nodeFieldCount; |
| let firstNodeOrdinal = 1; |
| for (let nodeIndex = firstNodeIndex, nodeOrdinal = firstNodeOrdinal; nodeIndex < nodes.length; nodeIndex += snapshot._nodeFieldCount, nodeOrdinal++) { |
| if (allowNodeIdentifierCallback && !allowNodeIdentifierCallback(nodes[nodeIndex + nodeIdOffset])) |
| continue; |
| |
| let classNameTableIndex = nodes[nodeIndex + nodeClassNameOffset]; |
| if (nodeClassNamesTable[classNameTableIndex] === className) |
| instances.push(nodeIndex); |
| } |
| |
| return instances.map(snapshot.serializeNode, snapshot); |
| } |
| |
| // Worker Methods |
| |
| allocationBucketCounts(bucketSizes) |
| { |
| return HeapSnapshot.allocationBucketCounts(this, bucketSizes); |
| } |
| |
| instancesWithClassName(className) |
| { |
| return HeapSnapshot.instancesWithClassName(this, className); |
| } |
| |
| update() |
| { |
| return HeapSnapshot.updateCategoriesAndMetadata(this); |
| } |
| |
| nodeWithIdentifier(nodeIdentifier) |
| { |
| let nodeOrdinal = this._nodeIdentifierToOrdinal.get(nodeIdentifier); |
| let nodeIndex = nodeOrdinal * this._nodeFieldCount; |
| return this.serializeNode(nodeIndex); |
| } |
| |
| shortestGCRootPath(nodeIdentifier) |
| { |
| // Returns an array from this node to a gcRoot node. |
| // E.g. [Node (target), Edge, Node, Edge, Node (root)]. |
| // Internal nodes are avoided, so if the path is empty this |
| // node is either a gcRoot or only reachable via Internal nodes. |
| |
| let paths = this._determineGCRootPaths(nodeIdentifier); |
| if (!paths.length) |
| return []; |
| |
| paths.sort((a, b) => a.length - b.length); |
| |
| let shortestPathWithGlobalObject = null; |
| for (let path of paths) { |
| let lastNodeIndex = path[path.length - 1].node; |
| if (this._isNodeGlobalObject(lastNodeIndex)) { |
| shortestPathWithGlobalObject = path; |
| break; |
| } |
| } |
| |
| let shortestPath = shortestPathWithGlobalObject || paths[0]; |
| console.assert("node" in shortestPath[0], "Path should start with a node"); |
| console.assert("node" in shortestPath[shortestPath.length - 1], "Path should end with a node"); |
| |
| return shortestPath.map((component) => { |
| if (component.node) |
| return this.serializeNode(component.node); |
| return this.serializeEdge(component.edge); |
| }); |
| } |
| |
| dominatedNodes(nodeIdentifier) |
| { |
| let dominatedNodes = []; |
| |
| let targetNodeOrdinal = this._nodeIdentifierToOrdinal.get(nodeIdentifier); |
| for (let nodeOrdinal = 0; nodeOrdinal < this._nodeCount; ++nodeOrdinal) { |
| if (this._nodeOrdinalToDominatorNodeOrdinal[nodeOrdinal] === targetNodeOrdinal) |
| dominatedNodes.push(nodeOrdinal * this._nodeFieldCount); |
| } |
| |
| return dominatedNodes.map(this.serializeNode, this); |
| } |
| |
| retainedNodes(nodeIdentifier) |
| { |
| let retainedNodes = []; |
| let edges = []; |
| |
| let nodeOrdinal = this._nodeIdentifierToOrdinal.get(nodeIdentifier); |
| let edgeIndex = this._nodeOrdinalToFirstOutgoingEdge[nodeOrdinal]; |
| for (; this._edges[edgeIndex + edgeFromIdOffset] === nodeIdentifier; edgeIndex += edgeFieldCount) { |
| let toNodeIdentifier = this._edges[edgeIndex + edgeToIdOffset]; |
| let toNodeOrdinal = this._nodeIdentifierToOrdinal.get(toNodeIdentifier); |
| let toNodeIndex = toNodeOrdinal * this._nodeFieldCount; |
| retainedNodes.push(toNodeIndex); |
| edges.push(edgeIndex); |
| } |
| |
| return { |
| retainedNodes: retainedNodes.map(this.serializeNode, this), |
| edges: edges.map(this.serializeEdge, this), |
| }; |
| } |
| |
| retainers(nodeIdentifier) |
| { |
| let retainers = []; |
| let edges = []; |
| |
| let nodeOrdinal = this._nodeIdentifierToOrdinal.get(nodeIdentifier); |
| let incomingEdgeIndex = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal]; |
| let incomingEdgeIndexEnd = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal + 1]; |
| for (let edgeIndex = incomingEdgeIndex; edgeIndex < incomingEdgeIndexEnd; ++edgeIndex) { |
| let fromNodeOrdinal = this._incomingNodes[edgeIndex]; |
| let fromNodeIndex = fromNodeOrdinal * this._nodeFieldCount; |
| retainers.push(fromNodeIndex); |
| edges.push(this._incomingEdges[edgeIndex]); |
| } |
| |
| return { |
| retainers: retainers.map(this.serializeNode, this), |
| edges: edges.map(this.serializeEdge, this), |
| }; |
| } |
| |
| updateDeadNodesAndGatherCollectionData(snapshots) |
| { |
| console.assert(!this._imported, "Should never use an imported snapshot to modify snapshots"); |
| console.assert(snapshots.every((x) => !x._imported), "Should never modify nodes of imported snapshots"); |
| |
| let previousSnapshotIndex = snapshots.indexOf(this) - 1; |
| let previousSnapshot = snapshots[previousSnapshotIndex]; |
| if (!previousSnapshot) |
| return null; |
| |
| let lastNodeIdentifier = previousSnapshot._lastNodeIdentifier; |
| |
| // All of the node identifiers that could have existed prior to this snapshot. |
| let known = new Map; |
| for (let nodeIndex = 0; nodeIndex < this._nodes.length; nodeIndex += this._nodeFieldCount) { |
| let nodeIdentifier = this._nodes[nodeIndex + nodeIdOffset]; |
| if (nodeIdentifier > lastNodeIdentifier) |
| continue; |
| known.set(nodeIdentifier, nodeIndex); |
| } |
| |
| // Determine which node identifiers have since been deleted. |
| let collectedNodesList = []; |
| for (let nodeIndex = 0; nodeIndex < previousSnapshot._nodes.length; nodeIndex += this._nodeFieldCount) { |
| let nodeIdentifier = previousSnapshot._nodes[nodeIndex + nodeIdOffset]; |
| let wasDeleted = !known.has(nodeIdentifier); |
| if (wasDeleted) |
| collectedNodesList.push(nodeIdentifier); |
| } |
| |
| // Update dead nodes in previous snapshots. |
| let affectedSnapshots = []; |
| for (let snapshot of snapshots) { |
| if (snapshot === this) |
| break; |
| if (snapshot._markDeadNodes(collectedNodesList)) |
| affectedSnapshots.push(snapshot._identifier); |
| } |
| |
| // Convert list to a map. |
| let collectedNodes = {}; |
| for (let i = 0; i < collectedNodesList.length; ++i) |
| collectedNodes[collectedNodesList[i]] = true; |
| |
| return { |
| collectedNodes, |
| affectedSnapshots, |
| }; |
| } |
| |
| // Public |
| |
| serialize() |
| { |
| return { |
| identifier: this._identifier, |
| title: this._title, |
| totalSize: this._totalSize, |
| totalObjectCount: this._nodeCount - 1, // <root>. |
| liveSize: this._liveSize, |
| categories: this._categories, |
| imported: this._imported, |
| }; |
| } |
| |
| serializeNode(nodeIndex) |
| { |
| console.assert((nodeIndex % this._nodeFieldCount) === 0, "Invalid nodeIndex to serialize"); |
| |
| let nodeIdentifier = this._nodes[nodeIndex + nodeIdOffset]; |
| let nodeOrdinal = nodeIndex / this._nodeFieldCount; |
| let edgeIndex = this._nodeOrdinalToFirstOutgoingEdge[nodeOrdinal]; |
| let hasChildren = this._edges[edgeIndex + edgeFromIdOffset] === nodeIdentifier; |
| let nodeFlags = this._nodes[nodeIndex + nodeFlagsOffset]; |
| |
| let dominatorNodeOrdinal = this._nodeOrdinalToDominatorNodeOrdinal[nodeOrdinal]; |
| let dominatorNodeIndex = dominatorNodeOrdinal * this._nodeFieldCount; |
| let dominatorNodeIdentifier = this._nodes[dominatorNodeIndex + nodeIdOffset]; |
| |
| return { |
| id: nodeIdentifier, |
| className: this._nodeClassNamesTable[this._nodes[nodeIndex + nodeClassNameOffset]], |
| size: this._nodes[nodeIndex + nodeSizeOffset], |
| retainedSize: this._nodeOrdinalToRetainedSizes[nodeOrdinal], |
| internal: nodeFlags & internalFlagsMask ? true : false, |
| isObjectType: nodeFlags & objectTypeMask ? true : false, |
| gcRoot: this._nodeOrdinalIsGCRoot[nodeOrdinal] ? true : false, |
| dead: this._nodeOrdinalIsDead[nodeOrdinal] ? true : false, |
| dominatorNodeIdentifier, |
| hasChildren, |
| }; |
| } |
| |
| serializeEdge(edgeIndex) |
| { |
| console.assert((edgeIndex % edgeFieldCount) === 0, "Invalid edgeIndex to serialize"); |
| |
| let edgeType = this._edgeTypesTable[this._edges[edgeIndex + edgeTypeOffset]]; |
| let edgeData = this._edges[edgeIndex + edgeDataOffset]; |
| switch (edgeType) { |
| case "Internal": |
| // edgeData can be ignored. |
| break; |
| case "Property": |
| case "Variable": |
| // edgeData is a table index. |
| edgeData = this._edgeNamesTable[edgeData]; |
| break; |
| case "Index": |
| // edgeData is the index. |
| break; |
| default: |
| console.error("Unexpected edge type: " + edgeType); |
| break; |
| } |
| |
| return { |
| from: this._edges[edgeIndex + edgeFromIdOffset], |
| to: this._edges[edgeIndex + edgeToIdOffset], |
| type: edgeType, |
| data: edgeData, |
| }; |
| } |
| |
| // Private |
| |
| _buildOutgoingEdges() |
| { |
| let lastFromIdentifier = -1; |
| for (let edgeIndex = 0; edgeIndex < this._edges.length; edgeIndex += edgeFieldCount) { |
| let fromIdentifier = this._edges[edgeIndex + edgeFromIdOffset]; |
| console.assert(lastFromIdentifier <= fromIdentifier, "Edge list should be ordered by from node identifier"); |
| if (fromIdentifier !== lastFromIdentifier) { |
| let nodeOrdinal = this._nodeIdentifierToOrdinal.get(fromIdentifier); |
| this._nodeOrdinalToFirstOutgoingEdge[nodeOrdinal] = edgeIndex; |
| lastFromIdentifier = fromIdentifier; |
| } |
| } |
| } |
| |
| _buildIncomingEdges() |
| { |
| // First calculate the count of incoming edges for each node. |
| for (let edgeIndex = 0; edgeIndex < this._edges.length; edgeIndex += edgeFieldCount) { |
| let toIdentifier = this._edges[edgeIndex + edgeToIdOffset]; |
| let toNodeOrdinal = this._nodeIdentifierToOrdinal.get(toIdentifier); |
| this._nodeOrdinalToFirstIncomingEdge[toNodeOrdinal]++; |
| } |
| |
| // Replace the counts with what will be the resulting index by running up the counts. |
| // Store the counts in what will be the edges list to use when placing edges in the list. |
| let runningFirstIndex = 0; |
| for (let nodeOrdinal = 0; nodeOrdinal < this._nodeCount; ++nodeOrdinal) { |
| let count = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal]; |
| this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal] = runningFirstIndex; |
| this._incomingNodes[runningFirstIndex] = count; |
| runningFirstIndex += count; |
| } |
| |
| // Fill in the incoming edges list. Use the count as an offset when placing edges in the list. |
| for (let edgeIndex = 0; edgeIndex < this._edges.length; edgeIndex += edgeFieldCount) { |
| let fromIdentifier = this._edges[edgeIndex + edgeFromIdOffset]; |
| let fromNodeOrdinal = this._nodeIdentifierToOrdinal.get(fromIdentifier); |
| let toIdentifier = this._edges[edgeIndex + edgeToIdOffset]; |
| let toNodeOrdinal = this._nodeIdentifierToOrdinal.get(toIdentifier); |
| |
| let firstIncomingEdgeIndex = this._nodeOrdinalToFirstIncomingEdge[toNodeOrdinal]; |
| console.assert(this._incomingNodes[firstIncomingEdgeIndex] > 0, "Should be expecting edges for this node"); |
| let countAsOffset = this._incomingNodes[firstIncomingEdgeIndex]--; |
| let index = firstIncomingEdgeIndex + countAsOffset - 1; |
| this._incomingNodes[index] = fromNodeOrdinal; |
| this._incomingEdges[index] = edgeIndex; |
| } |
| |
| // Duplicate value on the end. Incoming edge iteration walks firstIncomingEdge(ordinal) to firstIncomingEdge(ordinal+1). |
| this._nodeOrdinalToFirstIncomingEdge[this._nodeCount] = this._nodeOrdinalToFirstIncomingEdge[this._nodeCount - 1]; |
| } |
| |
| _buildPostOrderIndexes() |
| { |
| let postOrderIndex = 0; |
| let nodeOrdinalToPostOrderIndex = new Uint32Array(this._nodeCount); |
| let postOrderIndexToNodeOrdinal = new Uint32Array(this._nodeCount); |
| |
| let stackNodes = new Uint32Array(this._nodeCount); // nodeOrdinal. |
| let stackEdges = new Uint32Array(this._nodeCount); // edgeIndex. |
| let visited = new Uint8Array(this._nodeCount); |
| |
| let stackTop = 0; |
| stackNodes[stackTop] = rootNodeOrdinal; |
| stackEdges[stackTop] = this._nodeOrdinalToFirstOutgoingEdge[rootNodeOrdinal]; |
| |
| while (stackTop >= 0) { |
| let nodeOrdinal = stackNodes[stackTop]; |
| let nodeIdentifier = this._nodes[(nodeOrdinal * this._nodeFieldCount) + nodeIdOffset]; |
| let edgeIndex = stackEdges[stackTop]; |
| |
| if (this._edges[edgeIndex + edgeFromIdOffset] === nodeIdentifier) { |
| // Prepare the next child for the current node. |
| stackEdges[stackTop] += edgeFieldCount; |
| |
| let toIdentifier = this._edges[edgeIndex + edgeToIdOffset]; |
| let toNodeOrdinal = this._nodeIdentifierToOrdinal.get(toIdentifier); |
| if (visited[toNodeOrdinal]) |
| continue; |
| |
| // Child. |
| stackTop++; |
| stackNodes[stackTop] = toNodeOrdinal; |
| stackEdges[stackTop] = this._nodeOrdinalToFirstOutgoingEdge[toNodeOrdinal]; |
| visited[toNodeOrdinal] = 1; |
| } else { |
| // Self. |
| nodeOrdinalToPostOrderIndex[nodeOrdinal] = postOrderIndex; |
| postOrderIndexToNodeOrdinal[postOrderIndex] = nodeOrdinal; |
| postOrderIndex++; |
| stackTop--; |
| } |
| } |
| |
| // Unvisited nodes. |
| // This can happen if the parent node was disallowed on the backend, but other nodes |
| // that were only referenced from that disallowed node were eventually allowed because |
| // they may be generic system objects. Give these nodes a postOrderIndex anyways. |
| if (postOrderIndex !== this._nodeCount) { |
| // Root was the last node visited. Revert assigning it an index, add it back at the end. |
| postOrderIndex--; |
| |
| // Visit unvisited nodes. |
| for (let nodeOrdinal = 1; nodeOrdinal < this._nodeCount; ++nodeOrdinal) { |
| if (visited[nodeOrdinal]) |
| continue; |
| nodeOrdinalToPostOrderIndex[nodeOrdinal] = postOrderIndex; |
| postOrderIndexToNodeOrdinal[postOrderIndex] = nodeOrdinal; |
| postOrderIndex++; |
| } |
| |
| // Visit root again. |
| nodeOrdinalToPostOrderIndex[rootNodeOrdinal] = postOrderIndex; |
| postOrderIndexToNodeOrdinal[postOrderIndex] = rootNodeOrdinal; |
| postOrderIndex++; |
| } |
| |
| console.assert(postOrderIndex === this._nodeCount, "All nodes were visited"); |
| console.assert(nodeOrdinalToPostOrderIndex[rootNodeOrdinal] === this._nodeCount - 1, "Root node should have the last possible postOrderIndex"); |
| |
| return {nodeOrdinalToPostOrderIndex, postOrderIndexToNodeOrdinal}; |
| } |
| |
| _buildDominatorIndexes(nodeOrdinalToPostOrderIndex, postOrderIndexToNodeOrdinal) |
| { |
| // The algorithm is based on the article: |
| // K. Cooper, T. Harvey and K. Kennedy "A Simple, Fast Dominance Algorithm" |
| |
| let rootPostOrderIndex = this._nodeCount - 1; |
| let noEntry = this._nodeCount; |
| |
| let affected = new Uint8Array(this._nodeCount); |
| let dominators = new Uint32Array(this._nodeCount); |
| |
| // Initialize with unset value. |
| dominators.fill(noEntry); |
| |
| // Mark the root's dominator value. |
| dominators[rootPostOrderIndex] = rootPostOrderIndex; |
| |
| // Affect the root's children. Also use this opportunity to mark them as GC roots. |
| let rootEdgeIndex = this._nodeOrdinalToFirstOutgoingEdge[rootNodeOrdinal]; |
| for (let edgeIndex = rootEdgeIndex; this._edges[edgeIndex + edgeFromIdOffset] === rootNodeIdentifier; edgeIndex += edgeFieldCount) { |
| let toIdentifier = this._edges[edgeIndex + edgeToIdOffset]; |
| let toNodeOrdinal = this._nodeIdentifierToOrdinal.get(toIdentifier); |
| let toPostOrderIndex = nodeOrdinalToPostOrderIndex[toNodeOrdinal]; |
| affected[toPostOrderIndex] = 1; |
| this._nodeOrdinalIsGCRoot[toNodeOrdinal] = 1; |
| } |
| |
| let changed = true; |
| while (changed) { |
| changed = false; |
| |
| for (let postOrderIndex = rootPostOrderIndex - 1; postOrderIndex >= 0; --postOrderIndex) { |
| if (!affected[postOrderIndex]) |
| continue; |
| affected[postOrderIndex] = 0; |
| |
| // The dominator is already the root, nothing to do. |
| if (dominators[postOrderIndex] === rootPostOrderIndex) |
| continue; |
| |
| let newDominatorIndex = noEntry; |
| let nodeOrdinal = postOrderIndexToNodeOrdinal[postOrderIndex]; |
| let incomingEdgeIndex = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal]; |
| let incomingEdgeIndexEnd = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal + 1]; |
| for (let edgeIndex = incomingEdgeIndex; edgeIndex < incomingEdgeIndexEnd; ++edgeIndex) { |
| let fromNodeOrdinal = this._incomingNodes[edgeIndex]; |
| let fromPostOrderIndex = nodeOrdinalToPostOrderIndex[fromNodeOrdinal]; |
| if (dominators[fromPostOrderIndex] !== noEntry) { |
| if (newDominatorIndex === noEntry) |
| newDominatorIndex = fromPostOrderIndex; |
| else { |
| while (fromPostOrderIndex !== newDominatorIndex) { |
| while (fromPostOrderIndex < newDominatorIndex) |
| fromPostOrderIndex = dominators[fromPostOrderIndex]; |
| while (newDominatorIndex < fromPostOrderIndex) |
| newDominatorIndex = dominators[newDominatorIndex]; |
| } |
| } |
| } |
| if (newDominatorIndex === rootPostOrderIndex) |
| break; |
| } |
| |
| // Changed. Affect children. |
| if (newDominatorIndex !== noEntry && dominators[postOrderIndex] !== newDominatorIndex) { |
| dominators[postOrderIndex] = newDominatorIndex; |
| changed = true; |
| |
| let outgoingEdgeIndex = this._nodeOrdinalToFirstOutgoingEdge[nodeOrdinal]; |
| let nodeIdentifier = this._nodes[(nodeOrdinal * this._nodeFieldCount) + nodeIdOffset]; |
| for (let edgeIndex = outgoingEdgeIndex; this._edges[edgeIndex + edgeFromIdOffset] === nodeIdentifier; edgeIndex += edgeFieldCount) { |
| let toNodeIdentifier = this._edges[edgeIndex + edgeToIdOffset]; |
| let toNodeOrdinal = this._nodeIdentifierToOrdinal.get(toNodeIdentifier); |
| let toNodePostOrder = nodeOrdinalToPostOrderIndex[toNodeOrdinal]; |
| affected[toNodePostOrder] = 1; |
| } |
| } |
| } |
| } |
| |
| for (let postOrderIndex = 0; postOrderIndex < this._nodeCount; ++postOrderIndex) { |
| let nodeOrdinal = postOrderIndexToNodeOrdinal[postOrderIndex]; |
| let dominatorNodeOrdinal = postOrderIndexToNodeOrdinal[dominators[postOrderIndex]]; |
| this._nodeOrdinalToDominatorNodeOrdinal[nodeOrdinal] = dominatorNodeOrdinal; |
| } |
| } |
| |
| _buildRetainedSizes(postOrderIndexToNodeOrdinal) |
| { |
| // Self size. |
| for (let nodeIndex = 0, nodeOrdinal = 0; nodeOrdinal < this._nodeCount; nodeIndex += this._nodeFieldCount, nodeOrdinal++) |
| this._nodeOrdinalToRetainedSizes[nodeOrdinal] = this._nodes[nodeIndex + nodeSizeOffset]; |
| |
| // Attribute size to dominator. |
| for (let postOrderIndex = 0; postOrderIndex < this._nodeCount - 1; ++postOrderIndex) { |
| let nodeOrdinal = postOrderIndexToNodeOrdinal[postOrderIndex]; |
| let nodeRetainedSize = this._nodeOrdinalToRetainedSizes[nodeOrdinal]; |
| let dominatorNodeOrdinal = this._nodeOrdinalToDominatorNodeOrdinal[nodeOrdinal]; |
| this._nodeOrdinalToRetainedSizes[dominatorNodeOrdinal] += nodeRetainedSize; |
| } |
| } |
| |
| _markDeadNodes(collectedNodesList) |
| { |
| let affected = false; |
| |
| for (let i = 0; i < collectedNodesList.length; ++i) { |
| let nodeIdentifier = collectedNodesList[i]; |
| if (nodeIdentifier > this._lastNodeIdentifier) |
| continue; |
| let nodeOrdinal = this._nodeIdentifierToOrdinal.get(nodeIdentifier); |
| this._nodeOrdinalIsDead[nodeOrdinal] = 1; |
| affected = true; |
| } |
| |
| return affected; |
| } |
| |
| _isNodeGlobalObject(nodeIndex) |
| { |
| let className = this._nodeClassNamesTable[this._nodes[nodeIndex + nodeClassNameOffset]]; |
| return className === "Window" |
| || className === "JSWindowProxy" |
| || className === "GlobalObject"; |
| } |
| |
| _determineGCRootPaths(nodeIdentifier) |
| { |
| let targetNodeOrdinal = this._nodeIdentifierToOrdinal.get(nodeIdentifier); |
| |
| if (this._nodeOrdinalIsGCRoot[targetNodeOrdinal]) |
| return []; |
| |
| // FIXME: Array push/pop can affect performance here, but in practice it hasn't been an issue. |
| |
| let gcRootPaths = []; |
| let visited = new Uint8Array(this._nodeCount); |
| |
| let pathsBeingProcessed = [ |
| { |
| currentPath: [], |
| nodeOrdinal: targetNodeOrdinal, |
| }, |
| ]; |
| for (let i = 0; i < pathsBeingProcessed.length; ++i) { |
| let {currentPath, nodeOrdinal} = pathsBeingProcessed[i]; |
| |
| // Rather than use `Array.prototype.unshift`, which may be very expensive, keep track of |
| // the "current" position as `i` and "delete" the values already processed by clearing |
| // the value at that index. |
| pathsBeingProcessed[i] = undefined; |
| |
| if (this._nodeOrdinalIsGCRoot[nodeOrdinal]) { |
| let fullPath = currentPath.slice(); |
| let nodeIndex = nodeOrdinal * this._nodeFieldCount; |
| fullPath.push({node: nodeIndex}); |
| gcRootPaths.push(fullPath); |
| continue; |
| } |
| |
| if (visited[nodeOrdinal]) |
| continue; |
| |
| visited[nodeOrdinal] = 1; |
| |
| let nodeIndex = nodeOrdinal * this._nodeFieldCount; |
| currentPath.push({node: nodeIndex}); |
| |
| // Loop in reverse order because edges were added in reverse order. |
| // It doesn't particularly matter other then consistency with previous code. |
| let incomingEdgeIndexStart = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal]; |
| let incomingEdgeIndexEnd = this._nodeOrdinalToFirstIncomingEdge[nodeOrdinal + 1]; |
| for (let incomingEdgeIndex = incomingEdgeIndexEnd - 1; incomingEdgeIndex >= incomingEdgeIndexStart; --incomingEdgeIndex) { |
| let fromNodeOrdinal = this._incomingNodes[incomingEdgeIndex]; |
| let fromNodeIndex = fromNodeOrdinal * this._nodeFieldCount; |
| let fromNodeIsInternal = this._nodes[fromNodeIndex + nodeFlagsOffset] & internalFlagsMask; |
| if (fromNodeIsInternal) |
| continue; |
| |
| let newPath = currentPath.slice(); |
| newPath.push({edge: this._incomingEdges[incomingEdgeIndex]}); |
| pathsBeingProcessed.push({currentPath: newPath, nodeOrdinal: fromNodeOrdinal}); |
| } |
| } |
| |
| return gcRootPaths; |
| } |
| }; |
| |
| HeapSnapshotDiff = class HeapSnapshotDiff |
| { |
| constructor(objectId, snapshot1, snapshot2) |
| { |
| this._objectId = objectId; |
| |
| this._snapshot1 = snapshot1; |
| this._snapshot2 = snapshot2; |
| |
| this._totalSize = 0; |
| this._addedNodeIdentifiers = new Set; |
| |
| let known = new Map; |
| for (let nodeIndex = 0; nodeIndex < this._snapshot1._nodes.length; nodeIndex += nodeFieldCount) { |
| let nodeIdentifier = this._snapshot1._nodes[nodeIndex + nodeIdOffset]; |
| known.set(nodeIdentifier, nodeIndex); |
| } |
| |
| for (let nodeIndex = 0; nodeIndex < this._snapshot2._nodes.length; nodeIndex += nodeFieldCount) { |
| let nodeIdentifier = this._snapshot2._nodes[nodeIndex + nodeIdOffset]; |
| let existed = known.delete(nodeIdentifier); |
| if (!existed) { |
| this._addedNodeIdentifiers.add(nodeIdentifier); |
| this._totalSize += this._snapshot2._nodes[nodeIndex + nodeSizeOffset]; |
| } |
| } |
| |
| let {liveSize, categories} = HeapSnapshot.updateCategoriesAndMetadata(this._snapshot2, (nodeIdentifier) => this._addedNodeIdentifiers.has(nodeIdentifier)); |
| this._categories = categories; |
| } |
| |
| // Worker Methods |
| |
| allocationBucketCounts(bucketSizes) |
| { |
| return HeapSnapshot.allocationBucketCounts(this._snapshot2, bucketSizes, (nodeIdentifier) => this._addedNodeIdentifiers.has(nodeIdentifier)); |
| } |
| |
| instancesWithClassName(className) |
| { |
| return HeapSnapshot.instancesWithClassName(this._snapshot2, className, (nodeIdentifier) => this._addedNodeIdentifiers.has(nodeIdentifier)); |
| } |
| |
| update() |
| { |
| return HeapSnapshot.updateCategoriesAndMetadata(this._snapshot2, (nodeIdentifier) => this._addedNodeIdentifiers.has(nodeIdentifier)); |
| } |
| |
| nodeWithIdentifier(nodeIdentifier) { return this._snapshot2.nodeWithIdentifier(nodeIdentifier); } |
| shortestGCRootPath(nodeIdentifier) { return this._snapshot2.shortestGCRootPath(nodeIdentifier); } |
| dominatedNodes(nodeIdentifier) { return this._snapshot2.dominatedNodes(nodeIdentifier); } |
| retainedNodes(nodeIdentifier) { return this._snapshot2.retainedNodes(nodeIdentifier); } |
| retainers(nodeIdentifier) { return this._snapshot2.retainers(nodeIdentifier); } |
| |
| // Public |
| |
| serialize() |
| { |
| return { |
| snapshot1: this._snapshot1.serialize(), |
| snapshot2: this._snapshot2.serialize(), |
| totalSize: this._totalSize, |
| totalObjectCount: this._addedNodeIdentifiers.size, |
| categories: this._categories, |
| }; |
| } |
| }; |