blob: 6cb924f9e9467f6b87c81f822fab7ede0d172d14 [file] [log] [blame]
// 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.
/**
* @implements {UI.Searchable}
* @unrestricted
*/
Profiler.HeapProfileView = class extends Profiler.ProfileView {
/**
* @param {!Profiler.SamplingHeapProfileHeader} profileHeader
*/
constructor(profileHeader) {
super();
this._profileHeader = profileHeader;
this._profileType = profileHeader.profileType();
const views = [
Profiler.ProfileView.ViewTypes.Flame, Profiler.ProfileView.ViewTypes.Heavy, Profiler.ProfileView.ViewTypes.Tree
];
const isNativeProfile = this._profileType.id === Profiler.SamplingNativeHeapProfileType.TypeId ||
this._profileType.id === Profiler.SamplingNativeHeapSnapshotType.TypeId;
if (isNativeProfile) {
views.push(Profiler.ProfileView.ViewTypes.Text);
}
this.initialize(new Profiler.HeapProfileView.NodeFormatter(this), views);
const profile = new Profiler.SamplingHeapProfileModel(profileHeader._profile || profileHeader.protocolProfile());
this.adjustedTotal = profile.total;
this.setProfile(profile);
this._selectedSizeText = new UI.ToolbarText();
if (Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline')) {
this._timelineOverview = new Profiler.HeapTimelineOverview();
this._timelineOverview.addEventListener(
Profiler.HeapTimelineOverview.IdsRangeChanged, this._onIdsRangeChanged.bind(this));
this._timelineOverview.show(this.element, this.element.firstChild);
this._timelineOverview.start();
this._profileType.addEventListener(
Profiler.SamplingHeapProfileType.Events.StatsUpdate, this._onStatsUpdate, this);
this._profileType.once(Profiler.ProfileType.Events.ProfileComplete).then(() => {
this._profileType.removeEventListener(
Profiler.SamplingHeapProfileType.Events.StatsUpdate, this._onStatsUpdate, this);
this._timelineOverview.stop();
this._timelineOverview.updateGrid();
});
}
}
/**
* @override
* @return {!Array<!UI.ToolbarItem>}
*/
syncToolbarItems() {
return [...super.syncToolbarItems(), this._selectedSizeText];
}
/**
* @param {!Common.Event} event
*/
_onIdsRangeChanged(event) {
const minId = /** @type {number} */ (event.data.minId);
const maxId = /** @type {number} */ (event.data.maxId);
this._selectedSizeText.setText(ls`Selected size: ${Number.bytesToString(event.data.size)}`);
this._setSelectionRange(minId, maxId);
}
/**
* @param {number} minId
* @param {number} maxId
*/
_setSelectionRange(minId, maxId) {
const profile = new Profiler.SamplingHeapProfileModel(
this._profileHeader._profile || this._profileHeader.protocolProfile(), minId, maxId);
this.adjustedTotal = profile.total;
this.setProfile(profile);
}
/**
* @param {!Common.Event} event
*/
_onStatsUpdate(event) {
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);
this._lastOrdinal = profile.samples.reduce((res, sample) => Math.max(res, sample.ordinal), this._lastOrdinal);
for (const sample of profile.samples) {
const bucket = this._ordinals.upperBound(sample.ordinal) - 1;
this._sizes[bucket] += sample.size;
}
this._max.push(this._sizes.peekLast());
if (this._timestamps.peekLast() - this._timestamps[0] > this._totalTime) {
this._totalTime *= 2;
}
const samples = /** @type {!Profiler.HeapTimelineOverview.Samples} */ ({
sizes: this._sizes,
max: this._max,
ids: this._ordinals,
timestamps: this._timestamps,
totalTime: this._totalTime
});
this._timelineOverview.setSamples(samples);
}
/**
* @override
* @param {string} columnId
* @return {string}
*/
columnHeader(columnId) {
switch (columnId) {
case 'self':
return Common.UIString('Self Size (bytes)');
case 'total':
return Common.UIString('Total Size (bytes)');
}
return '';
}
/**
* @override
* @return {!PerfUI.FlameChartDataProvider}
*/
createFlameChartDataProvider() {
return new Profiler.HeapFlameChartDataProvider(
/** @type {!Profiler.SamplingHeapProfileModel} */ (this.profile()), this._profileHeader.heapProfilerModel());
}
/**
* @override
* @param {!UI.SimpleView} view
*/
populateTextView(view) {
const guides = '+!:|';
let text = `Sampling memory profile.\n\nDate/Time: ${new Date()}\n` +
`Report Version: 7\n` +
`App Version: ${/Chrom\S*/.exec(navigator.appVersion)[0] || 'Unknown'}\n` +
`Node Weight: 1 KiB\n` +
`Total Size: ${Math.round(this.profile().root.total / 1024)} KiB\n` +
`----\n\nCall graph:\n`;
const sortedChildren = this.profile().root.children.sort((a, b) => b.total - a.total);
const modules = this.profile().modules.map(
m => Object.assign({address: BigInt(m.baseAddress), endAddress: BigInt(m.baseAddress) + BigInt(m.size)}, m));
modules.sort((m1, m2) => m1.address > m2.address ? 1 : m1.address < m2.address ? -1 : 0);
for (const child of sortedChildren) {
printTree(' ', child !== sortedChildren.peekLast(), child);
}
text += '\nBinary Images:\n';
for (const module of modules) {
const fileName = /[^/\\]*$/.exec(module.name)[0];
const version = '1.0';
const formattedUuid = module.uuid.includes('-') ?
module.uuid :
module.uuid.replace(/(.{8})(.{4})(.{4})(.{4})(.{12}).*/, '$1-$2-$3-$4-$5');
text += `${('0x' + module.address.toString(16)).padStart(18)} - `;
text += `${('0x' + (module.endAddress - BigInt(1)).toString(16)).padStart(18)}`;
text += ` ${fileName} (${version}) <${formattedUuid}> ${module.name}\n`;
}
view.contentElement.createChild('pre', 'profile-text-view monospace').textContent = text;
/**
* @param {string} padding
* @param {boolean} drawGuide
* @param {!SDK.ProfileNode} node
*/
function printTree(padding, drawGuide, node) {
const addressText = /0x[0-9a-f]*|[0-9]*/.exec(node.functionName)[0] || '';
let module;
if (addressText) {
const address = BigInt(addressText);
const pos = modules.upperBound(address, (address, module) => address - module.address);
if (pos > 0 && address < modules[pos - 1].endAddress) {
module = modules[pos - 1];
}
}
const functionName =
(addressText ? node.functionName.substr(addressText.length + 1) : node.functionName) || '???';
text += `${padding}${Math.round(node.total / 1024)} ${functionName} `;
if (module) {
const fileName = /[^/\\]*$/.exec(module.name);
if (fileName) {
text += `(in ${fileName}) `;
}
const offset = BigInt(addressText) - module.address;
text += `load address ${module.baseAddress} + 0x${offset.toString(16)} `;
}
if (addressText) {
text += `[${addressText}]`;
}
text += '\n';
const guideChar = drawGuide ? guides[padding.length / 2 % guides.length] : ' ';
const nextPadding = padding + guideChar + ' ';
const sortedChildren = node.children.sort((a, b) => b.total - a.total);
for (const child of sortedChildren) {
printTree(nextPadding, child !== sortedChildren.peekLast(), child);
}
}
}
};
/**
* @unrestricted
*/
Profiler.SamplingHeapProfileTypeBase = class extends Profiler.ProfileType {
/**
* @param {string} typeId
* @param {string} description
*/
constructor(typeId, description) {
super(typeId, description);
this._recording = false;
}
/**
* @override
* @return {?Profiler.SamplingHeapProfileHeader}
*/
profileBeingRecorded() {
return /** @type {?Profiler.SamplingHeapProfileHeader} */ (super.profileBeingRecorded());
}
/**
* @override
* @return {string}
*/
typeName() {
return 'Heap';
}
/**
* @override
* @return {string}
*/
fileExtension() {
return '.heapprofile';
}
/**
* @override
*/
get buttonTooltip() {
return this._recording ? ls`Stop heap profiling` : ls`Start heap profiling`;
}
/**
* @override
* @return {boolean}
*/
buttonClicked() {
if (this._recording) {
this._stopRecordingProfile();
} else {
this._startRecordingProfile();
}
return this._recording;
}
_startRecordingProfile() {
const heapProfilerModel = UI.context.flavor(SDK.HeapProfilerModel);
if (this.profileBeingRecorded() || !heapProfilerModel) {
return;
}
const profileHeader = new Profiler.SamplingHeapProfileHeader(heapProfilerModel, this);
this.setProfileBeingRecorded(profileHeader);
this.addProfile(profileHeader);
profileHeader.updateStatus(ls`Recording\u2026`);
const icon = UI.Icon.create('smallicon-warning');
icon.title = ls`Heap profiler is recording`;
UI.inspectorView.setPanelIcon('heap_profiler', icon);
this._recording = true;
this._startSampling();
}
async _stopRecordingProfile() {
this._recording = false;
if (!this.profileBeingRecorded() || !this.profileBeingRecorded().heapProfilerModel()) {
return;
}
this.profileBeingRecorded().updateStatus(ls`Stopping\u2026`);
const profile = await this._stopSampling();
const recordedProfile = this.profileBeingRecorded();
if (recordedProfile) {
console.assert(profile);
recordedProfile.setProtocolProfile(profile);
recordedProfile.updateStatus('');
this.setProfileBeingRecorded(null);
}
UI.inspectorView.setPanelIcon('heap_profiler', null);
this.dispatchEventToListeners(Profiler.ProfileType.Events.ProfileComplete, recordedProfile);
}
/**
* @override
* @param {string} title
* @return {!Profiler.ProfileHeader}
*/
createProfileLoadedFromFile(title) {
return new Profiler.SamplingHeapProfileHeader(null, this, title);
}
/**
* @override
*/
profileBeingRecordedRemoved() {
this._stopRecordingProfile();
}
_startSampling() {
throw 'Not implemented';
}
/**
* return {!Promise<!Protocol.HeapProfiler.SamplingHeapProfile>}
*/
_stopSampling() {
throw 'Not implemented';
}
};
/**
* @unrestricted
*/
Profiler.SamplingHeapProfileType = class extends Profiler.SamplingHeapProfileTypeBase {
constructor() {
super(Profiler.SamplingHeapProfileType.TypeId, ls`Allocation sampling`);
Profiler.SamplingHeapProfileType.instance = this;
this._updateTimer = null;
this._updateIntervalMs = 200;
}
/**
* @override
*/
get treeItemTitle() {
return ls`SAMPLING PROFILES`;
}
/**
* @override
*/
get description() {
return ls`Record memory allocations using sampling method.
This profile type has minimal performance overhead and can be used for long running operations.
It provides good approximation of allocations broken down by JavaScript execution stack.`;
}
/**
* @override
* @return {boolean}
*/
hasTemporaryView() {
return Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline');
}
/**
* @override
*/
_startSampling() {
this.profileBeingRecorded().heapProfilerModel().startSampling();
if (Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline')) {
this._updateTimer = setTimeout(this._updateStats.bind(this), this._updateIntervalMs);
}
}
/**
* @override
* return {!Promise<!Protocol.HeapProfiler.SamplingHeapProfile>}
*/
_stopSampling() {
clearTimeout(this._updateTimer);
this._updateTimer = null;
this.dispatchEventToListeners(Profiler.SamplingHeapProfileType.Events.RecordingStopped);
return this.profileBeingRecorded().heapProfilerModel().stopSampling();
}
async _updateStats() {
const profile = await this.profileBeingRecorded().heapProfilerModel().getSamplingProfile();
if (!this._updateTimer) {
return;
}
this.dispatchEventToListeners(Profiler.SamplingHeapProfileType.Events.StatsUpdate, profile);
this._updateTimer = setTimeout(this._updateStats.bind(this), this._updateIntervalMs);
}
};
Profiler.SamplingHeapProfileType.TypeId = 'SamplingHeap';
/** @override @suppress {checkPrototypalTypes} @enum {symbol} */
Profiler.SamplingHeapProfileType.Events = {
RecordingStopped: Symbol('RecordingStopped'),
StatsUpdate: Symbol('StatsUpdate')
};
/**
* @unrestricted
*/
Profiler.SamplingNativeHeapProfileType = class extends Profiler.SamplingHeapProfileTypeBase {
constructor() {
super(Profiler.SamplingNativeHeapProfileType.TypeId, ls`Native memory allocation sampling`);
Profiler.SamplingNativeHeapProfileType.instance = this;
}
/**
* @override
*/
get treeItemTitle() {
return ls`NATIVE SAMPLING PROFILES`;
}
/**
* @override
*/
get description() {
return ls`Allocation profiles show sampled native memory allocations from the renderer process.`;
}
/**
* @override
*/
_startSampling() {
this.profileBeingRecorded().heapProfilerModel().startNativeSampling();
}
/**
* @override
* return {!Promise<!Protocol.HeapProfiler.SamplingHeapProfile>}
*/
_stopSampling() {
return this.profileBeingRecorded().heapProfilerModel().stopNativeSampling();
}
};
Profiler.SamplingNativeHeapProfileType.TypeId = 'SamplingNativeHeapRecording';
/**
* @unrestricted
*/
Profiler.SamplingNativeHeapSnapshotType = class extends Profiler.SamplingHeapProfileTypeBase {
/**
* @param {string} processType
*/
constructor(processType) {
super(Profiler.SamplingNativeHeapSnapshotType.TypeId, ls`Native memory allocation snapshot (${processType})`);
}
/**
* @override
* @return {boolean}
*/
isInstantProfile() {
return true;
}
/**
* @override
*/
get treeItemTitle() {
return ls`NATIVE SNAPSHOTS`;
}
/**
* @override
*/
get description() {
return ls`Native memory snapshots show sampled native allocations in the renderer process since start up.
Chrome has to be started with --memlog=all flag. Check flags at chrome://flags`;
}
/**
* @override
* @return {boolean}
*/
buttonClicked() {
this._takeSnapshot();
return false;
}
/**
* @return {!Promise}
*/
async _takeSnapshot() {
if (this.profileBeingRecorded()) {
return;
}
const heapProfilerModel = UI.context.flavor(SDK.HeapProfilerModel);
if (!heapProfilerModel) {
return;
}
const profile =
new Profiler.SamplingHeapProfileHeader(heapProfilerModel, this, ls`Snapshot ${this.nextProfileUid()}`);
this.setProfileBeingRecorded(profile);
this.addProfile(profile);
profile.updateStatus(ls`Snapshotting\u2026`);
const protocolProfile = await this._takeNativeSnapshot(/** @type {!SDK.HeapProfilerModel} */ (heapProfilerModel));
const recordedProfile = this.profileBeingRecorded();
if (recordedProfile) {
console.assert(protocolProfile);
recordedProfile.setProtocolProfile(/** @type {!Protocol.Profiler.Profile} */ (protocolProfile));
recordedProfile.updateStatus('');
this.setProfileBeingRecorded(null);
}
this.dispatchEventToListeners(Profiler.ProfileType.Events.ProfileComplete, recordedProfile);
}
/**
* @param {!SDK.HeapProfilerModel} heapProfilerModel
* @return {!Promise<!Protocol.HeapProfiler.SamplingHeapProfile>}
*/
_takeNativeSnapshot(heapProfilerModel) {
throw 'Not implemented';
}
};
Profiler.SamplingNativeHeapSnapshotType.TypeId = 'SamplingNativeHeapSnapshot';
Profiler.SamplingNativeHeapSnapshotBrowserType = class extends Profiler.SamplingNativeHeapSnapshotType {
constructor() {
super(ls`Browser`);
Profiler.SamplingNativeHeapSnapshotBrowserType.instance = this;
}
/**
* @override
* @param {!SDK.HeapProfilerModel} heapProfilerModel
* @return {!Promise<!Protocol.HeapProfiler.SamplingHeapProfile>}
*/
async _takeNativeSnapshot(heapProfilerModel) {
return await heapProfilerModel.takeNativeBrowserSnapshot();
}
};
Profiler.SamplingNativeHeapSnapshotRendererType = class extends Profiler.SamplingNativeHeapSnapshotType {
constructor() {
super(ls`Renderer`);
Profiler.SamplingNativeHeapSnapshotRendererType.instance = this;
}
/**
* @override
* @param {!SDK.HeapProfilerModel} heapProfilerModel
* @return {!Promise<!Protocol.HeapProfiler.SamplingHeapProfile>}
*/
async _takeNativeSnapshot(heapProfilerModel) {
return await heapProfilerModel.takeNativeSnapshot();
}
};
/**
* @unrestricted
*/
Profiler.SamplingHeapProfileHeader = class extends Profiler.WritableProfileHeader {
/**
* @param {?SDK.HeapProfilerModel} heapProfilerModel
* @param {!Profiler.SamplingHeapProfileTypeBase} type
* @param {string=} title
*/
constructor(heapProfilerModel, type, title) {
super(
heapProfilerModel && heapProfilerModel.debuggerModel(), type,
title || Common.UIString('Profile %d', type.nextProfileUid()));
this._heapProfilerModel = heapProfilerModel;
this._protocolProfile =
/** @type {!Protocol.HeapProfiler.SamplingHeapProfile} */ ({head: {callFrame: {}, children: []}});
}
/**
* @override
* @return {!Profiler.ProfileView}
*/
createView() {
return new Profiler.HeapProfileView(this);
}
/**
* @return {!Protocol.HeapProfiler.SamplingHeapProfile}
*/
protocolProfile() {
return this._protocolProfile;
}
/**
* @return {?SDK.HeapProfilerModel}
*/
heapProfilerModel() {
return this._heapProfilerModel;
}
};
/**
* @unrestricted
*/
Profiler.SamplingHeapProfileNode = class extends SDK.ProfileNode {
/**
* @param {!Protocol.HeapProfiler.SamplingHeapProfileNode} node
*/
constructor(node) {
const callFrame = node.callFrame || /** @type {!Protocol.Runtime.CallFrame} */ ({
// Backward compatibility for old CpuProfileNode format.
functionName: node['functionName'],
scriptId: node['scriptId'],
url: node['url'],
lineNumber: node['lineNumber'] - 1,
columnNumber: node['columnNumber'] - 1
});
super(callFrame);
this.self = node.selfSize;
}
};
/**
* @unrestricted
*/
Profiler.SamplingHeapProfileModel = class extends SDK.ProfileTreeModel {
/**
* @param {!Protocol.HeapProfiler.SamplingHeapProfile} profile
* @param {number=} minOrdinal
* @param {number=} maxOrdinal
*/
constructor(profile, minOrdinal, maxOrdinal) {
super();
this.modules = profile.modules || [];
/** @type {?Map<number, number>} */
let nodeIdToSizeMap = null;
if (minOrdinal || maxOrdinal) {
nodeIdToSizeMap = new Map();
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));
/**
* @param {!Protocol.HeapProfiler.SamplingHeapProfileNode} root
* @return {!Profiler.SamplingHeapProfileNode}
*/
function translateProfileTree(root) {
const resultRoot = new Profiler.SamplingHeapProfileNode(root);
const sourceNodeStack = [root];
const targetNodeStack = [resultRoot];
while (sourceNodeStack.length) {
const sourceNode = sourceNodeStack.pop();
const targetNode = targetNodeStack.pop();
targetNode.children = sourceNode.children.map(child => {
const targetChild = new Profiler.SamplingHeapProfileNode(child);
if (nodeIdToSizeMap) {
targetChild.self = nodeIdToSizeMap.get(child.id) || 0;
}
return targetChild;
});
sourceNodeStack.pushAll(sourceNode.children);
targetNodeStack.pushAll(targetNode.children);
}
pruneEmptyBranches(resultRoot);
return resultRoot;
}
/**
* @param {!SDK.ProfileNode} node
* @return {boolean}
*/
function pruneEmptyBranches(node) {
node.children = node.children.filter(pruneEmptyBranches);
return !!(node.children.length || node.self);
}
}
};
/**
* @implements {Profiler.ProfileDataGridNode.Formatter}
* @unrestricted
*/
Profiler.HeapProfileView.NodeFormatter = class {
/**
* @param {!Profiler.HeapProfileView} profileView
*/
constructor(profileView) {
this._profileView = profileView;
}
/**
* @override
* @param {number} value
* @return {string}
*/
formatValue(value) {
return Number.withThousandsSeparator(value);
}
/**
* @override
* @param {number} value
* @return {string}
*/
formatValueAccessibleText(value) {
return ls`${value} bytes`;
}
/**
* @override
* @param {number} value
* @param {!Profiler.ProfileDataGridNode} node
* @return {string}
*/
formatPercent(value, node) {
return Common.UIString('%.2f\xa0%%', value);
}
/**
* @override
* @param {!Profiler.ProfileDataGridNode} node
* @return {?Element}
*/
linkifyNode(node) {
const heapProfilerModel = this._profileView._profileHeader.heapProfilerModel();
return this._profileView.linkifier().maybeLinkifyConsoleCallFrame(
heapProfilerModel ? heapProfilerModel.target() : null, node.profileNode.callFrame, 'profile-node-file');
}
};
/**
* @unrestricted
*/
Profiler.HeapFlameChartDataProvider = class extends Profiler.ProfileFlameChartDataProvider {
/**
* @param {!SDK.ProfileTreeModel} profile
* @param {?SDK.HeapProfilerModel} heapProfilerModel
*/
constructor(profile, heapProfilerModel) {
super();
this._profile = profile;
this._heapProfilerModel = heapProfilerModel;
}
/**
* @override
* @return {number}
*/
minimumBoundary() {
return 0;
}
/**
* @override
* @return {number}
*/
totalTime() {
return this._profile.root.total;
}
/**
* @override
* @param {number} value
* @param {number=} precision
* @return {string}
*/
formatValue(value, precision) {
return Common.UIString('%s\xa0KB', Number.withThousandsSeparator(value / 1e3));
}
/**
* @override
* @return {!PerfUI.FlameChart.TimelineData}
*/
_calculateTimelineData() {
/**
* @param {!SDK.ProfileNode} node
* @return {number}
*/
function nodesCount(node) {
return node.children.reduce((count, node) => count + nodesCount(node), 1);
}
const count = nodesCount(this._profile.root);
/** @type {!Array<!SDK.ProfileNode>} */
const entryNodes = 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;
/**
* @param {!SDK.ProfileNode} node
*/
function addNode(node) {
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._maxStackDepth = maxDepth + 1;
this._entryNodes = entryNodes;
this._timelineData = new PerfUI.FlameChart.TimelineData(entryLevels, entryTotalTimes, entryStartTimes, null);
return this._timelineData;
}
/**
* @override
* @param {number} entryIndex
* @return {?Element}
*/
prepareHighlightedEntryInfo(entryIndex) {
const node = this._entryNodes[entryIndex];
if (!node) {
return null;
}
const entryInfo = [];
/**
* @param {string} title
* @param {string} value
*/
function pushEntryInfoRow(title, value) {
entryInfo.push({title: title, value: value});
}
pushEntryInfoRow(ls`Name`, UI.beautifyFunctionName(node.functionName));
pushEntryInfoRow(ls`Self size`, Number.bytesToString(node.self));
pushEntryInfoRow(ls`Total size`, Number.bytesToString(node.total));
const linkifier = new Components.Linkifier();
const link = linkifier.maybeLinkifyConsoleCallFrame(
this._heapProfilerModel ? this._heapProfilerModel.target() : null, node.callFrame);
if (link) {
pushEntryInfoRow(ls`URL`, link.textContent);
}
linkifier.dispose();
return Profiler.ProfileView.buildPopoverTable(entryInfo);
}
};