background.sys.mjs (20099B)
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 contains all of the background logic for controlling the state and 8 * configuration of the profiler. It is in a JSM so that the logic can be shared 9 * with both the popup client, and the keyboard shortcuts. The shortcuts don't need 10 * access to any UI, and need to be loaded independent of the popup. 11 */ 12 13 // The following are not lazily loaded as they are needed during initialization. 14 15 import { createLazyLoaders } from "resource://devtools/client/performance-new/shared/typescript-lazy-load.sys.mjs"; 16 17 /** 18 * @typedef {import("../@types/perf").PerformancePref} PerformancePref 19 * @typedef {import("../@types/perf").ProfilerWebChannel} ProfilerWebChannel 20 * @typedef {import("../@types/perf").PageContext} PageContext 21 * @typedef {import("../@types/perf").MessageFromFrontend} MessageFromFrontend 22 * @typedef {import("../@types/perf").RequestFromFrontend} RequestFromFrontend 23 * @typedef {import("../@types/perf").ResponseToFrontend} ResponseToFrontend 24 * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService 25 * @typedef {import("../@types/perf").ProfilerBrowserInfo} ProfilerBrowserInfo 26 * @typedef {import("../@types/perf").ProfileCaptureResult} ProfileCaptureResult 27 * @typedef {import("../@types/perf").ProfilerFaviconData} ProfilerFaviconData 28 * @typedef {import("../@types/perf").JSSources} JSSources 29 */ 30 31 /** @type {PerformancePref["PopupFeatureFlag"]} */ 32 const POPUP_FEATURE_FLAG_PREF = "devtools.performance.popup.feature-flag"; 33 34 // The version of the profiler WebChannel. 35 // This is reported from the STATUS_QUERY message, and identifies the 36 // capabilities of the WebChannel. The front-end can handle old WebChannel 37 // versions and has a full list of versions and capabilities here: 38 // https://github.com/firefox-devtools/profiler/blob/main/src/app-logic/web-channel.js 39 const CURRENT_WEBCHANNEL_VERSION = 6; 40 41 const lazyRequire = {}; 42 // eslint-disable-next-line mozilla/lazy-getter-object-name 43 ChromeUtils.defineESModuleGetters(lazyRequire, { 44 require: "resource://devtools/shared/loader/Loader.sys.mjs", 45 }); 46 // Lazily load the require function, when it's needed. 47 // Avoid using ChromeUtils.defineESModuleGetters for now as: 48 // * we can't replace createLazyLoaders as we still load commonjs+jsm+esm 49 // It will be easier once we only load sys.mjs files. 50 // * we would need to find a way to accomodate typescript to this special function. 51 // @ts-ignore:next-line 52 function require(path) { 53 // @ts-ignore:next-line 54 return lazyRequire.require(path); 55 } 56 57 // The following utilities are lazily loaded as they are not needed when controlling the 58 // global state of the profiler, and only are used during specific funcationality like 59 // symbolication or capturing a profile. 60 const lazy = createLazyLoaders({ 61 BrowserModule: () => 62 require("resource://devtools/client/performance-new/shared/browser.js"), 63 Errors: () => 64 ChromeUtils.importESModule( 65 "resource://devtools/shared/performance-new/errors.sys.mjs" 66 ), 67 PrefsPresets: () => 68 ChromeUtils.importESModule( 69 "resource://devtools/shared/performance-new/prefs-presets.sys.mjs" 70 ), 71 RecordingUtils: () => 72 ChromeUtils.importESModule( 73 "resource://devtools/shared/performance-new/recording-utils.sys.mjs" 74 ), 75 CustomizableUI: () => 76 ChromeUtils.importESModule( 77 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs" 78 ), 79 PerfSymbolication: () => 80 ChromeUtils.importESModule( 81 "resource://devtools/shared/performance-new/symbolication.sys.mjs" 82 ), 83 ProfilerMenuButton: () => 84 ChromeUtils.importESModule( 85 "resource://devtools/client/performance-new/popup/menu-button.sys.mjs" 86 ), 87 PlacesUtils: () => 88 ChromeUtils.importESModule("resource://gre/modules/PlacesUtils.sys.mjs") 89 .PlacesUtils, 90 }); 91 92 /** @type {{[key:string]: number} | null} */ 93 let gPreviousMozLogValues = null; 94 95 /** 96 * This function is called when the profile is captured with the shortcut keys, 97 * with the profiler toolbarbutton, with the button inside the popup, or with 98 * the about:logging page. 99 * 100 * @param {PageContext} pageContext 101 * @return {Promise<void>} 102 */ 103 export async function captureProfile(pageContext) { 104 if (!Services.profiler.IsActive()) { 105 // The profiler is not active, ignore. 106 return; 107 } 108 if (Services.profiler.IsPaused()) { 109 // The profiler is already paused for capture, ignore. 110 return; 111 } 112 113 const { profileCaptureResult, additionalInformation } = await lazy 114 .RecordingUtils() 115 .getProfileDataAsGzippedArrayBufferThenStop(); 116 cleanupMozLogs(); 117 const profilerViewMode = lazy 118 .PrefsPresets() 119 .getProfilerViewModeForCurrentPreset(pageContext); 120 const sharedLibraries = additionalInformation?.sharedLibraries 121 ? additionalInformation.sharedLibraries 122 : Services.profiler.sharedLibraries; 123 const objdirs = lazy.PrefsPresets().getObjdirPrefValue(); 124 125 const { createLocalSymbolicationService } = lazy.PerfSymbolication(); 126 const symbolicationService = createLocalSymbolicationService( 127 sharedLibraries, 128 objdirs 129 ); 130 131 const { openProfilerTab } = lazy.BrowserModule(); 132 const browser = await openProfilerTab({ profilerViewMode }); 133 registerProfileCaptureForBrowser( 134 browser, 135 profileCaptureResult, 136 symbolicationService, 137 additionalInformation?.jsSources ?? null 138 ); 139 } 140 141 /** 142 * This function is called when the profiler is started with the shortcut 143 * keys, with the profiler toolbarbutton, or with the button inside the 144 * popup. 145 * 146 * @param {PageContext} pageContext 147 */ 148 export function startProfiler(pageContext) { 149 const { entries, interval, features, threads, mozLogs, duration } = lazy 150 .PrefsPresets() 151 .getRecordingSettings(pageContext, Services.profiler.GetFeatures()); 152 153 // Get the active Browser ID from browser. 154 const { getActiveBrowserID } = lazy.RecordingUtils(); 155 const activeTabID = getActiveBrowserID(); 156 157 if (typeof mozLogs == "string") { 158 updateMozLogs(mozLogs); 159 } 160 161 Services.profiler.StartProfiler( 162 entries, 163 interval, 164 features, 165 threads, 166 activeTabID, 167 duration 168 ); 169 } 170 171 /** 172 * Given a MOZ_LOG string, toggles the expected preferences to enable the 173 * LogModules mentioned in the string at the expected level of logging. 174 * This will also record preference values in order to reset them on stop. 175 * `mozLogs` is a string similar to the one passed as MOZ_LOG env variable. 176 * 177 * @param {string} mozLogs 178 */ 179 function updateMozLogs(mozLogs) { 180 gPreviousMozLogValues = {}; 181 for (const module of mozLogs.split(",")) { 182 const lastColon = module.lastIndexOf(":"); 183 const logName = module.slice(0, lastColon).trim(); 184 const value = parseInt(module.slice(lastColon + 1).trim(), 10); 185 const prefName = `logging.${logName}`; 186 gPreviousMozLogValues[prefName] = Services.prefs.getIntPref( 187 prefName, 188 undefined 189 ); 190 // MOZ_LOG aren't profiler specific and enabled globally in Firefox. 191 // Preferences are the easiest (only?) way to toggle them from JavaScript. 192 Services.prefs.setIntPref(prefName, value); 193 } 194 } 195 196 /** 197 * This function is called directly by devtools/startup/DevToolsStartup.jsm when 198 * using the shortcut keys to capture a profile. 199 * 200 * @type {() => void} 201 */ 202 export function stopProfiler() { 203 Services.profiler.StopProfiler(); 204 205 cleanupMozLogs(); 206 } 207 208 /** 209 * This function should be called when we are done profiler in order to reset 210 * the MOZ_LOG enabled while profiling. 211 * 212 * @type {() => void} 213 */ 214 export function cleanupMozLogs() { 215 if (gPreviousMozLogValues) { 216 for (const [prefName, value] of Object.entries(gPreviousMozLogValues)) { 217 if (typeof value == "number") { 218 Services.prefs.setIntPref(prefName, value); 219 } else { 220 Services.prefs.clearUserPref(prefName); 221 } 222 } 223 gPreviousMozLogValues = null; 224 } 225 } 226 227 /** 228 * This function is called directly by devtools/startup/DevToolsStartup.jsm when 229 * using the shortcut keys to start and stop the profiler. 230 * 231 * @param {PageContext} pageContext 232 * @return {void} 233 */ 234 export function toggleProfiler(pageContext) { 235 if (Services.profiler.IsPaused()) { 236 // The profiler is currently paused, which means that the user is already 237 // attempting to capture a profile. Ignore this request. 238 return; 239 } 240 if (Services.profiler.IsActive()) { 241 stopProfiler(); 242 } else { 243 startProfiler(pageContext); 244 } 245 } 246 247 /** 248 * @param {PageContext} pageContext 249 */ 250 export function restartProfiler(pageContext) { 251 stopProfiler(); 252 startProfiler(pageContext); 253 } 254 255 /** 256 * This map stores information that is associated with a "profile capturing" 257 * action, so that we can look up this information for WebChannel messages 258 * from the profiler tab. 259 * Most importantly, this stores the captured profile. When the profiler tab 260 * requests the profile, we can respond to the message with the correct profile. 261 * This works even if the request happens long after the tab opened. It also 262 * works for an "old" tab even if new profiles have been captured since that 263 * tab was opened. 264 * Supporting tab refresh is important because the tab sometimes reloads itself: 265 * If an old version of the front-end is cached in the service worker, and the 266 * browser supplies a profile with a newer format version, then the front-end 267 * updates its service worker and reloads itself, so that the updated version 268 * can parse the profile. 269 * 270 * This is a WeakMap so that the profile can be garbage-collected when the tab 271 * is closed. 272 * 273 * @type {WeakMap<MockedExports.Browser, ProfilerBrowserInfo>} 274 */ 275 const infoForBrowserMap = new WeakMap(); 276 277 /** 278 * This handler computes the response for any messages coming 279 * from the WebChannel from profiler.firefox.com. 280 * 281 * @param {RequestFromFrontend} request 282 * @param {MockedExports.Browser} browser - The tab's browser. 283 * @return {Promise<ResponseToFrontend>} 284 */ 285 async function getResponseForMessage(request, browser) { 286 switch (request.type) { 287 case "STATUS_QUERY": { 288 // The content page wants to know if this channel exists. It does, so respond 289 // back to the ping. 290 const { ProfilerMenuButton } = lazy.ProfilerMenuButton(); 291 return { 292 version: CURRENT_WEBCHANNEL_VERSION, 293 menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(), 294 }; 295 } 296 case "ENABLE_MENU_BUTTON": { 297 const { ownerDocument } = browser; 298 if (!ownerDocument) { 299 throw new Error( 300 "Could not find the owner document for the current browser while enabling " + 301 "the profiler menu button" 302 ); 303 } 304 // Ensure the widget is enabled. 305 Services.prefs.setBoolPref(POPUP_FEATURE_FLAG_PREF, true); 306 307 // Force the preset to be "firefox-platform" if we enable the menu button 308 // via web channel. If user goes through profiler.firefox.com to enable 309 // it, it means that either user is a platform developer or filing a bug 310 // report for performance engineers to look at. 311 const supportedFeatures = Services.profiler.GetFeatures(); 312 lazy 313 .PrefsPresets() 314 .changePreset("aboutprofiling", "firefox-platform", supportedFeatures); 315 316 // Enable the profiler menu button. 317 const { ProfilerMenuButton } = lazy.ProfilerMenuButton(); 318 ProfilerMenuButton.addToNavbar(); 319 320 // Dispatch the change event manually, so that the shortcuts will also be 321 // added. 322 const { CustomizableUI } = lazy.CustomizableUI(); 323 CustomizableUI.dispatchToolboxEvent("customizationchange"); 324 325 // Open the popup with a message. 326 ProfilerMenuButton.openPopup(ownerDocument); 327 328 // There is no response data for this message. 329 return undefined; 330 } 331 case "GET_PROFILE": { 332 const infoForBrowser = infoForBrowserMap.get(browser); 333 if (infoForBrowser === undefined) { 334 throw new Error("Could not find a profile for this tab."); 335 } 336 const { profileCaptureResult } = infoForBrowser; 337 switch (profileCaptureResult.type) { 338 case "SUCCESS": 339 return profileCaptureResult.profile; 340 case "ERROR": 341 throw profileCaptureResult.error; 342 default: { 343 const { UnhandledCaseError } = lazy.Errors(); 344 throw new UnhandledCaseError( 345 profileCaptureResult, 346 "profileCaptureResult" 347 ); 348 } 349 } 350 } 351 case "GET_SYMBOL_TABLE": { 352 const { debugName, breakpadId } = request; 353 const symbolicationService = getSymbolicationServiceForBrowser(browser); 354 if (!symbolicationService) { 355 throw new Error("No symbolication service has been found for this tab"); 356 } 357 return symbolicationService.getSymbolTable(debugName, breakpadId); 358 } 359 case "QUERY_SYMBOLICATION_API": { 360 const { path, requestJson } = request; 361 const symbolicationService = getSymbolicationServiceForBrowser(browser); 362 if (!symbolicationService) { 363 throw new Error("No symbolication service has been found for this tab"); 364 } 365 return symbolicationService.querySymbolicationApi(path, requestJson); 366 } 367 case "GET_EXTERNAL_POWER_TRACKS": { 368 const { startTime, endTime } = request; 369 const externalPowerUrl = Services.prefs.getCharPref( 370 "devtools.performance.recording.power.external-url", 371 "" 372 ); 373 if (externalPowerUrl) { 374 const response = await fetch( 375 `${externalPowerUrl}?start=${startTime}&end=${endTime}` 376 ); 377 return response.json(); 378 } 379 return []; 380 } 381 case "GET_EXTERNAL_MARKERS": { 382 const { startTime, endTime } = request; 383 const externalMarkersUrl = Services.prefs.getCharPref( 384 "devtools.performance.recording.markers.external-url", 385 "" 386 ); 387 if (externalMarkersUrl) { 388 const response = await fetch( 389 `${externalMarkersUrl}?start=${startTime}&end=${endTime}` 390 ); 391 return response.json(); 392 } 393 return []; 394 } 395 case "GET_PAGE_FAVICONS": { 396 const { pageUrls } = request; 397 return getPageFavicons(pageUrls); 398 } 399 case "OPEN_SCRIPT_IN_DEBUGGER": { 400 // This webchannel message type is added with version 5. 401 const { tabId, scriptUrl, line, column } = request; 402 const { openScriptInDebugger } = lazy.BrowserModule(); 403 return openScriptInDebugger(tabId, scriptUrl, line, column); 404 } 405 case "GET_JS_SOURCES": { 406 const { sourceUuids } = request; 407 if (!Array.isArray(sourceUuids)) { 408 throw new Error("sourceUuids must be an array"); 409 } 410 411 const infoForBrowser = infoForBrowserMap.get(browser); 412 if (infoForBrowser === undefined) { 413 throw new Error("No JS source data found for this tab"); 414 } 415 416 const jsSources = infoForBrowser.jsSources; 417 if (jsSources === null) { 418 return sourceUuids.map(() => ({ 419 error: "Source not found in the browser", 420 })); 421 } 422 423 return sourceUuids.map(uuid => { 424 const sourceText = jsSources[uuid]; 425 if (!sourceText) { 426 return { error: "Source not found in the browser" }; 427 } 428 429 return { sourceText }; 430 }); 431 } 432 default: { 433 console.error( 434 "An unknown message type was received by the profiler's WebChannel handler.", 435 request 436 ); 437 const { UnhandledCaseError } = lazy.Errors(); 438 throw new UnhandledCaseError(request, "WebChannel request"); 439 } 440 } 441 } 442 443 /** 444 * Get the symbolicationService for the capture that opened this browser's 445 * tab, or a fallback service for browsers from tabs opened by the user. 446 * 447 * @param {MockedExports.Browser} browser 448 * @return {SymbolicationService | null} 449 */ 450 function getSymbolicationServiceForBrowser(browser) { 451 // We try to serve symbolication requests that come from tabs that we 452 // opened when a profile was captured, and for tabs that the user opened 453 // independently, for example because the user wants to load an existing 454 // profile from a file. 455 const infoForBrowser = infoForBrowserMap.get(browser); 456 if (infoForBrowser !== undefined) { 457 // We opened this tab when a profile was captured. Use the symbolication 458 // service for that capture. 459 return infoForBrowser.symbolicationService; 460 } 461 462 // For the "foreign" tabs, we provide a fallback symbolication service so that 463 // we can find symbols for any libraries that are loaded in this process. This 464 // means that symbolication will work if the existing file has been captured 465 // from the same build. 466 const { createLocalSymbolicationService } = lazy.PerfSymbolication(); 467 return createLocalSymbolicationService( 468 Services.profiler.sharedLibraries, 469 lazy.PrefsPresets().getObjdirPrefValue() 470 ); 471 } 472 473 /** 474 * This handler handles any messages coming from the WebChannel from profiler.firefox.com. 475 * 476 * @param {ProfilerWebChannel} channel 477 * @param {string} id 478 * @param {any} message 479 * @param {MockedExports.WebChannelTarget} target 480 */ 481 export async function handleWebChannelMessage(channel, id, message, target) { 482 if (typeof message !== "object" || typeof message.type !== "string") { 483 console.error( 484 "An malformed message was received by the profiler's WebChannel handler.", 485 message 486 ); 487 return; 488 } 489 const messageFromFrontend = /** @type {MessageFromFrontend} */ (message); 490 const { requestId } = messageFromFrontend; 491 492 try { 493 const response = await getResponseForMessage( 494 messageFromFrontend, 495 target.browser 496 ); 497 channel.send( 498 { 499 type: "SUCCESS_RESPONSE", 500 requestId, 501 response, 502 }, 503 target 504 ); 505 } catch (error) { 506 let errorMessage; 507 if (error instanceof Error) { 508 errorMessage = `${error.name}: ${error.message}`; 509 } else { 510 errorMessage = `${error}`; 511 } 512 channel.send( 513 { 514 type: "ERROR_RESPONSE", 515 requestId, 516 error: errorMessage, 517 }, 518 target 519 ); 520 } 521 } 522 523 /** 524 * @param {MockedExports.Browser} browser - The tab's browser. 525 * @param {ProfileCaptureResult} profileCaptureResult - The Gecko profile. 526 * @param {SymbolicationService | null} symbolicationService - An object which implements the 527 * SymbolicationService interface, whose getSymbolTable method will be invoked 528 * when profiler.firefox.com sends GET_SYMBOL_TABLE WebChannel messages to us. This 529 * method should obtain a symbol table for the requested binary and resolve the 530 * returned promise with it. 531 * @param {JSSources | null} jsSources - JS sources from the profile collection. 532 */ 533 export function registerProfileCaptureForBrowser( 534 browser, 535 profileCaptureResult, 536 symbolicationService, 537 jsSources 538 ) { 539 infoForBrowserMap.set(browser, { 540 profileCaptureResult, 541 symbolicationService, 542 jsSources, 543 }); 544 } 545 546 /** 547 * Get page favicons data and return them. 548 * 549 * @param {Array<string>} pageUrls 550 * 551 * @returns {Promise<Array<ProfilerFaviconData | null>>} favicon data as binary array. 552 */ 553 async function getPageFavicons(pageUrls) { 554 if (!pageUrls || pageUrls.length === 0) { 555 // Return early if the pages are not provided. 556 return []; 557 } 558 559 // Get the data of favicons and return them. 560 const { favicons, toURI } = lazy.PlacesUtils(); 561 562 const promises = pageUrls.map(pageUrl => 563 favicons 564 .getFaviconForPage(toURI(pageUrl), /* preferredWidth = */ 32) 565 .then(favicon => { 566 // Check if data is found in the database and return it if so. 567 if (favicon.rawData.length) { 568 return { 569 // PlacesUtils returns a number array for the data. Converting it to 570 // the Uint8Array here to send it to the tab more efficiently. 571 data: new Uint8Array(favicon.rawData).buffer, 572 mimeType: favicon.mimeType, 573 }; 574 } 575 576 return null; 577 }) 578 .catch(() => { 579 // Couldn't find a favicon for this page, return null explicitly. 580 return null; 581 }) 582 ); 583 584 return Promise.all(promises); 585 }