menu-button.sys.mjs (11504B)
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 enabling and disabling of the menu button for the profiler. 8 * Care should be taken to keep it minimal as it can be run with browser initialization. 9 */ 10 11 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 12 import { createLazyLoaders } from "resource://devtools/client/performance-new/shared/typescript-lazy-load.sys.mjs"; 13 14 const lazy = createLazyLoaders({ 15 CustomizableUI: () => 16 ChromeUtils.importESModule( 17 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs" 18 ), 19 CustomizableWidgets: () => 20 ChromeUtils.importESModule( 21 "moz-src:///browser/components/customizableui/CustomizableWidgets.sys.mjs" 22 ), 23 PopupLogic: () => 24 ChromeUtils.importESModule( 25 "resource://devtools/client/performance-new/popup/logic.sys.mjs" 26 ), 27 Background: () => 28 ChromeUtils.importESModule( 29 "resource://devtools/client/performance-new/shared/background.sys.mjs" 30 ), 31 }); 32 33 const WIDGET_ID = "profiler-button"; 34 const DROPMARKER_ID = "profiler-button-dropmarker"; 35 36 /** 37 * Add the profiler button to the navbar. 38 * 39 * @return {void} 40 */ 41 function addToNavbar() { 42 const { CustomizableUI } = lazy.CustomizableUI(); 43 44 CustomizableUI.addWidgetToArea(WIDGET_ID, CustomizableUI.AREA_NAVBAR); 45 } 46 47 /** 48 * Remove the widget and place it in the customization palette. This will also 49 * disable the shortcuts. 50 * 51 * @return {void} 52 */ 53 function remove() { 54 const { CustomizableUI } = lazy.CustomizableUI(); 55 CustomizableUI.removeWidgetFromArea(WIDGET_ID); 56 } 57 58 /** 59 * See if the profiler menu button is in the navbar, or other active areas. The 60 * placement is null when it's inactive in the customization palette. 61 * 62 * @return {boolean} 63 */ 64 function isInNavbar() { 65 if (AppConstants.MOZ_APP_NAME == "thunderbird") { 66 return false; 67 } 68 69 const { CustomizableUI } = lazy.CustomizableUI(); 70 return Boolean(CustomizableUI.getPlacementOfWidget("profiler-button")); 71 } 72 73 function ensureButtonInNavbar() { 74 // 1. Ensure the widget is enabled. 75 const featureFlagPref = "devtools.performance.popup.feature-flag"; 76 const isPopupFeatureFlagEnabled = Services.prefs.getBoolPref(featureFlagPref); 77 if (!isPopupFeatureFlagEnabled) { 78 // Setting the pref will also run the menubutton initialization thanks to 79 // the observer set in DevtoolsStartup. 80 Services.prefs.setBoolPref("devtools.performance.popup.feature-flag", true); 81 } 82 83 // 2. Ensure it's added to the nav bar 84 if (!isInNavbar()) { 85 // Enable the profiler menu button. 86 addToNavbar(); 87 88 // Dispatch the change event manually, so that the shortcuts will also be 89 // added. 90 const { CustomizableUI } = lazy.CustomizableUI(); 91 CustomizableUI.dispatchToolboxEvent("customizationchange"); 92 } 93 } 94 95 /** 96 * Opens the popup for the profiler. 97 * 98 * @param {Document} document 99 */ 100 function openPopup(document) { 101 // First find the button. 102 /** @type {HTMLButtonElement | null} */ 103 const button = document.querySelector("#profiler-button"); 104 if (!button) { 105 throw new Error("Could not find the profiler button."); 106 } 107 108 // Sending a click event anywhere on the button could start the profiler 109 // instead of opening the popup. Sending a command event on a view widget 110 // will make CustomizableUI show the view. 111 const cmdEvent = document.createEvent("xulcommandevent"); 112 // @ts-ignore - Bug 1674368 113 cmdEvent.initCommandEvent("command", true, true, button.ownerGlobal); 114 button.dispatchEvent(cmdEvent); 115 } 116 117 /** 118 * This function creates the widget definition for the CustomizableUI. It should 119 * only be run if the profiler button is enabled. 120 * 121 * @param {(isEnabled: boolean) => void} toggleProfilerKeyShortcuts 122 * @return {void} 123 */ 124 function initialize(toggleProfilerKeyShortcuts) { 125 const { CustomizableUI } = lazy.CustomizableUI(); 126 const { CustomizableWidgets } = lazy.CustomizableWidgets(); 127 128 const widget = CustomizableUI.getWidget(WIDGET_ID); 129 if (widget && widget.provider == CustomizableUI.PROVIDER_API) { 130 // This widget has already been created. 131 return; 132 } 133 134 const viewId = "PanelUI-profiler"; 135 136 /** 137 * This is mutable state that will be shared between panel displays. 138 * 139 * @type {import("devtools/client/performance-new/popup/logic.sys.mjs").State} 140 */ 141 const panelState = { 142 cleanup: [], 143 isInfoCollapsed: true, 144 }; 145 146 /** 147 * Handle when the customization changes for the button. This event is not 148 * very specific, and fires for any CustomizableUI widget. This event is 149 * pretty rare to fire, and only affects users of the profiler button, 150 * so it shouldn't have much overhead even if it runs a lot. 151 */ 152 function handleCustomizationChange() { 153 const isEnabled = isInNavbar(); 154 toggleProfilerKeyShortcuts(isEnabled); 155 156 if (!isEnabled) { 157 // The profiler menu button is no longer in the navbar, make sure that the 158 // "intro-displayed" preference is reset. 159 /** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */ 160 const popupIntroDisplayedPref = 161 "devtools.performance.popup.intro-displayed"; 162 Services.prefs.setBoolPref(popupIntroDisplayedPref, false); 163 164 // We stop the profiler when the button is removed for normal users, 165 // but we try to avoid interfering with profiling of automated tests. 166 if ( 167 Services.profiler.IsActive() && 168 (!Cu.isInAutomation || !Services.env.exists("MOZ_PROFILER_STARTUP")) 169 ) { 170 Services.profiler.StopProfiler(); 171 } 172 } 173 } 174 175 const item = { 176 id: WIDGET_ID, 177 type: "button-and-view", 178 viewId, 179 l10nId: "profiler-popup-button-idle", 180 181 onViewShowing: 182 /** 183 * @type {(event: { 184 * target: ChromeHTMLElement | XULElement, 185 * detail: { 186 * addBlocker: (blocker: Promise<void>) => void 187 * } 188 * }) => void} 189 */ 190 event => { 191 try { 192 // The popup logic is stored in a separate script so it doesn't have 193 // to be parsed at browser startup, and will only be lazily loaded 194 // when the popup is viewed. 195 const { initializePopup } = lazy.PopupLogic(); 196 197 initializePopup(panelState, event.target); 198 } catch (error) { 199 // Surface any errors better in the console. 200 console.error(error); 201 } 202 }, 203 204 /** 205 * @type {(event: { target: ChromeHTMLElement | XULElement }) => void} 206 */ 207 onViewHiding() { 208 // Clean-up the view. This removes all of the event listeners. 209 for (const fn of panelState.cleanup) { 210 fn(); 211 } 212 panelState.cleanup = []; 213 }, 214 215 /** 216 * Perform any general initialization for this widget. This is called once per 217 * browser window. 218 * 219 * @type {(document: HTMLDocument) => void} 220 */ 221 onBeforeCreated: document => { 222 /** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */ 223 const popupIntroDisplayedPref = 224 "devtools.performance.popup.intro-displayed"; 225 226 // Determine the state of the popup's info being collapsed BEFORE the view 227 // is shown, and update the collapsed state. This way the transition animation 228 // isn't run. 229 panelState.isInfoCollapsed = Services.prefs.getBoolPref( 230 popupIntroDisplayedPref 231 ); 232 if (!panelState.isInfoCollapsed) { 233 // We have displayed the intro, don't show it again by default. 234 Services.prefs.setBoolPref(popupIntroDisplayedPref, true); 235 } 236 237 // Handle customization event changes. If the profiler is no longer in the 238 // navbar, then reset the popup intro preference. 239 const window = document.defaultView; 240 if (window) { 241 /** @type {any} */ (window).gNavToolbox.addEventListener( 242 "customizationchange", 243 handleCustomizationChange 244 ); 245 } 246 247 toggleProfilerKeyShortcuts(isInNavbar()); 248 }, 249 250 /** 251 * This method is used when we need to operate upon the button element itself. 252 * This is called once per browser window. 253 * 254 * @type {(node: ChromeHTMLElement) => void} 255 */ 256 onCreated: node => { 257 const document = node.ownerDocument; 258 const window = document?.defaultView; 259 if (!document || !window) { 260 console.error( 261 "Unable to find the document or the window of the profiler toolbar item." 262 ); 263 return; 264 } 265 266 const firstButton = node.firstElementChild; 267 if (!firstButton) { 268 console.error( 269 "Unable to find the button element inside the profiler toolbar item." 270 ); 271 return; 272 } 273 274 // Assign the null-checked button element to a new variable so that 275 // TypeScript doesn't require additional null checks in the functions 276 // below. 277 const buttonElement = firstButton; 278 279 // This class is needed to show the subview arrow when our button 280 // is in the overflow menu. 281 buttonElement.classList.add("subviewbutton-nav"); 282 283 // Add l10n attributes for the dropmarker. 284 const dropmarker = node.querySelector("#" + DROPMARKER_ID); 285 if (dropmarker) { 286 document.l10n.setAttributes(dropmarker, DROPMARKER_ID); 287 } 288 289 function setButtonActive() { 290 document.l10n.setAttributes( 291 buttonElement, 292 "profiler-popup-button-recording" 293 ); 294 buttonElement.classList.toggle("profiler-active", true); 295 buttonElement.classList.toggle("profiler-paused", false); 296 } 297 function setButtonPaused() { 298 document.l10n.setAttributes( 299 buttonElement, 300 "profiler-popup-button-capturing" 301 ); 302 buttonElement.classList.toggle("profiler-active", false); 303 buttonElement.classList.toggle("profiler-paused", true); 304 } 305 function setButtonInactive() { 306 document.l10n.setAttributes( 307 buttonElement, 308 "profiler-popup-button-idle" 309 ); 310 buttonElement.classList.toggle("profiler-active", false); 311 buttonElement.classList.toggle("profiler-paused", false); 312 } 313 314 if (Services.profiler.IsPaused()) { 315 setButtonPaused(); 316 } 317 if (Services.profiler.IsActive()) { 318 setButtonActive(); 319 } 320 321 Services.obs.addObserver(setButtonActive, "profiler-started"); 322 Services.obs.addObserver(setButtonInactive, "profiler-stopped"); 323 Services.obs.addObserver(setButtonPaused, "profiler-paused"); 324 325 window.addEventListener("unload", () => { 326 Services.obs.removeObserver(setButtonActive, "profiler-started"); 327 Services.obs.removeObserver(setButtonInactive, "profiler-stopped"); 328 Services.obs.removeObserver(setButtonPaused, "profiler-paused"); 329 }); 330 }, 331 332 onCommand: () => { 333 if (Services.profiler.IsPaused()) { 334 // A profile is already being captured, ignore this event. 335 return; 336 } 337 const { startProfiler, captureProfile } = lazy.Background(); 338 if (Services.profiler.IsActive()) { 339 captureProfile("aboutprofiling"); 340 } else { 341 startProfiler("aboutprofiling"); 342 } 343 }, 344 }; 345 346 CustomizableUI.createWidget(item); 347 CustomizableWidgets.push(item); 348 } 349 350 export const ProfilerMenuButton = { 351 initialize, 352 addToNavbar, 353 ensureButtonInNavbar, 354 isInNavbar, 355 openPopup, 356 remove, 357 };