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/. */
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.
13 // The following are not lazily loaded as they are needed during initialization.
15 import { createLazyLoaders } from "resource://devtools/client/performance-new/shared/typescript-lazy-load.sys.mjs";
17 // For some reason TypeScript was giving me an error when de-structuring AppConstants. I
18 // suspect a bug in TypeScript was at play.
19 const AppConstants = ChromeUtils.importESModule(
20 "resource://gre/modules/AppConstants.sys.mjs"
24 * @typedef {import("../@types/perf").RecordingSettings} RecordingSettings
25 * @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
26 * @typedef {import("../@types/perf").Library} Library
27 * @typedef {import("../@types/perf").PerformancePref} PerformancePref
28 * @typedef {import("../@types/perf").ProfilerWebChannel} ProfilerWebChannel
29 * @typedef {import("../@types/perf").PageContext} PageContext
30 * @typedef {import("../@types/perf").PrefObserver} PrefObserver
31 * @typedef {import("../@types/perf").PrefPostfix} PrefPostfix
32 * @typedef {import("../@types/perf").Presets} Presets
33 * @typedef {import("../@types/perf").ProfilerViewMode} ProfilerViewMode
34 * @typedef {import("../@types/perf").MessageFromFrontend} MessageFromFrontend
35 * @typedef {import("../@types/perf").RequestFromFrontend} RequestFromFrontend
36 * @typedef {import("../@types/perf").ResponseToFrontend} ResponseToFrontend
37 * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService
38 * @typedef {import("../@types/perf").ProfilerBrowserInfo} ProfilerBrowserInfo
39 * @typedef {import("../@types/perf").ProfileCaptureResult} ProfileCaptureResult
40 * @typedef {import("../@types/perf").ProfilerFaviconData} ProfilerFaviconData
43 /** @type {PerformancePref["Entries"]} */
44 const ENTRIES_PREF = "devtools.performance.recording.entries";
45 /** @type {PerformancePref["Interval"]} */
46 const INTERVAL_PREF = "devtools.performance.recording.interval";
47 /** @type {PerformancePref["Features"]} */
48 const FEATURES_PREF = "devtools.performance.recording.features";
49 /** @type {PerformancePref["Threads"]} */
50 const THREADS_PREF = "devtools.performance.recording.threads";
51 /** @type {PerformancePref["ObjDirs"]} */
52 const OBJDIRS_PREF = "devtools.performance.recording.objdirs";
53 /** @type {PerformancePref["Duration"]} */
54 const DURATION_PREF = "devtools.performance.recording.duration";
55 /** @type {PerformancePref["Preset"]} */
56 const PRESET_PREF = "devtools.performance.recording.preset";
57 /** @type {PerformancePref["PopupFeatureFlag"]} */
58 const POPUP_FEATURE_FLAG_PREF = "devtools.performance.popup.feature-flag";
59 /* This will be used to observe all profiler-related prefs. */
60 const PREF_PREFIX = "devtools.performance.recording.";
62 // The version of the profiler WebChannel.
63 // This is reported from the STATUS_QUERY message, and identifies the
64 // capabilities of the WebChannel. The front-end can handle old WebChannel
65 // versions and has a full list of versions and capabilities here:
66 // https://github.com/firefox-devtools/profiler/blob/main/src/app-logic/web-channel.js
67 const CURRENT_WEBCHANNEL_VERSION = 5;
69 const lazyRequire = {};
70 // eslint-disable-next-line mozilla/lazy-getter-object-name
71 ChromeUtils.defineESModuleGetters(lazyRequire, {
72 require: "resource://devtools/shared/loader/Loader.sys.mjs",
74 // Lazily load the require function, when it's needed.
75 // Avoid using ChromeUtils.defineESModuleGetters for now as:
76 // * we can't replace createLazyLoaders as we still load commonjs+jsm+esm
77 // It will be easier once we only load sys.mjs files.
78 // * we would need to find a way to accomodate typescript to this special function.
79 // @ts-ignore:next-line
80 function require(path) {
81 // @ts-ignore:next-line
82 return lazyRequire.require(path);
85 // The following utilities are lazily loaded as they are not needed when controlling the
86 // global state of the profiler, and only are used during specific funcationality like
87 // symbolication or capturing a profile.
88 const lazy = createLazyLoaders({
90 require("resource://devtools/client/performance-new/shared/utils.js"),
92 require("resource://devtools/client/performance-new/shared/browser.js"),
94 require("resource://devtools/shared/performance-new/recording-utils.js"),
96 ChromeUtils.importESModule("resource:///modules/CustomizableUI.sys.mjs"),
97 PerfSymbolication: () =>
98 ChromeUtils.importESModule(
99 "resource://devtools/client/performance-new/shared/symbolication.sys.mjs"
101 ProfilerMenuButton: () =>
102 ChromeUtils.importESModule(
103 "resource://devtools/client/performance-new/popup/menu-button.sys.mjs"
106 ChromeUtils.importESModule("resource://gre/modules/PlacesUtils.sys.mjs")
110 // The presets that we find in all interfaces are defined here.
112 // The property l10nIds contain all FTL l10n IDs for these cases:
113 // - properties in "popup" are used in the popup's select box.
114 // - properties in "devtools" are used in other UIs (about:profiling and devtools panels).
116 // Properties for both cases have the same values, but because they're not used
117 // in the same way we need to duplicate them.
118 // Their values for the en-US locale are in the files:
119 // devtools/client/locales/en-US/perftools.ftl
120 // browser/locales/en-US/browser/appmenu.ftl
122 // IMPORTANT NOTE: Please keep the existing profiler presets in sync with their
123 // Fenix counterparts and consider adding any new presets to Fenix:
124 // https://github.com/mozilla-mobile/firefox-android/blob/1d177e7e78d027e8ab32cedf0fc68316787d7454/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerUtils.kt
126 /** @type {Presets} */
127 export const presets = {
129 entries: 128 * 1024 * 1024,
131 features: ["screenshots", "js", "cpu", "memory"],
132 threads: ["GeckoMain", "Compositor", "Renderer", "DOM Worker"],
134 profilerViewMode: "active-tab",
137 label: "profiler-popup-presets-web-developer-label",
138 description: "profiler-popup-presets-web-developer-description",
141 label: "perftools-presets-web-developer-label",
142 description: "perftools-presets-web-developer-description",
146 "firefox-platform": {
147 entries: 128 * 1024 * 1024,
168 label: "profiler-popup-presets-firefox-label",
169 description: "profiler-popup-presets-firefox-description",
172 label: "perftools-presets-firefox-label",
173 description: "perftools-presets-firefox-description",
178 entries: 128 * 1024 * 1024,
180 features: ["stackwalk", "js", "cpu", "java", "processcpu", "memory"],
196 label: "profiler-popup-presets-graphics-label",
197 description: "profiler-popup-presets-graphics-description",
200 label: "perftools-presets-graphics-label",
201 description: "perftools-presets-graphics-description",
206 entries: 128 * 1024 * 1024,
212 "audiocallbacktracing",
220 "BackgroundThreadPool",
229 "InotifyEventThread",
232 "ModuleProcessThread",
245 label: "profiler-popup-presets-media-label",
246 description: "profiler-popup-presets-media-description2",
249 label: "perftools-presets-media-label",
250 description: "perftools-presets-media-description2",
255 entries: 128 * 1024 * 1024,
281 label: "profiler-popup-presets-networking-label",
282 description: "profiler-popup-presets-networking-description",
285 label: "perftools-presets-networking-label",
286 description: "perftools-presets-networking-description",
291 entries: 128 * 1024 * 1024,
306 threads: ["GeckoMain", "Renderer"],
310 label: "profiler-popup-presets-power-label",
311 description: "profiler-popup-presets-power-description",
314 label: "perftools-presets-power-label",
315 description: "perftools-presets-power-description",
320 entries: 128 * 1024 * 1024,
328 "samplingallthreads",
330 "unregisteredthreads",
336 label: "profiler-popup-presets-debug-label",
337 description: "profiler-popup-presets-debug-description",
340 label: "perftools-presets-debug-label",
341 description: "perftools-presets-debug-description",
348 * Return the proper view mode for the Firefox Profiler front-end timeline by
349 * looking at the proper preset that is selected.
350 * Return value can be undefined when the preset is unknown or custom.
351 * @param {PageContext} pageContext
352 * @return {ProfilerViewMode | undefined}
354 export function getProfilerViewModeForCurrentPreset(pageContext) {
355 const prefPostfix = getPrefPostfix(pageContext);
356 const presetName = Services.prefs.getCharPref(PRESET_PREF + prefPostfix);
358 if (presetName === "custom") {
362 const preset = presets[presetName];
364 console.error(`Unknown profiler preset was encountered: "${presetName}"`);
367 return preset.profilerViewMode;
371 * This function is called when the profile is captured with the shortcut
372 * keys, with the profiler toolbarbutton, or with the button inside the
374 * @param {PageContext} pageContext
375 * @return {Promise<void>}
377 export async function captureProfile(pageContext) {
378 if (!Services.profiler.IsActive()) {
379 // The profiler is not active, ignore.
382 if (Services.profiler.IsPaused()) {
383 // The profiler is already paused for capture, ignore.
387 // Pause profiler before we collect the profile, so that we don't capture
388 // more samples while the parent process waits for subprocess profiles.
389 Services.profiler.Pause();
392 * @type {MockedExports.ProfileGenerationAdditionalInformation | undefined}
396 * @type {ProfileCaptureResult}
398 const profileCaptureResult = await Services.profiler
399 .getProfileDataAsGzippedArrayBuffer()
401 ({ profile, additionalInformation }) => {
402 additionalInfo = additionalInformation;
403 return { type: "SUCCESS", profile };
406 console.error(error);
407 return { type: "ERROR", error };
411 const profilerViewMode = getProfilerViewModeForCurrentPreset(pageContext);
412 const sharedLibraries = additionalInfo?.sharedLibraries
413 ? additionalInfo.sharedLibraries
414 : Services.profiler.sharedLibraries;
415 const objdirs = getObjdirPrefValue();
417 const { createLocalSymbolicationService } = lazy.PerfSymbolication();
418 const symbolicationService = createLocalSymbolicationService(
423 const { openProfilerTab } = lazy.BrowserModule();
424 const browser = await openProfilerTab({ profilerViewMode });
425 registerProfileCaptureForBrowser(
427 profileCaptureResult,
431 Services.profiler.StopProfiler();
435 * This function is called when the profiler is started with the shortcut
436 * keys, with the profiler toolbarbutton, or with the button inside the
438 * @param {PageContext} pageContext
440 export function startProfiler(pageContext) {
441 const { entries, interval, features, threads, duration } =
442 getRecordingSettings(pageContext, Services.profiler.GetFeatures());
444 // Get the active Browser ID from browser.
445 const { getActiveBrowserID } = lazy.RecordingUtils();
446 const activeTabID = getActiveBrowserID();
448 Services.profiler.StartProfiler(
459 * This function is called directly by devtools/startup/DevToolsStartup.jsm when
460 * using the shortcut keys to capture a profile.
463 export function stopProfiler() {
464 Services.profiler.StopProfiler();
468 * This function is called directly by devtools/startup/DevToolsStartup.jsm when
469 * using the shortcut keys to start and stop the profiler.
470 * @param {PageContext} pageContext
473 export function toggleProfiler(pageContext) {
474 if (Services.profiler.IsPaused()) {
475 // The profiler is currently paused, which means that the user is already
476 // attempting to capture a profile. Ignore this request.
479 if (Services.profiler.IsActive()) {
482 startProfiler(pageContext);
487 * @param {PageContext} pageContext
489 export function restartProfiler(pageContext) {
491 startProfiler(pageContext);
495 * @param {string} prefName
498 function _getArrayOfStringsPref(prefName) {
499 const text = Services.prefs.getCharPref(prefName);
500 return JSON.parse(text);
504 * The profiler recording workflow uses two different pref paths. One set of prefs
505 * is stored for local profiling, and another for remote profiling. This function
506 * decides which to use. The remote prefs have ".remote" appended to the end of
509 * @param {PageContext} pageContext
510 * @returns {PrefPostfix}
512 function getPrefPostfix(pageContext) {
513 switch (pageContext) {
515 case "aboutprofiling":
517 // Don't use any postfix on the prefs.
519 case "devtools-remote":
520 case "aboutprofiling-remote":
523 const { UnhandledCaseError } = lazy.Utils();
524 throw new UnhandledCaseError(pageContext, "Page Context");
530 * @param {string[]} objdirs
532 function setObjdirPrefValue(objdirs) {
533 Services.prefs.setCharPref(OBJDIRS_PREF, JSON.stringify(objdirs));
537 * Before Firefox 92, the objdir lists for local and remote profiling were
538 * stored in separate lists. In Firefox 92 those two prefs were merged into
539 * one. This function performs the migration.
541 function migrateObjdirsPrefsIfNeeded() {
542 const OLD_REMOTE_OBJDIRS_PREF = OBJDIRS_PREF + ".remote";
543 const remoteString = Services.prefs.getCharPref(OLD_REMOTE_OBJDIRS_PREF, "");
544 if (remoteString === "") {
545 // No migration necessary.
549 const remoteList = JSON.parse(remoteString);
550 const localList = _getArrayOfStringsPref(OBJDIRS_PREF);
552 // Merge the two lists, eliminating any duplicates.
553 const mergedList = [...new Set(localList.concat(remoteList))];
554 setObjdirPrefValue(mergedList);
555 Services.prefs.clearUserPref(OLD_REMOTE_OBJDIRS_PREF);
559 * @returns {string[]}
561 function getObjdirPrefValue() {
562 migrateObjdirsPrefsIfNeeded();
563 return _getArrayOfStringsPref(OBJDIRS_PREF);
567 * @param {PageContext} pageContext
568 * @param {string[]} supportedFeatures
569 * @returns {RecordingSettings}
571 export function getRecordingSettings(pageContext, supportedFeatures) {
572 const objdirs = getObjdirPrefValue();
573 const prefPostfix = getPrefPostfix(pageContext);
574 const presetName = Services.prefs.getCharPref(PRESET_PREF + prefPostfix);
576 // First try to get the values from a preset. If the preset is "custom" or
577 // unrecognized, getRecordingSettingsFromPreset will return null and we will
578 // get the settings from individual prefs instead.
580 getRecordingSettingsFromPreset(presetName, supportedFeatures, objdirs) ??
581 getRecordingSettingsFromPrefs(supportedFeatures, objdirs, prefPostfix)
586 * @param {string} presetName
587 * @param {string[]} supportedFeatures
588 * @param {string[]} objdirs
589 * @return {RecordingSettings | null}
591 function getRecordingSettingsFromPreset(
596 if (presetName === "custom") {
600 const preset = presets[presetName];
602 console.error(`Unknown profiler preset was encountered: "${presetName}"`);
608 entries: preset.entries,
609 interval: preset.interval,
610 // Validate the features before passing them to the profiler.
611 features: preset.features.filter(feature =>
612 supportedFeatures.includes(feature)
614 threads: preset.threads,
616 duration: preset.duration,
621 * @param {string[]} supportedFeatures
622 * @param {string[]} objdirs
623 * @param {PrefPostfix} prefPostfix
624 * @return {RecordingSettings}
626 function getRecordingSettingsFromPrefs(
631 // If you add a new preference here, please do not forget to update
632 // `revertRecordingSettings` as well.
634 const entries = Services.prefs.getIntPref(ENTRIES_PREF + prefPostfix);
635 const intervalInMicroseconds = Services.prefs.getIntPref(
636 INTERVAL_PREF + prefPostfix
638 const interval = intervalInMicroseconds / 1000;
639 const features = _getArrayOfStringsPref(FEATURES_PREF + prefPostfix);
640 const threads = _getArrayOfStringsPref(THREADS_PREF + prefPostfix);
641 const duration = Services.prefs.getIntPref(DURATION_PREF + prefPostfix);
644 presetName: "custom",
647 // Validate the features before passing them to the profiler.
648 features: features.filter(feature => supportedFeatures.includes(feature)),
656 * @param {PageContext} pageContext
657 * @param {RecordingSettings} prefs
659 export function setRecordingSettings(pageContext, prefs) {
660 const prefPostfix = getPrefPostfix(pageContext);
661 Services.prefs.setCharPref(PRESET_PREF + prefPostfix, prefs.presetName);
662 Services.prefs.setIntPref(ENTRIES_PREF + prefPostfix, prefs.entries);
663 // The interval pref stores the value in microseconds for extra precision.
664 const intervalInMicroseconds = prefs.interval * 1000;
665 Services.prefs.setIntPref(
666 INTERVAL_PREF + prefPostfix,
667 intervalInMicroseconds
669 Services.prefs.setCharPref(
670 FEATURES_PREF + prefPostfix,
671 JSON.stringify(prefs.features)
673 Services.prefs.setCharPref(
674 THREADS_PREF + prefPostfix,
675 JSON.stringify(prefs.threads)
677 setObjdirPrefValue(prefs.objdirs);
680 export const platform = AppConstants.platform;
683 * Revert the recording prefs for both local and remote profiling.
686 export function revertRecordingSettings() {
687 for (const prefPostfix of ["", ".remote"]) {
688 Services.prefs.clearUserPref(PRESET_PREF + prefPostfix);
689 Services.prefs.clearUserPref(ENTRIES_PREF + prefPostfix);
690 Services.prefs.clearUserPref(INTERVAL_PREF + prefPostfix);
691 Services.prefs.clearUserPref(FEATURES_PREF + prefPostfix);
692 Services.prefs.clearUserPref(THREADS_PREF + prefPostfix);
693 Services.prefs.clearUserPref(DURATION_PREF + prefPostfix);
695 Services.prefs.clearUserPref(OBJDIRS_PREF);
696 Services.prefs.clearUserPref(POPUP_FEATURE_FLAG_PREF);
700 * Change the prefs based on a preset. This mechanism is used by the popup to
701 * easily switch between different settings.
702 * @param {string} presetName
703 * @param {PageContext} pageContext
704 * @param {string[]} supportedFeatures
707 export function changePreset(pageContext, presetName, supportedFeatures) {
708 const prefPostfix = getPrefPostfix(pageContext);
709 const objdirs = getObjdirPrefValue();
710 let recordingSettings = getRecordingSettingsFromPreset(
716 if (!recordingSettings) {
717 // No recordingSettings were found for that preset. Most likely this means this
718 // is a custom preset, or it's one that we dont recognize for some reason.
719 // Get the preferences from the individual preference values.
720 Services.prefs.setCharPref(PRESET_PREF + prefPostfix, presetName);
721 recordingSettings = getRecordingSettings(pageContext, supportedFeatures);
724 setRecordingSettings(pageContext, recordingSettings);
728 * Add an observer for the profiler-related preferences.
729 * @param {PrefObserver} observer
732 export function addPrefObserver(observer) {
733 Services.prefs.addObserver(PREF_PREFIX, observer);
737 * Removes an observer for the profiler-related preferences.
738 * @param {PrefObserver} observer
741 export function removePrefObserver(observer) {
742 Services.prefs.removeObserver(PREF_PREFIX, observer);
746 * This map stores information that is associated with a "profile capturing"
747 * action, so that we can look up this information for WebChannel messages
748 * from the profiler tab.
749 * Most importantly, this stores the captured profile. When the profiler tab
750 * requests the profile, we can respond to the message with the correct profile.
751 * This works even if the request happens long after the tab opened. It also
752 * works for an "old" tab even if new profiles have been captured since that
754 * Supporting tab refresh is important because the tab sometimes reloads itself:
755 * If an old version of the front-end is cached in the service worker, and the
756 * browser supplies a profile with a newer format version, then the front-end
757 * updates its service worker and reloads itself, so that the updated version
758 * can parse the profile.
760 * This is a WeakMap so that the profile can be garbage-collected when the tab
763 * @type {WeakMap<MockedExports.Browser, ProfilerBrowserInfo>}
765 const infoForBrowserMap = new WeakMap();
768 * This handler computes the response for any messages coming
769 * from the WebChannel from profiler.firefox.com.
771 * @param {RequestFromFrontend} request
772 * @param {MockedExports.Browser} browser - The tab's browser.
773 * @return {Promise<ResponseToFrontend>}
775 async function getResponseForMessage(request, browser) {
776 switch (request.type) {
777 case "STATUS_QUERY": {
778 // The content page wants to know if this channel exists. It does, so respond
780 const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
782 version: CURRENT_WEBCHANNEL_VERSION,
783 menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(),
786 case "ENABLE_MENU_BUTTON": {
787 const { ownerDocument } = browser;
788 if (!ownerDocument) {
790 "Could not find the owner document for the current browser while enabling " +
791 "the profiler menu button"
794 // Ensure the widget is enabled.
795 Services.prefs.setBoolPref(POPUP_FEATURE_FLAG_PREF, true);
797 // Force the preset to be "firefox-platform" if we enable the menu button
798 // via web channel. If user goes through profiler.firefox.com to enable
799 // it, it means that either user is a platform developer or filing a bug
800 // report for performance engineers to look at.
801 const supportedFeatures = Services.profiler.GetFeatures();
802 changePreset("aboutprofiling", "firefox-platform", supportedFeatures);
804 // Enable the profiler menu button.
805 const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
806 ProfilerMenuButton.addToNavbar();
808 // Dispatch the change event manually, so that the shortcuts will also be
810 const { CustomizableUI } = lazy.CustomizableUI();
811 CustomizableUI.dispatchToolboxEvent("customizationchange");
813 // Open the popup with a message.
814 ProfilerMenuButton.openPopup(ownerDocument);
816 // There is no response data for this message.
819 case "GET_PROFILE": {
820 const infoForBrowser = infoForBrowserMap.get(browser);
821 if (infoForBrowser === undefined) {
822 throw new Error("Could not find a profile for this tab.");
824 const { profileCaptureResult } = infoForBrowser;
825 switch (profileCaptureResult.type) {
827 return profileCaptureResult.profile;
829 throw profileCaptureResult.error;
831 const { UnhandledCaseError } = lazy.Utils();
832 throw new UnhandledCaseError(
833 profileCaptureResult,
834 "profileCaptureResult"
839 case "GET_SYMBOL_TABLE": {
840 const { debugName, breakpadId } = request;
841 const symbolicationService = getSymbolicationServiceForBrowser(browser);
842 return symbolicationService.getSymbolTable(debugName, breakpadId);
844 case "QUERY_SYMBOLICATION_API": {
845 const { path, requestJson } = request;
846 const symbolicationService = getSymbolicationServiceForBrowser(browser);
847 return symbolicationService.querySymbolicationApi(path, requestJson);
849 case "GET_EXTERNAL_POWER_TRACKS": {
850 const { startTime, endTime } = request;
851 const externalPowerUrl = Services.prefs.getCharPref(
852 "devtools.performance.recording.power.external-url",
855 if (externalPowerUrl) {
856 const response = await fetch(
857 `${externalPowerUrl}?start=${startTime}&end=${endTime}`
859 return response.json();
863 case "GET_EXTERNAL_MARKERS": {
864 const { startTime, endTime } = request;
865 const externalMarkersUrl = Services.prefs.getCharPref(
866 "devtools.performance.recording.markers.external-url",
869 if (externalMarkersUrl) {
870 const response = await fetch(
871 `${externalMarkersUrl}?start=${startTime}&end=${endTime}`
873 return response.json();
877 case "GET_PAGE_FAVICONS": {
878 const { pageUrls } = request;
879 return getPageFavicons(pageUrls);
881 case "OPEN_SCRIPT_IN_DEBUGGER": {
882 // This webchannel message type is added with version 5.
883 const { tabId, scriptUrl, line, column } = request;
884 const { openScriptInDebugger } = lazy.BrowserModule();
885 return openScriptInDebugger(tabId, scriptUrl, line, column);
890 "An unknown message type was received by the profiler's WebChannel handler.",
893 const { UnhandledCaseError } = lazy.Utils();
894 throw new UnhandledCaseError(request, "WebChannel request");
900 * Get the symbolicationService for the capture that opened this browser's
901 * tab, or a fallback service for browsers from tabs opened by the user.
903 * @param {MockedExports.Browser} browser
904 * @return {SymbolicationService}
906 function getSymbolicationServiceForBrowser(browser) {
907 // We try to serve symbolication requests that come from tabs that we
908 // opened when a profile was captured, and for tabs that the user opened
909 // independently, for example because the user wants to load an existing
910 // profile from a file.
911 const infoForBrowser = infoForBrowserMap.get(browser);
912 if (infoForBrowser !== undefined) {
913 // We opened this tab when a profile was captured. Use the symbolication
914 // service for that capture.
915 return infoForBrowser.symbolicationService;
918 // For the "foreign" tabs, we provide a fallback symbolication service so that
919 // we can find symbols for any libraries that are loaded in this process. This
920 // means that symbolication will work if the existing file has been captured
921 // from the same build.
922 const { createLocalSymbolicationService } = lazy.PerfSymbolication();
923 return createLocalSymbolicationService(
924 Services.profiler.sharedLibraries,
930 * This handler handles any messages coming from the WebChannel from profiler.firefox.com.
932 * @param {ProfilerWebChannel} channel
934 * @param {any} message
935 * @param {MockedExports.WebChannelTarget} target
937 export async function handleWebChannelMessage(channel, id, message, target) {
938 if (typeof message !== "object" || typeof message.type !== "string") {
940 "An malformed message was received by the profiler's WebChannel handler.",
945 const messageFromFrontend = /** @type {MessageFromFrontend} */ (message);
946 const { requestId } = messageFromFrontend;
949 const response = await getResponseForMessage(
955 type: "SUCCESS_RESPONSE",
963 if (error instanceof Error) {
964 errorMessage = `${error.name}: ${error.message}`;
966 errorMessage = `${error}`;
970 type: "ERROR_RESPONSE",
980 * @param {MockedExports.Browser} browser - The tab's browser.
981 * @param {ProfileCaptureResult} profileCaptureResult - The Gecko profile.
982 * @param {SymbolicationService} symbolicationService - An object which implements the
983 * SymbolicationService interface, whose getSymbolTable method will be invoked
984 * when profiler.firefox.com sends GET_SYMBOL_TABLE WebChannel messages to us. This
985 * method should obtain a symbol table for the requested binary and resolve the
986 * returned promise with it.
988 export function registerProfileCaptureForBrowser(
990 profileCaptureResult,
993 infoForBrowserMap.set(browser, {
994 profileCaptureResult,
995 symbolicationService,
1000 * Get page favicons data and return them.
1002 * @param {Array<string>} pageUrls
1004 * @returns {Promise<Array<ProfilerFaviconData | null>>} favicon data as binary array.
1006 async function getPageFavicons(pageUrls) {
1007 if (!pageUrls || pageUrls.length === 0) {
1008 // Return early if the pages are not provided.
1012 // Get the data of favicons and return them.
1013 const { promiseFaviconData } = lazy.PlacesUtils();
1015 const promises = pageUrls.map(pageUrl =>
1016 promiseFaviconData(pageUrl, /* preferredWidth = */ 32)
1018 // Check if data is found in the database and return it if so.
1019 if (favicon.dataLen > 0 && favicon.data) {
1021 // PlacesUtils returns a number array for the data. Converting it to
1022 // the Uint8Array here to send it to the tab more efficiently.
1023 data: new Uint8Array(favicon.data).buffer,
1024 mimeType: favicon.mimeType,
1031 // Couldn't find a favicon for this page, return null explicitly.
1036 return Promise.all(promises);