chrome-devtools-frontend
Version:
Chrome DevTools UI
835 lines (756 loc) • 24.7 kB
JavaScript
// 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.
import * as Common from '../common/common.js'; // eslint-disable-line no-unused-vars
import * as Components from '../components/components.js';
import * as i18n from '../i18n/i18n.js';
import * as PerfUI from '../perf_ui/perf_ui.js';
import * as Platform from '../platform/platform.js';
import * as Root from '../root/root.js';
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';
import {ProfileFlameChartDataProvider} from './CPUProfileFlameChart.js';
import {HeapTimelineOverview, IdsRangeChanged, Samples} from './HeapTimelineOverview.js'; // eslint-disable-line no-unused-vars
import {Formatter, ProfileDataGridNode} from './ProfileDataGrid.js'; // eslint-disable-line no-unused-vars
import {ProfileEvents, ProfileHeader, ProfileType} from './ProfileHeader.js'; // eslint-disable-line no-unused-vars
import {ProfileView, WritableProfileHeader} from './ProfileView.js';
export 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 (bytes)',
/**
*@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 (bytes)',
/**
*@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 name of the collection of profiles that are gathered from various snapshots of the heap
*/
samplingProfiles: 'SAMPLING PROFILES',
/**
*@description Description (part 1) in Heap Profile View of a profiler tool
*/
recordMemoryAllocations: 'Record memory allocations using sampling method.',
/**
*@description Description (part 2) in Heap Profile View of a profiler tool
*/
thisProfileTypeHasMinimal:
'This profile type has minimal performance overhead and can be used for long running operations.',
/**
*@description Description (part 3) in Heap Profile View of a profiler tool
*/
itProvidesGoodApproximation:
'It provides good approximation of allocations broken down 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',
};
const str_ = i18n.i18n.registerUIStrings('profiler/HeapProfileView.js', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
/**
* @param {!SamplingHeapProfileHeader} profileHeader
* @return {!Protocol.HeapProfiler.SamplingHeapProfile}
*/
function convertToSamplingHeapProfile(profileHeader) {
return /** @type {!Protocol.HeapProfiler.SamplingHeapProfile} */ (
profileHeader._profile || profileHeader.protocolProfile());
}
/**
* @implements {UI.SearchableView.Searchable}
*/
export class HeapProfileView extends ProfileView {
/**
* @param {!SamplingHeapProfileHeader} profileHeader
*/
constructor(profileHeader) {
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();
/** @type {!Array<number>} */
this._timestamps = [];
/** @type {!Array<number>} */
this._sizes = [];
/** @type {!Array<number>} */
this._max = [];
/** @type {!Array<number>} */
this._ordinals = [];
/** @type {number} */
this._totalTime = 0;
/** @type {number} */
this._lastOrdinal = 0;
this._timelineOverview = new HeapTimelineOverview();
if (Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline')) {
this._timelineOverview.addEventListener(IdsRangeChanged, this._onIdsRangeChanged.bind(this));
this._timelineOverview.show(this.element, this.element.firstChild);
this._timelineOverview.start();
this._profileType.addEventListener(SamplingHeapProfileType.Events.StatsUpdate, this._onStatsUpdate, this);
this._profileType.once(ProfileEvents.ProfileComplete).then(() => {
this._profileType.removeEventListener(SamplingHeapProfileType.Events.StatsUpdate, this._onStatsUpdate, this);
this._timelineOverview.stop();
this._timelineOverview.updateGrid();
});
}
}
/**
* @override
* @return {!Promise<!Array<!UI.Toolbar.ToolbarItem>>}
*/
async toolbarItems() {
return [...await super.toolbarItems(), this._selectedSizeText];
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_onIdsRangeChanged(event) {
const minId = /** @type {number} */ (event.data.minId);
const maxId = /** @type {number} */ (event.data.maxId);
this._selectedSizeText.setText(
i18nString(UIStrings.selectedSizeS, {PH1: Platform.NumberUtilities.bytesToString(event.data.size)}));
this._setSelectionRange(minId, maxId);
}
/**
* @param {number} minId
* @param {number} maxId
*/
_setSelectionRange(minId, maxId) {
const profileData = convertToSamplingHeapProfile(/** @type {!SamplingHeapProfileHeader} */ (this.profileHeader));
const profile = new SamplingHeapProfileModel(profileData, minId, maxId);
this.adjustedTotal = profile.total;
this.setProfile(profile);
}
/**
* @param {!Common.EventTarget.EventTargetEvent} 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);
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 = /** @type {!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 {!Platform.UIString.LocalizedString}
*/
columnHeader(columnId) {
switch (columnId) {
case 'self':
return i18nString(UIStrings.selfSizeBytes);
case 'total':
return i18nString(UIStrings.totalSizeBytes);
}
return Common.UIString.LocalizedEmptyString;
}
/**
* @override
* @return {!ProfileFlameChartDataProvider}
*/
createFlameChartDataProvider() {
return new HeapFlameChartDataProvider(
/** @type {!SamplingHeapProfileModel} */ (this.profile()), this.profileHeader.heapProfilerModel());
}
}
export class SamplingHeapProfileTypeBase extends ProfileType {
/**
* @param {string} typeId
* @param {string} description
*/
constructor(typeId, description) {
super(typeId, description);
this._recording = false;
}
/**
* @override
* @return {?SamplingHeapProfileHeader}
*/
profileBeingRecorded() {
return /** @type {?SamplingHeapProfileHeader} */ (super.profileBeingRecorded());
}
/**
* @override
* @return {string}
*/
typeName() {
return 'Heap';
}
/**
* @override
* @return {string}
*/
fileExtension() {
return '.heapprofile';
}
/**
* @override
*/
get buttonTooltip() {
return this._recording ? i18nString(UIStrings.stopHeapProfiling) : i18nString(UIStrings.startHeapProfiling);
}
/**
* @override
* @return {boolean}
*/
buttonClicked() {
if (this._recording) {
this._stopRecordingProfile();
} else {
this._startRecordingProfile();
}
return this._recording;
}
_startRecordingProfile() {
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 icon = UI.Icon.Icon.create('smallicon-warning');
UI.Tooltip.Tooltip.install(icon, i18nString(UIStrings.heapProfilerIsRecording));
UI.InspectorView.InspectorView.instance().setPanelIcon('heap_profiler', icon);
this._recording = true;
this._startSampling();
}
async _stopRecordingProfile() {
this._recording = false;
const recordedProfile = this.profileBeingRecorded();
if (!recordedProfile || !recordedProfile.heapProfilerModel()) {
return;
}
recordedProfile.updateStatus(i18nString(UIStrings.stopping));
const profile = await this._stopSampling();
if (recordedProfile) {
console.assert(profile !== undefined);
recordedProfile.setProtocolProfile(/** @type {?} */ (profile));
recordedProfile.updateStatus('');
this.setProfileBeingRecorded(null);
}
UI.InspectorView.InspectorView.instance().setPanelIcon('heap_profiler', null);
this.dispatchEventToListeners(ProfileEvents.ProfileComplete, recordedProfile);
}
/**
* @override
* @param {string} title
* @return {!ProfileHeader}
*/
createProfileLoadedFromFile(title) {
return new SamplingHeapProfileHeader(null, this, title);
}
/**
* @override
*/
profileBeingRecordedRemoved() {
this._stopRecordingProfile();
}
_startSampling() {
throw 'Not implemented';
}
/**
* @return {!Promise<!Protocol.HeapProfiler.SamplingHeapProfile>}
*/
_stopSampling() {
throw 'Not implemented';
}
}
/** @type {!SamplingHeapProfileType} */
let samplingHeapProfileTypeInstance;
export class SamplingHeapProfileType extends SamplingHeapProfileTypeBase {
constructor() {
super(SamplingHeapProfileType.TypeId, i18nString(UIStrings.allocationSampling));
if (!samplingHeapProfileTypeInstance) {
samplingHeapProfileTypeInstance = this;
}
/** @type {number} */
this._updateTimer = 0;
this._updateIntervalMs = 200;
}
static get instance() {
return samplingHeapProfileTypeInstance;
}
/**
* @override
*/
get treeItemTitle() {
return i18nString(UIStrings.samplingProfiles);
}
/**
* @override
*/
get description() {
// TODO(l10n): Do not concatenate localized strings.
const formattedDescription = [
i18nString(UIStrings.recordMemoryAllocations), i18nString(UIStrings.thisProfileTypeHasMinimal),
i18nString(UIStrings.itProvidesGoodApproximation)
];
return formattedDescription.join('\n');
}
/**
* @override
* @return {boolean}
*/
hasTemporaryView() {
return Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline');
}
/**
* @override
*/
_startSampling() {
const heapProfilerModel = this._obtainRecordingProfile();
if (!heapProfilerModel) {
return;
}
heapProfilerModel.startSampling();
if (Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline')) {
this._updateTimer = window.setTimeout(() => {
this._updateStats();
}, this._updateIntervalMs);
}
}
/**
* @return {?SDK.HeapProfilerModel.HeapProfilerModel}
*/
_obtainRecordingProfile() {
const recordingProfile = this.profileBeingRecorded();
if (recordingProfile) {
const heapProfilerModel = recordingProfile.heapProfilerModel();
return heapProfilerModel;
}
return null;
}
/**
* @override
* @return {!Promise<!Protocol.HeapProfiler.SamplingHeapProfile>}
*/
async _stopSampling() {
window.clearTimeout(this._updateTimer);
this._updateTimer = 0;
this.dispatchEventToListeners(SamplingHeapProfileType.Events.RecordingStopped);
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() {
const heapProfilerModel = this._obtainRecordingProfile();
if (!heapProfilerModel) {
return;
}
const profile = await heapProfilerModel.getSamplingProfile();
if (!this._updateTimer) {
return;
}
this.dispatchEventToListeners(SamplingHeapProfileType.Events.StatsUpdate, profile);
this._updateTimer = window.setTimeout(() => {
this._updateStats();
}, this._updateIntervalMs);
}
}
SamplingHeapProfileType.TypeId = 'SamplingHeap';
/** @override @enum {symbol} */
SamplingHeapProfileType.Events = {
RecordingStopped: Symbol('RecordingStopped'),
StatsUpdate: Symbol('StatsUpdate')
};
export class SamplingHeapProfileHeader extends WritableProfileHeader {
/**
* @param {?SDK.HeapProfilerModel.HeapProfilerModel} heapProfilerModel
* @param {!SamplingHeapProfileTypeBase} type
* @param {string=} title
*/
constructor(heapProfilerModel, type, title) {
super(
heapProfilerModel && heapProfilerModel.debuggerModel(), type,
title || i18nString(UIStrings.profileD, {PH1: type.nextProfileUid()}));
this._heapProfilerModel = heapProfilerModel;
this._protocolProfile = {
head: {
callFrame: {
functionName: '',
scriptId: '',
url: '',
lineNumber: 0,
columnNumber: 0,
},
children: [],
selfSize: 0,
id: 0,
},
samples: [],
startTime: 0,
endTime: 0,
nodes: [],
};
}
/**
* @override
* @return {!HeapProfileView}
*/
createView() {
return new HeapProfileView(this);
}
/**
* @return {!Protocol.HeapProfiler.SamplingHeapProfile}
*/
protocolProfile() {
return this._protocolProfile;
}
/**
* @return {?SDK.HeapProfilerModel.HeapProfilerModel}
*/
heapProfilerModel() {
return this._heapProfilerModel;
}
}
export class SamplingHeapProfileNode extends SDK.ProfileTreeModel.ProfileNode {
/**
* @param {!Protocol.HeapProfiler.SamplingHeapProfileNode} node
*/
constructor(node) {
const callFrame = node.callFrame || /** @type {!Protocol.Runtime.CallFrame} */ ({
// Backward compatibility for old CpuProfileNode format.
// @ts-ignore https://crbug.com/1150777
functionName: node['functionName'],
// @ts-ignore https://crbug.com/1150777
scriptId: node['scriptId'],
// @ts-ignore https://crbug.com/1150777
url: node['url'],
// @ts-ignore https://crbug.com/1150777
lineNumber: node['lineNumber'] - 1,
// @ts-ignore https://crbug.com/1150777
columnNumber: node['columnNumber'] - 1,
});
super(callFrame);
this.self = node.selfSize;
}
}
export class SamplingHeapProfileModel extends SDK.ProfileTreeModel.ProfileTreeModel {
/**
* @param {!Protocol.HeapProfiler.SamplingHeapProfile} profile
* @param {number=} minOrdinal
* @param {number=} maxOrdinal
*/
constructor(profile, minOrdinal, maxOrdinal) {
super();
this.modules = /** @type {?} */ (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 {!SamplingHeapProfileNode}
*/
function translateProfileTree(root) {
const resultRoot = new SamplingHeapProfileNode(root);
const sourceNodeStack = [root];
const targetNodeStack = [resultRoot];
while (sourceNodeStack.length) {
const sourceNode = /** @type {!Protocol.HeapProfiler.SamplingHeapProfileNode} */ (sourceNodeStack.pop());
const targetNode = /** @type {!SamplingHeapProfileNode} */ (targetNodeStack.pop());
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;
}
/**
* @param {!SDK.ProfileTreeModel.ProfileNode} node
* @return {boolean}
*/
function pruneEmptyBranches(node) {
node.children = node.children.filter(pruneEmptyBranches);
return Boolean(node.children.length || node.self);
}
}
}
/**
* @implements {Formatter}
*/
export class NodeFormatter {
/**
* @param {!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 i18nString(UIStrings.sBytes, {PH1: value});
}
/**
* @override
* @param {number} value
* @param {!ProfileDataGridNode} node
* @return {string}
*/
formatPercent(value, node) {
return i18nString(UIStrings.formatPercent, {PH1: value.toFixed(2)});
}
/**
* @override
* @param {!ProfileDataGridNode} node
* @return {?Element}
*/
linkifyNode(node) {
const heapProfilerModel = this._profileView.profileHeader.heapProfilerModel();
const target = heapProfilerModel ? heapProfilerModel.target() : null;
const options = {
className: 'profile-node-file',
columnNumber: undefined,
tabStop: undefined,
};
return this._profileView.linkifier().maybeLinkifyConsoleCallFrame(target, node.profileNode.callFrame, options);
}
}
export class HeapFlameChartDataProvider extends ProfileFlameChartDataProvider {
/**
* @param {!SDK.ProfileTreeModel.ProfileTreeModel} profile
* @param {?SDK.HeapProfilerModel.HeapProfilerModel} heapProfilerModel
*/
constructor(profile, heapProfilerModel) {
super();
this._profile = profile;
this._heapProfilerModel = heapProfilerModel;
/** @type {!Array<!SDK.ProfileTreeModel.ProfileNode>} */
this._entryNodes = [];
}
/**
* @override
* @return {number}
*/
minimumBoundary() {
return 0;
}
/**
* @override
* @return {number}
*/
totalTime() {
return this._profile.root.total;
}
/**
* @override
* @param {number} entryIndex
* @return {boolean}
*/
entryHasDeoptReason(entryIndex) {
return false;
}
/**
* @override
* @param {number} value
* @param {number=} precision
* @return {string}
*/
formatValue(value, precision) {
return i18nString(UIStrings.skb, {PH1: Number.withThousandsSeparator(value / 1e3)});
}
/**
* @override
* @return {!PerfUI.FlameChart.TimelineData}
*/
_calculateTimelineData() {
/**
* @param {!SDK.ProfileTreeModel.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.ProfileTreeModel.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.ProfileTreeModel.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;
}
/** @type {!Array<{ title: string, value: string }>} */
const entryInfo = [];
/**
* @param {string} title
* @param {string} value
*/
function pushEntryInfoRow(title, value) {
entryInfo.push({title: title, value: value});
}
pushEntryInfoRow(i18nString(UIStrings.name), UI.UIUtils.beautifyFunctionName(node.functionName));
pushEntryInfoRow(i18nString(UIStrings.selfSize), Platform.NumberUtilities.bytesToString(node.self));
pushEntryInfoRow(i18nString(UIStrings.totalSize), Platform.NumberUtilities.bytesToString(node.total));
const linkifier = new Components.Linkifier.Linkifier();
const link = linkifier.maybeLinkifyConsoleCallFrame(
this._heapProfilerModel ? this._heapProfilerModel.target() : null, node.callFrame);
if (link) {
pushEntryInfoRow(i18nString(UIStrings.url), /** @type {string} */ (link.textContent));
}
linkifier.dispose();
return ProfileView.buildPopoverTable(entryInfo);
}
}