UNPKG

chrome-devtools-frontend

Version:
1,397 lines (1,208 loc) 147 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/prefer-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'; import type {HeapSnapshotWorkerDispatcher} from './HeapSnapshotWorkerDispatcher.js'; export interface HeapSnapshotItem { itemIndex(): number; serialize(): Object; } export class HeapSnapshotEdge implements HeapSnapshotItem { snapshot: HeapSnapshot; protected readonly edges: Platform.TypedArrayUtilities.BigUint32Array; edgeIndex: number; constructor(snapshot: HeapSnapshot, edgeIndex?: number) { this.snapshot = snapshot; this.edges = snapshot.containmentEdges; this.edgeIndex = edgeIndex || 0; } clone(): HeapSnapshotEdge { return new HeapSnapshotEdge(this.snapshot, this.edgeIndex); } hasStringName(): boolean { throw new Error('Not implemented'); } name(): string { throw new Error('Not implemented'); } node(): HeapSnapshotNode { return this.snapshot.createNode(this.nodeIndex()); } nodeIndex(): number { if (typeof this.snapshot.edgeToNodeOffset === 'undefined') { throw new Error('edgeToNodeOffset is undefined'); } return this.edges.getValue(this.edgeIndex + this.snapshot.edgeToNodeOffset); } toString(): string { return 'HeapSnapshotEdge: ' + this.name(); } type(): string { return this.snapshot.edgeTypes[this.rawType()]; } itemIndex(): number { return this.edgeIndex; } serialize(): HeapSnapshotModel.HeapSnapshotModel.Edge { return new HeapSnapshotModel.HeapSnapshotModel.Edge( this.name(), this.node().serialize(), this.type(), this.edgeIndex); } rawType(): number { if (typeof this.snapshot.edgeTypeOffset === 'undefined') { throw new Error('edgeTypeOffset is undefined'); } return this.edges.getValue(this.edgeIndex + this.snapshot.edgeTypeOffset); } isInternal(): boolean { throw new Error('Not implemented'); } isInvisible(): boolean { throw new Error('Not implemented'); } isWeak(): boolean { throw new Error('Not implemented'); } getValueForSorting(_fieldName: string): number { throw new Error('Not implemented'); } nameIndex(): number { throw new Error('Not implemented'); } } export interface HeapSnapshotItemIterator { hasNext(): boolean; item(): HeapSnapshotItem; next(): void; } export interface HeapSnapshotItemIndexProvider { itemForIndex(newIndex: number): HeapSnapshotItem; } export class HeapSnapshotNodeIndexProvider implements HeapSnapshotItemIndexProvider { #node: HeapSnapshotNode; constructor(snapshot: HeapSnapshot) { this.#node = snapshot.createNode(); } itemForIndex(index: number): HeapSnapshotNode { this.#node.nodeIndex = index; return this.#node; } } export class HeapSnapshotEdgeIndexProvider implements HeapSnapshotItemIndexProvider { #edge: JSHeapSnapshotEdge; constructor(snapshot: HeapSnapshot) { this.#edge = snapshot.createEdge(0); } itemForIndex(index: number): HeapSnapshotEdge { this.#edge.edgeIndex = index; return this.#edge; } } export class HeapSnapshotRetainerEdgeIndexProvider implements HeapSnapshotItemIndexProvider { readonly #retainerEdge: JSHeapSnapshotRetainerEdge; constructor(snapshot: HeapSnapshot) { this.#retainerEdge = snapshot.createRetainingEdge(0); } itemForIndex(index: number): HeapSnapshotRetainerEdge { this.#retainerEdge.setRetainerIndex(index); return this.#retainerEdge; } } export class HeapSnapshotEdgeIterator implements HeapSnapshotItemIterator { readonly #sourceNode: HeapSnapshotNode; edge: JSHeapSnapshotEdge; constructor(node: HeapSnapshotNode) { this.#sourceNode = node; this.edge = node.snapshot.createEdge(node.edgeIndexesStart()); } hasNext(): boolean { return this.edge.edgeIndex < this.#sourceNode.edgeIndexesEnd(); } item(): HeapSnapshotEdge { return this.edge; } next(): void { if (typeof this.edge.snapshot.edgeFieldsCount === 'undefined') { throw new Error('edgeFieldsCount is undefined'); } this.edge.edgeIndex += this.edge.snapshot.edgeFieldsCount; } } export class HeapSnapshotRetainerEdge implements HeapSnapshotItem { protected snapshot: HeapSnapshot; #retainerIndexInternal!: number; #globalEdgeIndex!: number; #retainingNodeIndex?: number; #edgeInstance?: JSHeapSnapshotEdge|null; #nodeInstance?: HeapSnapshotNode|null; constructor(snapshot: HeapSnapshot, retainerIndex: number) { this.snapshot = snapshot; this.setRetainerIndex(retainerIndex); } clone(): HeapSnapshotRetainerEdge { return new HeapSnapshotRetainerEdge(this.snapshot, this.retainerIndex()); } hasStringName(): boolean { return this.edge().hasStringName(); } name(): string { return this.edge().name(); } nameIndex(): number { return this.edge().nameIndex(); } node(): HeapSnapshotNode { return this.nodeInternal(); } nodeIndex(): number { if (typeof this.#retainingNodeIndex === 'undefined') { throw new Error('retainingNodeIndex is undefined'); } return this.#retainingNodeIndex; } retainerIndex(): number { return this.#retainerIndexInternal; } setRetainerIndex(retainerIndex: number): void { 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: number) { this.setRetainerIndex(edgeIndex); } private nodeInternal(): HeapSnapshotNode { if (!this.#nodeInstance) { this.#nodeInstance = this.snapshot.createNode(this.#retainingNodeIndex); } return this.#nodeInstance; } protected edge(): JSHeapSnapshotEdge { if (!this.#edgeInstance) { this.#edgeInstance = this.snapshot.createEdge(this.#globalEdgeIndex); } return this.#edgeInstance; } toString(): string { return this.edge().toString(); } itemIndex(): number { return this.#retainerIndexInternal; } serialize(): HeapSnapshotModel.HeapSnapshotModel.Edge { const node = this.node(); const serializedNode = node.serialize(); serializedNode.distance = this.#distance(); serializedNode.ignored = this.snapshot.isNodeIgnoredInRetainersView(node.nodeIndex); return new HeapSnapshotModel.HeapSnapshotModel.Edge( this.name(), serializedNode, this.type(), this.#globalEdgeIndex); } type(): string { return this.edge().type(); } isInternal(): boolean { return this.edge().isInternal(); } getValueForSorting(fieldName: string): number { if (fieldName === '!edgeDistance') { return this.#distance(); } throw new Error('Invalid field name'); } #distance(): number { if (this.snapshot.isEdgeIgnoredInRetainersView(this.#globalEdgeIndex)) { return HeapSnapshotModel.HeapSnapshotModel.baseUnreachableDistance; } return this.node().distanceForRetainersView(); } } export class HeapSnapshotRetainerEdgeIterator implements HeapSnapshotItemIterator { readonly #retainersEnd: number; retainer: JSHeapSnapshotRetainerEdge; constructor(retainedNode: HeapSnapshotNode) { 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(): boolean { return this.retainer.retainerIndex() < this.#retainersEnd; } item(): HeapSnapshotRetainerEdge { return this.retainer; } next(): void { this.retainer.setRetainerIndex(this.retainer.retainerIndex() + 1); } } export class HeapSnapshotNode implements HeapSnapshotItem { snapshot: HeapSnapshot; nodeIndex: number; constructor(snapshot: HeapSnapshot, nodeIndex?: number) { this.snapshot = snapshot; this.nodeIndex = nodeIndex || 0; } distance(): number { return this.snapshot.nodeDistances[this.nodeIndex / this.snapshot.nodeFieldCount]; } distanceForRetainersView(): number { return this.snapshot.getDistanceForRetainersView(this.nodeIndex); } className(): string { return this.snapshot.strings[this.classIndex()]; } classIndex(): number { return this.#detachednessAndClassIndex() >>> SHIFT_FOR_CLASS_INDEX; } // Returns a key which can uniquely describe both the class name for this node // and its Location, if relevant. These keys are meant to be cheap to produce, // so that building aggregates is fast. These keys are NOT the same as the // keys exposed to the frontend by functions such as aggregatesWithFilter and // aggregatesForDiff. classKeyInternal(): string|number { // It is common for multiple JavaScript constructors to have the same // name, so the class key includes the location if available for nodes of // type 'object'. // // JavaScript Functions (node type 'closure') also have locations, but it // would not be helpful to split them into categories by location because // many of those categories would have only one instance. if (this.rawType() !== this.snapshot.nodeObjectType) { return this.classIndex(); } const location = this.snapshot.getLocation(this.nodeIndex); return location ? `${location.scriptId},${location.lineNumber},${location.columnNumber},${this.className()}` : this.classIndex(); } setClassIndex(index: number): void { let value = this.#detachednessAndClassIndex(); value &= BITMASK_FOR_DOM_LINK_STATE; // Clear previous class index. value |= (index << SHIFT_FOR_CLASS_INDEX); // Set new class index. this.#setDetachednessAndClassIndex(value); if (this.classIndex() !== index) { throw new Error('String index overflow'); } } dominatorIndex(): number { const nodeFieldCount = this.snapshot.nodeFieldCount; return this.snapshot.dominatorsTree[this.nodeIndex / this.snapshot.nodeFieldCount] * nodeFieldCount; } edges(): HeapSnapshotEdgeIterator { return new HeapSnapshotEdgeIterator(this); } edgesCount(): number { return (this.edgeIndexesEnd() - this.edgeIndexesStart()) / this.snapshot.edgeFieldsCount; } id(): number { throw new Error('Not implemented'); } rawName(): string { return this.snapshot.strings[this.rawNameIndex()]; } isRoot(): boolean { return this.nodeIndex === this.snapshot.rootNodeIndex; } isUserRoot(): boolean { throw new Error('Not implemented'); } isHidden(): boolean { throw new Error('Not implemented'); } isArray(): boolean { throw new Error('Not implemented'); } isSynthetic(): boolean { throw new Error('Not implemented'); } isDocumentDOMTreesRoot(): boolean { throw new Error('Not implemented'); } name(): string { return this.rawName(); } retainedSize(): number { return this.snapshot.retainedSizes[this.ordinal()]; } retainers(): HeapSnapshotRetainerEdgeIterator { return new HeapSnapshotRetainerEdgeIterator(this); } retainersCount(): number { const snapshot = this.snapshot; const ordinal = this.ordinal(); return snapshot.firstRetainerIndex[ordinal + 1] - snapshot.firstRetainerIndex[ordinal]; } selfSize(): number { const snapshot = this.snapshot; return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeSelfSizeOffset); } type(): string { return this.snapshot.nodeTypes[this.rawType()]; } traceNodeId(): number { const snapshot = this.snapshot; return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeTraceNodeIdOffset); } itemIndex(): number { return this.nodeIndex; } serialize(): HeapSnapshotModel.HeapSnapshotModel.Node { return new HeapSnapshotModel.HeapSnapshotModel.Node( this.id(), this.name(), this.distance(), this.nodeIndex, this.retainedSize(), this.selfSize(), this.type()); } rawNameIndex(): number { const snapshot = this.snapshot; return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeNameOffset); } edgeIndexesStart(): number { return this.snapshot.firstEdgeIndexes[this.ordinal()]; } edgeIndexesEnd(): number { return this.snapshot.firstEdgeIndexes[this.ordinal() + 1]; } ordinal(): number { return this.nodeIndex / this.snapshot.nodeFieldCount; } nextNodeIndex(): number { return this.nodeIndex + this.snapshot.nodeFieldCount; } rawType(): number { const snapshot = this.snapshot; return snapshot.nodes.getValue(this.nodeIndex + snapshot.nodeTypeOffset); } isFlatConsString(): boolean { if (this.rawType() !== this.snapshot.nodeConsStringType) { return false; } for (let iter = this.edges(); iter.hasNext(); iter.next()) { const edge = iter.edge; if (!edge.isInternal()) { continue; } const edgeName = edge.name(); if ((edgeName === 'first' || edgeName === 'second') && edge.node().name() === '') { return true; } } return false; } #detachednessAndClassIndex(): number { const {snapshot, nodeIndex} = this; const nodeDetachednessAndClassIndexOffset = snapshot.nodeDetachednessAndClassIndexOffset; return nodeDetachednessAndClassIndexOffset !== -1 ? snapshot.nodes.getValue(nodeIndex + nodeDetachednessAndClassIndexOffset) : (snapshot.detachednessAndClassIndexArray as Uint32Array)[nodeIndex / snapshot.nodeFieldCount]; } #setDetachednessAndClassIndex(value: number): void { const {snapshot, nodeIndex} = this; const nodeDetachednessAndClassIndexOffset = snapshot.nodeDetachednessAndClassIndexOffset; if (nodeDetachednessAndClassIndexOffset !== -1) { snapshot.nodes.setValue(nodeIndex + nodeDetachednessAndClassIndexOffset, value); } else { (snapshot.detachednessAndClassIndexArray as Uint32Array)[nodeIndex / snapshot.nodeFieldCount] = value; } } detachedness(): DOMLinkState { return this.#detachednessAndClassIndex() & BITMASK_FOR_DOM_LINK_STATE; } setDetachedness(detachedness: DOMLinkState): void { let value = this.#detachednessAndClassIndex(); value &= ~BITMASK_FOR_DOM_LINK_STATE; // Clear the old bits. value |= detachedness; // Set the new bits. this.#setDetachednessAndClassIndex(value); } } export class HeapSnapshotNodeIterator implements HeapSnapshotItemIterator { node: HeapSnapshotNode; readonly #nodesLength: number; constructor(node: HeapSnapshotNode) { this.node = node; this.#nodesLength = node.snapshot.nodes.length; } hasNext(): boolean { return this.node.nodeIndex < this.#nodesLength; } item(): HeapSnapshotNode { return this.node; } next(): void { this.node.nodeIndex = this.node.nextNodeIndex(); } } export class HeapSnapshotIndexRangeIterator implements HeapSnapshotItemIterator { readonly #itemProvider: HeapSnapshotItemIndexProvider; readonly #indexes: number[]|Uint32Array; #position: number; constructor(itemProvider: HeapSnapshotItemIndexProvider, indexes: number[]|Uint32Array) { this.#itemProvider = itemProvider; this.#indexes = indexes; this.#position = 0; } hasNext(): boolean { return this.#position < this.#indexes.length; } item(): HeapSnapshotItem { const index = this.#indexes[this.#position]; return this.#itemProvider.itemForIndex(index); } next(): void { ++this.#position; } } export class HeapSnapshotFilteredIterator implements HeapSnapshotItemIterator { #iterator: HeapSnapshotItemIterator; #filter: ((arg0: HeapSnapshotItem) => boolean)|undefined; constructor(iterator: HeapSnapshotItemIterator, filter?: ((arg0: HeapSnapshotItem) => boolean)) { this.#iterator = iterator; this.#filter = filter; this.skipFilteredItems(); } hasNext(): boolean { return this.#iterator.hasNext(); } item(): HeapSnapshotItem { return this.#iterator.item(); } next(): void { this.#iterator.next(); this.skipFilteredItems(); } private skipFilteredItems(): void { while (this.#iterator.hasNext() && this.#filter && !this.#filter(this.#iterator.item())) { this.#iterator.next(); } } } export class HeapSnapshotProgress { readonly #dispatcher: HeapSnapshotWorkerDispatcher|undefined; constructor(dispatcher?: HeapSnapshotWorkerDispatcher) { this.#dispatcher = dispatcher; } updateStatus(status: string): void { this.sendUpdateEvent(i18n.i18n.serializeUIString(status)); } updateProgress(title: string, value: number, total: number): void { const percentValue = ((total ? (value / total) : 0) * 100).toFixed(0); this.sendUpdateEvent(i18n.i18n.serializeUIString(title, {PH1: percentValue})); } reportProblem(error: string): void { // May be undefined in tests. if (this.#dispatcher) { this.#dispatcher.sendEvent(HeapSnapshotModel.HeapSnapshotModel.HeapSnapshotProgressEvent.BrokenSnapshot, error); } } private sendUpdateEvent(serializedText: string): void { // May be undefined in tests. if (this.#dispatcher) { this.#dispatcher.sendEvent(HeapSnapshotModel.HeapSnapshotModel.HeapSnapshotProgressEvent.Update, serializedText); } } } // An "interface" to be used when classifying plain JS objects in the snapshot. // An object matches the interface if it contains every listed property (even // if it also contains extra properties). interface InterfaceDefinition { name: string; properties: string[]; } type HeapSnapshotProblemReport = Array<string|number>; function appendToProblemReport(report: HeapSnapshotProblemReport, messageOrNodeIndex: string|number): void { if (report.length > 100) { return; } report.push(messageOrNodeIndex); } function formatProblemReport(snapshot: HeapSnapshot, report: HeapSnapshotProblemReport): string { const node = snapshot.rootNode(); return report .map(messageOrNodeIndex => { if (typeof messageOrNodeIndex === 'string') { return messageOrNodeIndex; } node.nodeIndex = messageOrNodeIndex; return `${node.name()} @${node.id()}`; }) .join('\n '); } function reportProblemToPrimaryWorker(problemReport: HeapSnapshotProblemReport, port: MessagePort): void { port.postMessage({problemReport}); } export interface Profile { /* eslint-disable @typescript-eslint/naming-convention */ root_index: number; nodes: Platform.TypedArrayUtilities.BigUint32Array; edges: Platform.TypedArrayUtilities.BigUint32Array; snapshot: HeapSnapshotHeader; samples: number[]; strings: string[]; locations: number[]; trace_function_infos: Uint32Array; trace_tree: Object; /* eslint-enable @typescript-eslint/naming-convention */ } export type LiveObjects = Record<number, {count: number, size: number, ids: number[]}>; // The first batch of data sent from the primary worker to the secondary. interface SecondaryInitArgumentsStep1 { // For each edge ordinal, this array contains the ordinal of the pointed-to node. edgeToNodeOrdinals: Uint32Array; // A copy of HeapSnapshot.firstEdgeIndexes. For each node ordinal, this array // contains the edge index of the first outgoing edge. firstEdgeIndexes: Uint32Array; nodeCount: number; edgeFieldsCount: number; nodeFieldCount: number; } // The second batch of data sent from the primary worker to the secondary. interface SecondaryInitArgumentsStep2 { rootNodeOrdinal: number; // An array with one bit per edge, where each bit indicates whether the edge // should be used when computing dominators. essentialEdgesBuffer: ArrayBuffer; } // The third batch of data sent from the primary worker to the secondary. interface SecondaryInitArgumentsStep3 { // For each node ordinal, this array contains the node's shallow size. nodeSelfSizes: Uint32Array; } type ArgumentsToBuildRetainers = SecondaryInitArgumentsStep1; interface Retainers { // For each node ordinal, this array contains the index of the first retaining edge // in the retainingEdges and retainingNodes arrays. firstRetainerIndex: Uint32Array; // For each retaining edge, this array contains the "from" node's index. retainingNodes: Uint32Array; // For each retaining edge, this array contains the index in containmentEdges // where you can find other info about the edge, such as its type and name. retainingEdges: Uint32Array; } interface ArgumentsToComputeDominatorsAndRetainedSizes extends SecondaryInitArgumentsStep1, Retainers, SecondaryInitArgumentsStep2 { // For each edge ordinal, this bit vector contains whether the edge // should be used when computing dominators. essentialEdges: Platform.TypedArrayUtilities.BitVector; // A message port for reporting problems to the primary worker. port: MessagePort; // For each node ordinal, this array will contain the node's shallow size. nodeSelfSizesPromise: Promise<Uint32Array>; } interface DominatorsAndRetainedSizes { // For each node ordinal, this array contains the ordinal of its immediate dominating node. dominatorsTree: Uint32Array; // For each node ordinal, this array contains the size of the subgraph it dominates, including its own size. retainedSizes: Float64Array; } interface ArgumentsToBuildDominatedNodes extends ArgumentsToComputeDominatorsAndRetainedSizes, DominatorsAndRetainedSizes {} interface DominatedNodes { // For each node ordinal, the index of its first child node in dominatedNodes. // Together with dominatedNodes, this allows traversing down the dominators tree, // whereas dominatorsTree allows upward traversal. firstDominatedNodeIndex: Uint32Array; // Node indexes of child nodes in the dominator tree. dominatedNodes: Uint32Array; } // The data transferred from the secondary worker to the primary. interface ResultsFromSecondWorker extends Retainers, DominatorsAndRetainedSizes, DominatedNodes {} // Initialization work is split into two threads. This class is the entry point // for work done by the second thread. export class SecondaryInitManager { argsStep1: Promise<SecondaryInitArgumentsStep1>; argsStep2: Promise<SecondaryInitArgumentsStep2>; argsStep3: Promise<SecondaryInitArgumentsStep3>; constructor(port: MessagePort) { const {promise: argsStep1, resolve: resolveArgsStep1} = Promise.withResolvers<SecondaryInitArgumentsStep1>(); this.argsStep1 = argsStep1; const {promise: argsStep2, resolve: resolveArgsStep2} = Promise.withResolvers<SecondaryInitArgumentsStep2>(); this.argsStep2 = argsStep2; const {promise: argsStep3, resolve: resolveArgsStep3} = Promise.withResolvers<SecondaryInitArgumentsStep3>(); this.argsStep3 = argsStep3; port.onmessage = e => { const data = e.data; switch (data.step) { case 1: resolveArgsStep1(data.args); break; case 2: resolveArgsStep2(data.args); break; case 3: resolveArgsStep3(data.args); break; } }; void this.initialize(port); } private async getNodeSelfSizes(): Promise<Uint32Array> { return (await this.argsStep3).nodeSelfSizes; } private async initialize(port: MessagePort): Promise<void> { try { const argsStep1 = await this.argsStep1; const retainers = HeapSnapshot.buildRetainers(argsStep1); const argsStep2 = await this.argsStep2; const args = { ...argsStep2, ...argsStep1, ...retainers, essentialEdges: Platform.TypedArrayUtilities.createBitVector(argsStep2.essentialEdgesBuffer), port, nodeSelfSizesPromise: this.getNodeSelfSizes() }; const dominatorsAndRetainedSizes = await HeapSnapshot.calculateDominatorsAndRetainedSizes(args); const dominatedNodesOutputs = HeapSnapshot.buildDominatedNodes({...args, ...dominatorsAndRetainedSizes}); const results: ResultsFromSecondWorker = { ...retainers, ...dominatorsAndRetainedSizes, ...dominatedNodesOutputs, }; port.postMessage({resultsFromSecondWorker: results}, { transfer: [ results.dominatorsTree.buffer, results.firstRetainerIndex.buffer, results.retainedSizes.buffer, results.retainingEdges.buffer, results.retainingNodes.buffer, results.dominatedNodes.buffer, results.firstDominatedNodeIndex.buffer, ] }); } catch (e) { port.postMessage({error: e + '\n' + e?.stack}); } } } /** * DOM node link state. */ const enum DOMLinkState { UNKNOWN = 0, ATTACHED = 1, DETACHED = 2, } const BITMASK_FOR_DOM_LINK_STATE = 3; // The class index is stored in the upper 30 bits of the detachedness field. const SHIFT_FOR_CLASS_INDEX = 2; // After this many properties, inferInterfaceDefinitions can stop adding more // properties to an interface definition if the name is getting too long. const MIN_INTERFACE_PROPERTY_COUNT = 1; // The maximum length of an interface name produced by inferInterfaceDefinitions. // This limit can be exceeded if the first MIN_INTERFACE_PROPERTY_COUNT property // names are long. const MAX_INTERFACE_NAME_LENGTH = 120; // Each interface definition produced by inferInterfaceDefinitions will match at // least this many objects. There's no point in defining interfaces which match // only a single object. const MIN_OBJECT_COUNT_PER_INTERFACE = 2; // Each interface definition produced by inferInterfaceDefinitions should // match at least 1 out of 1000 Objects in the heap. Otherwise, we end up with a // long tail of unpopular interfaces that don't help analysis. const MIN_OBJECT_PROPORTION_PER_INTERFACE = 1000; export abstract class HeapSnapshot { nodes: Platform.TypedArrayUtilities.BigUint32Array; containmentEdges: Platform.TypedArrayUtilities.BigUint32Array; readonly #metaNode: HeapSnapshotMetaInfo; readonly #rawSamples: number[]; #samples: HeapSnapshotModel.HeapSnapshotModel.Samples|null = null; strings: string[]; readonly #locations: number[]; readonly #progress: HeapSnapshotProgress; readonly #noDistance = -5; rootNodeIndexInternal = 0; #snapshotDiffs: Record<string, Record<string, HeapSnapshotModel.HeapSnapshotModel.Diff>> = {}; #aggregatesForDiffInternal?: { interfaceDefinitions: string, aggregates: Record<string, HeapSnapshotModel.HeapSnapshotModel.AggregateForDiff>, }; #aggregates: Record<string, Record<string, AggregatedInfo>> = {}; #aggregatesSortedFlags: Record<string, boolean> = {}; profile: Profile; nodeTypeOffset!: number; nodeNameOffset!: number; nodeIdOffset!: number; nodeSelfSizeOffset!: number; #nodeEdgeCountOffset!: number; nodeTraceNodeIdOffset!: number; nodeFieldCount!: number; nodeTypes!: string[]; nodeArrayType!: number; nodeHiddenType!: number; nodeObjectType!: number; nodeNativeType!: number; nodeStringType!: number; nodeConsStringType!: number; nodeSlicedStringType!: number; nodeCodeType!: number; nodeSyntheticType!: number; nodeClosureType!: number; nodeRegExpType!: number; edgeFieldsCount!: number; edgeTypeOffset!: number; edgeNameOffset!: number; edgeToNodeOffset!: number; edgeTypes!: string[]; edgeElementType!: number; edgeHiddenType!: number; edgeInternalType!: number; edgeShortcutType!: number; edgeWeakType!: number; edgeInvisibleType!: number; edgePropertyType!: number; #locationIndexOffset!: number; #locationScriptIdOffset!: number; #locationLineOffset!: number; #locationColumnOffset!: number; #locationFieldCount!: number; nodeCount!: number; #edgeCount!: number; retainedSizes!: Float64Array; firstEdgeIndexes!: Uint32Array; retainingNodes!: Uint32Array; retainingEdges!: Uint32Array; firstRetainerIndex!: Uint32Array; nodeDistances!: Int32Array; firstDominatedNodeIndex!: Uint32Array; dominatedNodes!: Uint32Array; dominatorsTree!: Uint32Array; #allocationProfile!: AllocationProfile; nodeDetachednessAndClassIndexOffset!: number; #locationMap!: Map<number, HeapSnapshotModel.HeapSnapshotModel.Location>; #ignoredNodesInRetainersView = new Set<number>(); #ignoredEdgesInRetainersView = new Set<number>(); #nodeDistancesForRetainersView: Int32Array|undefined; #edgeNamesThatAreNotWeakMaps: Platform.TypedArrayUtilities.BitVector; detachednessAndClassIndexArray?: Uint32Array; #interfaceNames = new Map<string, number>(); #interfaceDefinitions?: InterfaceDefinition[]; constructor(profile: Profile, progress: HeapSnapshotProgress) { this.nodes = profile.nodes; this.containmentEdges = profile.edges; this.#metaNode = profile.snapshot.meta; this.#rawSamples = profile.samples; this.strings = profile.strings; this.#locations = profile.locations; this.#progress = progress; if (profile.snapshot.root_index) { this.rootNodeIndexInternal = profile.snapshot.root_index; } this.profile = profile; this.#edgeNamesThatAreNotWeakMaps = Platform.TypedArrayUtilities.createBitVector(this.strings.length); } async initialize(secondWorker: MessagePort): Promise<void> { 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.nodeDetachednessAndClassIndexOffset = 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.nodeStringType = this.nodeTypes.indexOf('string'); 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.nodeClosureType = this.nodeTypes.indexOf('closure'); this.nodeRegExpType = this.nodeTypes.indexOf('regexp'); 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'); this.edgePropertyType = this.edgeTypes.indexOf('property'); 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.#progress.updateStatus('Building edge indexes…'); this.firstEdgeIndexes = new Uint32Array(this.nodeCount + 1); this.buildEdgeIndexes(); this.#progress.updateStatus('Building retainers…'); const resultsFromSecondWorker = this.startInitStep1InSecondThread(secondWorker); this.#progress.updateStatus('Propagating DOM state…'); this.propagateDOMState(); this.#progress.updateStatus('Calculating node flags…'); this.calculateFlags(); this.#progress.updateStatus('Building dominated nodes…'); this.startInitStep2InSecondThread(secondWorker); this.#progress.updateStatus('Calculating shallow sizes…'); this.calculateShallowSizes(); this.#progress.updateStatus('Calculating retained sizes…'); this.startInitStep3InSecondThread(secondWorker); this.#progress.updateStatus('Calculating distances…'); this.nodeDistances = new Int32Array(this.nodeCount); this.calculateDistances(/* isForRetainersView=*/ false); this.#progress.updateStatus('Calculating object names…'); this.calculateObjectNames(); this.applyInterfaceDefinitions(this.inferInterfaceDefinitions()); this.#progress.updateStatus('Calculating samples…'); this.buildSamples(); this.#progress.updateStatus('Building locations…'); this.buildLocationMap(); this.#progress.updateStatus('Calculating retained sizes…'); await this.installResultsFromSecondThread(resultsFromSecondWorker); this.#progress.updateStatus('Calculating statistics…'); this.calculateStatistics(); 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: LiveObjects = {}; for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) { node.nodeIndex = nodeIndex; const traceNodeId = node.traceNodeId(); let stats: { count: number, size: number, ids: number[], } = 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('Finished processing.'); } private startInitStep1InSecondThread(secondWorker: MessagePort): Promise<ResultsFromSecondWorker> { const resultsFromSecondWorker = new Promise<ResultsFromSecondWorker>((resolve, reject) => { secondWorker.onmessage = (event: MessageEvent) => { const data = event.data; if (data?.problemReport) { const problemReport: HeapSnapshotProblemReport = data.problemReport; console.warn(formatProblemReport(this, problemReport)); } else if (data?.resultsFromSecondWorker) { const resultsFromSecondWorker: ResultsFromSecondWorker = data.resultsFromSecondWorker; resolve(resultsFromSecondWorker); } else if (data?.error) { reject(data.error); } }; }); const edgeCount = this.#edgeCount; const {containmentEdges, edgeToNodeOffset, edgeFieldsCount, nodeFieldCount} = this; const edgeToNodeOrdinals = new Uint32Array(edgeCount); for (let edgeOrdinal = 0; edgeOrdinal < edgeCount; ++edgeOrdinal) { const toNodeIndex = containmentEdges.getValue(edgeOrdinal * edgeFieldsCount + edgeToNodeOffset); if (toNodeIndex % nodeFieldCount) { throw new Error('Invalid toNodeIndex ' + toNodeIndex); } edgeToNodeOrdinals[edgeOrdinal] = toNodeIndex / nodeFieldCount; } const args: SecondaryInitArgumentsStep1 = { edgeToNodeOrdinals, firstEdgeIndexes: this.firstEdgeIndexes, nodeCount: this.nodeCount, edgeFieldsCount: this.edgeFieldsCount, nodeFieldCount: this.nodeFieldCount, }; // Note that firstEdgeIndexes is not transferred; each thread needs its own copy. secondWorker.postMessage({step: 1, args}, [edgeToNodeOrdinals.buffer]); return resultsFromSecondWorker; } private startInitStep2InSecondThread(secondWorker: MessagePort): void { const rootNodeOrdinal = this.rootNodeIndexInternal / this.nodeFieldCount; const essentialEdges = this.initEssentialEdges(); const args: SecondaryInitArgumentsStep2 = {rootNodeOrdinal, essentialEdgesBuffer: essentialEdges.buffer}; secondWorker.postMessage({step: 2, args}, [essentialEdges.buffer]); } private startInitStep3InSecondThread(secondWorker: MessagePort): void { const {nodes, nodeFieldCount, nodeSelfSizeOffset, nodeCount} = this; const nodeSelfSizes = new Uint32Array(nodeCount); for (let nodeOrdinal = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) { nodeSelfSizes[nodeOrdinal] = nodes.getValue(nodeOrdinal * nodeFieldCount + nodeSelfSizeOffset); } const args: SecondaryInitArgumentsStep3 = {nodeSelfSizes}; secondWorker.postMessage({step: 3, args}, [nodeSelfSizes.buffer]); } private async installResultsFromSecondThread(resultsFromSecondWorker: Promise<ResultsFromSecondWorker>): Promise<void> { const results = await resultsFromSecondWorker; this.dominatedNodes = results.dominatedNodes; this.dominatorsTree = results.dominatorsTree; this.firstDominatedNodeIndex = results.firstDominatedNodeIndex; this.firstRetainerIndex = results.firstRetainerIndex; this.retainedSizes = results.retainedSizes; this.retainingEdges = results.retainingEdges; this.retainingNodes = results.retainingNodes; } private buildEdgeIndexes(): void { 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.getValue(nodeOrdinal * nodeFieldCount + nodeEdgeCountOffset) * edgeFieldsCount; } } static buildRetainers(inputs: ArgumentsToBuildRetainers): Retainers { const {edgeToNodeOrdinals, firstEdgeIndexes, nodeCount, edgeFieldsCount, nodeFieldCount} = inputs; const edgeCount = edgeToNodeOrdinals.length; const retainingNodes = new Uint32Array(edgeCount); const retainingEdges = new Uint32Array(edgeCount); const firstRetainerIndex = new Uint32Array(nodeCount + 1); for (let edgeOrdinal = 0; edgeOrdinal < edgeCount; ++edgeOrdinal) { const toNodeOrdinal = edgeToNodeOrdinals[edgeOrdinal]; ++firstRetainerIndex[toNodeOrdinal]; } 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: number = 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 toNodeOrdinal = edgeToNodeOrdinals[edgeIndex / edgeFieldsCount]; const firstRetainerSlotIndex = firstRetainerIndex[toNodeOrdinal]; const nextUnusedRetainerSlotIndex = firstRetainerSlotIndex + (--retainingNodes[firstRetainerSlotIndex]); retainingNodes[nextUnusedRetainerSlotIndex] = srcNodeIndex; retainingEdges[nextUnusedRetainerSlotIndex] = edgeIndex; } } return { retainingNodes, retainingEdges, firstRetainerIndex, }; } abstract createNode(_nodeIndex?: number): HeapSnapshotNode; abstract createEdge(_edgeIndex: number): JSHeapSnapshotEdge; abstract createRetainingEdge(_retainerIndex: number): JSHeapSnapshotRetainerEdge; private allNodes(): HeapSnapshotNodeIterator { return new HeapSnapshotNodeIterator(this.rootNode()); } rootNode(): HeapSnapshotNode { return this.createNode(this.rootNodeIndexInternal); } get rootNodeIndex(): number { return this.rootNodeIndexInternal; } get totalSize(): number { return this.rootNode().retainedSize() + (this.profile.snapshot.extra_native_bytes ?? 0); } private createFilter(nodeFilter: HeapSnapshotModel.HeapSnapshotModel.NodeFilter): ((arg0: HeapSnapshotNode) => boolean)|undefined { const {minNodeId, maxNodeId, allocationNodeId, filterName} = nodeFilter; let filter; if (typeof allocationNodeId === 'number') { filter = this.createAllocationStackFilter(allocationNodeId); if (!filter) { throw new Error('Unable to create filter'); } // @ts-expect-error 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-expect-error key can be added as a static property filter.key = 'NodeIdRange: ' + minNodeId + '..' + maxNodeId; } else if (filterName !== undefined) { filter = this.createNamedFilter(filterName); // @ts-expect-error key can be added as a static property filter.key = 'NamedFilter: ' + filterName; } return filter; } search( searchConfig: HeapSnapshotModel.HeapSnapshotModel.SearchConfig, nodeFilter: HeapSnapshotModel.HeapSnapshotModel.NodeFilter): number[] { const query = searchConfig.query; function filterString(matchedStringIndexes: Set<number>, string: string, index: number): Set<number> { 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: Set<number>, string: string, index: number): Set<number> { if (regexp.test(string)) { matchedStringIndexes.add(index); } return matchedStringIndexes; } const useRegExp = searchConfig.isRegex || !searchConfig.caseSensitive; const stringFilter = useRegExp ? filterRegexp : filterString; const stringIndexes = this.strings.reduce(stringFilter, new Set()); 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 (node.selfSize() === 0) { // Nodes with size zero are omitted in the data grid, so avoid returning // search results that can't be navigated to. continue; } const name = node.name(); if (name === node.rawName()) { // If the string displayed to the user matches the raw name from the // snapshot, then we can use the Set computed above. This avoids // repeated work when multiple nodes have the same name. if (stringIndexes.has(nodes.getValue(nodeIndex + nodeNameOffset))) { nodeIds.push(nodes.getValue(nodeIndex + nodeIdOffset)); } // If the node is displaying a customized name, then we must perform the // full string search within that name here. } else if (useRegExp ? regexp.test(name) : (name.indexOf(query) !== -1)) { nodeIds.push(nodes.getValue(nodeIndex + nodeIdOffset)); } } return nodeIds; } aggregatesWithFilter(nodeFilter: HeapSnapshotModel.HeapSnapshotModel.NodeFilter): Record<string, HeapSnapshotModel.HeapSnapshotModel.Aggregate> { const filter = this.createFilter(nodeFilter); // @ts-expect-error key is added in createFilter const key = filter ? filter.key : 'allObjects'; return this.getAggregatesByClassKey(false, key, filter); } private createNodeIdFilter(minNodeId: number, maxNodeId: number): (arg0: HeapSnapshotNode) => boolean { function nodeIdFilter(node: HeapSnapshotNode): boolean { const id = node.id(); return id > minNodeId && id <= maxNodeId; } return nodeIdFilter; } private createAllocationStackFilter(bottomUpAllocationNodeId: number): ((arg0: HeapSnapshotNode) => boolean)|undefined { if (!this.#allocationProfile) { throw new Error('No Allocation Profile provided'); } const traceIds = this.#allocationProfile.traceIds(bottomUpAllocationNodeId); if (!traceIds.length) { return undefined; } const set: Record<number, boolean> = {}; for (let i = 0; i < traceIds.length; i++) { set[traceIds[i]] = true; } function traceIdFilter(node: HeapSnapshotNode): boolean { return Boolean(set[node.traceNodeId()]); } return traceIdFilter; } private createNamedFilter(filterName: string): (node: HeapSnapshotNode) => boolean { // Allocate an array with a single bit per node, which can be used by each // specific filter implemented below. const bitmap = Platform.TypedArrayUtilities.createBitVector(this.nodeCount); const getBit = (node: HeapSnapshotNode): boolean => { const ordinal = node.nodeIndex / this.nodeFieldCount; return bitmap.getBit(ordinal); }; // Traverses the graph in breadth-first order with the given filter, and // sets the bit in `bitmap` for every visited node. const traverse = (filter: (node: HeapSnapshotNode, edge: HeapSnapshotEdge) => boolean): void => { const distances = new Int32Array(this.nodeCount); for (let i = 0; i < this.nodeCount; ++i) { distances[i] = this.#noDistance; } const nodesToVisit = new Uint32Array(this.nodeCount); distances[this.rootNode().ordinal()] = 0; nodesToVisit[0] = this.rootNode().nodeIndex; const nodesToVisitLength = 1; this.bfs(nodesToVisit, nodesToVisitLength, distances, filter); for (let i = 0; i < this.nodeCount; ++i) { if (distances[i] !== this.#noDistance) { bitmap.setBit(i); } } }; const markUnreachableNodes = (): void => { for (let i = 0; i < this.nodeCount; ++i) { if (this.nodeDistances[i] === this.#noDistance) { bitmap.setBit(i); } } }; switch (filterName) { case 'objectsRetainedByDetachedDomNodes': // Traverse the graph, avoiding detached nodes. traverse((_node: HeapSnapshotNode, edge: HeapSnapshotEdge) => { return edge.node().detachedness() !== DOMLinkState.DETACHED; }); markUnreachableNodes(); return (node: HeapSnapshotNode) => !getBit(node); case 'objectsRetainedByConsole': // Traverse the graph, avoiding edges that represent globals owned by // the DevTools console. traverse((node: HeapSnapshotNode, edge: HeapSnapshotEdge) => { return !(node.isSynthetic() && edge.hasStringName() && edge.name().endsWith(' / DevTools console'));