// Copyright 2011 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/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 {createIcon} from '../../ui/kit/kit.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 {} export class HeapSnapshotGridNode extends Common.ObjectWrapper.eventMixin( HeapSnapshotGridNodeBase) { dataGridInternal: HeapSnapshotSortableDataGrid; instanceCount: number; readonly savedChildren: Map; 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 { throw new Error('Not implemented.'); } tryQueryObjectContent(_heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel, _objectGroupName: string): Promise { 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`` 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`${this.data[columnId]}`; div.appendChild(valueSpan); const percentColumn = columnId + '-percent'; if (percentColumn in this.data) { const percentSpan = UI.Fragment.html`${this.data[percentColumn]}`; 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 { // 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 { 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: ...x