summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/popup
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance-new/popup')
-rw-r--r--devtools/client/performance-new/popup/README.md3
-rw-r--r--devtools/client/performance-new/popup/background.jsm.js916
-rw-r--r--devtools/client/performance-new/popup/menu-button.jsm.js331
-rw-r--r--devtools/client/performance-new/popup/moz.build13
-rw-r--r--devtools/client/performance-new/popup/panel.jsm.js346
5 files changed, 1609 insertions, 0 deletions
diff --git a/devtools/client/performance-new/popup/README.md b/devtools/client/performance-new/popup/README.md
new file mode 100644
index 0000000000..78ef8e54c7
--- /dev/null
+++ b/devtools/client/performance-new/popup/README.md
@@ -0,0 +1,3 @@
+# Profiler Popup
+
+This directory collects the code that powers the profiler popup. See devtools/client/performance-new/README.md for more information.
diff --git a/devtools/client/performance-new/popup/background.jsm.js b/devtools/client/performance-new/popup/background.jsm.js
new file mode 100644
index 0000000000..3c05ac4731
--- /dev/null
+++ b/devtools/client/performance-new/popup/background.jsm.js
@@ -0,0 +1,916 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// @ts-check
+"use strict";
+
+/**
+ * This file contains all of the background logic for controlling the state and
+ * configuration of the profiler. It is in a JSM so that the logic can be shared
+ * with both the popup client, and the keyboard shortcuts. The shortcuts don't need
+ * access to any UI, and need to be loaded independent of the popup.
+ */
+
+// The following are not lazily loaded as they are needed during initialization.
+
+const { createLazyLoaders } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
+);
+// For some reason TypeScript was giving me an error when de-structuring AppConstants. I
+// suspect a bug in TypeScript was at play.
+const AppConstants = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+).AppConstants;
+
+/**
+ * @typedef {import("../@types/perf").RecordingSettings} RecordingSettings
+ * @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
+ * @typedef {import("../@types/perf").Library} Library
+ * @typedef {import("../@types/perf").PerformancePref} PerformancePref
+ * @typedef {import("../@types/perf").ProfilerWebChannel} ProfilerWebChannel
+ * @typedef {import("../@types/perf").PageContext} PageContext
+ * @typedef {import("../@types/perf").PrefObserver} PrefObserver
+ * @typedef {import("../@types/perf").PrefPostfix} PrefPostfix
+ * @typedef {import("../@types/perf").Presets} Presets
+ * @typedef {import("../@types/perf").ProfilerViewMode} ProfilerViewMode
+ * @typedef {import("../@types/perf").MessageFromFrontend} MessageFromFrontend
+ * @typedef {import("../@types/perf").RequestFromFrontend} RequestFromFrontend
+ * @typedef {import("../@types/perf").ResponseToFrontend} ResponseToFrontend
+ * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService
+ * @typedef {import("../@types/perf").ProfilerBrowserInfo} ProfilerBrowserInfo
+ * @typedef {import("../@types/perf").ProfileCaptureResult} ProfileCaptureResult
+ */
+
+/** @type {PerformancePref["Entries"]} */
+const ENTRIES_PREF = "devtools.performance.recording.entries";
+/** @type {PerformancePref["Interval"]} */
+const INTERVAL_PREF = "devtools.performance.recording.interval";
+/** @type {PerformancePref["Features"]} */
+const FEATURES_PREF = "devtools.performance.recording.features";
+/** @type {PerformancePref["Threads"]} */
+const THREADS_PREF = "devtools.performance.recording.threads";
+/** @type {PerformancePref["ObjDirs"]} */
+const OBJDIRS_PREF = "devtools.performance.recording.objdirs";
+/** @type {PerformancePref["Duration"]} */
+const DURATION_PREF = "devtools.performance.recording.duration";
+/** @type {PerformancePref["Preset"]} */
+const PRESET_PREF = "devtools.performance.recording.preset";
+/** @type {PerformancePref["PopupFeatureFlag"]} */
+const POPUP_FEATURE_FLAG_PREF = "devtools.performance.popup.feature-flag";
+/* This will be used to observe all profiler-related prefs. */
+const PREF_PREFIX = "devtools.performance.recording.";
+
+// The version of the profiler WebChannel.
+// This is reported from the STATUS_QUERY message, and identifies the
+// capabilities of the WebChannel. The front-end can handle old WebChannel
+// versions and has a full list of versions and capabilities here:
+// https://github.com/firefox-devtools/profiler/blob/main/src/app-logic/web-channel.js
+const CURRENT_WEBCHANNEL_VERSION = 1;
+
+const lazyRequire = {};
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(lazyRequire, {
+ require: "resource://devtools/shared/loader/Loader.sys.mjs",
+});
+// Lazily load the require function, when it's needed.
+// Avoid using ChromeUtils.defineESModuleGetters for now as:
+// * we can't replace createLazyLoaders as we still load commonjs+jsm+esm
+// It will be easier once we only load sys.mjs files.
+// * we would need to find a way to accomodate typescript to this special function.
+// @ts-ignore:next-line
+function require(path) {
+ // @ts-ignore:next-line
+ return lazyRequire.require(path);
+}
+
+// The following utilities are lazily loaded as they are not needed when controlling the
+// global state of the profiler, and only are used during specific funcationality like
+// symbolication or capturing a profile.
+const lazy = createLazyLoaders({
+ Utils: () => require("devtools/client/performance-new/utils"),
+ BrowserModule: () => require("devtools/client/performance-new/browser"),
+ RecordingUtils: () =>
+ require("resource://devtools/shared/performance-new/recording-utils.js"),
+ CustomizableUI: () =>
+ ChromeUtils.import("resource:///modules/CustomizableUI.jsm"),
+ PerfSymbolication: () =>
+ ChromeUtils.import(
+ "resource://devtools/client/performance-new/symbolication.jsm.js"
+ ),
+ ProfilerMenuButton: () =>
+ ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/menu-button.jsm.js"
+ ),
+});
+
+// The presets that we find in all interfaces are defined here.
+
+// The property l10nIds contain all FTL l10n IDs for these cases:
+// - properties in "popup" are used in the popup's select box.
+// - properties in "devtools" are used in other UIs (about:profiling and devtools panels).
+//
+// Properties for both cases have the same values, but because they're not used
+// in the same way we need to duplicate them.
+// Their values for the en-US locale are in the files:
+// devtools/client/locales/en-US/perftools.ftl
+// browser/locales/en-US/browser/appmenu.ftl
+
+/** @type {Presets} */
+const presets = {
+ "web-developer": {
+ entries: 128 * 1024 * 1024,
+ interval: 1,
+ features: ["screenshots", "js", "cpu"],
+ threads: ["GeckoMain", "Compositor", "Renderer", "DOM Worker"],
+ duration: 0,
+ profilerViewMode: "active-tab",
+ l10nIds: {
+ popup: {
+ label: "profiler-popup-presets-web-developer-label",
+ description: "profiler-popup-presets-web-developer-description",
+ },
+ devtools: {
+ label: "perftools-presets-web-developer-label",
+ description: "perftools-presets-web-developer-description",
+ },
+ },
+ },
+ "firefox-platform": {
+ entries: 128 * 1024 * 1024,
+ interval: 1,
+ features: ["screenshots", "js", "stackwalk", "cpu", "java", "processcpu"],
+ threads: [
+ "GeckoMain",
+ "Compositor",
+ "Renderer",
+ "SwComposite",
+ "DOM Worker",
+ ],
+ duration: 0,
+ l10nIds: {
+ popup: {
+ label: "profiler-popup-presets-firefox-label",
+ description: "profiler-popup-presets-firefox-description",
+ },
+ devtools: {
+ label: "perftools-presets-firefox-label",
+ description: "perftools-presets-firefox-description",
+ },
+ },
+ },
+ graphics: {
+ entries: 128 * 1024 * 1024,
+ interval: 1,
+ features: ["stackwalk", "js", "cpu", "java", "processcpu"],
+ threads: [
+ "GeckoMain",
+ "Compositor",
+ "Renderer",
+ "SwComposite",
+ "RenderBackend",
+ "SceneBuilder",
+ "WrWorker",
+ "CanvasWorkers",
+ ],
+ duration: 0,
+ l10nIds: {
+ popup: {
+ label: "profiler-popup-presets-graphics-label",
+ description: "profiler-popup-presets-graphics-description",
+ },
+ devtools: {
+ label: "perftools-presets-graphics-label",
+ description: "perftools-presets-graphics-description",
+ },
+ },
+ },
+ media: {
+ entries: 128 * 1024 * 1024,
+ interval: 1,
+ features: [
+ "js",
+ "stackwalk",
+ "cpu",
+ "audiocallbacktracing",
+ "ipcmessages",
+ "processcpu",
+ ],
+ threads: [
+ "cubeb",
+ "audio",
+ "BackgroundThreadPool",
+ "camera",
+ "capture",
+ "Compositor",
+ "decoder",
+ "GeckoMain",
+ "gmp",
+ "graph",
+ "grph",
+ "InotifyEventThread",
+ "IPDL Background",
+ "media",
+ "ModuleProcessThread",
+ "PacerThread",
+ "RemVidChild",
+ "RenderBackend",
+ "Renderer",
+ "Socket Thread",
+ "SwComposite",
+ "webrtc",
+ ],
+ duration: 0,
+ l10nIds: {
+ popup: {
+ label: "profiler-popup-presets-media-label",
+ description: "profiler-popup-presets-media-description2",
+ },
+ devtools: {
+ label: "perftools-presets-media-label",
+ description: "perftools-presets-media-description2",
+ },
+ },
+ },
+ networking: {
+ entries: 128 * 1024 * 1024,
+ interval: 1,
+ features: ["screenshots", "js", "stackwalk", "cpu", "java", "processcpu"],
+ threads: [
+ "Compositor",
+ "DNS Resolver",
+ "DOM Worker",
+ "GeckoMain",
+ "Renderer",
+ "Socket Thread",
+ "StreamTrans",
+ "SwComposite",
+ "TRR Background",
+ ],
+ duration: 0,
+ l10nIds: {
+ popup: {
+ label: "profiler-popup-presets-networking-label",
+ description: "profiler-popup-presets-networking-description",
+ },
+ devtools: {
+ label: "perftools-presets-networking-label",
+ description: "perftools-presets-networking-description",
+ },
+ },
+ },
+ power: {
+ entries: 128 * 1024 * 1024,
+ interval: 10,
+ features: [
+ "screenshots",
+ "js",
+ "stackwalk",
+ "cpu",
+ "processcpu",
+ "nostacksampling",
+ "ipcmessages",
+ "markersallthreads",
+ "power",
+ ],
+ threads: ["GeckoMain", "Renderer"],
+ duration: 0,
+ l10nIds: {
+ popup: {
+ label: "profiler-popup-presets-power-label",
+ description: "profiler-popup-presets-power-description",
+ },
+ devtools: {
+ label: "perftools-presets-power-label",
+ description: "perftools-presets-power-description",
+ },
+ },
+ },
+};
+
+/**
+ * Return the proper view mode for the Firefox Profiler front-end timeline by
+ * looking at the proper preset that is selected.
+ * Return value can be undefined when the preset is unknown or custom.
+ * @param {PageContext} pageContext
+ * @return {ProfilerViewMode | undefined}
+ */
+function getProfilerViewModeForCurrentPreset(pageContext) {
+ const prefPostfix = getPrefPostfix(pageContext);
+ const presetName = Services.prefs.getCharPref(PRESET_PREF + prefPostfix);
+
+ if (presetName === "custom") {
+ return undefined;
+ }
+
+ const preset = presets[presetName];
+ if (!preset) {
+ console.error(`Unknown profiler preset was encountered: "${presetName}"`);
+ return undefined;
+ }
+ return preset.profilerViewMode;
+}
+
+/**
+ * This function is called when the profile is captured with the shortcut
+ * keys, with the profiler toolbarbutton, or with the button inside the
+ * popup.
+ * @param {PageContext} pageContext
+ * @return {Promise<void>}
+ */
+async function captureProfile(pageContext) {
+ if (!Services.profiler.IsActive()) {
+ // The profiler is not active, ignore.
+ return;
+ }
+ if (Services.profiler.IsPaused()) {
+ // The profiler is already paused for capture, ignore.
+ return;
+ }
+
+ // Pause profiler before we collect the profile, so that we don't capture
+ // more samples while the parent process waits for subprocess profiles.
+ Services.profiler.Pause();
+
+ /**
+ * @type {ProfileCaptureResult}
+ */
+ const profileCaptureResult = await Services.profiler
+ .getProfileDataAsGzippedArrayBuffer()
+ .then(
+ profile => ({ type: "SUCCESS", profile }),
+ error => {
+ console.error(error);
+ return { type: "ERROR", error };
+ }
+ );
+
+ const profilerViewMode = getProfilerViewModeForCurrentPreset(pageContext);
+ const sharedLibraries = Services.profiler.sharedLibraries;
+ const objdirs = getObjdirPrefValue();
+
+ const { createLocalSymbolicationService } = lazy.PerfSymbolication();
+ const symbolicationService = createLocalSymbolicationService(
+ sharedLibraries,
+ objdirs
+ );
+
+ const { openProfilerTab } = lazy.BrowserModule();
+ const browser = await openProfilerTab(profilerViewMode);
+ registerProfileCaptureForBrowser(
+ browser,
+ profileCaptureResult,
+ symbolicationService
+ );
+
+ Services.profiler.StopProfiler();
+}
+
+/**
+ * This function is called when the profiler is started with the shortcut
+ * keys, with the profiler toolbarbutton, or with the button inside the
+ * popup.
+ * @param {PageContext} pageContext
+ */
+function startProfiler(pageContext) {
+ const {
+ entries,
+ interval,
+ features,
+ threads,
+ duration,
+ } = getRecordingSettings(pageContext, Services.profiler.GetFeatures());
+
+ // Get the active Browser ID from browser.
+ const { getActiveBrowserID } = lazy.RecordingUtils();
+ const activeTabID = getActiveBrowserID();
+
+ Services.profiler.StartProfiler(
+ entries,
+ interval,
+ features,
+ threads,
+ activeTabID,
+ duration
+ );
+}
+
+/**
+ * This function is called directly by devtools/startup/DevToolsStartup.jsm when
+ * using the shortcut keys to capture a profile.
+ * @type {() => void}
+ */
+function stopProfiler() {
+ Services.profiler.StopProfiler();
+}
+
+/**
+ * This function is called directly by devtools/startup/DevToolsStartup.jsm when
+ * using the shortcut keys to start and stop the profiler.
+ * @param {PageContext} pageContext
+ * @return {void}
+ */
+function toggleProfiler(pageContext) {
+ if (Services.profiler.IsPaused()) {
+ // The profiler is currently paused, which means that the user is already
+ // attempting to capture a profile. Ignore this request.
+ return;
+ }
+ if (Services.profiler.IsActive()) {
+ stopProfiler();
+ } else {
+ startProfiler(pageContext);
+ }
+}
+
+/**
+ * @param {PageContext} pageContext
+ */
+function restartProfiler(pageContext) {
+ stopProfiler();
+ startProfiler(pageContext);
+}
+
+/**
+ * @param {string} prefName
+ * @return {string[]}
+ */
+function _getArrayOfStringsPref(prefName) {
+ const text = Services.prefs.getCharPref(prefName);
+ return JSON.parse(text);
+}
+
+/**
+ * The profiler recording workflow uses two different pref paths. One set of prefs
+ * is stored for local profiling, and another for remote profiling. This function
+ * decides which to use. The remote prefs have ".remote" appended to the end of
+ * their pref names.
+ *
+ * @param {PageContext} pageContext
+ * @returns {PrefPostfix}
+ */
+function getPrefPostfix(pageContext) {
+ switch (pageContext) {
+ case "devtools":
+ case "aboutprofiling":
+ case "aboutlogging":
+ // Don't use any postfix on the prefs.
+ return "";
+ case "devtools-remote":
+ case "aboutprofiling-remote":
+ return ".remote";
+ default: {
+ const { UnhandledCaseError } = lazy.Utils();
+ throw new UnhandledCaseError(pageContext, "Page Context");
+ }
+ }
+}
+
+/**
+ * @param {string[]} objdirs
+ */
+function setObjdirPrefValue(objdirs) {
+ Services.prefs.setCharPref(OBJDIRS_PREF, JSON.stringify(objdirs));
+}
+
+/**
+ * Before Firefox 92, the objdir lists for local and remote profiling were
+ * stored in separate lists. In Firefox 92 those two prefs were merged into
+ * one. This function performs the migration.
+ */
+function migrateObjdirsPrefsIfNeeded() {
+ const OLD_REMOTE_OBJDIRS_PREF = OBJDIRS_PREF + ".remote";
+ const remoteString = Services.prefs.getCharPref(OLD_REMOTE_OBJDIRS_PREF, "");
+ if (remoteString === "") {
+ // No migration necessary.
+ return;
+ }
+
+ const remoteList = JSON.parse(remoteString);
+ const localList = _getArrayOfStringsPref(OBJDIRS_PREF);
+
+ // Merge the two lists, eliminating any duplicates.
+ const mergedList = [...new Set(localList.concat(remoteList))];
+ setObjdirPrefValue(mergedList);
+ Services.prefs.clearUserPref(OLD_REMOTE_OBJDIRS_PREF);
+}
+
+/**
+ * @returns {string[]}
+ */
+function getObjdirPrefValue() {
+ migrateObjdirsPrefsIfNeeded();
+ return _getArrayOfStringsPref(OBJDIRS_PREF);
+}
+
+/**
+ * @param {PageContext} pageContext
+ * @param {string[]} supportedFeatures
+ * @returns {RecordingSettings}
+ */
+function getRecordingSettings(pageContext, supportedFeatures) {
+ const objdirs = getObjdirPrefValue();
+ const prefPostfix = getPrefPostfix(pageContext);
+ const presetName = Services.prefs.getCharPref(PRESET_PREF + prefPostfix);
+
+ // First try to get the values from a preset. If the preset is "custom" or
+ // unrecognized, getRecordingSettingsFromPreset will return null and we will
+ // get the settings from individual prefs instead.
+ return (
+ getRecordingSettingsFromPreset(presetName, supportedFeatures, objdirs) ??
+ getRecordingSettingsFromPrefs(supportedFeatures, objdirs, prefPostfix)
+ );
+}
+
+/**
+ * @param {string} presetName
+ * @param {string[]} supportedFeatures
+ * @param {string[]} objdirs
+ * @return {RecordingSettings | null}
+ */
+function getRecordingSettingsFromPreset(
+ presetName,
+ supportedFeatures,
+ objdirs
+) {
+ if (presetName === "custom") {
+ return null;
+ }
+
+ const preset = presets[presetName];
+ if (!preset) {
+ console.error(`Unknown profiler preset was encountered: "${presetName}"`);
+ return null;
+ }
+
+ return {
+ presetName,
+ entries: preset.entries,
+ interval: preset.interval,
+ // Validate the features before passing them to the profiler.
+ features: preset.features.filter(feature =>
+ supportedFeatures.includes(feature)
+ ),
+ threads: preset.threads,
+ objdirs,
+ duration: preset.duration,
+ };
+}
+
+/**
+ * @param {string[]} supportedFeatures
+ * @param {string[]} objdirs
+ * @param {PrefPostfix} prefPostfix
+ * @return {RecordingSettings}
+ */
+function getRecordingSettingsFromPrefs(
+ supportedFeatures,
+ objdirs,
+ prefPostfix
+) {
+ // If you add a new preference here, please do not forget to update
+ // `revertRecordingSettings` as well.
+
+ const entries = Services.prefs.getIntPref(ENTRIES_PREF + prefPostfix);
+ const intervalInMicroseconds = Services.prefs.getIntPref(
+ INTERVAL_PREF + prefPostfix
+ );
+ const interval = intervalInMicroseconds / 1000;
+ const features = _getArrayOfStringsPref(FEATURES_PREF + prefPostfix);
+ const threads = _getArrayOfStringsPref(THREADS_PREF + prefPostfix);
+ const duration = Services.prefs.getIntPref(DURATION_PREF + prefPostfix);
+
+ return {
+ presetName: "custom",
+ entries,
+ interval,
+ // Validate the features before passing them to the profiler.
+ features: features.filter(feature => supportedFeatures.includes(feature)),
+ threads,
+ objdirs,
+ duration,
+ };
+}
+
+/**
+ * @param {PageContext} pageContext
+ * @param {RecordingSettings} prefs
+ */
+function setRecordingSettings(pageContext, prefs) {
+ const prefPostfix = getPrefPostfix(pageContext);
+ Services.prefs.setCharPref(PRESET_PREF + prefPostfix, prefs.presetName);
+ Services.prefs.setIntPref(ENTRIES_PREF + prefPostfix, prefs.entries);
+ // The interval pref stores the value in microseconds for extra precision.
+ const intervalInMicroseconds = prefs.interval * 1000;
+ Services.prefs.setIntPref(
+ INTERVAL_PREF + prefPostfix,
+ intervalInMicroseconds
+ );
+ Services.prefs.setCharPref(
+ FEATURES_PREF + prefPostfix,
+ JSON.stringify(prefs.features)
+ );
+ Services.prefs.setCharPref(
+ THREADS_PREF + prefPostfix,
+ JSON.stringify(prefs.threads)
+ );
+ setObjdirPrefValue(prefs.objdirs);
+}
+
+const platform = AppConstants.platform;
+
+/**
+ * Revert the recording prefs for both local and remote profiling.
+ * @return {void}
+ */
+function revertRecordingSettings() {
+ for (const prefPostfix of ["", ".remote"]) {
+ Services.prefs.clearUserPref(PRESET_PREF + prefPostfix);
+ Services.prefs.clearUserPref(ENTRIES_PREF + prefPostfix);
+ Services.prefs.clearUserPref(INTERVAL_PREF + prefPostfix);
+ Services.prefs.clearUserPref(FEATURES_PREF + prefPostfix);
+ Services.prefs.clearUserPref(THREADS_PREF + prefPostfix);
+ Services.prefs.clearUserPref(DURATION_PREF + prefPostfix);
+ }
+ Services.prefs.clearUserPref(OBJDIRS_PREF);
+ Services.prefs.clearUserPref(POPUP_FEATURE_FLAG_PREF);
+}
+
+/**
+ * Change the prefs based on a preset. This mechanism is used by the popup to
+ * easily switch between different settings.
+ * @param {string} presetName
+ * @param {PageContext} pageContext
+ * @param {string[]} supportedFeatures
+ * @return {void}
+ */
+function changePreset(pageContext, presetName, supportedFeatures) {
+ const prefPostfix = getPrefPostfix(pageContext);
+ const objdirs = getObjdirPrefValue();
+ let recordingSettings = getRecordingSettingsFromPreset(
+ presetName,
+ supportedFeatures,
+ objdirs
+ );
+
+ if (!recordingSettings) {
+ // No recordingSettings were found for that preset. Most likely this means this
+ // is a custom preset, or it's one that we dont recognize for some reason.
+ // Get the preferences from the individual preference values.
+ Services.prefs.setCharPref(PRESET_PREF + prefPostfix, presetName);
+ recordingSettings = getRecordingSettings(pageContext, supportedFeatures);
+ }
+
+ setRecordingSettings(pageContext, recordingSettings);
+}
+
+/**
+ * Add an observer for the profiler-related preferences.
+ * @param {PrefObserver} observer
+ * @return {void}
+ */
+function addPrefObserver(observer) {
+ Services.prefs.addObserver(PREF_PREFIX, observer);
+}
+
+/**
+ * Removes an observer for the profiler-related preferences.
+ * @param {PrefObserver} observer
+ * @return {void}
+ */
+function removePrefObserver(observer) {
+ Services.prefs.removeObserver(PREF_PREFIX, observer);
+}
+
+/**
+ * This map stores information that is associated with a "profile capturing"
+ * action, so that we can look up this information for WebChannel messages
+ * from the profiler tab.
+ * Most importantly, this stores the captured profile. When the profiler tab
+ * requests the profile, we can respond to the message with the correct profile.
+ * This works even if the request happens long after the tab opened. It also
+ * works for an "old" tab even if new profiles have been captured since that
+ * tab was opened.
+ * Supporting tab refresh is important because the tab sometimes reloads itself:
+ * If an old version of the front-end is cached in the service worker, and the
+ * browser supplies a profile with a newer format version, then the front-end
+ * updates its service worker and reloads itself, so that the updated version
+ * can parse the profile.
+ *
+ * This is a WeakMap so that the profile can be garbage-collected when the tab
+ * is closed.
+ *
+ * @type {WeakMap<MockedExports.Browser, ProfilerBrowserInfo>}
+ */
+const infoForBrowserMap = new WeakMap();
+
+/**
+ * This handler computes the response for any messages coming
+ * from the WebChannel from profiler.firefox.com.
+ *
+ * @param {RequestFromFrontend} request
+ * @param {MockedExports.Browser} browser - The tab's browser.
+ * @return {Promise<ResponseToFrontend>}
+ */
+async function getResponseForMessage(request, browser) {
+ switch (request.type) {
+ case "STATUS_QUERY": {
+ // The content page wants to know if this channel exists. It does, so respond
+ // back to the ping.
+ const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
+ return {
+ version: CURRENT_WEBCHANNEL_VERSION,
+ menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(),
+ };
+ }
+ case "ENABLE_MENU_BUTTON": {
+ const { ownerDocument } = browser;
+ if (!ownerDocument) {
+ throw new Error(
+ "Could not find the owner document for the current browser while enabling " +
+ "the profiler menu button"
+ );
+ }
+ // Ensure the widget is enabled.
+ Services.prefs.setBoolPref(POPUP_FEATURE_FLAG_PREF, true);
+
+ // Force the preset to be "firefox-platform" if we enable the menu button
+ // via web channel. If user goes through profiler.firefox.com to enable
+ // it, it means that either user is a platform developer or filing a bug
+ // report for performance engineers to look at.
+ const supportedFeatures = Services.profiler.GetFeatures();
+ changePreset("aboutprofiling", "firefox-platform", supportedFeatures);
+
+ // Enable the profiler menu button.
+ const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
+ ProfilerMenuButton.addToNavbar(ownerDocument);
+
+ // Dispatch the change event manually, so that the shortcuts will also be
+ // added.
+ const { CustomizableUI } = lazy.CustomizableUI();
+ CustomizableUI.dispatchToolboxEvent("customizationchange");
+
+ // Open the popup with a message.
+ ProfilerMenuButton.openPopup(ownerDocument);
+
+ // There is no response data for this message.
+ return undefined;
+ }
+ case "GET_PROFILE": {
+ const infoForBrowser = infoForBrowserMap.get(browser);
+ if (infoForBrowser === undefined) {
+ throw new Error("Could not find a profile for this tab.");
+ }
+ const { profileCaptureResult } = infoForBrowser;
+ switch (profileCaptureResult.type) {
+ case "SUCCESS":
+ return profileCaptureResult.profile;
+ case "ERROR":
+ throw profileCaptureResult.error;
+ default:
+ const { UnhandledCaseError } = lazy.Utils();
+ throw new UnhandledCaseError(
+ profileCaptureResult,
+ "profileCaptureResult"
+ );
+ }
+ }
+ case "GET_SYMBOL_TABLE": {
+ const { debugName, breakpadId } = request;
+ const symbolicationService = getSymbolicationServiceForBrowser(browser);
+ return symbolicationService.getSymbolTable(debugName, breakpadId);
+ }
+ case "QUERY_SYMBOLICATION_API": {
+ const { path, requestJson } = request;
+ const symbolicationService = getSymbolicationServiceForBrowser(browser);
+ return symbolicationService.querySymbolicationApi(path, requestJson);
+ }
+ default:
+ console.error(
+ "An unknown message type was received by the profiler's WebChannel handler.",
+ request
+ );
+ const { UnhandledCaseError } = lazy.Utils();
+ throw new UnhandledCaseError(request, "WebChannel request");
+ }
+}
+
+/**
+ * Get the symbolicationService for the capture that opened this browser's
+ * tab, or a fallback service for browsers from tabs opened by the user.
+ *
+ * @param {MockedExports.Browser} browser
+ * @return {SymbolicationService}
+ */
+function getSymbolicationServiceForBrowser(browser) {
+ // We try to serve symbolication requests that come from tabs that we
+ // opened when a profile was captured, and for tabs that the user opened
+ // independently, for example because the user wants to load an existing
+ // profile from a file.
+ const infoForBrowser = infoForBrowserMap.get(browser);
+ if (infoForBrowser !== undefined) {
+ // We opened this tab when a profile was captured. Use the symbolication
+ // service for that capture.
+ return infoForBrowser.symbolicationService;
+ }
+
+ // For the "foreign" tabs, we provide a fallback symbolication service so that
+ // we can find symbols for any libraries that are loaded in this process. This
+ // means that symbolication will work if the existing file has been captured
+ // from the same build.
+ const { createLocalSymbolicationService } = lazy.PerfSymbolication();
+ return createLocalSymbolicationService(
+ Services.profiler.sharedLibraries,
+ getObjdirPrefValue()
+ );
+}
+
+/**
+ * This handler handles any messages coming from the WebChannel from profiler.firefox.com.
+ *
+ * @param {ProfilerWebChannel} channel
+ * @param {string} id
+ * @param {any} message
+ * @param {MockedExports.WebChannelTarget} target
+ */
+async function handleWebChannelMessage(channel, id, message, target) {
+ if (typeof message !== "object" || typeof message.type !== "string") {
+ console.error(
+ "An malformed message was received by the profiler's WebChannel handler.",
+ message
+ );
+ return;
+ }
+ const messageFromFrontend = /** @type {MessageFromFrontend} */ (message);
+ const { requestId } = messageFromFrontend;
+
+ try {
+ const response = await getResponseForMessage(
+ messageFromFrontend,
+ target.browser
+ );
+ channel.send(
+ {
+ type: "SUCCESS_RESPONSE",
+ requestId,
+ response,
+ },
+ target
+ );
+ } catch (error) {
+ channel.send(
+ {
+ type: "ERROR_RESPONSE",
+ requestId,
+ error: `${error.name}: ${error.message}`,
+ },
+ target
+ );
+ }
+}
+
+/**
+ * @param {MockedExports.Browser} browser - The tab's browser.
+ * @param {ProfileCaptureResult} profileCaptureResult - The Gecko profile.
+ * @param {SymbolicationService} symbolicationService - An object which implements the
+ * SymbolicationService interface, whose getSymbolTable method will be invoked
+ * when profiler.firefox.com sends GET_SYMBOL_TABLE WebChannel messages to us. This
+ * method should obtain a symbol table for the requested binary and resolve the
+ * returned promise with it.
+ */
+function registerProfileCaptureForBrowser(
+ browser,
+ profileCaptureResult,
+ symbolicationService
+) {
+ infoForBrowserMap.set(browser, {
+ profileCaptureResult,
+ symbolicationService,
+ });
+}
+
+// Provide a fake module.exports for the JSM to be properly read by TypeScript.
+/** @type {any} */
+var module = { exports: {} };
+
+module.exports = {
+ presets,
+ captureProfile,
+ startProfiler,
+ stopProfiler,
+ restartProfiler,
+ toggleProfiler,
+ platform,
+ getRecordingSettings,
+ setRecordingSettings,
+ revertRecordingSettings,
+ changePreset,
+ handleWebChannelMessage,
+ registerProfileCaptureForBrowser,
+ addPrefObserver,
+ removePrefObserver,
+ getProfilerViewModeForCurrentPreset,
+};
+
+// Object.keys() confuses the linting which expects a static array expression.
+// eslint-disable-next-line
+var EXPORTED_SYMBOLS = Object.keys(module.exports);
diff --git a/devtools/client/performance-new/popup/menu-button.jsm.js b/devtools/client/performance-new/popup/menu-button.jsm.js
new file mode 100644
index 0000000000..434c19f0d1
--- /dev/null
+++ b/devtools/client/performance-new/popup/menu-button.jsm.js
@@ -0,0 +1,331 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// @ts-check
+"use strict";
+
+/**
+ * This file controls the enabling and disabling of the menu button for the profiler.
+ * Care should be taken to keep it minimal as it can be run with browser initialization.
+ */
+
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */
+var exports = {};
+
+const { createLazyLoaders } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
+);
+
+const lazy = createLazyLoaders({
+ CustomizableUI: () =>
+ ChromeUtils.import("resource:///modules/CustomizableUI.jsm"),
+ CustomizableWidgets: () =>
+ ChromeUtils.import("resource:///modules/CustomizableWidgets.jsm"),
+ PopupPanel: () =>
+ ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/panel.jsm.js"
+ ),
+ Background: () =>
+ ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ ),
+});
+
+const WIDGET_ID = "profiler-button";
+
+/**
+ * Add the profiler button to the navbar.
+ *
+ * @param {ChromeDocument} document The browser's document.
+ * @return {void}
+ */
+function addToNavbar(document) {
+ const { CustomizableUI } = lazy.CustomizableUI();
+
+ CustomizableUI.addWidgetToArea(WIDGET_ID, CustomizableUI.AREA_NAVBAR);
+}
+
+/**
+ * Remove the widget and place it in the customization palette. This will also
+ * disable the shortcuts.
+ *
+ * @return {void}
+ */
+function remove() {
+ const { CustomizableUI } = lazy.CustomizableUI();
+ CustomizableUI.removeWidgetFromArea(WIDGET_ID);
+}
+
+/**
+ * See if the profiler menu button is in the navbar, or other active areas. The
+ * placement is null when it's inactive in the customization palette.
+ *
+ * @return {boolean}
+ */
+function isInNavbar() {
+ const { CustomizableUI } = lazy.CustomizableUI();
+ return Boolean(CustomizableUI.getPlacementOfWidget("profiler-button"));
+}
+
+/**
+ * Opens the popup for the profiler.
+ * @param {Document} document
+ */
+function openPopup(document) {
+ // First find the button.
+ /** @type {HTMLButtonElement | null} */
+ const button = document.querySelector("#profiler-button");
+ if (!button) {
+ throw new Error("Could not find the profiler button.");
+ }
+
+ // Sending a click event anywhere on the button could start the profiler
+ // instead of opening the popup. Sending a command event on a view widget
+ // will make CustomizableUI show the view.
+ const cmdEvent = document.createEvent("xulcommandevent");
+ // @ts-ignore - Bug 1674368
+ cmdEvent.initCommandEvent("command", true, true, button.ownerGlobal);
+ button.dispatchEvent(cmdEvent);
+}
+
+/**
+ * This function creates the widget definition for the CustomizableUI. It should
+ * only be run if the profiler button is enabled.
+ * @param {(isEnabled: boolean) => void} toggleProfilerKeyShortcuts
+ * @return {void}
+ */
+function initialize(toggleProfilerKeyShortcuts) {
+ const { CustomizableUI } = lazy.CustomizableUI();
+ const { CustomizableWidgets } = lazy.CustomizableWidgets();
+
+ const widget = CustomizableUI.getWidget(WIDGET_ID);
+ if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
+ // This widget has already been created.
+ return;
+ }
+
+ const viewId = "PanelUI-profiler";
+
+ /**
+ * This is mutable state that will be shared between panel displays.
+ *
+ * @type {import("devtools/client/performance-new/popup/panel.jsm.js").State}
+ */
+ const panelState = {
+ cleanup: [],
+ isInfoCollapsed: true,
+ };
+
+ /**
+ * Handle when the customization changes for the button. This event is not
+ * very specific, and fires for any CustomizableUI widget. This event is
+ * pretty rare to fire, and only affects users of the profiler button,
+ * so it shouldn't have much overhead even if it runs a lot.
+ */
+ function handleCustomizationChange() {
+ const isEnabled = isInNavbar();
+ toggleProfilerKeyShortcuts(isEnabled);
+
+ if (!isEnabled) {
+ // The profiler menu button is no longer in the navbar, make sure that the
+ // "intro-displayed" preference is reset.
+ /** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */
+ const popupIntroDisplayedPref =
+ "devtools.performance.popup.intro-displayed";
+ Services.prefs.setBoolPref(popupIntroDisplayedPref, false);
+
+ // We stop the profiler when the button is removed for normal users,
+ // but we try to avoid interfering with profiling of automated tests.
+ if (
+ Services.profiler.IsActive() &&
+ (!Cu.isInAutomation || !Services.env.exists("MOZ_PROFILER_STARTUP"))
+ ) {
+ Services.profiler.StopProfiler();
+ }
+ }
+ }
+
+ const item = {
+ id: WIDGET_ID,
+ type: "button-and-view",
+ viewId,
+ l10nId: "profiler-popup-button-idle",
+
+ onViewShowing:
+ /**
+ * @type {(event: {
+ * target: ChromeHTMLElement | XULElement,
+ * detail: {
+ * addBlocker: (blocker: Promise<void>) => void
+ * }
+ * }) => void}
+ */
+ event => {
+ try {
+ // The popup logic is stored in a separate script so it doesn't have
+ // to be parsed at browser startup, and will only be lazily loaded
+ // when the popup is viewed.
+ const { initializePopup } = lazy.PopupPanel();
+
+ initializePopup(panelState, event.target);
+ } catch (error) {
+ // Surface any errors better in the console.
+ console.error(error);
+ }
+ },
+
+ /**
+ * @type {(event: { target: ChromeHTMLElement | XULElement }) => void}
+ */
+ onViewHiding(event) {
+ // Clean-up the view. This removes all of the event listeners.
+ for (const fn of panelState.cleanup) {
+ fn();
+ }
+ panelState.cleanup = [];
+ },
+
+ /**
+ * Perform any general initialization for this widget. This is called once per
+ * browser window.
+ *
+ * @type {(document: HTMLDocument) => void}
+ */
+ onBeforeCreated: document => {
+ /** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */
+ const popupIntroDisplayedPref =
+ "devtools.performance.popup.intro-displayed";
+
+ // Determine the state of the popup's info being collapsed BEFORE the view
+ // is shown, and update the collapsed state. This way the transition animation
+ // isn't run.
+ panelState.isInfoCollapsed = Services.prefs.getBoolPref(
+ popupIntroDisplayedPref
+ );
+ if (!panelState.isInfoCollapsed) {
+ // We have displayed the intro, don't show it again by default.
+ Services.prefs.setBoolPref(popupIntroDisplayedPref, true);
+ }
+
+ // Handle customization event changes. If the profiler is no longer in the
+ // navbar, then reset the popup intro preference.
+ const window = document.defaultView;
+ if (window) {
+ /** @type {any} */ (window).gNavToolbox.addEventListener(
+ "customizationchange",
+ handleCustomizationChange
+ );
+ }
+
+ toggleProfilerKeyShortcuts(isInNavbar());
+ },
+
+ /**
+ * This method is used when we need to operate upon the button element itself.
+ * This is called once per browser window.
+ *
+ * @type {(node: ChromeHTMLElement) => void}
+ */
+ onCreated: node => {
+ const document = node.ownerDocument;
+ const window = document?.defaultView;
+ if (!document || !window) {
+ console.error(
+ "Unable to find the document or the window of the profiler toolbar item."
+ );
+ return;
+ }
+
+ const firstButton = node.firstElementChild;
+ if (!firstButton) {
+ console.error(
+ "Unable to find the button element inside the profiler toolbar item."
+ );
+ return;
+ }
+
+ // Assign the null-checked button element to a new variable so that
+ // TypeScript doesn't require additional null checks in the functions
+ // below.
+ const buttonElement = firstButton;
+
+ // This class is needed to show the subview arrow when our button
+ // is in the overflow menu.
+ buttonElement.classList.add("subviewbutton-nav");
+
+ function setButtonActive() {
+ document.l10n.setAttributes(
+ buttonElement,
+ "profiler-popup-button-recording"
+ );
+ buttonElement.classList.toggle("profiler-active", true);
+ buttonElement.classList.toggle("profiler-paused", false);
+ }
+ function setButtonPaused() {
+ document.l10n.setAttributes(
+ buttonElement,
+ "profiler-popup-button-capturing"
+ );
+ buttonElement.classList.toggle("profiler-active", false);
+ buttonElement.classList.toggle("profiler-paused", true);
+ }
+ function setButtonInactive() {
+ document.l10n.setAttributes(
+ buttonElement,
+ "profiler-popup-button-idle"
+ );
+ buttonElement.classList.toggle("profiler-active", false);
+ buttonElement.classList.toggle("profiler-paused", false);
+ }
+
+ if (Services.profiler.IsPaused()) {
+ setButtonPaused();
+ }
+ if (Services.profiler.IsActive()) {
+ setButtonActive();
+ }
+
+ Services.obs.addObserver(setButtonActive, "profiler-started");
+ Services.obs.addObserver(setButtonInactive, "profiler-stopped");
+ Services.obs.addObserver(setButtonPaused, "profiler-paused");
+
+ window.addEventListener("unload", () => {
+ Services.obs.removeObserver(setButtonActive, "profiler-started");
+ Services.obs.removeObserver(setButtonInactive, "profiler-stopped");
+ Services.obs.removeObserver(setButtonPaused, "profiler-paused");
+ });
+ },
+
+ // @ts-ignore - Bug 1674368
+ onCommand: event => {
+ if (Services.profiler.IsPaused()) {
+ // A profile is already being captured, ignore this event.
+ return;
+ }
+ const { startProfiler, captureProfile } = lazy.Background();
+ if (Services.profiler.IsActive()) {
+ captureProfile("aboutprofiling");
+ } else {
+ startProfiler("aboutprofiling");
+ }
+ },
+ };
+
+ CustomizableUI.createWidget(item);
+ CustomizableWidgets.push(item);
+}
+
+const ProfilerMenuButton = {
+ initialize,
+ addToNavbar,
+ isInNavbar,
+ openPopup,
+ remove,
+};
+
+exports.ProfilerMenuButton = ProfilerMenuButton;
+
+// Object.keys() confuses the linting which expects a static array expression.
+// eslint-disable-next-line
+var EXPORTED_SYMBOLS = Object.keys(exports);
diff --git a/devtools/client/performance-new/popup/moz.build b/devtools/client/performance-new/popup/moz.build
new file mode 100644
index 0000000000..857cc3a3c7
--- /dev/null
+++ b/devtools/client/performance-new/popup/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "background.jsm.js",
+ "menu-button.jsm.js",
+ "panel.jsm.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Performance Tools (Profiler/Timeline)")
diff --git a/devtools/client/performance-new/popup/panel.jsm.js b/devtools/client/performance-new/popup/panel.jsm.js
new file mode 100644
index 0000000000..9701697139
--- /dev/null
+++ b/devtools/client/performance-new/popup/panel.jsm.js
@@ -0,0 +1,346 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// @ts-check
+"use strict";
+
+/**
+ * This file controls the logic of the profiler popup view.
+ */
+
+/**
+ * @typedef {ReturnType<typeof selectElementsInPanelview>} Elements
+ * @typedef {ReturnType<typeof createViewControllers>} ViewController
+ */
+
+/**
+ * @typedef {Object} State - The mutable state of the popup.
+ * @property {Array<() => void>} cleanup - Functions to cleanup once the view is hidden.
+ * @property {boolean} isInfoCollapsed
+ */
+
+const { createLazyLoaders } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
+);
+
+const lazy = createLazyLoaders({
+ PanelMultiView: () =>
+ ChromeUtils.import("resource:///modules/PanelMultiView.jsm"),
+ Background: () =>
+ ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ ),
+});
+
+/**
+ * This function collects all of the selection of the elements inside of the panel.
+ *
+ * @param {XULElement} panelview
+ */
+function selectElementsInPanelview(panelview) {
+ const document = panelview.ownerDocument;
+ /**
+ * Get an element or throw an error if it's not found. This is more friendly
+ * for TypeScript.
+ *
+ * @param {string} id
+ * @return {HTMLElement}
+ */
+ function getElementById(id) {
+ /** @type {HTMLElement | null} */
+ // @ts-ignore - Bug 1674368
+ const { PanelMultiView } = lazy.PanelMultiView();
+ const element = PanelMultiView.getViewNode(document, id);
+ if (!element) {
+ throw new Error(`Could not find the element from the ID "${id}"`);
+ }
+ return element;
+ }
+
+ // Forcefully cast the window to the type ChromeWindow.
+ /** @type {any} */
+ const chromeWindowAny = document.defaultView;
+ /** @type {ChromeWindow} */
+ const chromeWindow = chromeWindowAny;
+
+ return {
+ document,
+ panelview,
+ window: chromeWindow,
+ inactive: getElementById("PanelUI-profiler-inactive"),
+ active: getElementById("PanelUI-profiler-active"),
+ presetDescription: getElementById("PanelUI-profiler-content-description"),
+ presetsEditSettings: getElementById(
+ "PanelUI-profiler-content-edit-settings"
+ ),
+ presetsMenuList: /** @type {MenuListElement} */ (getElementById(
+ "PanelUI-profiler-presets"
+ )),
+ header: getElementById("PanelUI-profiler-header"),
+ info: getElementById("PanelUI-profiler-info"),
+ menupopup: getElementById("PanelUI-profiler-presets-menupopup"),
+ infoButton: getElementById("PanelUI-profiler-info-button"),
+ learnMore: getElementById("PanelUI-profiler-learn-more"),
+ startRecording: getElementById("PanelUI-profiler-startRecording"),
+ stopAndDiscard: getElementById("PanelUI-profiler-stopAndDiscard"),
+ stopAndCapture: getElementById("PanelUI-profiler-stopAndCapture"),
+ settingsSection: getElementById("PanelUI-profiler-content-settings"),
+ contentRecording: getElementById("PanelUI-profiler-content-recording"),
+ };
+}
+
+/**
+ * This function returns an interface that can be used to control the view of the
+ * panel based on the current mutable State.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ */
+function createViewControllers(state, elements) {
+ return {
+ updateInfoCollapse() {
+ const { header, info, infoButton } = elements;
+ header.setAttribute(
+ "isinfocollapsed",
+ state.isInfoCollapsed ? "true" : "false"
+ );
+ // @ts-ignore - Bug 1674368
+ infoButton.checked = !state.isInfoCollapsed;
+
+ if (state.isInfoCollapsed) {
+ const { height } = info.getBoundingClientRect();
+ info.style.marginBlockEnd = `-${height}px`;
+ } else {
+ info.style.marginBlockEnd = "0";
+ }
+ },
+
+ updatePresets() {
+ const { presets, getRecordingSettings } = lazy.Background();
+ const { presetName } = getRecordingSettings(
+ "aboutprofiling",
+ Services.profiler.GetFeatures()
+ );
+ const preset = presets[presetName];
+ if (preset) {
+ elements.presetDescription.style.display = "block";
+ elements.document.l10n.setAttributes(
+ elements.presetDescription,
+ preset.l10nIds.popup.description
+ );
+ elements.presetsMenuList.value = presetName;
+ } else {
+ elements.presetDescription.style.display = "none";
+ // We don't remove the l10n-id attribute as the element is hidden anyway.
+ // It will be updated again when it's displayed next time.
+ elements.presetsMenuList.value = "custom";
+ }
+ },
+
+ updateProfilerState() {
+ if (Services.profiler.IsActive()) {
+ elements.inactive.hidden = true;
+ elements.active.hidden = false;
+ elements.settingsSection.hidden = true;
+ elements.contentRecording.hidden = false;
+ } else {
+ elements.inactive.hidden = false;
+ elements.active.hidden = true;
+ elements.settingsSection.hidden = false;
+ elements.contentRecording.hidden = true;
+ }
+ },
+
+ createPresetsList() {
+ // Check the DOM if the presets were built or not. We can't cache this value
+ // in the `State` object, as the `State` object will be removed if the
+ // button is removed from the toolbar, but the DOM changes will still persist.
+ if (elements.menupopup.getAttribute("presetsbuilt") === "true") {
+ // The presets were already built.
+ return;
+ }
+
+ const { presets } = lazy.Background();
+ const currentPreset = Services.prefs.getCharPref(
+ "devtools.performance.recording.preset"
+ );
+
+ const menuitems = Object.entries(presets).map(([id, preset]) => {
+ const { document, presetsMenuList } = elements;
+ const menuitem = document.createXULElement("menuitem");
+ document.l10n.setAttributes(menuitem, preset.l10nIds.popup.label);
+ menuitem.setAttribute("value", id);
+ if (id === currentPreset) {
+ presetsMenuList.setAttribute("value", id);
+ }
+ return menuitem;
+ });
+
+ elements.menupopup.prepend(...menuitems);
+ elements.menupopup.setAttribute("presetsbuilt", "true");
+ },
+
+ hidePopup() {
+ const panel = elements.panelview.closest("panel");
+ if (!panel) {
+ throw new Error("Could not find the panel from the panelview.");
+ }
+ /** @type {any} */ (panel).hidePopup();
+ },
+ };
+}
+
+/**
+ * Perform all of the business logic to present the popup view once it is open.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ * @param {ViewController} view
+ */
+function initializeView(state, elements, view) {
+ view.createPresetsList();
+
+ state.cleanup.push(() => {
+ // The UI should be collapsed by default for the next time the popup
+ // is open.
+ state.isInfoCollapsed = true;
+ view.updateInfoCollapse();
+ });
+
+ // Turn off all animations while initializing the popup.
+ elements.header.setAttribute("animationready", "false");
+
+ elements.window.requestAnimationFrame(() => {
+ // Allow the elements to layout once, the updateInfoCollapse implementation measures
+ // the size of the container. It needs to wait a second before the bounding box
+ // returns an actual size.
+ view.updateInfoCollapse();
+ view.updateProfilerState();
+ view.updatePresets();
+
+ // Now wait for another rAF, and turn the animations back on.
+ elements.window.requestAnimationFrame(() => {
+ elements.header.setAttribute("animationready", "true");
+ });
+ });
+}
+
+/**
+ * This function is in charge of settings all of the events handlers for the view.
+ * The handlers must also add themselves to the `state.cleanup` for them to be
+ * properly cleaned up once the view is destroyed.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ * @param {ViewController} view
+ */
+function addPopupEventHandlers(state, elements, view) {
+ const {
+ changePreset,
+ startProfiler,
+ stopProfiler,
+ captureProfile,
+ } = lazy.Background();
+
+ /**
+ * Adds a handler that automatically is removed once the panel is hidden.
+ *
+ * @param {HTMLElement} element
+ * @param {string} type
+ * @param {(event: Event) => void} handler
+ */
+ function addHandler(element, type, handler) {
+ element.addEventListener(type, handler);
+ state.cleanup.push(() => {
+ element.removeEventListener(type, handler);
+ });
+ }
+
+ addHandler(elements.infoButton, "click", event => {
+ // Any button command event in the popup will cause it to close. Prevent this
+ // from happening on click.
+ event.preventDefault();
+
+ state.isInfoCollapsed = !state.isInfoCollapsed;
+ view.updateInfoCollapse();
+ });
+
+ addHandler(elements.startRecording, "click", () => {
+ startProfiler("aboutprofiling");
+ });
+
+ addHandler(elements.stopAndDiscard, "click", () => {
+ stopProfiler();
+ });
+
+ addHandler(elements.stopAndCapture, "click", () => {
+ captureProfile("aboutprofiling");
+ view.hidePopup();
+ });
+
+ addHandler(elements.learnMore, "click", () => {
+ elements.window.openWebLinkIn("https://profiler.firefox.com/docs/", "tab");
+ view.hidePopup();
+ });
+
+ addHandler(elements.presetsMenuList, "command", () => {
+ changePreset(
+ "aboutprofiling",
+ elements.presetsMenuList.value,
+ Services.profiler.GetFeatures()
+ );
+ view.updatePresets();
+ });
+
+ addHandler(elements.presetsMenuList, "popuphidden", event => {
+ // Changing a preset makes the popup autohide, this handler stops the
+ // propagation of that event, so that only the menulist's popup closes,
+ // and not the rest of the popup.
+ event.stopPropagation();
+ });
+
+ addHandler(elements.presetsMenuList, "click", event => {
+ // Clicking on a preset makes the popup autohide, this preventDefault stops
+ // the CustomizableUI from closing the popup.
+ event.preventDefault();
+ });
+
+ addHandler(elements.presetsEditSettings, "click", () => {
+ elements.window.openTrustedLinkIn("about:profiling", "tab");
+ view.hidePopup();
+ });
+
+ // Update the view when the profiler starts/stops.
+ // These are all events that can affect the current state of the profiler.
+ const events = ["profiler-started", "profiler-stopped"];
+ for (const event of events) {
+ Services.obs.addObserver(view.updateProfilerState, event);
+ state.cleanup.push(() => {
+ Services.obs.removeObserver(view.updateProfilerState, event);
+ });
+ }
+}
+
+/**
+ * Initialize everything needed for the popup to work fine.
+ * @param {State} panelState
+ * @param {XULElement} panelview
+ */
+function initializePopup(panelState, panelview) {
+ const panelElements = selectElementsInPanelview(panelview);
+ const panelviewControllers = createViewControllers(panelState, panelElements);
+ addPopupEventHandlers(panelState, panelElements, panelviewControllers);
+ initializeView(panelState, panelElements, panelviewControllers);
+}
+
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */
+var module = {};
+
+module.exports = {
+ initializePopup,
+};
+
+// Object.keys() confuses the linting which expects a static array expression.
+// eslint-disable-next-line
+var EXPORTED_SYMBOLS = Object.keys(module.exports);