| // Copyright 2020 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. |
| |
| /* |
| * Copyright (C) 2008 Apple Inc. All Rights Reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /* eslint-disable rulesdir/no_underscored_properties */ |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as Host from '../../core/host/host.js'; |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as SDK from '../../core/sdk/sdk.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 type * as Protocol from '../../generated/protocol.js'; |
| |
| import {ProfileFlameChartDataProvider} from './CPUProfileFlameChart.js'; |
| |
| import type {Formatter, ProfileDataGridNode} from './ProfileDataGrid.js'; |
| import type {ProfileHeader} from './ProfileHeader.js'; |
| import {ProfileEvents, ProfileType} from './ProfileHeader.js'; |
| import {ProfileView, WritableProfileHeader} from './ProfileView.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Time of a single activity, as opposed to the total time |
| */ |
| selfTime: 'Self Time', |
| /** |
| *@description Text for the total time of something |
| */ |
| totalTime: 'Total Time', |
| /** |
| *@description Text in CPUProfile View of a profiler tool |
| */ |
| recordJavascriptCpuProfile: 'Record JavaScript CPU Profile', |
| /** |
| *@description Text in CPUProfile View of a profiler tool |
| */ |
| stopCpuProfiling: 'Stop CPU profiling', |
| /** |
| *@description Text in CPUProfile View of a profiler tool |
| */ |
| startCpuProfiling: 'Start CPU profiling', |
| /** |
| *@description Text in CPUProfile View of a profiler tool |
| */ |
| cpuProfiles: 'CPU PROFILES', |
| /** |
| *@description Text in CPUProfile View of a profiler tool, that show how much time a script spend executing a function. |
| */ |
| cpuProfilesShow: 'CPU profiles show where the execution time is spent in your page\'s JavaScript functions.', |
| /** |
| *@description Text in CPUProfile View of a profiler tool |
| */ |
| recording: 'Recording…', |
| /** |
| *@description Time in miliseconds |
| *@example {30.1} PH1 |
| */ |
| fms: '{PH1} ms', |
| /** |
| *@description Text in CPUProfile View of a profiler tool |
| *@example {21.33} PH1 |
| */ |
| formatPercent: '{PH1} %', |
| /** |
| *@description Text for the name of something |
| */ |
| name: 'Name', |
| /** |
| *@description Text for web URLs |
| */ |
| url: 'URL', |
| /** |
| *@description Text in CPUProfile View of a profiler tool |
| */ |
| aggregatedSelfTime: 'Aggregated self time', |
| /** |
| *@description Text in CPUProfile View of a profiler tool |
| */ |
| aggregatedTotalTime: 'Aggregated total time', |
| /** |
| *@description Text that indicates a JavaScript function in a CPU profile is not optimized. |
| */ |
| notOptimized: 'Not optimized', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('panels/profiler/CPUProfileView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| export class CPUProfileView extends ProfileView implements UI.SearchableView.Searchable { |
| profileHeader: CPUProfileHeader; |
| adjustedTotal: number; |
| constructor(profileHeader: CPUProfileHeader) { |
| super(); |
| this.profileHeader = profileHeader; |
| this.initialize(new NodeFormatter(this)); |
| const profile = profileHeader.profileModel(); |
| this.adjustedTotal = profile.profileHead.total; |
| this.adjustedTotal -= profile.idleNode ? profile.idleNode.total : 0; |
| this.setProfile(profile); |
| } |
| |
| wasShown(): void { |
| super.wasShown(); |
| PerfUI.LineLevelProfile.Performance.instance().reset(); |
| PerfUI.LineLevelProfile.Performance.instance().appendCPUProfile(this.profileHeader.profileModel()); |
| } |
| |
| columnHeader(columnId: string): Common.UIString.LocalizedString { |
| switch (columnId) { |
| case 'self': |
| return i18nString(UIStrings.selfTime); |
| case 'total': |
| return i18nString(UIStrings.totalTime); |
| } |
| return Common.UIString.LocalizedEmptyString; |
| } |
| |
| createFlameChartDataProvider(): ProfileFlameChartDataProvider { |
| return new CPUFlameChartDataProvider(this.profileHeader.profileModel(), this.profileHeader._cpuProfilerModel); |
| } |
| } |
| |
| export class CPUProfileType extends ProfileType { |
| _recording: boolean; |
| constructor() { |
| super(CPUProfileType.TypeId, i18nString(UIStrings.recordJavascriptCpuProfile)); |
| this._recording = false; |
| |
| SDK.TargetManager.TargetManager.instance().addModelListener( |
| SDK.CPUProfilerModel.CPUProfilerModel, SDK.CPUProfilerModel.Events.ConsoleProfileFinished, |
| this._consoleProfileFinished, this); |
| } |
| |
| profileBeingRecorded(): ProfileHeader|null { |
| return super.profileBeingRecorded() as ProfileHeader | null; |
| } |
| |
| typeName(): string { |
| return 'CPU'; |
| } |
| |
| fileExtension(): string { |
| return '.cpuprofile'; |
| } |
| |
| get buttonTooltip(): Common.UIString.LocalizedString { |
| return this._recording ? i18nString(UIStrings.stopCpuProfiling) : i18nString(UIStrings.startCpuProfiling); |
| } |
| |
| buttonClicked(): boolean { |
| if (this._recording) { |
| this._stopRecordingProfile(); |
| return false; |
| } |
| this._startRecordingProfile(); |
| return true; |
| } |
| |
| get treeItemTitle(): Common.UIString.LocalizedString { |
| return i18nString(UIStrings.cpuProfiles); |
| } |
| |
| get description(): Common.UIString.LocalizedString { |
| return i18nString(UIStrings.cpuProfilesShow); |
| } |
| |
| _consoleProfileFinished(event: Common.EventTarget.EventTargetEvent): void { |
| const data = (event.data as SDK.CPUProfilerModel.EventData); |
| const cpuProfile = (data.cpuProfile as Protocol.Profiler.Profile); |
| const profile = new CPUProfileHeader(data.cpuProfilerModel, this, data.title); |
| profile.setProtocolProfile(cpuProfile); |
| this.addProfile(profile); |
| } |
| |
| _startRecordingProfile(): void { |
| const cpuProfilerModel = UI.Context.Context.instance().flavor(SDK.CPUProfilerModel.CPUProfilerModel); |
| if (this.profileBeingRecorded() || !cpuProfilerModel) { |
| return; |
| } |
| const profile = new CPUProfileHeader(cpuProfilerModel, this); |
| this.setProfileBeingRecorded(profile as ProfileHeader); |
| SDK.TargetManager.TargetManager.instance().suspendAllTargets(); |
| this.addProfile(profile as ProfileHeader); |
| profile.updateStatus(i18nString(UIStrings.recording)); |
| this._recording = true; |
| cpuProfilerModel.startRecording(); |
| Host.userMetrics.actionTaken(Host.UserMetrics.Action.ProfilesCPUProfileTaken); |
| } |
| |
| async _stopRecordingProfile(): Promise<void> { |
| this._recording = false; |
| const profileBeingRecorded = this.profileBeingRecorded() as CPUProfileHeader; |
| if (!profileBeingRecorded || !profileBeingRecorded._cpuProfilerModel) { |
| return; |
| } |
| |
| const profile = await profileBeingRecorded._cpuProfilerModel.stopRecording(); |
| const recordedProfile = this.profileBeingRecorded() as CPUProfileHeader; |
| if (recordedProfile) { |
| if (!profile) { |
| throw new Error('Expected profile to be non-null'); |
| } |
| recordedProfile.setProtocolProfile(profile); |
| recordedProfile.updateStatus(''); |
| this.setProfileBeingRecorded(null); |
| } |
| |
| await SDK.TargetManager.TargetManager.instance().resumeAllTargets(); |
| this.dispatchEventToListeners(ProfileEvents.ProfileComplete, recordedProfile); |
| } |
| |
| createProfileLoadedFromFile(title: string): ProfileHeader { |
| return new CPUProfileHeader(null, this, title); |
| } |
| |
| profileBeingRecordedRemoved(): void { |
| this._stopRecordingProfile(); |
| } |
| |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| static readonly TypeId = 'CPU'; |
| } |
| |
| |
| export class CPUProfileHeader extends WritableProfileHeader { |
| _cpuProfilerModel: SDK.CPUProfilerModel.CPUProfilerModel|null; |
| _profileModel?: SDK.CPUProfileDataModel.CPUProfileDataModel; |
| |
| constructor(cpuProfilerModel: SDK.CPUProfilerModel.CPUProfilerModel|null, type: CPUProfileType, title?: string) { |
| super(cpuProfilerModel && cpuProfilerModel.debuggerModel(), type, title); |
| this._cpuProfilerModel = cpuProfilerModel; |
| } |
| |
| createView(): ProfileView { |
| return new CPUProfileView(this); |
| } |
| |
| protocolProfile(): Protocol.Profiler.Profile { |
| if (!this._protocolProfile) { |
| throw new Error('Expected _protocolProfile to be available'); |
| } |
| return this._protocolProfile; |
| } |
| |
| profileModel(): SDK.CPUProfileDataModel.CPUProfileDataModel { |
| if (!this._profileModel) { |
| throw new Error('Expected _profileModel to be available'); |
| } |
| return this._profileModel; |
| } |
| |
| setProfile(profile: Protocol.Profiler.Profile): void { |
| const target = this._cpuProfilerModel && this._cpuProfilerModel.target() || null; |
| this._profileModel = new SDK.CPUProfileDataModel.CPUProfileDataModel(profile, target); |
| } |
| } |
| |
| export class NodeFormatter implements Formatter { |
| _profileView: CPUProfileView; |
| constructor(profileView: CPUProfileView) { |
| this._profileView = profileView; |
| } |
| |
| formatValue(value: number): string { |
| return i18nString(UIStrings.fms, {PH1: value.toFixed(1)}); |
| } |
| |
| formatValueAccessibleText(value: number): string { |
| return this.formatValue(value); |
| } |
| |
| formatPercent(value: number, node: ProfileDataGridNode): string { |
| if (this._profileView) { |
| const profile = this._profileView.profile(); |
| if (profile && node.profileNode !== (profile as SDK.CPUProfileDataModel.CPUProfileDataModel).idleNode) { |
| return i18nString(UIStrings.formatPercent, {PH1: value.toFixed(2)}); |
| } |
| } |
| return ''; |
| } |
| |
| linkifyNode(node: ProfileDataGridNode): Element|null { |
| const cpuProfilerModel = this._profileView.profileHeader._cpuProfilerModel; |
| const target = cpuProfilerModel ? cpuProfilerModel.target() : null; |
| const options = {className: 'profile-node-file', columnNumber: undefined, inlineFrameIndex: 0, tabStop: undefined}; |
| return this._profileView.linkifier().maybeLinkifyConsoleCallFrame(target, node.profileNode.callFrame, options); |
| } |
| } |
| |
| export class CPUFlameChartDataProvider extends ProfileFlameChartDataProvider { |
| _cpuProfile: SDK.CPUProfileDataModel.CPUProfileDataModel; |
| _cpuProfilerModel: SDK.CPUProfilerModel.CPUProfilerModel|null; |
| _entrySelfTimes?: Float32Array; |
| |
| constructor( |
| cpuProfile: SDK.CPUProfileDataModel.CPUProfileDataModel, |
| cpuProfilerModel: SDK.CPUProfilerModel.CPUProfilerModel|null) { |
| super(); |
| this._cpuProfile = cpuProfile; |
| this._cpuProfilerModel = cpuProfilerModel; |
| } |
| |
| minimumBoundary(): number { |
| return this._cpuProfile.profileStartTime; |
| } |
| |
| totalTime(): number { |
| return this._cpuProfile.profileHead.total; |
| } |
| |
| entryHasDeoptReason(entryIndex: number): boolean { |
| const node = (this.entryNodes[entryIndex] as SDK.CPUProfileDataModel.CPUProfileNode); |
| return Boolean(node.deoptReason); |
| } |
| |
| _calculateTimelineData(): PerfUI.FlameChart.TimelineData { |
| const entries: (CPUFlameChartDataProvider.ChartEntry|null)[] = []; |
| const stack: number[] = []; |
| let maxDepth = 5; |
| |
| function onOpenFrame(): void { |
| stack.push(entries.length); |
| // Reserve space for the entry, as they have to be ordered by startTime. |
| // The entry itself will be put there in onCloseFrame. |
| entries.push(null); |
| } |
| function onCloseFrame( |
| depth: number, node: SDK.CPUProfileDataModel.CPUProfileNode, startTime: number, totalTime: number, |
| selfTime: number): void { |
| const index = (stack.pop() as number); |
| entries[index] = new CPUFlameChartDataProvider.ChartEntry(depth, totalTime, startTime, selfTime, node); |
| maxDepth = Math.max(maxDepth, depth); |
| } |
| this._cpuProfile.forEachFrame(onOpenFrame, onCloseFrame); |
| |
| const entryNodes: SDK.CPUProfileDataModel.CPUProfileNode[] = new Array(entries.length); |
| const entryLevels = new Uint16Array(entries.length); |
| const entryTotalTimes = new Float32Array(entries.length); |
| const entrySelfTimes = new Float32Array(entries.length); |
| const entryStartTimes = new Float64Array(entries.length); |
| |
| for (let i = 0; i < entries.length; ++i) { |
| const entry = entries[i]; |
| if (!entry) { |
| continue; |
| } |
| entryNodes[i] = entry.node; |
| entryLevels[i] = entry.depth; |
| entryTotalTimes[i] = entry.duration; |
| entryStartTimes[i] = entry.startTime; |
| entrySelfTimes[i] = entry.selfTime; |
| } |
| |
| this._maxStackDepth = maxDepth + 1; |
| this.entryNodes = entryNodes; |
| this.timelineData_ = new PerfUI.FlameChart.TimelineData(entryLevels, entryTotalTimes, entryStartTimes, null); |
| |
| this._entrySelfTimes = entrySelfTimes; |
| |
| return this.timelineData_; |
| } |
| |
| prepareHighlightedEntryInfo(entryIndex: number): Element|null { |
| const timelineData = this.timelineData_; |
| const node = this.entryNodes[entryIndex]; |
| if (!node) { |
| return null; |
| } |
| |
| const entryInfo: { |
| title: string, |
| value: string, |
| }[] = []; |
| function pushEntryInfoRow(title: string, value: string): void { |
| entryInfo.push({title: title, value: value}); |
| } |
| function millisecondsToString(ms: number): string { |
| if (ms === 0) { |
| return '0'; |
| } |
| if (ms < 1000) { |
| return i18nString(UIStrings.fms, {PH1: ms.toFixed(1)}); |
| } |
| return i18n.i18n.secondsToString(ms / 1000, true); |
| } |
| const name = UI.UIUtils.beautifyFunctionName(node.functionName); |
| pushEntryInfoRow(i18nString(UIStrings.name), name); |
| const selfTime = millisecondsToString((this._entrySelfTimes as Float32Array)[entryIndex]); |
| const totalTime = |
| millisecondsToString((timelineData as PerfUI.FlameChart.TimelineData).entryTotalTimes[entryIndex]); |
| pushEntryInfoRow(i18nString(UIStrings.selfTime), selfTime); |
| pushEntryInfoRow(i18nString(UIStrings.totalTime), totalTime); |
| const linkifier = new Components.Linkifier.Linkifier(); |
| const link = linkifier.maybeLinkifyConsoleCallFrame( |
| this._cpuProfilerModel && this._cpuProfilerModel.target(), node.callFrame); |
| if (link) { |
| pushEntryInfoRow(i18nString(UIStrings.url), link.textContent || ''); |
| } |
| linkifier.dispose(); |
| pushEntryInfoRow(i18nString(UIStrings.aggregatedSelfTime), i18n.i18n.secondsToString(node.self / 1000, true)); |
| pushEntryInfoRow(i18nString(UIStrings.aggregatedTotalTime), i18n.i18n.secondsToString(node.total / 1000, true)); |
| const deoptReason = (node as SDK.CPUProfileDataModel.CPUProfileNode).deoptReason; |
| if (deoptReason) { |
| pushEntryInfoRow(i18nString(UIStrings.notOptimized), deoptReason); |
| } |
| |
| return ProfileView.buildPopoverTable(entryInfo); |
| } |
| } |
| |
| export namespace CPUFlameChartDataProvider { |
| export class ChartEntry { |
| depth: number; |
| duration: number; |
| startTime: number; |
| selfTime: number; |
| node: SDK.CPUProfileDataModel.CPUProfileNode; |
| |
| constructor( |
| depth: number, duration: number, startTime: number, selfTime: number, |
| node: SDK.CPUProfileDataModel.CPUProfileNode) { |
| this.depth = depth; |
| this.duration = duration; |
| this.startTime = startTime; |
| this.selfTime = selfTime; |
| this.node = node; |
| } |
| } |
| } |