Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / devtools / client / performance-new / shared / background.sys.mjs
blob0e270c35cf67edf89930284b52933c074819177a
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
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  */
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"
21 ).AppConstants;
23 /**
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
41  */
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",
73 });
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({
89   Utils: () =>
90     require("resource://devtools/client/performance-new/shared/utils.js"),
91   BrowserModule: () =>
92     require("resource://devtools/client/performance-new/shared/browser.js"),
93   RecordingUtils: () =>
94     require("resource://devtools/shared/performance-new/recording-utils.js"),
95   CustomizableUI: () =>
96     ChromeUtils.importESModule("resource:///modules/CustomizableUI.sys.mjs"),
97   PerfSymbolication: () =>
98     ChromeUtils.importESModule(
99       "resource://devtools/client/performance-new/shared/symbolication.sys.mjs"
100     ),
101   ProfilerMenuButton: () =>
102     ChromeUtils.importESModule(
103       "resource://devtools/client/performance-new/popup/menu-button.sys.mjs"
104     ),
105   PlacesUtils: () =>
106     ChromeUtils.importESModule("resource://gre/modules/PlacesUtils.sys.mjs")
107       .PlacesUtils,
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 = {
128   "web-developer": {
129     entries: 128 * 1024 * 1024,
130     interval: 1,
131     features: ["screenshots", "js", "cpu", "memory"],
132     threads: ["GeckoMain", "Compositor", "Renderer", "DOM Worker"],
133     duration: 0,
134     profilerViewMode: "active-tab",
135     l10nIds: {
136       popup: {
137         label: "profiler-popup-presets-web-developer-label",
138         description: "profiler-popup-presets-web-developer-description",
139       },
140       devtools: {
141         label: "perftools-presets-web-developer-label",
142         description: "perftools-presets-web-developer-description",
143       },
144     },
145   },
146   "firefox-platform": {
147     entries: 128 * 1024 * 1024,
148     interval: 1,
149     features: [
150       "screenshots",
151       "js",
152       "stackwalk",
153       "cpu",
154       "java",
155       "processcpu",
156       "memory",
157     ],
158     threads: [
159       "GeckoMain",
160       "Compositor",
161       "Renderer",
162       "SwComposite",
163       "DOM Worker",
164     ],
165     duration: 0,
166     l10nIds: {
167       popup: {
168         label: "profiler-popup-presets-firefox-label",
169         description: "profiler-popup-presets-firefox-description",
170       },
171       devtools: {
172         label: "perftools-presets-firefox-label",
173         description: "perftools-presets-firefox-description",
174       },
175     },
176   },
177   graphics: {
178     entries: 128 * 1024 * 1024,
179     interval: 1,
180     features: ["stackwalk", "js", "cpu", "java", "processcpu", "memory"],
181     threads: [
182       "GeckoMain",
183       "Compositor",
184       "Renderer",
185       "SwComposite",
186       "RenderBackend",
187       "GlyphRasterizer",
188       "SceneBuilder",
189       "WrWorker",
190       "CanvasWorkers",
191       "TextureUpdate",
192     ],
193     duration: 0,
194     l10nIds: {
195       popup: {
196         label: "profiler-popup-presets-graphics-label",
197         description: "profiler-popup-presets-graphics-description",
198       },
199       devtools: {
200         label: "perftools-presets-graphics-label",
201         description: "perftools-presets-graphics-description",
202       },
203     },
204   },
205   media: {
206     entries: 128 * 1024 * 1024,
207     interval: 1,
208     features: [
209       "js",
210       "stackwalk",
211       "cpu",
212       "audiocallbacktracing",
213       "ipcmessages",
214       "processcpu",
215       "memory",
216     ],
217     threads: [
218       "cubeb",
219       "audio",
220       "BackgroundThreadPool",
221       "camera",
222       "capture",
223       "Compositor",
224       "decoder",
225       "GeckoMain",
226       "gmp",
227       "graph",
228       "grph",
229       "InotifyEventThread",
230       "IPDL Background",
231       "media",
232       "ModuleProcessThread",
233       "PacerThread",
234       "RemVidChild",
235       "RenderBackend",
236       "Renderer",
237       "Socket Thread",
238       "SwComposite",
239       "webrtc",
240       "TextureUpdate",
241     ],
242     duration: 0,
243     l10nIds: {
244       popup: {
245         label: "profiler-popup-presets-media-label",
246         description: "profiler-popup-presets-media-description2",
247       },
248       devtools: {
249         label: "perftools-presets-media-label",
250         description: "perftools-presets-media-description2",
251       },
252     },
253   },
254   networking: {
255     entries: 128 * 1024 * 1024,
256     interval: 1,
257     features: [
258       "screenshots",
259       "js",
260       "stackwalk",
261       "cpu",
262       "java",
263       "processcpu",
264       "bandwidth",
265       "memory",
266     ],
267     threads: [
268       "Compositor",
269       "DNS Resolver",
270       "DOM Worker",
271       "GeckoMain",
272       "Renderer",
273       "Socket Thread",
274       "StreamTrans",
275       "SwComposite",
276       "TRR Background",
277     ],
278     duration: 0,
279     l10nIds: {
280       popup: {
281         label: "profiler-popup-presets-networking-label",
282         description: "profiler-popup-presets-networking-description",
283       },
284       devtools: {
285         label: "perftools-presets-networking-label",
286         description: "perftools-presets-networking-description",
287       },
288     },
289   },
290   power: {
291     entries: 128 * 1024 * 1024,
292     interval: 10,
293     features: [
294       "screenshots",
295       "js",
296       "stackwalk",
297       "cpu",
298       "processcpu",
299       "nostacksampling",
300       "ipcmessages",
301       "markersallthreads",
302       "power",
303       "bandwidth",
304       "memory",
305     ],
306     threads: ["GeckoMain", "Renderer"],
307     duration: 0,
308     l10nIds: {
309       popup: {
310         label: "profiler-popup-presets-power-label",
311         description: "profiler-popup-presets-power-description",
312       },
313       devtools: {
314         label: "perftools-presets-power-label",
315         description: "perftools-presets-power-description",
316       },
317     },
318   },
319   debug: {
320     entries: 128 * 1024 * 1024,
321     interval: 1,
322     features: [
323       "cpu",
324       "ipcmessages",
325       "js",
326       "markersallthreads",
327       "processcpu",
328       "samplingallthreads",
329       "stackwalk",
330       "unregisteredthreads",
331     ],
332     threads: ["*"],
333     duration: 0,
334     l10nIds: {
335       popup: {
336         label: "profiler-popup-presets-debug-label",
337         description: "profiler-popup-presets-debug-description",
338       },
339       devtools: {
340         label: "perftools-presets-debug-label",
341         description: "perftools-presets-debug-description",
342       },
343     },
344   },
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}
353  */
354 export function getProfilerViewModeForCurrentPreset(pageContext) {
355   const prefPostfix = getPrefPostfix(pageContext);
356   const presetName = Services.prefs.getCharPref(PRESET_PREF + prefPostfix);
358   if (presetName === "custom") {
359     return undefined;
360   }
362   const preset = presets[presetName];
363   if (!preset) {
364     console.error(`Unknown profiler preset was encountered: "${presetName}"`);
365     return undefined;
366   }
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
373  * popup.
374  * @param {PageContext} pageContext
375  * @return {Promise<void>}
376  */
377 export async function captureProfile(pageContext) {
378   if (!Services.profiler.IsActive()) {
379     // The profiler is not active, ignore.
380     return;
381   }
382   if (Services.profiler.IsPaused()) {
383     // The profiler is already paused for capture, ignore.
384     return;
385   }
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();
391   /**
392    * @type {MockedExports.ProfileGenerationAdditionalInformation | undefined}
393    */
394   let additionalInfo;
395   /**
396    * @type {ProfileCaptureResult}
397    */
398   const profileCaptureResult = await Services.profiler
399     .getProfileDataAsGzippedArrayBuffer()
400     .then(
401       ({ profile, additionalInformation }) => {
402         additionalInfo = additionalInformation;
403         return { type: "SUCCESS", profile };
404       },
405       error => {
406         console.error(error);
407         return { type: "ERROR", error };
408       }
409     );
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(
419     sharedLibraries,
420     objdirs
421   );
423   const { openProfilerTab } = lazy.BrowserModule();
424   const browser = await openProfilerTab({ profilerViewMode });
425   registerProfileCaptureForBrowser(
426     browser,
427     profileCaptureResult,
428     symbolicationService
429   );
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
437  * popup.
438  * @param {PageContext} pageContext
439  */
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(
449     entries,
450     interval,
451     features,
452     threads,
453     activeTabID,
454     duration
455   );
459  * This function is called directly by devtools/startup/DevToolsStartup.jsm when
460  * using the shortcut keys to capture a profile.
461  * @type {() => void}
462  */
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
471  * @return {void}
472  */
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.
477     return;
478   }
479   if (Services.profiler.IsActive()) {
480     stopProfiler();
481   } else {
482     startProfiler(pageContext);
483   }
487  * @param {PageContext} pageContext
488  */
489 export function restartProfiler(pageContext) {
490   stopProfiler();
491   startProfiler(pageContext);
495  * @param {string} prefName
496  * @return {string[]}
497  */
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
507  * their pref names.
509  * @param {PageContext} pageContext
510  * @returns {PrefPostfix}
511  */
512 function getPrefPostfix(pageContext) {
513   switch (pageContext) {
514     case "devtools":
515     case "aboutprofiling":
516     case "aboutlogging":
517       // Don't use any postfix on the prefs.
518       return "";
519     case "devtools-remote":
520     case "aboutprofiling-remote":
521       return ".remote";
522     default: {
523       const { UnhandledCaseError } = lazy.Utils();
524       throw new UnhandledCaseError(pageContext, "Page Context");
525     }
526   }
530  * @param {string[]} objdirs
531  */
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.
540  */
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.
546     return;
547   }
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[]}
560  */
561 function getObjdirPrefValue() {
562   migrateObjdirsPrefsIfNeeded();
563   return _getArrayOfStringsPref(OBJDIRS_PREF);
567  * @param {PageContext} pageContext
568  * @param {string[]} supportedFeatures
569  * @returns {RecordingSettings}
570  */
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.
579   return (
580     getRecordingSettingsFromPreset(presetName, supportedFeatures, objdirs) ??
581     getRecordingSettingsFromPrefs(supportedFeatures, objdirs, prefPostfix)
582   );
586  * @param {string} presetName
587  * @param {string[]} supportedFeatures
588  * @param {string[]} objdirs
589  * @return {RecordingSettings | null}
590  */
591 function getRecordingSettingsFromPreset(
592   presetName,
593   supportedFeatures,
594   objdirs
595 ) {
596   if (presetName === "custom") {
597     return null;
598   }
600   const preset = presets[presetName];
601   if (!preset) {
602     console.error(`Unknown profiler preset was encountered: "${presetName}"`);
603     return null;
604   }
606   return {
607     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)
613     ),
614     threads: preset.threads,
615     objdirs,
616     duration: preset.duration,
617   };
621  * @param {string[]} supportedFeatures
622  * @param {string[]} objdirs
623  * @param {PrefPostfix} prefPostfix
624  * @return {RecordingSettings}
625  */
626 function getRecordingSettingsFromPrefs(
627   supportedFeatures,
628   objdirs,
629   prefPostfix
630 ) {
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
637   );
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);
643   return {
644     presetName: "custom",
645     entries,
646     interval,
647     // Validate the features before passing them to the profiler.
648     features: features.filter(feature => supportedFeatures.includes(feature)),
649     threads,
650     objdirs,
651     duration,
652   };
656  * @param {PageContext} pageContext
657  * @param {RecordingSettings} prefs
658  */
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
668   );
669   Services.prefs.setCharPref(
670     FEATURES_PREF + prefPostfix,
671     JSON.stringify(prefs.features)
672   );
673   Services.prefs.setCharPref(
674     THREADS_PREF + prefPostfix,
675     JSON.stringify(prefs.threads)
676   );
677   setObjdirPrefValue(prefs.objdirs);
680 export const platform = AppConstants.platform;
683  * Revert the recording prefs for both local and remote profiling.
684  * @return {void}
685  */
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);
694   }
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
705  * @return {void}
706  */
707 export function changePreset(pageContext, presetName, supportedFeatures) {
708   const prefPostfix = getPrefPostfix(pageContext);
709   const objdirs = getObjdirPrefValue();
710   let recordingSettings = getRecordingSettingsFromPreset(
711     presetName,
712     supportedFeatures,
713     objdirs
714   );
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);
722   }
724   setRecordingSettings(pageContext, recordingSettings);
728  * Add an observer for the profiler-related preferences.
729  * @param {PrefObserver} observer
730  * @return {void}
731  */
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
739  * @return {void}
740  */
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
753  * tab was opened.
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
761  * is closed.
763  * @type {WeakMap<MockedExports.Browser, ProfilerBrowserInfo>}
764  */
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>}
774  */
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
779       // back to the ping.
780       const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
781       return {
782         version: CURRENT_WEBCHANNEL_VERSION,
783         menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(),
784       };
785     }
786     case "ENABLE_MENU_BUTTON": {
787       const { ownerDocument } = browser;
788       if (!ownerDocument) {
789         throw new Error(
790           "Could not find the owner document for the current browser while enabling " +
791             "the profiler menu button"
792         );
793       }
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
809       // added.
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.
817       return undefined;
818     }
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.");
823       }
824       const { profileCaptureResult } = infoForBrowser;
825       switch (profileCaptureResult.type) {
826         case "SUCCESS":
827           return profileCaptureResult.profile;
828         case "ERROR":
829           throw profileCaptureResult.error;
830         default: {
831           const { UnhandledCaseError } = lazy.Utils();
832           throw new UnhandledCaseError(
833             profileCaptureResult,
834             "profileCaptureResult"
835           );
836         }
837       }
838     }
839     case "GET_SYMBOL_TABLE": {
840       const { debugName, breakpadId } = request;
841       const symbolicationService = getSymbolicationServiceForBrowser(browser);
842       return symbolicationService.getSymbolTable(debugName, breakpadId);
843     }
844     case "QUERY_SYMBOLICATION_API": {
845       const { path, requestJson } = request;
846       const symbolicationService = getSymbolicationServiceForBrowser(browser);
847       return symbolicationService.querySymbolicationApi(path, requestJson);
848     }
849     case "GET_EXTERNAL_POWER_TRACKS": {
850       const { startTime, endTime } = request;
851       const externalPowerUrl = Services.prefs.getCharPref(
852         "devtools.performance.recording.power.external-url",
853         ""
854       );
855       if (externalPowerUrl) {
856         const response = await fetch(
857           `${externalPowerUrl}?start=${startTime}&end=${endTime}`
858         );
859         return response.json();
860       }
861       return [];
862     }
863     case "GET_EXTERNAL_MARKERS": {
864       const { startTime, endTime } = request;
865       const externalMarkersUrl = Services.prefs.getCharPref(
866         "devtools.performance.recording.markers.external-url",
867         ""
868       );
869       if (externalMarkersUrl) {
870         const response = await fetch(
871           `${externalMarkersUrl}?start=${startTime}&end=${endTime}`
872         );
873         return response.json();
874       }
875       return [];
876     }
877     case "GET_PAGE_FAVICONS": {
878       const { pageUrls } = request;
879       return getPageFavicons(pageUrls);
880     }
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);
886     }
888     default: {
889       console.error(
890         "An unknown message type was received by the profiler's WebChannel handler.",
891         request
892       );
893       const { UnhandledCaseError } = lazy.Utils();
894       throw new UnhandledCaseError(request, "WebChannel request");
895     }
896   }
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}
905  */
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;
916   }
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,
925     getObjdirPrefValue()
926   );
930  * This handler handles any messages coming from the WebChannel from profiler.firefox.com.
932  * @param {ProfilerWebChannel} channel
933  * @param {string} id
934  * @param {any} message
935  * @param {MockedExports.WebChannelTarget} target
936  */
937 export async function handleWebChannelMessage(channel, id, message, target) {
938   if (typeof message !== "object" || typeof message.type !== "string") {
939     console.error(
940       "An malformed message was received by the profiler's WebChannel handler.",
941       message
942     );
943     return;
944   }
945   const messageFromFrontend = /** @type {MessageFromFrontend} */ (message);
946   const { requestId } = messageFromFrontend;
948   try {
949     const response = await getResponseForMessage(
950       messageFromFrontend,
951       target.browser
952     );
953     channel.send(
954       {
955         type: "SUCCESS_RESPONSE",
956         requestId,
957         response,
958       },
959       target
960     );
961   } catch (error) {
962     let errorMessage;
963     if (error instanceof Error) {
964       errorMessage = `${error.name}: ${error.message}`;
965     } else {
966       errorMessage = `${error}`;
967     }
968     channel.send(
969       {
970         type: "ERROR_RESPONSE",
971         requestId,
972         error: errorMessage,
973       },
974       target
975     );
976   }
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.
987  */
988 export function registerProfileCaptureForBrowser(
989   browser,
990   profileCaptureResult,
991   symbolicationService
992 ) {
993   infoForBrowserMap.set(browser, {
994     profileCaptureResult,
995     symbolicationService,
996   });
1000  * Get page favicons data and return them.
1002  * @param {Array<string>} pageUrls
1004  * @returns {Promise<Array<ProfilerFaviconData | null>>} favicon data as binary array.
1005  */
1006 async function getPageFavicons(pageUrls) {
1007   if (!pageUrls || pageUrls.length === 0) {
1008     // Return early if the pages are not provided.
1009     return [];
1010   }
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)
1017       .then(favicon => {
1018         // Check if data is found in the database and return it if so.
1019         if (favicon.dataLen > 0 && favicon.data) {
1020           return {
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,
1025           };
1026         }
1028         return null;
1029       })
1030       .catch(() => {
1031         // Couldn't find a favicon for this page, return null explicitly.
1032         return null;
1033       })
1034   );
1036   return Promise.all(promises);