UNPKG

chrome-devtools-frontend

Version:
710 lines (626 loc) 25.2 kB
// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* 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 Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import * as CPUProfile from '../../models/cpu_profile/cpu_profile.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import * as UI from '../../ui/legacy/legacy.js'; import {Events, HeapTimelineOverview, type IdsRangeChangedEvent, type Samples} from './HeapTimelineOverview.js'; import type {Formatter, ProfileDataGridNode} from './ProfileDataGrid.js'; import {ProfileFlameChartDataProvider} from './ProfileFlameChartDataProvider.js'; import {ProfileEvents, type ProfileHeader, ProfileType} from './ProfileHeader.js'; import {ProfileView, WritableProfileHeader} from './ProfileView.js'; const UIStrings = { /** *@description The reported total size used in the selected time frame of the allocation sampling profile *@example {3 MB} PH1 */ selectedSizeS: 'Selected size: {PH1}', /** *@description Name of column header that reports the size (in terms of bytes) used for a particular part of the heap, excluding the size of the children nodes of this part of the heap */ selfSizeBytes: 'Self size', /** *@description Name of column header that reports the total size (in terms of bytes) used for a particular part of the heap */ totalSizeBytes: 'Total size', /** *@description Button text to stop profiling the heap */ stopHeapProfiling: 'Stop heap profiling', /** *@description Button text to start profiling the heap */ startHeapProfiling: 'Start heap profiling', /** *@description Progress update that the profiler is recording the contents of the heap */ recording: 'Recording…', /** *@description Icon title in Heap Profile View of a profiler tool */ heapProfilerIsRecording: 'Heap profiler is recording', /** *@description Progress update that the profiler is in the process of stopping its recording of the heap */ stopping: 'Stopping…', /** *@description Sampling category to only profile allocations happening on the heap */ allocationSampling: 'Allocation sampling', /** *@description The title for the collection of profiles that are gathered from various snapshots of the heap, using a sampling (e.g. every 1/100) technique. */ samplingProfiles: 'Sampling profiles', /** *@description Description in Heap Profile View of a profiler tool */ recordMemoryAllocations: 'Approximate memory allocations by sampling long operations with minimal overhead and get a breakdown by JavaScript execution stack', /** *@description Name of a profile *@example {2} PH1 */ profileD: 'Profile {PH1}', /** *@description Accessible text for the value in bytes in memory allocation or coverage view. *@example {12345} PH1 */ sBytes: '{PH1} bytes', /** *@description Text in CPUProfile View of a profiler tool *@example {21.33} PH1 */ formatPercent: '{PH1} %', /** *@description The formatted size in kilobytes, abbreviated to kB *@example {1,021} PH1 */ skb: '{PH1} kB', /** *@description Text for the name of something */ name: 'Name', /** *@description Tooltip of a cell that reports the size used for a particular part of the heap, excluding the size of the children nodes of this part of the heap */ selfSize: 'Self size', /** *@description Tooltip of a cell that reports the total size used for a particular part of the heap */ totalSize: 'Total size', /** *@description Text for web URLs */ url: 'URL', } as const; const str_ = i18n.i18n.registerUIStrings('panels/profiler/HeapProfileView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); function convertToSamplingHeapProfile(profileHeader: SamplingHeapProfileHeader): Protocol.HeapProfiler.SamplingHeapProfile { return (profileHeader.profile || profileHeader.protocolProfile()) as Protocol.HeapProfiler.SamplingHeapProfile; } export class HeapProfileView extends ProfileView implements UI.SearchableView.Searchable { override profileHeader: SamplingHeapProfileHeader; readonly profileType: SamplingHeapProfileTypeBase; override adjustedTotal: number; readonly selectedSizeText: UI.Toolbar.ToolbarText; timestamps: number[]; sizes: number[]; max: number[]; ordinals: number[]; totalTime: number; lastOrdinal: number; readonly timelineOverview: HeapTimelineOverview; constructor(profileHeader: SamplingHeapProfileHeader) { super(); this.profileHeader = profileHeader; this.profileType = profileHeader.profileType(); this.initialize(new NodeFormatter(this)); const profile = new SamplingHeapProfileModel(convertToSamplingHeapProfile(profileHeader)); this.adjustedTotal = profile.total; this.setProfile(profile); this.selectedSizeText = new UI.Toolbar.ToolbarText(); this.timestamps = []; this.sizes = []; this.max = []; this.ordinals = []; this.totalTime = 0; this.lastOrdinal = 0; this.timelineOverview = new HeapTimelineOverview(); if (Root.Runtime.experiments.isEnabled('sampling-heap-profiler-timeline')) { this.timelineOverview.addEventListener(Events.IDS_RANGE_CHANGED, this.onIdsRangeChanged.bind(this)); this.timelineOverview.show(this.element, this.element.firstChild); this.timelineOverview.start(); this.profileType.addEventListener(SamplingHeapProfileType.Events.STATS_UPDATE, this.onStatsUpdate, this); void this.profileType.once(ProfileEvents.PROFILE_COMPLETE).then(() => { this.profileType.removeEventListener(SamplingHeapProfileType.Events.STATS_UPDATE, this.onStatsUpdate, this); this.timelineOverview.stop(); this.timelineOverview.updateGrid(); }); } } override async toolbarItems(): Promise<UI.Toolbar.ToolbarItem[]> { return [...await super.toolbarItems(), this.selectedSizeText]; } onIdsRangeChanged(event: Common.EventTarget.EventTargetEvent<IdsRangeChangedEvent>): void { const {minId, maxId} = event.data; this.selectedSizeText.setText( i18nString(UIStrings.selectedSizeS, {PH1: i18n.ByteUtilities.bytesToString(event.data.size)})); this.setSelectionRange(minId, maxId); } setSelectionRange(minId: number, maxId: number): void { const profileData = convertToSamplingHeapProfile((this.profileHeader)); const profile = new SamplingHeapProfileModel(profileData, minId, maxId); this.adjustedTotal = profile.total; this.setProfile(profile); } onStatsUpdate(event: Common.EventTarget.EventTargetEvent<Protocol.HeapProfiler.SamplingHeapProfile|null>): void { const profile = event.data; if (!this.totalTime) { this.timestamps = []; this.sizes = []; this.max = []; this.ordinals = []; this.totalTime = 30000; this.lastOrdinal = 0; } this.sizes.fill(0); this.sizes.push(0); this.timestamps.push(Date.now()); this.ordinals.push(this.lastOrdinal + 1); for (const sample of profile?.samples ?? []) { this.lastOrdinal = Math.max(this.lastOrdinal, sample.ordinal); const bucket = Platform.ArrayUtilities.upperBound( this.ordinals, sample.ordinal, Platform.ArrayUtilities.DEFAULT_COMPARATOR) - 1; this.sizes[bucket] += sample.size; } this.max.push(this.sizes[this.sizes.length - 1]); const lastTimestamp = this.timestamps[this.timestamps.length - 1]; if (lastTimestamp - this.timestamps[0] > this.totalTime) { this.totalTime *= 2; } const samples = ({ sizes: this.sizes, max: this.max, ids: this.ordinals, timestamps: this.timestamps, totalTime: this.totalTime, } as Samples); this.timelineOverview.setSamples(samples); } override columnHeader(columnId: string): Common.UIString.LocalizedString { switch (columnId) { case 'self': return i18nString(UIStrings.selfSizeBytes); case 'total': return i18nString(UIStrings.totalSizeBytes); } return Common.UIString.LocalizedEmptyString; } override createFlameChartDataProvider(): ProfileFlameChartDataProvider { return new HeapFlameChartDataProvider( (this.profile() as SamplingHeapProfileModel), this.profileHeader.heapProfilerModel()); } } export class SamplingHeapProfileTypeBase extends Common.ObjectWrapper.eventMixin<SamplingHeapProfileType.EventTypes, typeof ProfileType>(ProfileType) { recording: boolean; clearedDuringRecording: boolean; constructor(typeId: string, description: string) { super(typeId, description); this.recording = false; this.clearedDuringRecording = false; } override profileBeingRecorded(): SamplingHeapProfileHeader|null { return super.profileBeingRecorded() as SamplingHeapProfileHeader | null; } override typeName(): string { return 'Heap'; } override fileExtension(): string { return '.heapprofile'; } override get buttonTooltip(): Common.UIString.LocalizedString { return this.recording ? i18nString(UIStrings.stopHeapProfiling) : i18nString(UIStrings.startHeapProfiling); } override buttonClicked(): boolean { if (this.recording) { void this.stopRecordingProfile(); } else { void this.startRecordingProfile(); } return this.recording; } async startRecordingProfile(): Promise<void> { const heapProfilerModel = UI.Context.Context.instance().flavor(SDK.HeapProfilerModel.HeapProfilerModel); if (this.profileBeingRecorded() || !heapProfilerModel) { return; } const profileHeader = new SamplingHeapProfileHeader(heapProfilerModel, this); this.setProfileBeingRecorded(profileHeader); this.addProfile(profileHeader); profileHeader.updateStatus(i18nString(UIStrings.recording)); const warnings = [i18nString(UIStrings.heapProfilerIsRecording)]; UI.InspectorView.InspectorView.instance().setPanelWarnings('heap-profiler', warnings); this.recording = true; const target = heapProfilerModel.target(); const animationModel = target.model(SDK.AnimationModel.AnimationModel); if (animationModel) { // TODO(b/406904348): Remove this once we correctly release animations on the backend. await animationModel.releaseAllAnimations(); } this.startSampling(); } async stopRecordingProfile(): Promise<void> { this.recording = false; const recordedProfile = this.profileBeingRecorded(); if (!recordedProfile?.heapProfilerModel()) { return; } recordedProfile.updateStatus(i18nString(UIStrings.stopping)); const profile = await this.stopSampling(); if (recordedProfile) { console.assert(profile !== undefined); // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any recordedProfile.setProtocolProfile((profile as any)); recordedProfile.updateStatus(''); this.setProfileBeingRecorded(null); } UI.InspectorView.InspectorView.instance().setPanelWarnings('heap-profiler', []); // If the data was cleared during the middle of the recording we no // longer treat the profile as being completed. This means we avoid // a change of view to the profile list. const wasClearedDuringRecording = this.clearedDuringRecording; this.clearedDuringRecording = false; if (wasClearedDuringRecording) { return; } this.dispatchEventToListeners(ProfileEvents.PROFILE_COMPLETE, recordedProfile); } override createProfileLoadedFromFile(title: string): ProfileHeader { return new SamplingHeapProfileHeader(null, this, title); } override profileBeingRecordedRemoved(): void { this.clearedDuringRecording = true; void this.stopRecordingProfile(); } startSampling(): void { throw new Error('Not implemented'); } stopSampling(): Promise<Protocol.HeapProfiler.SamplingHeapProfile> { throw new Error('Not implemented'); } } let samplingHeapProfileTypeInstance: SamplingHeapProfileType; export class SamplingHeapProfileType extends SamplingHeapProfileTypeBase { updateTimer: number; updateIntervalMs: number; constructor() { super(SamplingHeapProfileType.TypeId, i18nString(UIStrings.allocationSampling)); if (!samplingHeapProfileTypeInstance) { samplingHeapProfileTypeInstance = this; } this.updateTimer = 0; this.updateIntervalMs = 200; } static get instance(): SamplingHeapProfileType { return samplingHeapProfileTypeInstance; } override get treeItemTitle(): Common.UIString.LocalizedString { return i18nString(UIStrings.samplingProfiles); } override get description(): string { // TODO(l10n): Do not concatenate localized strings. const formattedDescription = [i18nString(UIStrings.recordMemoryAllocations)]; return formattedDescription.join('\n'); } override hasTemporaryView(): boolean { return Root.Runtime.experiments.isEnabled('sampling-heap-profiler-timeline'); } override startSampling(): void { const heapProfilerModel = this.obtainRecordingProfile(); if (!heapProfilerModel) { return; } void heapProfilerModel.startSampling(); if (Root.Runtime.experiments.isEnabled('sampling-heap-profiler-timeline')) { this.updateTimer = window.setTimeout(() => { void this.updateStats(); }, this.updateIntervalMs); } } obtainRecordingProfile(): SDK.HeapProfilerModel.HeapProfilerModel|null { const recordingProfile = this.profileBeingRecorded(); if (recordingProfile) { const heapProfilerModel = recordingProfile.heapProfilerModel(); return heapProfilerModel; } return null; } override async stopSampling(): Promise<Protocol.HeapProfiler.SamplingHeapProfile> { window.clearTimeout(this.updateTimer); this.updateTimer = 0; this.dispatchEventToListeners(SamplingHeapProfileType.Events.RECORDING_STOPPED); const heapProfilerModel = this.obtainRecordingProfile(); if (!heapProfilerModel) { throw new Error('No heap profiler model'); } const samplingProfile = await heapProfilerModel.stopSampling(); if (!samplingProfile) { throw new Error('No sampling profile found'); } return samplingProfile; } async updateStats(): Promise<void> { const heapProfilerModel = this.obtainRecordingProfile(); if (!heapProfilerModel) { return; } const profile = await heapProfilerModel.getSamplingProfile(); if (!this.updateTimer) { return; } this.dispatchEventToListeners(SamplingHeapProfileType.Events.STATS_UPDATE, profile); this.updateTimer = window.setTimeout(() => { void this.updateStats(); }, this.updateIntervalMs); } // eslint-disable-next-line @typescript-eslint/naming-convention static readonly TypeId = 'SamplingHeap'; } export namespace SamplingHeapProfileType { export const enum Events { RECORDING_STOPPED = 'RecordingStopped', STATS_UPDATE = 'StatsUpdate', } export interface EventTypes { [Events.RECORDING_STOPPED]: void; [Events.STATS_UPDATE]: Protocol.HeapProfiler.SamplingHeapProfile|null; } } export class SamplingHeapProfileHeader extends WritableProfileHeader { readonly heapProfilerModelInternal: SDK.HeapProfilerModel.HeapProfilerModel|null; override protocolProfileInternal: { head: { callFrame: { functionName: string, scriptId: Protocol.Runtime.ScriptId, url: string, lineNumber: number, columnNumber: number, }, children: never[], selfSize: number, id: number, }, samples: never[], startTime: number, endTime: number, nodes: never[], }; constructor( heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null, type: SamplingHeapProfileTypeBase, title?: string) { super( heapProfilerModel?.debuggerModel() ?? null, type, title || i18nString(UIStrings.profileD, {PH1: type.nextProfileUid()})); this.heapProfilerModelInternal = heapProfilerModel; this.protocolProfileInternal = { head: { callFrame: { functionName: '', scriptId: '' as Protocol.Runtime.ScriptId, url: '', lineNumber: 0, columnNumber: 0, }, children: [], selfSize: 0, id: 0, }, samples: [], startTime: 0, endTime: 0, nodes: [], }; } override createView(): HeapProfileView { return new HeapProfileView(this); } protocolProfile(): Protocol.HeapProfiler.SamplingHeapProfile { return this.protocolProfileInternal; } heapProfilerModel(): SDK.HeapProfilerModel.HeapProfilerModel|null { return this.heapProfilerModelInternal; } override profileType(): SamplingHeapProfileTypeBase { return super.profileType() as SamplingHeapProfileTypeBase; } } export class SamplingHeapProfileNode extends CPUProfile.ProfileTreeModel.ProfileNode { override self: number; constructor(node: Protocol.HeapProfiler.SamplingHeapProfileNode) { const callFrame = node.callFrame || ({ // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // @ts-expect-error functionName: node['functionName'], // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // @ts-expect-error scriptId: node['scriptId'], // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // @ts-expect-error url: node['url'], // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // @ts-expect-error lineNumber: node['lineNumber'] - 1, // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // @ts-expect-error columnNumber: node['columnNumber'] - 1, } as Protocol.Runtime.CallFrame); super(callFrame); this.self = node.selfSize; } } export class SamplingHeapProfileModel extends CPUProfile.ProfileTreeModel.ProfileTreeModel { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any modules: any; constructor(profile: Protocol.HeapProfiler.SamplingHeapProfile, minOrdinal?: number, maxOrdinal?: number) { super(); // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any this.modules = (profile as any).modules || []; let nodeIdToSizeMap: Map<number, number>|null = null; if (minOrdinal || maxOrdinal) { nodeIdToSizeMap = new Map<number, number>(); minOrdinal = minOrdinal || 0; maxOrdinal = maxOrdinal || Infinity; for (const sample of profile.samples) { if (sample.ordinal < minOrdinal || sample.ordinal > maxOrdinal) { continue; } const size = nodeIdToSizeMap.get(sample.nodeId) || 0; nodeIdToSizeMap.set(sample.nodeId, size + sample.size); } } this.initialize(translateProfileTree(profile.head)); function translateProfileTree(root: Protocol.HeapProfiler.SamplingHeapProfileNode): SamplingHeapProfileNode { const resultRoot = new SamplingHeapProfileNode(root); const sourceNodeStack = [root]; const targetNodeStack = [resultRoot]; while (sourceNodeStack.length) { const sourceNode = (sourceNodeStack.pop() as Protocol.HeapProfiler.SamplingHeapProfileNode); const targetNode = (targetNodeStack.pop() as SamplingHeapProfileNode); targetNode.children = sourceNode.children.map(child => { const targetChild = new SamplingHeapProfileNode(child); if (nodeIdToSizeMap) { targetChild.self = nodeIdToSizeMap.get(child.id) || 0; } return targetChild; }); sourceNodeStack.push(...sourceNode.children); targetNodeStack.push(...targetNode.children); } pruneEmptyBranches(resultRoot); return resultRoot; } function pruneEmptyBranches(node: CPUProfile.ProfileTreeModel.ProfileNode): boolean { node.children = node.children.filter(pruneEmptyBranches); return Boolean(node.children.length || node.self); } } } export class NodeFormatter implements Formatter { readonly profileView: HeapProfileView; constructor(profileView: HeapProfileView) { this.profileView = profileView; } formatValue(value: number): string { return i18n.ByteUtilities.bytesToString(value); } formatValueAccessibleText(value: number): string { return i18nString(UIStrings.sBytes, {PH1: value}); } formatPercent(value: number, _node: ProfileDataGridNode): string { return i18nString(UIStrings.formatPercent, {PH1: value.toFixed(2)}); } linkifyNode(node: ProfileDataGridNode): Element|null { const heapProfilerModel = this.profileView.profileHeader.heapProfilerModel(); const target = heapProfilerModel ? heapProfilerModel.target() : null; const options = { className: 'profile-node-file', inlineFrameIndex: 0, }; return this.profileView.linkifier().maybeLinkifyConsoleCallFrame(target, node.profileNode.callFrame, options); } } export class HeapFlameChartDataProvider extends ProfileFlameChartDataProvider { readonly profile: CPUProfile.ProfileTreeModel.ProfileTreeModel; readonly heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null; constructor( profile: CPUProfile.ProfileTreeModel.ProfileTreeModel, heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null) { super(); this.profile = profile; this.heapProfilerModel = heapProfilerModel; } override minimumBoundary(): number { return 0; } override totalTime(): number { return this.profile.root.total; } override entryHasDeoptReason(_entryIndex: number): boolean { return false; } override formatValue(value: number, _precision?: number): string { return i18nString(UIStrings.skb, {PH1: Platform.NumberUtilities.withThousandsSeparator(value / 1e3)}); } override calculateTimelineData(): PerfUI.FlameChart.FlameChartTimelineData { function nodesCount(node: CPUProfile.ProfileTreeModel.ProfileNode): number { return node.children.reduce((count, node) => count + nodesCount(node), 1); } const count = nodesCount(this.profile.root); const entryNodes: CPUProfile.ProfileTreeModel.ProfileNode[] = new Array(count); const entryLevels = new Uint16Array(count); const entryTotalTimes = new Float32Array(count); const entryStartTimes = new Float64Array(count); let depth = 0; let maxDepth = 0; let position = 0; let index = 0; function addNode(node: CPUProfile.ProfileTreeModel.ProfileNode): void { const start = position; entryNodes[index] = node; entryLevels[index] = depth; entryTotalTimes[index] = node.total; entryStartTimes[index] = position; ++index; ++depth; node.children.forEach(addNode); --depth; maxDepth = Math.max(maxDepth, depth); position = start + node.total; } addNode(this.profile.root); this.maxStackDepthInternal = maxDepth + 1; this.entryNodes = entryNodes; this.timelineDataInternal = PerfUI.FlameChart.FlameChartTimelineData.create({entryLevels, entryTotalTimes, entryStartTimes, groups: null}); return this.timelineDataInternal; } override preparePopoverElement(entryIndex: number): Element|null { const node = this.entryNodes[entryIndex]; if (!node) { return null; } const popoverInfo: Array<{ title: string, value: string, }> = []; function pushRow(title: string, value: string): void { popoverInfo.push({title, value}); } pushRow(i18nString(UIStrings.name), UI.UIUtils.beautifyFunctionName(node.functionName)); pushRow(i18nString(UIStrings.selfSize), i18n.ByteUtilities.bytesToString(node.self)); pushRow(i18nString(UIStrings.totalSize), i18n.ByteUtilities.bytesToString(node.total)); const linkifier = new Components.Linkifier.Linkifier(); const link = linkifier.maybeLinkifyConsoleCallFrame( this.heapProfilerModel ? this.heapProfilerModel.target() : null, node.callFrame); if (link) { pushRow(i18nString(UIStrings.url), (link.textContent as string)); } linkifier.dispose(); return ProfileView.buildPopoverTable(popoverInfo); } }