|
1 | 1 | // Copyright 2011 The Chromium Authors |
2 | 2 | // Use of this source code is governed by a BSD-style license that can be |
3 | 3 | // found in the LICENSE file. |
4 | | -/* eslint-disable @devtools/no-imperative-dom-api */ |
5 | 4 |
|
6 | 5 | import * as Common from '../../core/common/common.js'; |
7 | 6 | import * as i18n from '../../core/i18n/i18n.js'; |
8 | 7 | import * as Buttons from '../../ui/components/buttons/buttons.js'; |
9 | 8 | import * as UI from '../../ui/legacy/legacy.js'; |
| 9 | +import {html, nothing, render} from '../../ui/lit/lit.js'; |
| 10 | +import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; |
10 | 11 |
|
11 | 12 | import {IsolateSelector} from './IsolateSelector.js'; |
12 | 13 | import type {ProfileType} from './ProfileHeader.js'; |
@@ -41,166 +42,233 @@ const UIStrings = { |
41 | 42 | } as const; |
42 | 43 | const str_ = i18n.i18n.registerUIStrings('panels/profiler/ProfileLauncherView.ts', UIStrings); |
43 | 44 | const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| 45 | +const {widget, widgetRef} = UI.Widget; |
| 46 | + |
| 47 | +interface ProfileTypeEntry { |
| 48 | + profileType: ProfileType; |
| 49 | + selected: boolean; |
| 50 | + customContent: Element|null; |
| 51 | +} |
| 52 | + |
| 53 | +export interface ViewInput { |
| 54 | + headerText: string; |
| 55 | + profileTypes: ProfileTypeEntry[]; |
| 56 | + controlButtonText: string; |
| 57 | + controlButtonDisabled: boolean; |
| 58 | + controlButtonTooltip: string; |
| 59 | + isProfiling: boolean; |
| 60 | + isolateSelector: IsolateSelector|null; |
| 61 | + onControlClick: () => void; |
| 62 | + onLoadClick: () => void; |
| 63 | + onProfileTypeChange: (profileType: ProfileType) => void; |
| 64 | +} |
| 65 | + |
| 66 | +export interface ViewOutput { |
| 67 | + isolateSelector: IsolateSelector; |
| 68 | +} |
| 69 | + |
| 70 | +export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; |
| 71 | + |
| 72 | +// clang-format off |
| 73 | +export const DEFAULT_VIEW: View = (input, output, target) => { |
| 74 | + render(html` |
| 75 | + <style>${profileLauncherViewStyles}</style> |
| 76 | + <div class="profile-launcher-view-content vbox"> |
| 77 | + <div class="vbox"> |
| 78 | + <h1>${input.headerText}</h1> |
| 79 | + <form role="radiogroup" aria-label=${input.headerText}> |
| 80 | + ${input.profileTypes.map(entry => { |
| 81 | + const radioId = `profile-type-${entry.profileType.id}`; |
| 82 | + const customContent = entry.customContent; |
| 83 | + return html` |
| 84 | + <input id=${radioId} type="radio" name="profile-type" |
| 85 | + .checked=${entry.selected} |
| 86 | + ?disabled=${input.isProfiling} |
| 87 | + @change=${() => input.onProfileTypeChange(entry.profileType)} |
| 88 | + jslog=${VisualLogging.toggle().track({change: true}).context('profiler.profile-type')} |
| 89 | + /> |
| 90 | + <label for=${radioId}>${entry.profileType.name}</label> |
| 91 | + <p>${entry.profileType.description}</p> |
| 92 | + ${customContent ? html` |
| 93 | + <p> |
| 94 | + <span role="group" aria-labelledby=${radioId}> |
| 95 | + ${customContent} |
| 96 | + </span> |
| 97 | + </p> |
| 98 | + ` : nothing} |
| 99 | + `; |
| 100 | + })} |
| 101 | + </form> |
| 102 | + </div> |
| 103 | + <div class="vbox profile-isolate-selector-block"> |
| 104 | + <h1>${i18nString(UIStrings.selectJavascriptVmInstance)}</h1> |
| 105 | + <div class="vbox profile-launcher-target-list profile-launcher-target-list-container"> |
| 106 | + <devtools-widget |
| 107 | + ${widget(IsolateSelector)} |
| 108 | + ${widgetRef(IsolateSelector, e => {output.isolateSelector = e;})} |
| 109 | + ></devtools-widget> |
| 110 | + </div> |
| 111 | + ${input.isolateSelector?.totalMemoryElement() ?? nothing} |
| 112 | + </div> |
| 113 | + <div class="hbox profile-launcher-buttons"> |
| 114 | + <devtools-button |
| 115 | + .variant=${Buttons.Button.Variant.OUTLINED} |
| 116 | + .iconName=${'import'} |
| 117 | + @click=${input.onLoadClick} |
| 118 | + .jslogContext=${'profiler.load-from-file'} |
| 119 | + >${i18nString(UIStrings.load)}</devtools-button> |
| 120 | + <devtools-button |
| 121 | + .variant=${Buttons.Button.Variant.PRIMARY} |
| 122 | + ?disabled=${input.controlButtonDisabled} |
| 123 | + title=${input.controlButtonTooltip} |
| 124 | + @click=${input.onControlClick} |
| 125 | + .jslogContext=${'profiler.heap-toggle-recording'} |
| 126 | + >${input.controlButtonText}</devtools-button> |
| 127 | + </div> |
| 128 | + </div> |
| 129 | + `, target); |
| 130 | +}; |
| 131 | +// clang-format on |
| 132 | + |
44 | 133 | export class ProfileLauncherView extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>( |
45 | 134 | UI.Widget.VBox) { |
46 | 135 | readonly panel: ProfilesPanel; |
47 | | - #contentElement: HTMLElement; |
48 | 136 | readonly selectedProfileTypeSetting: Common.Settings.Setting<string>; |
49 | | - profileTypeHeaderElement: HTMLElement; |
50 | | - readonly profileTypeSelectorForm: HTMLElement; |
51 | | - controlButton: Buttons.Button.Button; |
52 | | - readonly loadButton: Buttons.Button.Button; |
53 | | - recordButtonEnabled: boolean; |
54 | | - typeIdToOptionElementAndProfileType: Map<string, { |
55 | | - optionElement: HTMLInputElement, |
56 | | - profileType: ProfileType, |
57 | | - }>; |
58 | | - isProfiling?: boolean; |
59 | | - isInstantProfile?: boolean; |
60 | | - isEnabled?: boolean; |
61 | | - |
62 | | - constructor(profilesPanel: ProfilesPanel) { |
63 | | - super(); |
64 | | - this.registerRequiredCSS(profileLauncherViewStyles); |
| 137 | + readonly #view: View; |
| 138 | + #isolateSelector: IsolateSelector|null = null; |
| 139 | + #profileTypes = new Map<string, ProfileType>(); |
| 140 | + #isProfiling = false; |
| 141 | + #isInstantProfile = false; |
| 142 | + #isEnabled = false; |
| 143 | + #recordButtonEnabled = true; |
| 144 | + #selectedTypeId = ''; |
65 | 145 |
|
66 | | - this.panel = profilesPanel; |
67 | | - this.element.classList.add('profile-launcher-view'); |
68 | | - this.#contentElement = this.element.createChild('div', 'profile-launcher-view-content vbox'); |
| 146 | + constructor(profilesPanel: ProfilesPanel, view: View = DEFAULT_VIEW) { |
| 147 | + super({classes: ['profile-launcher-view']}); |
69 | 148 |
|
70 | | - const profileTypeSelectorElement = this.#contentElement.createChild('div', 'vbox'); |
| 149 | + this.#view = view; |
| 150 | + this.panel = profilesPanel; |
71 | 151 | this.selectedProfileTypeSetting = Common.Settings.Settings.instance().createSetting('selected-profile-type', 'CPU'); |
72 | | - this.profileTypeHeaderElement = profileTypeSelectorElement.createChild('h1'); |
73 | | - this.profileTypeSelectorForm = profileTypeSelectorElement.createChild('form'); |
74 | | - UI.ARIAUtils.markAsRadioGroup(this.profileTypeSelectorForm); |
75 | | - |
76 | | - const isolateSelectorElement = this.#contentElement.createChild('div', 'vbox profile-isolate-selector-block'); |
77 | | - isolateSelectorElement.createChild('h1').textContent = i18nString(UIStrings.selectJavascriptVmInstance); |
78 | | - const isolateSelector = new IsolateSelector(); |
79 | | - const isolateSelectorElementChild = isolateSelectorElement.createChild('div', 'vbox profile-launcher-target-list'); |
80 | | - isolateSelectorElementChild.classList.add('profile-launcher-target-list-container'); |
81 | | - isolateSelector.show(isolateSelectorElementChild); |
82 | | - isolateSelectorElement.appendChild(isolateSelector.totalMemoryElement()); |
83 | | - |
84 | | - const buttonsDiv = this.#contentElement.createChild('div', 'hbox profile-launcher-buttons'); |
85 | | - this.controlButton = UI.UIUtils.createTextButton('', this.controlButtonClicked.bind(this), { |
86 | | - jslogContext: 'profiler.heap-toggle-recording', |
87 | | - variant: Buttons.Button.Variant.PRIMARY, |
88 | | - }); |
89 | | - this.loadButton = new Buttons.Button.Button(); |
90 | | - this.loadButton |
91 | | - .data = {iconName: 'import', variant: Buttons.Button.Variant.OUTLINED, jslogContext: 'profiler.load-from-file'}; |
92 | | - this.loadButton.textContent = i18nString(UIStrings.load); |
93 | | - this.loadButton.addEventListener('click', this.loadButtonClicked.bind(this)); |
94 | | - buttonsDiv.appendChild(this.loadButton); |
95 | | - buttonsDiv.appendChild(this.controlButton); |
96 | | - this.recordButtonEnabled = true; |
97 | | - |
98 | | - this.typeIdToOptionElementAndProfileType = new Map(); |
99 | 152 | } |
100 | 153 |
|
101 | | - loadButtonClicked(): void { |
102 | | - const loadFromFileAction = UI.ActionRegistry.ActionRegistry.instance().getAction('profiler.load-from-file'); |
103 | | - void loadFromFileAction.execute(); |
| 154 | + override wasShown(): void { |
| 155 | + super.wasShown(); |
| 156 | + this.requestUpdate(); |
104 | 157 | } |
105 | 158 |
|
106 | | - updateControls(): void { |
107 | | - if (this.isEnabled && this.recordButtonEnabled) { |
108 | | - this.controlButton.removeAttribute('disabled'); |
109 | | - } else { |
110 | | - this.controlButton.setAttribute('disabled', ''); |
111 | | - } |
112 | | - UI.Tooltip.Tooltip.install( |
113 | | - this.controlButton, this.recordButtonEnabled ? '' : UI.UIUtils.anotherProfilerActiveLabel()); |
114 | | - if (this.isInstantProfile) { |
115 | | - this.controlButton.classList.remove('running'); |
116 | | - this.controlButton.textContent = i18nString(UIStrings.takeSnapshot); |
117 | | - } else if (this.isProfiling) { |
118 | | - this.controlButton.classList.add('running'); |
119 | | - this.controlButton.textContent = i18nString(UIStrings.stop); |
120 | | - } else { |
121 | | - this.controlButton.classList.remove('running'); |
122 | | - this.controlButton.textContent = i18nString(UIStrings.start); |
123 | | - } |
124 | | - for (const {optionElement} of this.typeIdToOptionElementAndProfileType.values()) { |
125 | | - optionElement.disabled = Boolean(this.isProfiling); |
126 | | - } |
| 159 | + #getHeaderText(): string { |
| 160 | + return this.#profileTypes.size > 1 ? i18nString(UIStrings.selectProfilingType) : |
| 161 | + (this.#profileTypes.values().next().value?.name ?? ''); |
127 | 162 | } |
128 | 163 |
|
129 | 164 | profileStarted(): void { |
130 | | - this.isProfiling = true; |
131 | | - this.updateControls(); |
| 165 | + this.#isProfiling = true; |
| 166 | + this.requestUpdate(); |
132 | 167 | } |
133 | 168 |
|
134 | 169 | profileFinished(): void { |
135 | | - this.isProfiling = false; |
136 | | - this.updateControls(); |
| 170 | + this.#isProfiling = false; |
| 171 | + this.requestUpdate(); |
137 | 172 | } |
138 | 173 |
|
139 | 174 | updateProfileType(profileType: ProfileType, recordButtonEnabled: boolean): void { |
140 | | - this.isInstantProfile = profileType.isInstantProfile(); |
141 | | - this.recordButtonEnabled = recordButtonEnabled; |
142 | | - this.isEnabled = profileType.isEnabled(); |
143 | | - this.updateControls(); |
| 175 | + this.#isInstantProfile = profileType.isInstantProfile(); |
| 176 | + this.#recordButtonEnabled = recordButtonEnabled; |
| 177 | + this.#isEnabled = profileType.isEnabled(); |
| 178 | + this.requestUpdate(); |
144 | 179 | } |
145 | 180 |
|
146 | 181 | addProfileType(profileType: ProfileType): void { |
147 | | - const {radio, label} = UI.UIUtils.createRadioButton('profile-type', profileType.name, 'profiler.profile-type'); |
148 | | - this.profileTypeSelectorForm.appendChild(label); |
149 | | - this.typeIdToOptionElementAndProfileType.set(profileType.id, {optionElement: radio, profileType}); |
150 | | - radio.addEventListener('change', this.profileTypeChanged.bind(this, profileType), false); |
151 | | - const descriptionElement = this.profileTypeSelectorForm.createChild('p'); |
152 | | - descriptionElement.textContent = profileType.description; |
153 | | - UI.ARIAUtils.setDescription(radio, profileType.description); |
154 | | - const customContent = profileType.customContent(); |
155 | | - if (customContent) { |
156 | | - customContent.setAttribute('role', 'group'); |
157 | | - customContent.setAttribute('aria-labelledby', `${radio.id}`); |
158 | | - this.profileTypeSelectorForm.createChild('p').appendChild(customContent); |
159 | | - profileType.setCustomContentEnabled(false); |
160 | | - } |
161 | | - const headerText = this.typeIdToOptionElementAndProfileType.size > 1 ? i18nString(UIStrings.selectProfilingType) : |
162 | | - profileType.name; |
163 | | - this.profileTypeHeaderElement.textContent = headerText; |
164 | | - UI.ARIAUtils.setLabel(this.profileTypeSelectorForm, headerText); |
| 182 | + this.#profileTypes.set(profileType.id, profileType); |
| 183 | + profileType.setCustomContentEnabled(false); |
| 184 | + this.requestUpdate(); |
165 | 185 | } |
166 | 186 |
|
167 | 187 | restoreSelectedProfileType(): void { |
168 | 188 | let typeId = this.selectedProfileTypeSetting.get(); |
169 | | - if (!this.typeIdToOptionElementAndProfileType.has(typeId)) { |
170 | | - typeId = this.typeIdToOptionElementAndProfileType.keys().next().value as string; |
| 189 | + if (!this.#profileTypes.has(typeId)) { |
| 190 | + typeId = this.#profileTypes.keys().next().value as string; |
171 | 191 | this.selectedProfileTypeSetting.set(typeId); |
172 | 192 | } |
173 | 193 |
|
174 | | - const optionElementAndProfileType = (this.typeIdToOptionElementAndProfileType.get(typeId) as { |
175 | | - optionElement: HTMLInputElement, |
176 | | - profileType: ProfileType, |
177 | | - }); |
178 | | - optionElementAndProfileType.optionElement.checked = true; |
179 | | - const type = optionElementAndProfileType.profileType; |
180 | | - for (const [id, {profileType}] of this.typeIdToOptionElementAndProfileType) { |
181 | | - const enabled = (id === typeId); |
182 | | - profileType.setCustomContentEnabled(enabled); |
| 194 | + this.#selectedTypeId = typeId; |
| 195 | + const selectedType = this.#profileTypes.get(typeId); |
| 196 | + if (!selectedType) { |
| 197 | + return; |
183 | 198 | } |
184 | | - this.dispatchEventToListeners(Events.PROFILE_TYPE_SELECTED, type); |
185 | | - } |
186 | | - |
187 | | - controlButtonClicked(): void { |
188 | | - this.panel.toggleRecord(); |
| 199 | + for (const [id, profileType] of this.#profileTypes) { |
| 200 | + profileType.setCustomContentEnabled(id === typeId); |
| 201 | + } |
| 202 | + this.dispatchEventToListeners(Events.PROFILE_TYPE_SELECTED, selectedType); |
| 203 | + this.requestUpdate(); |
189 | 204 | } |
190 | 205 |
|
191 | | - profileTypeChanged(profileType: ProfileType): void { |
192 | | - const typeId = this.selectedProfileTypeSetting.get(); |
193 | | - const type = (this.typeIdToOptionElementAndProfileType.get(typeId) as { |
194 | | - optionElement: HTMLInputElement, |
195 | | - profileType: ProfileType, |
196 | | - }).profileType; |
197 | | - type.setCustomContentEnabled(false); |
| 206 | + #profileTypeChanged(profileType: ProfileType): void { |
| 207 | + const previousTypeId = this.#selectedTypeId; |
| 208 | + const previousType = this.#profileTypes.get(previousTypeId); |
| 209 | + if (previousType) { |
| 210 | + previousType.setCustomContentEnabled(false); |
| 211 | + } |
198 | 212 | profileType.setCustomContentEnabled(true); |
199 | | - this.dispatchEventToListeners(Events.PROFILE_TYPE_SELECTED, profileType); |
200 | | - this.isInstantProfile = profileType.isInstantProfile(); |
201 | | - this.isEnabled = profileType.isEnabled(); |
202 | | - this.updateControls(); |
| 213 | + this.#selectedTypeId = profileType.id; |
203 | 214 | this.selectedProfileTypeSetting.set(profileType.id); |
| 215 | + this.#isInstantProfile = profileType.isInstantProfile(); |
| 216 | + this.#isEnabled = profileType.isEnabled(); |
| 217 | + this.dispatchEventToListeners(Events.PROFILE_TYPE_SELECTED, profileType); |
| 218 | + this.requestUpdate(); |
| 219 | + } |
| 220 | + |
| 221 | + override performUpdate(): void { |
| 222 | + const profileTypeEntries: ProfileTypeEntry[] = []; |
| 223 | + for (const [id, profileType] of this.#profileTypes) { |
| 224 | + const selected = id === this.#selectedTypeId; |
| 225 | + const customContent = profileType.customContent(); |
| 226 | + profileType.setCustomContentEnabled(selected); |
| 227 | + profileTypeEntries.push({ |
| 228 | + profileType, |
| 229 | + selected, |
| 230 | + customContent, |
| 231 | + }); |
| 232 | + } |
| 233 | + |
| 234 | + const controlButtonText = this.#isInstantProfile ? |
| 235 | + i18nString(UIStrings.takeSnapshot) : |
| 236 | + (this.#isProfiling ? i18nString(UIStrings.stop) : i18nString(UIStrings.start)); |
| 237 | + const controlButtonDisabled = !(this.#isEnabled && this.#recordButtonEnabled); |
| 238 | + const controlButtonTooltip = this.#recordButtonEnabled ? '' : UI.UIUtils.anotherProfilerActiveLabel(); |
| 239 | + const that = this; |
| 240 | + |
| 241 | + this.#view( |
| 242 | + { |
| 243 | + headerText: this.#getHeaderText(), |
| 244 | + profileTypes: profileTypeEntries, |
| 245 | + controlButtonText, |
| 246 | + controlButtonDisabled, |
| 247 | + controlButtonTooltip, |
| 248 | + isProfiling: this.#isProfiling, |
| 249 | + isolateSelector: this.#isolateSelector, |
| 250 | + onControlClick: () => { |
| 251 | + this.panel.toggleRecord(); |
| 252 | + }, |
| 253 | + onLoadClick: () => { |
| 254 | + const loadFromFileAction = UI.ActionRegistry.ActionRegistry.instance().getAction('profiler.load-from-file'); |
| 255 | + void loadFromFileAction.execute(); |
| 256 | + }, |
| 257 | + onProfileTypeChange: (profileType: ProfileType) => { |
| 258 | + this.#profileTypeChanged(profileType); |
| 259 | + }, |
| 260 | + }, |
| 261 | + { |
| 262 | + set isolateSelector(isolateSelector: IsolateSelector) { |
| 263 | + if (that.#isolateSelector === isolateSelector) { |
| 264 | + return; |
| 265 | + } |
| 266 | + that.#isolateSelector = isolateSelector; |
| 267 | + that.requestUpdate(); |
| 268 | + }, |
| 269 | + }, |
| 270 | + this.contentElement, |
| 271 | + ); |
204 | 272 | } |
205 | 273 | } |
206 | 274 |
|
|
0 commit comments