logic.sys.mjs (10582B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 // @ts-check 5 6 /** 7 * This file controls the logic of the profiler popup view. 8 */ 9 10 /** 11 * @typedef {ReturnType<typeof selectElementsInPanelview>} Elements 12 * @typedef {ReturnType<typeof createViewControllers>} ViewController 13 */ 14 15 /** 16 * @typedef {object} State - The mutable state of the popup. 17 * @property {Array<() => void>} cleanup - Functions to cleanup once the view is hidden. 18 * @property {boolean} isInfoCollapsed 19 */ 20 21 import { createLazyLoaders } from "resource://devtools/client/performance-new/shared/typescript-lazy-load.sys.mjs"; 22 23 const lazy = createLazyLoaders({ 24 PanelMultiView: () => 25 ChromeUtils.importESModule( 26 "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs" 27 ), 28 Background: () => 29 ChromeUtils.importESModule( 30 "resource://devtools/client/performance-new/shared/background.sys.mjs" 31 ), 32 PrefsPresets: () => 33 ChromeUtils.importESModule( 34 "resource://devtools/shared/performance-new/prefs-presets.sys.mjs" 35 ), 36 }); 37 38 /** 39 * This function collects all of the selection of the elements inside of the panel. 40 * 41 * @param {XULElement} panelview 42 */ 43 function selectElementsInPanelview(panelview) { 44 const document = panelview.ownerDocument; 45 46 // Forcefully cast the window to the type Window 47 /** @type {any} */ 48 const windowAny = document.defaultView; 49 /** @type {Window} */ 50 const window = windowAny; 51 52 /** 53 * Get an element or throw an error if it's not found. This is more friendly 54 * for TypeScript. 55 * 56 * @param {string} id 57 * @return {HTMLElement} 58 */ 59 function getElementById(id) { 60 /** @type {HTMLElement | null} */ 61 // @ts-ignore - Bug 1674368 62 const { PanelMultiView } = lazy.PanelMultiView(); 63 const element = PanelMultiView.getViewNode(document, id); 64 if (!element) { 65 throw new Error(`Could not find the element from the ID "${id}"`); 66 } 67 return element; 68 } 69 70 return { 71 document, 72 panelview, 73 window, 74 inactive: getElementById("PanelUI-profiler-inactive"), 75 active: getElementById("PanelUI-profiler-active"), 76 presetDescription: getElementById("PanelUI-profiler-content-description"), 77 presetsEditSettings: getElementById( 78 "PanelUI-profiler-content-edit-settings" 79 ), 80 presetsMenuList: /** @type {MenuListElement} */ ( 81 getElementById("PanelUI-profiler-presets") 82 ), 83 header: getElementById("PanelUI-profiler-header"), 84 info: getElementById("PanelUI-profiler-info"), 85 menupopup: getElementById("PanelUI-profiler-presets-menupopup"), 86 infoButton: getElementById("PanelUI-profiler-info-button"), 87 learnMore: getElementById("PanelUI-profiler-learn-more"), 88 startRecording: getElementById("PanelUI-profiler-startRecording"), 89 stopAndDiscard: getElementById("PanelUI-profiler-stopAndDiscard"), 90 stopAndCapture: getElementById("PanelUI-profiler-stopAndCapture"), 91 settingsSection: getElementById("PanelUI-profiler-content-settings"), 92 contentRecording: getElementById("PanelUI-profiler-content-recording"), 93 }; 94 } 95 96 /** 97 * This function returns an interface that can be used to control the view of the 98 * panel based on the current mutable State. 99 * 100 * @param {State} state 101 * @param {Elements} elements 102 */ 103 function createViewControllers(state, elements) { 104 return { 105 updateInfoCollapse() { 106 const { header, info, infoButton } = elements; 107 header.setAttribute( 108 "isinfocollapsed", 109 state.isInfoCollapsed ? "true" : "false" 110 ); 111 // @ts-ignore - Bug 1674368 112 infoButton.checked = !state.isInfoCollapsed; 113 114 if (state.isInfoCollapsed) { 115 const { height } = info.getBoundingClientRect(); 116 info.style.marginBlockEnd = `-${height}px`; 117 } else { 118 info.style.marginBlockEnd = "0"; 119 } 120 }, 121 122 updatePresets() { 123 const { presets, getRecordingSettings } = lazy.PrefsPresets(); 124 const { presetName } = getRecordingSettings( 125 "aboutprofiling", 126 Services.profiler.GetFeatures() 127 ); 128 const preset = presets[presetName]; 129 if (preset) { 130 elements.presetDescription.style.display = "block"; 131 elements.document.l10n.setAttributes( 132 elements.presetDescription, 133 preset.l10nIds.popup.description 134 ); 135 elements.presetsMenuList.value = presetName; 136 } else { 137 elements.presetDescription.style.display = "none"; 138 // We don't remove the l10n-id attribute as the element is hidden anyway. 139 // It will be updated again when it's displayed next time. 140 elements.presetsMenuList.value = "custom"; 141 } 142 }, 143 144 updateProfilerState() { 145 if (Services.profiler.IsActive()) { 146 elements.inactive.hidden = true; 147 elements.active.hidden = false; 148 elements.settingsSection.hidden = true; 149 elements.contentRecording.hidden = false; 150 } else { 151 elements.inactive.hidden = false; 152 elements.active.hidden = true; 153 elements.settingsSection.hidden = false; 154 elements.contentRecording.hidden = true; 155 } 156 }, 157 158 createPresetsList() { 159 // Check the DOM if the presets were built or not. We can't cache this value 160 // in the `State` object, as the `State` object will be removed if the 161 // button is removed from the toolbar, but the DOM changes will still persist. 162 if (elements.menupopup.getAttribute("presetsbuilt") === "true") { 163 // The presets were already built. 164 return; 165 } 166 167 const { presets } = lazy.PrefsPresets(); 168 const currentPreset = Services.prefs.getCharPref( 169 "devtools.performance.recording.preset" 170 ); 171 172 const menuitems = Object.entries(presets).map(([id, preset]) => { 173 const { document, presetsMenuList } = elements; 174 const menuitem = document.createXULElement("menuitem"); 175 document.l10n.setAttributes(menuitem, preset.l10nIds.popup.label); 176 menuitem.setAttribute("value", id); 177 if (id === currentPreset) { 178 presetsMenuList.setAttribute("value", id); 179 } 180 return menuitem; 181 }); 182 183 elements.menupopup.prepend(...menuitems); 184 elements.menupopup.setAttribute("presetsbuilt", "true"); 185 }, 186 187 hidePopup() { 188 const panel = elements.panelview.closest("panel"); 189 if (!panel) { 190 throw new Error("Could not find the panel from the panelview."); 191 } 192 /** @type {any} */ (panel).hidePopup(); 193 }, 194 }; 195 } 196 197 /** 198 * Perform all of the business logic to present the popup view once it is open. 199 * 200 * @param {State} state 201 * @param {Elements} elements 202 * @param {ViewController} view 203 */ 204 function initializeView(state, elements, view) { 205 view.createPresetsList(); 206 207 state.cleanup.push(() => { 208 // The UI should be collapsed by default for the next time the popup 209 // is open. 210 state.isInfoCollapsed = true; 211 view.updateInfoCollapse(); 212 }); 213 214 // Turn off all animations while initializing the popup. 215 elements.header.setAttribute("animationready", "false"); 216 217 elements.window.requestAnimationFrame(() => { 218 // Allow the elements to layout once, the updateInfoCollapse implementation measures 219 // the size of the container. It needs to wait a second before the bounding box 220 // returns an actual size. 221 view.updateInfoCollapse(); 222 view.updateProfilerState(); 223 view.updatePresets(); 224 225 // Now wait for another rAF, and turn the animations back on. 226 elements.window.requestAnimationFrame(() => { 227 elements.header.setAttribute("animationready", "true"); 228 }); 229 }); 230 } 231 232 /** 233 * This function is in charge of settings all of the events handlers for the view. 234 * The handlers must also add themselves to the `state.cleanup` for them to be 235 * properly cleaned up once the view is destroyed. 236 * 237 * @param {State} state 238 * @param {Elements} elements 239 * @param {ViewController} view 240 */ 241 function addPopupEventHandlers(state, elements, view) { 242 const { startProfiler, stopProfiler, captureProfile } = lazy.Background(); 243 244 /** 245 * Adds a handler that automatically is removed once the panel is hidden. 246 * 247 * @param {HTMLElement} element 248 * @param {string} type 249 * @param {(event: Event) => void} handler 250 */ 251 function addHandler(element, type, handler) { 252 element.addEventListener(type, handler); 253 state.cleanup.push(() => { 254 element.removeEventListener(type, handler); 255 }); 256 } 257 258 addHandler(elements.infoButton, "click", event => { 259 // Any button command event in the popup will cause it to close. Prevent this 260 // from happening on click. 261 event.preventDefault(); 262 263 state.isInfoCollapsed = !state.isInfoCollapsed; 264 view.updateInfoCollapse(); 265 }); 266 267 addHandler(elements.startRecording, "click", () => { 268 startProfiler("aboutprofiling"); 269 }); 270 271 addHandler(elements.stopAndDiscard, "click", () => { 272 stopProfiler(); 273 }); 274 275 addHandler(elements.stopAndCapture, "click", () => { 276 captureProfile("aboutprofiling"); 277 view.hidePopup(); 278 }); 279 280 addHandler(elements.learnMore, "click", () => { 281 elements.window.openWebLinkIn("https://profiler.firefox.com/docs/", "tab"); 282 view.hidePopup(); 283 }); 284 285 addHandler(elements.presetsMenuList, "command", () => { 286 lazy 287 .PrefsPresets() 288 .changePreset( 289 "aboutprofiling", 290 elements.presetsMenuList.value, 291 Services.profiler.GetFeatures() 292 ); 293 view.updatePresets(); 294 }); 295 296 addHandler(elements.presetsEditSettings, "click", () => { 297 elements.window.openTrustedLinkIn("about:profiling", "tab"); 298 view.hidePopup(); 299 }); 300 301 // Update the view when the profiler starts/stops. 302 // These are all events that can affect the current state of the profiler. 303 const events = ["profiler-started", "profiler-stopped"]; 304 for (const event of events) { 305 Services.obs.addObserver(view.updateProfilerState, event); 306 state.cleanup.push(() => { 307 Services.obs.removeObserver(view.updateProfilerState, event); 308 }); 309 } 310 } 311 312 /** 313 * Initialize everything needed for the popup to work fine. 314 * 315 * @param {State} panelState 316 * @param {XULElement} panelview 317 */ 318 export function initializePopup(panelState, panelview) { 319 const panelElements = selectElementsInPanelview(panelview); 320 const panelviewControllers = createViewControllers(panelState, panelElements); 321 addPopupEventHandlers(panelState, panelElements, panelviewControllers); 322 initializeView(panelState, panelElements, panelviewControllers); 323 }