chrome-devtools-frontend
Version:
Chrome DevTools UI
1,262 lines (1,138 loc) • 58.3 kB
text/typescript
/*
* 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/no-imperative-dom-api */
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as HeapSnapshotModel from '../../models/heap_snapshot_model/heap_snapshot_model.js';
import * as IconButton from '../../ui/components/icon_button/icon_button.js';
import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import type {ChildrenProvider} from './ChildrenProvider.js';
import {
type AllocationDataGrid,
type HeapSnapshotConstructorsDataGrid,
type HeapSnapshotDiffDataGrid,
type HeapSnapshotSortableDataGrid,
HeapSnapshotSortableDataGridEvents,
} from './HeapSnapshotDataGrids.js';
import type {HeapSnapshotProviderProxy, HeapSnapshotProxy} from './HeapSnapshotProxy.js';
import type {DataDisplayDelegate} from './ProfileHeader.js';
const UIStrings = {
/**
*@description Generic text with two placeholders separated by a comma
*@example {1 613 680} PH1
*@example {44 %} PH2
*/
genericStringsTwoPlaceholders: '{PH1}, {PH2}',
/**
*@description Text in Heap Snapshot Grid Nodes of a profiler tool
*/
internalArray: '(internal array)[]',
/**
*@description Text in Heap Snapshot Grid Nodes of a profiler tool
*/
userObjectReachableFromWindow: 'User object reachable from window',
/**
*@description Text in Heap Snapshot Grid Nodes of a profiler tool
*/
detachedFromDomTree: 'Detached from DOM tree',
/**
*@description Text in Heap Snapshot Grid Nodes of a profiler tool
*/
previewIsNotAvailable: 'Preview is not available',
/**
*@description A context menu item in the Heap Profiler Panel of a profiler tool
*/
revealInSummaryView: 'Reveal in Summary view',
/**
*@description Text for the summary view
*/
summary: 'Summary',
/**
*@description A context menu item in the Heap Profiler Panel of a profiler tool
*@example {SomeClassConstructor} PH1
*@example {12345} PH2
*/
revealObjectSWithIdSInSummary: 'Reveal object \'\'{PH1}\'\' with id @{PH2} in Summary view',
/**
*@description Text to store an HTML element or JavaScript variable or expression result as a global variable
*/
storeAsGlobalVariable: 'Store as global variable',
/**
*@description Text to ignore an object shown in the Retainers pane
*/
ignoreThisRetainer: 'Ignore this retainer',
/**
*@description Text to undo the "Ignore this retainer" action
*/
stopIgnoringThisRetainer: 'Stop ignoring this retainer',
/**
*@description Text indicating that a node has been ignored with the "Ignore this retainer" action
*/
ignored: 'ignored',
/**
*@description Text in Heap Snapshot Grid Nodes of a profiler tool that indicates an element contained in another
* element.
*/
inElement: 'in',
/**
*@description A short summary of the text at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#compiled-code
*/
compiledCodeSummary: 'Internal data which V8 uses to run functions defined by JavaScript or WebAssembly.',
/**
*@description A short summary of the text at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#concatenated-string
*/
concatenatedStringSummary: 'A string which represents the contents of two other strings joined together.',
/**
*@description A short summary of the text at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#system-context
*/
contextSummary:
'An internal object containing variables from a JavaScript scope which may be needed by a function created within that scope.',
/**
*@description A short description of the data type internal type DescriptorArray, which is described more fully at https://v8.dev/blog/fast-properties
*/
descriptorArraySummary: 'A list of the property names used by a JavaScript Object.',
/**
*@description A short summary of the text at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#array
*/
internalArraySummary: 'An internal array-like data structure (not a JavaScript Array).',
/**
*@description A short summary of the text at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#internal-node
*/
internalNodeSummary: 'An object allocated by a component other than V8, such as C++ objects defined by Blink.',
/**
*@description A short description of the data type "system / Map" described at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#object-shape
*/
mapSummary: 'An internal object representing the shape of a JavaScript Object (not a JavaScript Map).',
/**
*@description A short summary of the "(object elements)[]" described at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#array
*/
objectElementsSummary:
'An internal object which stores the indexed properties in a JavaScript Object, such as the contents of an Array.',
/**
*@description A short summary of the "(object properties)[]" described at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#array
*/
objectPropertiesSummary: 'An internal object which stores the named properties in a JavaScript Object.',
/**
*@description A short summary of the text at https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#sliced-string
*/
slicedStringSummary: 'A string which represents some of the characters from another string.',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/profiler/HeapSnapshotGridNodes.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
class HeapSnapshotGridNodeBase extends DataGrid.DataGrid.DataGridNode<HeapSnapshotGridNode> {}
export class HeapSnapshotGridNode extends
Common.ObjectWrapper.eventMixin<HeapSnapshotGridNode.EventTypes, typeof HeapSnapshotGridNodeBase>(
HeapSnapshotGridNodeBase) {
dataGridInternal: HeapSnapshotSortableDataGrid;
instanceCount: number;
readonly savedChildren: Map<number, HeapSnapshotGridNode>;
retrievedChildrenRanges: Array<{
from: number,
to: number,
}>;
providerObject: ChildrenProvider|null;
reachableFromWindow: boolean;
populated?: boolean;
constructor(tree: HeapSnapshotSortableDataGrid, hasChildren: boolean) {
super(null, hasChildren);
this.dataGridInternal = tree;
this.instanceCount = 0;
this.savedChildren = new Map();
/**
* List of position ranges for all visible nodes: [startPos1, endPos1),...,[startPosN, endPosN)
* Position is an item position in the provider.
*/
this.retrievedChildrenRanges = [];
this.providerObject = null;
this.reachableFromWindow = false;
}
get name(): string|undefined {
return undefined;
}
createProvider(): ChildrenProvider {
throw new Error('Not implemented.');
}
comparator(): HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig {
throw new Error('Not implemented.');
}
getHash(): number {
throw new Error('Not implemented.');
}
createChildNode(_item: HeapSnapshotModel.HeapSnapshotModel.Node|HeapSnapshotModel.HeapSnapshotModel.Edge):
HeapSnapshotGridNode {
throw new Error('Not implemented.');
}
retainersDataSource(): {
snapshot: HeapSnapshotProxy,
snapshotNodeIndex: number,
snapshotNodeId: number|undefined,
}|null {
return null;
}
provider(): ChildrenProvider {
if (!this.providerObject) {
this.providerObject = this.createProvider();
}
return this.providerObject;
}
override createCell(columnId: string): HTMLElement {
return super.createCell(columnId);
}
override collapse(): void {
super.collapse();
this.dataGridInternal.updateVisibleNodes(true);
}
override expand(): void {
super.expand();
this.dataGridInternal.updateVisibleNodes(true);
}
dispose(): void {
if (this.providerObject) {
this.providerObject.dispose();
}
for (let node: (HeapSnapshotGridNode|null) = (this.children[0] as HeapSnapshotGridNode | null); node;
node = (node.traverseNextNode(true, this, true) as HeapSnapshotGridNode | null)) {
node.dispose();
}
}
queryObjectContent(_heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel, _objectGroupName: string):
Promise<SDK.RemoteObject.RemoteObject|{description: string, link: string}> {
throw new Error('Not implemented.');
}
tryQueryObjectContent(_heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel, _objectGroupName: string):
Promise<SDK.RemoteObject.RemoteObject|null> {
throw new Error('Not implemented.');
}
populateContextMenu(
_contextMenu: UI.ContextMenu.ContextMenu, _dataDisplayDelegate: DataDisplayDelegate,
_heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null): void {
}
toPercentString(num: number): string {
return num.toFixed(0) + '\xa0%'; // \xa0 is a non-breaking space.
}
toUIDistance(distance: number): string {
const baseSystemDistance = HeapSnapshotModel.HeapSnapshotModel.baseSystemDistance;
return distance >= 0 && distance < baseSystemDistance ? distance.toString() : '\u2212';
}
allChildren(): HeapSnapshotGridNode[] {
return this.dataGridInternal.allChildren(this) as HeapSnapshotGridNode[];
}
removeChildByIndex(index: number): void {
this.dataGridInternal.removeChildByIndex(this, index);
}
childForPosition(nodePosition: number): HeapSnapshotGridNode|null {
let indexOfFirstChildInRange = 0;
for (let i = 0; i < this.retrievedChildrenRanges.length; i++) {
const range = this.retrievedChildrenRanges[i];
if (range.from <= nodePosition && nodePosition < range.to) {
const childIndex = indexOfFirstChildInRange + nodePosition - range.from;
return this.allChildren()[childIndex];
}
indexOfFirstChildInRange += range.to - range.from + 1;
}
return null;
}
createValueCell(columnId: string): HTMLElement {
const jslog = VisualLogging.tableCell('numeric-column').track({click: true});
const cell = (UI.Fragment.html`<td class="numeric-column" jslog=${jslog} />` as HTMLElement);
const dataGrid = (this.dataGrid as HeapSnapshotSortableDataGrid);
if (dataGrid.snapshot && dataGrid.snapshot.totalSize !== 0) {
const div = document.createElement('div');
const valueSpan = UI.Fragment.html`<span>${this.data[columnId]}</span>`;
div.appendChild(valueSpan);
const percentColumn = columnId + '-percent';
if (percentColumn in this.data) {
const percentSpan = UI.Fragment.html`<span class="percent-column">${this.data[percentColumn]}</span>`;
div.appendChild(percentSpan);
div.classList.add('profile-multiple-values');
UI.ARIAUtils.setHidden(valueSpan, true);
UI.ARIAUtils.setHidden(percentSpan, true);
this.setCellAccessibleName(
i18nString(
UIStrings.genericStringsTwoPlaceholders, {PH1: this.data[columnId], PH2: this.data[percentColumn]}),
cell, columnId);
}
cell.appendChild(div);
}
return cell;
}
override populate(): void {
if (this.populated) {
return;
}
this.populated = true;
void this.provider().sortAndRewind(this.comparator()).then(() => this.populateChildren());
}
expandWithoutPopulate(): Promise<void> {
// Make sure default populate won't take action.
this.populated = true;
this.expand();
return this.provider().sortAndRewind(this.comparator());
}
childHashForEntity(entity: HeapSnapshotModel.HeapSnapshotModel.Node|HeapSnapshotModel.HeapSnapshotModel.Edge):
number {
if ('edgeIndex' in entity) {
return entity.edgeIndex;
}
return entity.id;
}
populateChildren(fromPosition?: number|null, toPosition?: number|null): Promise<void> {
return new Promise(resolve => {
fromPosition = fromPosition || 0;
toPosition = toPosition || fromPosition + this.dataGridInternal.defaultPopulateCount();
let firstNotSerializedPosition: number = fromPosition;
serializeNextChunk.call(this, toPosition);
function serializeNextChunk(this: HeapSnapshotGridNode, toPosition: number): void {
if (firstNotSerializedPosition >= toPosition) {
return;
}
const end = Math.min(firstNotSerializedPosition + this.dataGridInternal.defaultPopulateCount(), toPosition);
void this.provider()
.serializeItemsRange(firstNotSerializedPosition, end)
.then(itemsRange => childrenRetrieved.call(this, itemsRange, toPosition));
firstNotSerializedPosition = end;
}
function insertRetrievedChild(
this: HeapSnapshotGridNode,
item: HeapSnapshotModel.HeapSnapshotModel.Node|HeapSnapshotModel.HeapSnapshotModel.Edge,
insertionIndex: number): void {
if (this.savedChildren) {
const hash = this.childHashForEntity(item);
const child = this.savedChildren.get(hash);
if (child) {
this.dataGridInternal.insertChild(this, child, insertionIndex);
return;
}
}
this.dataGridInternal.insertChild(this, this.createChildNode(item), insertionIndex);
}
function insertShowMoreButton(
this: HeapSnapshotGridNode, from: number, to: number, insertionIndex: number): void {
const button = (new DataGrid.ShowMoreDataGridNode.ShowMoreDataGridNode(
this.populateChildren.bind(this), from, to, this.dataGridInternal.defaultPopulateCount()));
this.dataGridInternal.insertChild(this, (button as unknown as HeapSnapshotGridNode), insertionIndex);
}
function childrenRetrieved(
this: HeapSnapshotGridNode, itemsRange: HeapSnapshotModel.HeapSnapshotModel.ItemsRange,
toPosition: number): void {
let itemIndex = 0;
let itemPosition: number = itemsRange.startPosition;
const items = itemsRange.items;
let insertionIndex = 0;
if (!this.retrievedChildrenRanges.length) {
if (itemsRange.startPosition > 0) {
this.retrievedChildrenRanges.push({from: 0, to: 0});
insertShowMoreButton.call(this, 0, itemsRange.startPosition, insertionIndex++);
}
this.retrievedChildrenRanges.push({from: itemsRange.startPosition, to: itemsRange.endPosition});
for (let i = 0, l = items.length; i < l; ++i) {
insertRetrievedChild.call(this, items[i], insertionIndex++);
}
if (itemsRange.endPosition < itemsRange.totalLength) {
insertShowMoreButton.call(this, itemsRange.endPosition, itemsRange.totalLength, insertionIndex++);
}
} else {
let rangeIndex = 0;
let found = false;
let range: {
from: number,
to: number,
} = {from: 0, to: 0};
while (rangeIndex < this.retrievedChildrenRanges.length) {
range = this.retrievedChildrenRanges[rangeIndex];
if (range.to >= itemPosition) {
found = true;
break;
}
insertionIndex += range.to - range.from;
// Skip the button if there is one.
if (range.to < itemsRange.totalLength) {
insertionIndex += 1;
}
++rangeIndex;
}
if (!found || itemsRange.startPosition < range.from) {
// Update previous button.
const button =
this.allChildren()[insertionIndex - 1] as unknown as DataGrid.ShowMoreDataGridNode.ShowMoreDataGridNode;
button.setEndPosition(itemsRange.startPosition);
insertShowMoreButton.call(
this, itemsRange.startPosition, found ? range.from : itemsRange.totalLength, insertionIndex);
range = {from: itemsRange.startPosition, to: itemsRange.startPosition};
if (!found) {
rangeIndex = this.retrievedChildrenRanges.length;
}
this.retrievedChildrenRanges.splice(rangeIndex, 0, range);
} else {
insertionIndex += itemPosition - range.from;
}
// At this point insertionIndex is always an index before button or between nodes.
// Also it is always true here that range.from <= itemPosition <= range.to
// Stretch the range right bound to include all new items.
while (range.to < itemsRange.endPosition) {
// Skip already added nodes.
const skipCount = range.to - itemPosition;
insertionIndex += skipCount;
itemIndex += skipCount;
itemPosition = range.to;
// We're at the position before button: ...<?node>x<button>
const nextRange = this.retrievedChildrenRanges[rangeIndex + 1];
let newEndOfRange: number = nextRange ? nextRange.from : itemsRange.totalLength;
if (newEndOfRange > itemsRange.endPosition) {
newEndOfRange = itemsRange.endPosition;
}
while (itemPosition < newEndOfRange) {
insertRetrievedChild.call(this, items[itemIndex++], insertionIndex++);
++itemPosition;
}
// Merge with the next range.
if (nextRange && newEndOfRange === nextRange.from) {
range.to = nextRange.to;
// Remove "show next" button if there is one.
this.removeChildByIndex(insertionIndex);
this.retrievedChildrenRanges.splice(rangeIndex + 1, 1);
} else {
range.to = newEndOfRange;
// Remove or update next button.
if (newEndOfRange === itemsRange.totalLength) {
this.removeChildByIndex(insertionIndex);
} else {
(this.allChildren()[insertionIndex] as unknown as DataGrid.ShowMoreDataGridNode.ShowMoreDataGridNode)
.setStartPosition(itemsRange.endPosition);
}
}
}
}
this.instanceCount += items.length;
if (firstNotSerializedPosition < toPosition && firstNotSerializedPosition < itemsRange.totalLength) {
serializeNextChunk.call(this, toPosition);
return;
}
if (this.expanded) {
this.dataGridInternal.updateVisibleNodes(true);
}
resolve();
this.dispatchEventToListeners(HeapSnapshotGridNode.Events.PopulateComplete);
}
});
}
saveChildren(): void {
this.savedChildren.clear();
const children = this.allChildren();
for (let i = 0, l = children.length; i < l; ++i) {
const child = children[i];
if (!child.expanded) {
continue;
}
this.savedChildren.set(child.getHash(), child);
}
}
async sort(): Promise<void> {
this.dataGridInternal.recursiveSortingEnter();
await this.provider().sortAndRewind(this.comparator());
this.saveChildren();
this.dataGridInternal.removeAllChildren(this);
this.retrievedChildrenRanges = [];
const instanceCount = this.instanceCount;
this.instanceCount = 0;
await this.populateChildren(0, instanceCount);
for (const child of this.allChildren()) {
if (child.expanded) {
void child.sort();
}
}
this.dataGridInternal.recursiveSortingLeave();
}
}
export namespace HeapSnapshotGridNode {
export enum Events {
/* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
PopulateComplete = 'PopulateComplete',
/* eslint-enable @typescript-eslint/naming-convention */
}
export interface EventTypes {
[Events.PopulateComplete]: void;
}
}
export abstract class HeapSnapshotGenericObjectNode extends HeapSnapshotGridNode {
referenceName?: string|null;
readonly nameInternal: string|undefined;
readonly type: string|undefined;
readonly distance: number|undefined;
shallowSize: number|undefined;
readonly retainedSize: number|undefined;
snapshotNodeId: number|undefined;
snapshotNodeIndex: number|undefined;
detachedDOMTreeNode: boolean|undefined;
linkElement?: Element;
constructor(dataGrid: HeapSnapshotSortableDataGrid, node: HeapSnapshotModel.HeapSnapshotModel.Node) {
super(dataGrid, false);
// node is null for DataGrid root nodes.
if (!node) {
return;
}
this.referenceName = null;
this.nameInternal = node.name;
this.type = node.type;
this.distance = node.distance;
this.shallowSize = node.selfSize;
this.retainedSize = node.retainedSize;
this.snapshotNodeId = node.id;
this.snapshotNodeIndex = node.nodeIndex;
if (this.type === 'string') {
this.reachableFromWindow = true;
} else if (this.type === 'object' && this.nameInternal.startsWith('Window')) {
this.nameInternal = this.shortenWindowURL(this.nameInternal, false);
this.reachableFromWindow = true;
} else if (node.canBeQueried) {
this.reachableFromWindow = true;
}
if (node.detachedDOMTreeNode) {
this.detachedDOMTreeNode = true;
}
const snapshot = (dataGrid.snapshot as HeapSnapshotProxy);
const shallowSizePercent = this.shallowSize / snapshot.totalSize * 100.0;
const retainedSizePercent = this.retainedSize / snapshot.totalSize * 100.0;
this.data = {
distance: this.toUIDistance(this.distance),
shallowSize: i18n.ByteUtilities.formatBytesToKb(this.shallowSize),
retainedSize: i18n.ByteUtilities.formatBytesToKb(this.retainedSize),
'shallowSize-percent': this.toPercentString(shallowSizePercent),
'retainedSize-percent': this.toPercentString(retainedSizePercent),
};
}
override get name(): string|undefined {
return this.nameInternal;
}
override retainersDataSource(): {
snapshot: HeapSnapshotProxy,
snapshotNodeIndex: number,
snapshotNodeId: number|undefined,
}|null {
return this.snapshotNodeIndex === undefined ? null : {
snapshot: (this.dataGridInternal.snapshot as HeapSnapshotProxy),
snapshotNodeIndex: this.snapshotNodeIndex,
snapshotNodeId: this.snapshotNodeId,
};
}
override createCell(columnId: string): HTMLElement {
const cell = columnId !== 'object' ? this.createValueCell(columnId) : this.createObjectCell();
return cell;
}
createObjectCell(): HTMLElement {
let value: string|(string | undefined) = this.nameInternal;
let valueStyle = 'object';
switch (this.type) {
case 'concatenated string':
case 'string':
value = `"${value}"`;
valueStyle = 'string';
break;
case 'regexp':
value = `/${value}/`;
valueStyle = 'string';
break;
case 'closure':
value = `${value}()`;
valueStyle = 'function';
break;
case 'bigint':
valueStyle = 'bigint';
break;
case 'number':
valueStyle = 'number';
break;
case 'hidden':
case 'object shape':
valueStyle = 'null';
break;
case 'array':
value = value ? `${value}[]` : i18nString(UIStrings.internalArray);
break;
}
return this.createObjectCellWithValue(valueStyle, value || '');
}
createObjectCellWithValue(valueStyle: string, value: string): HTMLElement {
const jslog = VisualLogging.tableCell('object-column').track({click: true});
const fragment = UI.Fragment.Fragment.build`
<td class="object-column disclosure" jslog=${jslog}>
<div class="source-code event-properties" style="overflow: visible;" $="container">
<span class="value object-value-${valueStyle}">${value}</span>
<span class="object-value-id">@${this.snapshotNodeId}</span>
</div>
</td>`;
const div = fragment.$('container');
this.prefixObjectCell(div);
if (this.reachableFromWindow) {
const frameIcon = IconButton.Icon.create('frame', 'heap-object-tag');
UI.Tooltip.Tooltip.install(frameIcon, i18nString(UIStrings.userObjectReachableFromWindow));
div.appendChild(frameIcon);
}
if (this.detachedDOMTreeNode) {
const frameIcon = IconButton.Icon.create('scissors', 'heap-object-tag');
UI.Tooltip.Tooltip.install(frameIcon, i18nString(UIStrings.detachedFromDomTree));
div.appendChild(frameIcon);
}
void this.appendSourceLocation(div);
const cell = (fragment.element() as HTMLElement);
if (this.depth) {
cell.style.setProperty(
'padding-left', (this.depth * (this.dataGrid as HeapSnapshotSortableDataGrid).indentWidth) + 'px');
}
return cell;
}
prefixObjectCell(_div: Element): void {
}
async appendSourceLocation(div: Element): Promise<void> {
const linkContainer = UI.Fragment.html`<span class="heap-object-source-link" />`;
div.appendChild(linkContainer);
const link = await this.dataGridInternal.dataDisplayDelegate().linkifyObject((this.snapshotNodeIndex as number));
if (link) {
link.setAttribute('tabindex', '0');
linkContainer.appendChild(link);
this.linkElement = link;
} else {
linkContainer.remove();
}
}
override async queryObjectContent(
heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel,
objectGroupName: string): Promise<SDK.RemoteObject.RemoteObject|{description: string, link: string}> {
const remoteObject = await this.tryQueryObjectContent(heapProfilerModel, objectGroupName);
return remoteObject || this.tryGetTooltipDescription() ||
heapProfilerModel.runtimeModel().createRemoteObjectFromPrimitiveValue(
i18nString(UIStrings.previewIsNotAvailable));
}
override async tryQueryObjectContent(
heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel,
objectGroupName: string): Promise<SDK.RemoteObject.RemoteObject|null> {
if (this.type === 'string') {
return heapProfilerModel.runtimeModel().createRemoteObjectFromPrimitiveValue(this.nameInternal);
}
return await heapProfilerModel.objectForSnapshotObjectId(
String(this.snapshotNodeId) as Protocol.HeapProfiler.HeapSnapshotObjectId, objectGroupName);
}
tryGetTooltipDescription(): {description: string, link: string}|undefined {
const baseLink = 'https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#';
switch (this.type) {
case 'code':
return {description: i18nString(UIStrings.compiledCodeSummary), link: baseLink + 'compiled-code'};
case 'concatenated string':
return {description: i18nString(UIStrings.concatenatedStringSummary), link: baseLink + 'concatenated-string'};
case 'sliced string':
return {description: i18nString(UIStrings.slicedStringSummary), link: baseLink + 'sliced-string'};
}
switch (this.type + ':' + this.nameInternal) {
case 'array:': // If nameInternal is empty, then the object is shown as "(internal array)[]".
return {description: i18nString(UIStrings.internalArraySummary), link: baseLink + 'array'};
case 'array:(object elements)':
return {description: i18nString(UIStrings.objectElementsSummary), link: baseLink + 'array'};
case 'array:(object properties)':
case 'hidden:system / PropertyArray':
return {description: i18nString(UIStrings.objectPropertiesSummary), link: baseLink + 'array'};
case 'object:system / Context':
return {description: i18nString(UIStrings.contextSummary), link: baseLink + 'system-context'};
case 'object shape:system / DescriptorArray':
return {description: i18nString(UIStrings.descriptorArraySummary), link: baseLink + 'object-shape'};
case 'object shape:system / Map':
return {description: i18nString(UIStrings.mapSummary), link: baseLink + 'object-shape'};
case 'native:InternalNode':
return {description: i18nString(UIStrings.internalNodeSummary), link: baseLink + 'internal-node'};
}
return undefined;
}
async updateHasChildren(): Promise<void> {
const isEmpty = await this.provider().isEmpty();
this.setHasChildren(!isEmpty);
}
shortenWindowURL(fullName: string, hasObjectId: boolean): string {
const startPos = fullName.indexOf('/');
const endPos = hasObjectId ? fullName.indexOf('@') : fullName.length;
if (startPos === -1 || endPos === -1) {
return fullName;
}
const fullURL = fullName.substring(startPos + 1, endPos).trimLeft();
let url = Platform.StringUtilities.trimURL(fullURL);
if (url.length > 40) {
url = Platform.StringUtilities.trimMiddle(url, 40);
}
return fullName.substr(0, startPos + 2) + url + fullName.substr(endPos);
}
override populateContextMenu(
contextMenu: UI.ContextMenu.ContextMenu, dataDisplayDelegate: DataDisplayDelegate,
heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null): void {
if (this.shallowSize !== 0) {
contextMenu.revealSection().appendItem(i18nString(UIStrings.revealInSummaryView), () => {
dataDisplayDelegate.showObject(String(this.snapshotNodeId), i18nString(UIStrings.summary));
}, {jslogContext: 'reveal-in-summary'});
}
if (this.referenceName) {
for (const match of this.referenceName.matchAll(/\((?<objectName>[^@)]*) @(?<snapshotNodeId>\d+)\)/g)) {
const {objectName, snapshotNodeId} = (match.groups as {
objectName: string,
snapshotNodeId: string,
});
contextMenu.revealSection().appendItem(
i18nString(UIStrings.revealObjectSWithIdSInSummary, {PH1: objectName, PH2: snapshotNodeId}), () => {
dataDisplayDelegate.showObject(snapshotNodeId, i18nString(UIStrings.summary));
}, {jslogContext: 'reveal-in-summary'});
}
}
if (heapProfilerModel) {
contextMenu.revealSection().appendItem(i18nString(UIStrings.storeAsGlobalVariable), async () => {
const remoteObject = await this.tryQueryObjectContent((heapProfilerModel), '');
if (!remoteObject) {
Common.Console.Console.instance().error(i18nString(UIStrings.previewIsNotAvailable));
} else {
const consoleModel = heapProfilerModel.target().model(SDK.ConsoleModel.ConsoleModel);
await consoleModel?.saveToTempVariable(
UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext), remoteObject);
}
}, {jslogContext: 'store-as-global-variable'});
}
}
}
export class HeapSnapshotObjectNode extends HeapSnapshotGenericObjectNode {
override referenceName: string;
readonly referenceType: string;
readonly edgeIndex: number;
readonly snapshot: HeapSnapshotProxy;
parentObjectNode: HeapSnapshotObjectNode|null;
readonly cycledWithAncestorGridNode: HeapSnapshotObjectNode|null;
constructor(
dataGrid: HeapSnapshotSortableDataGrid, snapshot: HeapSnapshotProxy,
edge: HeapSnapshotModel.HeapSnapshotModel.Edge, parentObjectNode: HeapSnapshotObjectNode|null) {
super(dataGrid, edge.node);
this.referenceName = edge.name;
this.referenceType = edge.type;
this.edgeIndex = edge.edgeIndex;
this.snapshot = snapshot;
this.parentObjectNode = parentObjectNode;
this.cycledWithAncestorGridNode = this.findAncestorWithSameSnapshotNodeId();
if (!this.cycledWithAncestorGridNode) {
void this.updateHasChildren();
}
const data = this.data;
data['count'] = '';
data['addedCount'] = '';
data['removedCount'] = '';
data['countDelta'] = '';
data['addedSize'] = '';
data['removedSize'] = '';
data['sizeDelta'] = '';
}
override retainersDataSource(): {
snapshot: HeapSnapshotProxy,
snapshotNodeIndex: number,
snapshotNodeId: number|undefined,
}|null {
return this.snapshotNodeIndex === undefined ?
null :
{snapshot: this.snapshot, snapshotNodeIndex: this.snapshotNodeIndex, snapshotNodeId: this.snapshotNodeId};
}
override createProvider(): HeapSnapshotProviderProxy {
if (this.snapshotNodeIndex === undefined) {
throw new Error('Cannot create a provider on a root node');
}
return this.snapshot.createEdgesProvider(this.snapshotNodeIndex);
}
findAncestorWithSameSnapshotNodeId(): HeapSnapshotObjectNode|null {
let ancestor: (HeapSnapshotObjectNode|null) = this.parentObjectNode;
while (ancestor) {
if (ancestor.snapshotNodeId === this.snapshotNodeId) {
return ancestor;
}
ancestor = ancestor.parentObjectNode;
}
return null;
}
override createChildNode(item: HeapSnapshotModel.HeapSnapshotModel.Node|HeapSnapshotModel.HeapSnapshotModel.Edge):
HeapSnapshotObjectNode {
return new HeapSnapshotObjectNode(
this.dataGridInternal, this.snapshot, (item as HeapSnapshotModel.HeapSnapshotModel.Edge), this);
}
override getHash(): number {
return this.edgeIndex;
}
override comparator(): HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig {
const sortAscending = this.dataGridInternal.isSortOrderAscending();
const sortColumnId = this.dataGridInternal.sortColumnId();
switch (sortColumnId) {
case 'object':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig(
'!edgeName', sortAscending, 'retainedSize', false);
case 'count':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('!edgeName', true, 'retainedSize', false);
case 'shallowSize':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('selfSize', sortAscending, '!edgeName', true);
case 'retainedSize':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig(
'retainedSize', sortAscending, '!edgeName', true);
case 'distance':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('distance', sortAscending, 'name', true);
default:
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('!edgeName', true, 'retainedSize', false);
}
}
override prefixObjectCell(div: Element): void {
let name: string = this.referenceName || '(empty)';
let nameClass = 'name';
switch (this.referenceType) {
case 'context':
nameClass = 'object-value-number';
break;
case 'internal':
case 'hidden':
case 'weak':
nameClass = 'object-value-null';
break;
case 'element':
name = `[${name}]`;
break;
}
if (this.cycledWithAncestorGridNode) {
div.classList.add('cycled-ancestor-node');
}
div.prepend(UI.Fragment.html`<span class="property-name ${nameClass}">${name}</span>
<span class="grayed">${this.edgeNodeSeparator()}</span>`);
}
edgeNodeSeparator(): string {
return '::';
}
}
export class HeapSnapshotRetainingObjectNode extends HeapSnapshotObjectNode {
#ignored: boolean;
constructor(
dataGrid: HeapSnapshotSortableDataGrid, snapshot: HeapSnapshotProxy,
edge: HeapSnapshotModel.HeapSnapshotModel.Edge, parentRetainingObjectNode: HeapSnapshotRetainingObjectNode|null) {
super(dataGrid, snapshot, edge, parentRetainingObjectNode);
this.#ignored = edge.node.ignored;
if (this.#ignored) {
this.data['distance'] = i18nString(UIStrings.ignored);
}
}
override createProvider(): HeapSnapshotProviderProxy {
if (this.snapshotNodeIndex === undefined) {
throw new Error('Cannot create providers on root nodes');
}
return this.snapshot.createRetainingEdgesProvider(this.snapshotNodeIndex);
}
override createChildNode(item: HeapSnapshotModel.HeapSnapshotModel.Node|HeapSnapshotModel.HeapSnapshotModel.Edge):
HeapSnapshotRetainingObjectNode {
return new HeapSnapshotRetainingObjectNode(
this.dataGridInternal, this.snapshot, (item as HeapSnapshotModel.HeapSnapshotModel.Edge), this);
}
override edgeNodeSeparator(): string {
// TODO(l10n): improve description or clarify intention.
return i18nString(UIStrings.inElement);
}
override expand(): void {
this.expandRetainersChain(20);
}
override populateContextMenu(
contextMenu: UI.ContextMenu.ContextMenu, dataDisplayDelegate: DataDisplayDelegate,
heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null): void {
super.populateContextMenu(contextMenu, dataDisplayDelegate, heapProfilerModel);
const snapshotNodeIndex = this.snapshotNodeIndex;
if (snapshotNodeIndex === undefined) {
return;
}
if (this.#ignored) {
contextMenu.revealSection().appendItem(i18nString(UIStrings.stopIgnoringThisRetainer), async () => {
await this.snapshot.unignoreNodeInRetainersView(snapshotNodeIndex);
await this.dataGridInternal.dataSourceChanged();
}, {jslogContext: 'stop-ignoring-this-retainer'});
} else {
contextMenu.revealSection().appendItem(i18nString(UIStrings.ignoreThisRetainer), async () => {
await this.snapshot.ignoreNodeInRetainersView(snapshotNodeIndex);
await this.dataGridInternal.dataSourceChanged();
}, {jslogContext: 'ignore-this-retainer'});
}
}
isReachable(): boolean {
return (this.distance ?? 0) < HeapSnapshotModel.HeapSnapshotModel.baseUnreachableDistance;
}
override prefixObjectCell(div: Element): void {
super.prefixObjectCell(div);
if (!this.isReachable()) {
div.classList.add('unreachable-ancestor-node');
}
}
expandRetainersChain(maxExpandLevels: number): void {
if (!this.populated) {
void this.once(HeapSnapshotGridNode.Events.PopulateComplete)
.then(() => this.expandRetainersChain(maxExpandLevels));
this.populate();
return;
}
super.expand();
if (--maxExpandLevels > 0 && this.children.length > 0) {
const retainer = (this.children[0] as HeapSnapshotRetainingObjectNode);
if ((retainer.distance || 0) > 1 && retainer.isReachable()) {
retainer.expandRetainersChain(maxExpandLevels);
return;
}
}
this.dataGridInternal.dispatchEventToListeners(HeapSnapshotSortableDataGridEvents.ExpandRetainersComplete);
}
override comparator(): HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig {
const result = super.comparator();
if (result.fieldName1 === 'distance') {
result.fieldName1 = '!edgeDistance';
}
if (result.fieldName2 === 'distance') {
result.fieldName2 = '!edgeDistance';
}
return result;
}
}
export class HeapSnapshotInstanceNode extends HeapSnapshotGenericObjectNode {
readonly baseSnapshotOrSnapshot: HeapSnapshotProxy;
readonly isDeletedNode: boolean;
constructor(
dataGrid: HeapSnapshotSortableDataGrid, snapshot: HeapSnapshotProxy,
node: HeapSnapshotModel.HeapSnapshotModel.Node, isDeletedNode: boolean) {
super(dataGrid, node);
this.baseSnapshotOrSnapshot = snapshot;
this.isDeletedNode = isDeletedNode;
void this.updateHasChildren();
const data = this.data;
data['count'] = '';
data['countDelta'] = '';
data['sizeDelta'] = '';
if (this.isDeletedNode) {
data['addedCount'] = '';
data['addedSize'] = '';
data['removedCount'] = '\u2022';
data['removedSize'] = i18n.ByteUtilities.formatBytesToKb(this.shallowSize || 0);
} else {
data['addedCount'] = '\u2022';
data['addedSize'] = i18n.ByteUtilities.formatBytesToKb(this.shallowSize || 0);
data['removedCount'] = '';
data['removedSize'] = '';
}
}
override retainersDataSource(): {
snapshot: HeapSnapshotProxy,
snapshotNodeIndex: number,
snapshotNodeId: number|undefined,
}|null {
return this.snapshotNodeIndex === undefined ? null : {
snapshot: this.baseSnapshotOrSnapshot,
snapshotNodeIndex: this.snapshotNodeIndex,
snapshotNodeId: this.snapshotNodeId,
};
}
override createProvider(): HeapSnapshotProviderProxy {
if (this.snapshotNodeIndex === undefined) {
throw new Error('Cannot create providers on root nodes');
}
return this.baseSnapshotOrSnapshot.createEdgesProvider(this.snapshotNodeIndex);
}
override createChildNode(item: HeapSnapshotModel.HeapSnapshotModel.Node|HeapSnapshotModel.HeapSnapshotModel.Edge):
HeapSnapshotObjectNode {
return new HeapSnapshotObjectNode(
this.dataGridInternal, this.baseSnapshotOrSnapshot, (item as HeapSnapshotModel.HeapSnapshotModel.Edge), null);
}
override getHash(): number {
if (this.snapshotNodeId === undefined) {
throw new Error('Cannot hash root nodes');
}
return this.snapshotNodeId;
}
override comparator(): HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig {
const sortAscending = this.dataGridInternal.isSortOrderAscending();
const sortColumnId = this.dataGridInternal.sortColumnId();
switch (sortColumnId) {
case 'object':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig(
'!edgeName', sortAscending, 'retainedSize', false);
case 'distance':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig(
'distance', sortAscending, 'retainedSize', false);
case 'count':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('!edgeName', true, 'retainedSize', false);
case 'addedSize':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('selfSize', sortAscending, '!edgeName', true);
case 'removedSize':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('selfSize', sortAscending, '!edgeName', true);
case 'shallowSize':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('selfSize', sortAscending, '!edgeName', true);
case 'retainedSize':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig(
'retainedSize', sortAscending, '!edgeName', true);
default:
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('!edgeName', true, 'retainedSize', false);
}
}
}
export class HeapSnapshotConstructorNode extends HeapSnapshotGridNode {
readonly nameInternal: string;
readonly nodeFilter: HeapSnapshotModel.HeapSnapshotModel.NodeFilter;
readonly distance: number;
readonly count: number;
readonly shallowSize: number;
readonly retainedSize: number;
readonly classKey: string;
constructor(
dataGrid: HeapSnapshotConstructorsDataGrid, classKey: string,
aggregate: HeapSnapshotModel.HeapSnapshotModel.Aggregate,
nodeFilter: HeapSnapshotModel.HeapSnapshotModel.NodeFilter) {
super(dataGrid, aggregate.count > 0);
this.nameInternal = aggregate.name;
this.nodeFilter = nodeFilter;
this.distance = aggregate.distance;
this.count = aggregate.count;
this.shallowSize = aggregate.self;
this.retainedSize = aggregate.maxRet;
this.classKey = classKey;
const snapshot = (dataGrid.snapshot as HeapSnapshotProxy);
const retainedSizePercent = this.retainedSize / snapshot.totalSize * 100.0;
const shallowSizePercent = this.shallowSize / snapshot.totalSize * 100.0;
this.data = {
object: this.nameInternal,
count: Platform.NumberUtilities.withThousandsSeparator(this.count),
distance: this.toUIDistance(this.distance),
shallowSize: i18n.ByteUtilities.formatBytesToKb(this.shallowSize),
retainedSize: i18n.ByteUtilities.formatBytesToKb(this.retainedSize),
'shallowSize-percent': this.toPercentString(shallowSizePercent),
'retainedSize-percent': this.toPercentString(retainedSizePercent),
};
}
override get name(): string|undefined {
return this.nameInternal;
}
override createProvider(): HeapSnapshotProviderProxy {
return (this.dataGridInternal.snapshot as HeapSnapshotProxy)
.createNodesProviderForClass(this.classKey, this.nodeFilter);
}
async populateNodeBySnapshotObjectId(snapshotObjectId: number): Promise<HeapSnapshotGridNode[]> {
this.dataGridInternal.resetNameFilter();
await this.expandWithoutPopulate();
const nodePosition = await this.provider().nodePosition(snapshotObjectId);
if (nodePosition === -1) {
this.collapse();
return [];
}
await this.populateChildren(nodePosition, null);
const node = (this.childForPosition(nodePosition));
return node ? [this, node] : [];
}
filteredOut(filterValue: string): boolean {
return this.nameInternal.toLowerCase().indexOf(filterValue) === -1;
}
override createCell(columnId: string): HTMLElement {
const cell = columnId === 'object' ? super.createCell(columnId) : this.createValueCell(columnId);
if (columnId === 'object' && this.count > 1) {
cell.appendChild(UI.Fragment.html`<span class="objects-count">×${this.count}</span>`);
}
return cell;
}
override createChildNode(item: HeapSnapshotModel.HeapSnapshotModel.Node|HeapSnapshotModel.HeapSnapshotModel.Edge):
HeapSnapshotInstanceNode {
return new HeapSnapshotInstanceNode(
this.dataGridInternal, (this.dataGridInternal.snapshot as HeapSnapshotProxy),
(item as HeapSnapshotModel.HeapSnapshotModel.Node), false);
}
override comparator(): HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig {
const sortAscending = this.dataGridInternal.isSortOrderAscending();
const sortColumnId = this.dataGridInternal.sortColumnId();
switch (sortColumnId) {
case 'object':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('name', sortAscending, 'id', true);
case 'distance':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig(
'distance', sortAscending, 'retainedSize', false);
case 'shallowSize':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('selfSize', sortAscending, 'id', true);
case 'retainedSize':
return new HeapSnapshotModel.HeapSnapshotModel.ComparatorConfig('retainedSize', sortAscending, 'id', true);
default:
throw new Error(`Invalid sort column id ${sortColumnId}`);
}
}
}
export class HeapSnapshotDiffNodesProvider implements ChildrenProvider {
addedNodesProvider: HeapSnapshotProviderProxy;
deletedNodesProvider: HeapSnapshotProviderProxy;
addedCount: number;
removedCount: number;
constructor(
addedNodesProvider: HeapSnapshotProviderProxy, deletedNodesProvider: HeapSnapshotProviderProxy,
addedCount: number, removedCount: number) {
this.addedNodesProvider = addedNodesProvider;
this.deletedNodesProvider = deletedNodesProvider;
this.addedCount = addedCount;
this.removedCount = removedCount;
}
dispose(): void {
this.addedNodesProvider.dispose();
this.deletedNodesProvider.dispose();
}
nodePosition(_snapshotObjectId: number): Promise<number> {
throw new Error('Unreachable');
}
isEmpty(): Promise<boolean> {
return Promise.resolve(false);
}
async serializeItemsRange(beginPosition: number, endPosition: number):
Promise<HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
let itemsRange;
let addedItems;
if (beginPosition < this.addedCount) {
itemsRange = await this.addedNodesProvider.serializeItemsRange(beginPosition, endPosition);
for (const item of itemsRange.items) {
item.isAddedNotRemoved = true;
}
if (itemsRange.endPosition >= endPosition) {
itemsRange.totalLength = this.addedCount + this.removedCount;
return itemsRange;
}
addedItems = itemsRange;
itemsRange = await this.deletedNodesProvider.serializeItemsRange(0, endPosition - itemsRange.endPosition);
} else {
addedItems = new HeapSnapshotModel.HeapSnapshotModel.ItemsRange(0, 0, 0, []);
itemsRange = await this.deletedNodesProvider.serializeItemsRange(
beginPosition - this.addedCount, endPosition - this.addedCount);
}
if (!addedItems.items.length) {
addedItems.startPosition = this.addedCount + itemsRange.startPosition;
}
for (const item of itemsRange.items) {
item.isAddedNotRemoved = false;
}
addedItems.items.push(...itemsRange.items);
addedItems.endPosition = this.addedCount + itemsRange.endPosition;
addedItems.totalLength = this.a