@quick-game/cli
Version:
Command line interface for rapid qg development
592 lines • 23.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 '../../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 * as IconButton from '../../ui/components/icon_button/icon_button.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 * as CPUProfile from '../../models/cpu_profile/cpu_profile.js';
import { ProfileFlameChartDataProvider } from './CPUProfileFlameChart.js';
import { HeapTimelineOverview } from './HeapTimelineOverview.js';
import { ProfileEvents, 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 (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 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 (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('panels/profiler/HeapProfileView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
function convertToSamplingHeapProfile(profileHeader) {
return (profileHeader.profile || profileHeader.protocolProfile());
}
export class HeapProfileView extends ProfileView {
profileHeader;
profileType;
adjustedTotal;
selectedSizeText;
timestamps;
sizes;
max;
ordinals;
totalTime;
lastOrdinal;
timelineOverview;
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();
this.timestamps = [];
this.sizes = [];
this.max = [];
this.ordinals = [];
this.totalTime = 0;
this.lastOrdinal = 0;
this.timelineOverview = new HeapTimelineOverview();
if (Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline')) {
this.timelineOverview.addEventListener("IdsRangeChanged" /* Events.IdsRangeChanged */, this.onIdsRangeChanged.bind(this));
this.timelineOverview.show(this.element, this.element.firstChild);
this.timelineOverview.start();
this.profileType.addEventListener("StatsUpdate" /* SamplingHeapProfileType.Events.StatsUpdate */, this.onStatsUpdate, this);
void this.profileType.once(ProfileEvents.ProfileComplete).then(() => {
this.profileType.removeEventListener("StatsUpdate" /* SamplingHeapProfileType.Events.StatsUpdate */, this.onStatsUpdate, this);
this.timelineOverview.stop();
this.timelineOverview.updateGrid();
});
}
}
async toolbarItems() {
return [...await super.toolbarItems(), this.selectedSizeText];
}
onIdsRangeChanged(event) {
const { minId, maxId } = event.data;
this.selectedSizeText.setText(i18nString(UIStrings.selectedSizeS, { PH1: Platform.NumberUtilities.bytesToString(event.data.size) }));
this.setSelectionRange(minId, maxId);
}
setSelectionRange(minId, maxId) {
const profileData = convertToSamplingHeapProfile(this.profileHeader);
const profile = new SamplingHeapProfileModel(profileData, minId, maxId);
this.adjustedTotal = profile.total;
this.setProfile(profile);
}
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 = {
sizes: this.sizes,
max: this.max,
ids: this.ordinals,
timestamps: this.timestamps,
totalTime: this.totalTime,
};
this.timelineOverview.setSamples(samples);
}
columnHeader(columnId) {
switch (columnId) {
case 'self':
return i18nString(UIStrings.selfSizeBytes);
case 'total':
return i18nString(UIStrings.totalSizeBytes);
}
return Common.UIString.LocalizedEmptyString;
}
createFlameChartDataProvider() {
return new HeapFlameChartDataProvider(this.profile(), this.profileHeader.heapProfilerModel());
}
}
export class SamplingHeapProfileTypeBase extends Common.ObjectWrapper.eventMixin(ProfileType) {
recording;
clearedDuringRecording;
constructor(typeId, description) {
super(typeId, description);
this.recording = false;
this.clearedDuringRecording = false;
}
profileBeingRecorded() {
return super.profileBeingRecorded();
}
typeName() {
return 'Heap';
}
fileExtension() {
return '.heapprofile';
}
get buttonTooltip() {
return this.recording ? i18nString(UIStrings.stopHeapProfiling) : i18nString(UIStrings.startHeapProfiling);
}
buttonClicked() {
if (this.recording) {
void 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 = new IconButton.Icon.Icon();
icon.data = { iconName: 'warning-filled', color: 'var(--icon-warning)', width: '14px', height: '14px' };
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);
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
recordedProfile.setProtocolProfile(profile);
recordedProfile.updateStatus('');
this.setProfileBeingRecorded(null);
}
UI.InspectorView.InspectorView.instance().setPanelIcon('heap_profiler', null);
// 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.ProfileComplete, recordedProfile);
}
createProfileLoadedFromFile(title) {
return new SamplingHeapProfileHeader(null, this, title);
}
profileBeingRecordedRemoved() {
this.clearedDuringRecording = true;
void this.stopRecordingProfile();
}
startSampling() {
throw 'Not implemented';
}
stopSampling() {
throw 'Not implemented';
}
}
let samplingHeapProfileTypeInstance;
export class SamplingHeapProfileType extends SamplingHeapProfileTypeBase {
updateTimer;
updateIntervalMs;
constructor() {
super(SamplingHeapProfileType.TypeId, i18nString(UIStrings.allocationSampling));
if (!samplingHeapProfileTypeInstance) {
samplingHeapProfileTypeInstance = this;
}
this.updateTimer = 0;
this.updateIntervalMs = 200;
}
static get instance() {
return samplingHeapProfileTypeInstance;
}
get treeItemTitle() {
return i18nString(UIStrings.samplingProfiles);
}
get description() {
// TODO(l10n): Do not concatenate localized strings.
const formattedDescription = [
i18nString(UIStrings.recordMemoryAllocations),
i18nString(UIStrings.thisProfileTypeHasMinimal),
i18nString(UIStrings.itProvidesGoodApproximation),
];
return formattedDescription.join('\n');
}
hasTemporaryView() {
return Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline');
}
startSampling() {
const heapProfilerModel = this.obtainRecordingProfile();
if (!heapProfilerModel) {
return;
}
void heapProfilerModel.startSampling();
if (Root.Runtime.experiments.isEnabled('samplingHeapProfilerTimeline')) {
this.updateTimer = window.setTimeout(() => {
void this.updateStats();
}, this.updateIntervalMs);
}
}
obtainRecordingProfile() {
const recordingProfile = this.profileBeingRecorded();
if (recordingProfile) {
const heapProfilerModel = recordingProfile.heapProfilerModel();
return heapProfilerModel;
}
return null;
}
async stopSampling() {
window.clearTimeout(this.updateTimer);
this.updateTimer = 0;
this.dispatchEventToListeners("RecordingStopped" /* 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("StatsUpdate" /* SamplingHeapProfileType.Events.StatsUpdate */, profile);
this.updateTimer = window.setTimeout(() => {
void this.updateStats();
}, this.updateIntervalMs);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static TypeId = 'SamplingHeap';
}
export class SamplingHeapProfileHeader extends WritableProfileHeader {
heapProfilerModelInternal;
protocolProfileInternal;
constructor(heapProfilerModel, type, title) {
super(heapProfilerModel && heapProfilerModel.debuggerModel(), type, title || i18nString(UIStrings.profileD, { PH1: type.nextProfileUid() }));
this.heapProfilerModelInternal = heapProfilerModel;
this.protocolProfileInternal = {
head: {
callFrame: {
functionName: '',
scriptId: '',
url: '',
lineNumber: 0,
columnNumber: 0,
},
children: [],
selfSize: 0,
id: 0,
},
samples: [],
startTime: 0,
endTime: 0,
nodes: [],
};
}
createView() {
return new HeapProfileView(this);
}
protocolProfile() {
return this.protocolProfileInternal;
}
heapProfilerModel() {
return this.heapProfilerModelInternal;
}
profileType() {
return super.profileType();
}
}
export class SamplingHeapProfileNode extends CPUProfile.ProfileTreeModel.ProfileNode {
self;
constructor(node) {
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,
};
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;
constructor(profile, minOrdinal, maxOrdinal) {
super();
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.modules = profile.modules || [];
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));
function translateProfileTree(root) {
const resultRoot = new 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 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) {
node.children = node.children.filter(pruneEmptyBranches);
return Boolean(node.children.length || node.self);
}
}
}
export class NodeFormatter {
profileView;
constructor(profileView) {
this.profileView = profileView;
}
formatValue(value) {
return Platform.NumberUtilities.withThousandsSeparator(value);
}
formatValueAccessibleText(value) {
return i18nString(UIStrings.sBytes, { PH1: value });
}
formatPercent(value, _node) {
return i18nString(UIStrings.formatPercent, { PH1: value.toFixed(2) });
}
linkifyNode(node) {
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 {
profile;
heapProfilerModel;
timelineDataInternal;
constructor(profile, heapProfilerModel) {
super();
this.profile = profile;
this.heapProfilerModel = heapProfilerModel;
}
minimumBoundary() {
return 0;
}
totalTime() {
return this.profile.root.total;
}
entryHasDeoptReason(_entryIndex) {
return false;
}
formatValue(value, _precision) {
return i18nString(UIStrings.skb, { PH1: Platform.NumberUtilities.withThousandsSeparator(value / 1e3) });
}
calculateTimelineData() {
function nodesCount(node) {
return node.children.reduce((count, node) => count + nodesCount(node), 1);
}
const count = nodesCount(this.profile.root);
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;
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.maxStackDepthInternal = maxDepth + 1;
this.entryNodes = entryNodes;
this.timelineDataInternal =
PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels, entryTotalTimes, entryStartTimes, groups: null });
return this.timelineDataInternal;
}
prepareHighlightedEntryInfo(entryIndex) {
const node = this.entryNodes[entryIndex];
if (!node) {
return null;
}
const entryInfo = [];
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), link.textContent);
}
linkifier.dispose();
return ProfileView.buildPopoverTable(entryInfo);
}
}
//# sourceMappingURL=HeapProfileView.js.map