summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/shared
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/performance-new/shared
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/performance-new/shared')
-rw-r--r--devtools/client/performance-new/shared/README.md5
-rw-r--r--devtools/client/performance-new/shared/background.jsm.js922
-rw-r--r--devtools/client/performance-new/shared/browser.js172
-rw-r--r--devtools/client/performance-new/shared/moz.build17
-rw-r--r--devtools/client/performance-new/shared/profiler_get_symbols.js498
-rw-r--r--devtools/client/performance-new/shared/symbolication-worker.js279
-rw-r--r--devtools/client/performance-new/shared/symbolication.jsm.js364
-rw-r--r--devtools/client/performance-new/shared/typescript-lazy-load.jsm.js55
-rw-r--r--devtools/client/performance-new/shared/utils.js566
9 files changed, 2878 insertions, 0 deletions
diff --git a/devtools/client/performance-new/shared/README.md b/devtools/client/performance-new/shared/README.md
new file mode 100644
index 0000000000..c83272c882
--- /dev/null
+++ b/devtools/client/performance-new/shared/README.md
@@ -0,0 +1,5 @@
+This directory contains files that are common to all UIs (popup, devtools panel,
+about:profiling) interacting with the profiler.
+Other UIs external to the profiler (one example is about:logging) can also use
+these files, especially background.jsm.js, to interact with the profiler with
+more capabilities than Services.profiler.
diff --git a/devtools/client/performance-new/shared/background.jsm.js b/devtools/client/performance-new/shared/background.jsm.js
new file mode 100644
index 0000000000..968a827557
--- /dev/null
+++ b/devtools/client/performance-new/shared/background.jsm.js
@@ -0,0 +1,922 @@
+/* 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/shared/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("resource://devtools/client/performance-new/shared/utils.js"),
+ BrowserModule: () =>
+ require("resource://devtools/client/performance-new/shared/browser.js"),
+ RecordingUtils: () =>
+ require("resource://devtools/shared/performance-new/recording-utils.js"),
+ CustomizableUI: () =>
+ ChromeUtils.importESModule("resource:///modules/CustomizableUI.sys.mjs"),
+ PerfSymbolication: () =>
+ ChromeUtils.import(
+ "resource://devtools/client/performance-new/shared/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 {MockedExports.ProfileGenerationAdditionalInformation | undefined}
+ */
+ let additionalInfo;
+ /**
+ * @type {ProfileCaptureResult}
+ */
+ const profileCaptureResult = await Services.profiler
+ .getProfileDataAsGzippedArrayBuffer()
+ .then(
+ ({ profile, additionalInformation }) => {
+ additionalInfo = additionalInformation;
+ return { type: "SUCCESS", profile };
+ },
+ error => {
+ console.error(error);
+ return { type: "ERROR", error };
+ }
+ );
+
+ const profilerViewMode = getProfilerViewModeForCurrentPreset(pageContext);
+ const sharedLibraries = additionalInfo?.sharedLibraries
+ ? additionalInfo.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/shared/browser.js b/devtools/client/performance-new/shared/browser.js
new file mode 100644
index 0000000000..1f3b91e278
--- /dev/null
+++ b/devtools/client/performance-new/shared/browser.js
@@ -0,0 +1,172 @@
+/* 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";
+
+/**
+ * @typedef {import("../@types/perf").Action} Action
+ * @typedef {import("../@types/perf").Library} Library
+ * @typedef {import("../@types/perf").PerfFront} PerfFront
+ * @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
+ * @typedef {import("../@types/perf").RecordingState} RecordingState
+ * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService
+ * @typedef {import("../@types/perf").PreferenceFront} PreferenceFront
+ * @typedef {import("../@types/perf").PerformancePref} PerformancePref
+ * @typedef {import("../@types/perf").RecordingSettings} RecordingSettings
+ * @typedef {import("../@types/perf").RestartBrowserWithEnvironmentVariable} RestartBrowserWithEnvironmentVariable
+ * @typedef {import("../@types/perf").GetActiveBrowserID} GetActiveBrowserID
+ * @typedef {import("../@types/perf").MinimallyTypedGeckoProfile} MinimallyTypedGeckoProfile
+ * * @typedef {import("../@types/perf").ProfilerViewMode} ProfilerViewMode
+ */
+
+/** @type {PerformancePref["UIBaseUrl"]} */
+const UI_BASE_URL_PREF = "devtools.performance.recording.ui-base-url";
+/** @type {PerformancePref["UIBaseUrlPathPref"]} */
+const UI_BASE_URL_PATH_PREF = "devtools.performance.recording.ui-base-url-path";
+
+/** @type {PerformancePref["UIEnableActiveTabView"]} */
+const UI_ENABLE_ACTIVE_TAB_PREF =
+ "devtools.performance.recording.active-tab-view.enabled";
+
+const UI_BASE_URL_DEFAULT = "https://profiler.firefox.com";
+const UI_BASE_URL_PATH_DEFAULT = "/from-browser";
+
+/**
+ * This file contains all of the privileged browser-specific functionality. This helps
+ * keep a clear separation between the privileged and non-privileged client code. It
+ * is also helpful in being able to mock out browser behavior for tests, without
+ * worrying about polluting the browser environment.
+ */
+
+/**
+ * Once a profile is received from the actor, it needs to be opened up in
+ * profiler.firefox.com to be analyzed. This function opens up profiler.firefox.com
+ * into a new browser tab.
+ * @param {ProfilerViewMode | undefined} profilerViewMode - View mode for the Firefox Profiler
+ * front-end timeline. While opening the url, we should append a query string
+ * if a view other than "full" needs to be displayed.
+ * @returns {Promise<MockedExports.Browser>} The browser for the opened tab.
+ */
+async function openProfilerTab(profilerViewMode) {
+ // Allow the user to point to something other than profiler.firefox.com.
+ const baseUrl = Services.prefs.getStringPref(
+ UI_BASE_URL_PREF,
+ UI_BASE_URL_DEFAULT
+ );
+ // Allow tests to override the path.
+ const baseUrlPath = Services.prefs.getStringPref(
+ UI_BASE_URL_PATH_PREF,
+ UI_BASE_URL_PATH_DEFAULT
+ );
+ // This controls whether we enable the active tab view when capturing in web
+ // developer preset.
+ const enableActiveTab = Services.prefs.getBoolPref(
+ UI_ENABLE_ACTIVE_TAB_PREF,
+ false
+ );
+
+ // We automatically open up the "full" mode if no query string is present.
+ // `undefined` also means nothing is specified, and it should open the "full"
+ // timeline view in that case.
+ let viewModeQueryString = "";
+ if (profilerViewMode === "active-tab") {
+ // We're not enabling the active-tab view in all environments until we
+ // iron out all its issues.
+ if (enableActiveTab) {
+ viewModeQueryString = "?view=active-tab&implementation=js";
+ } else {
+ viewModeQueryString = "?implementation=js";
+ }
+ } else if (profilerViewMode !== undefined && profilerViewMode !== "full") {
+ viewModeQueryString = `?view=${profilerViewMode}`;
+ }
+
+ const urlToLoad = `${baseUrl}${baseUrlPath}${viewModeQueryString}`;
+
+ // Find the most recently used window, as the DevTools client could be in a variety
+ // of hosts.
+ // Note that when running from the browser toolbox, there won't be the browser window,
+ // but only the browser toolbox document.
+ const win =
+ Services.wm.getMostRecentWindow("navigator:browser") ||
+ Services.wm.getMostRecentWindow("devtools:toolbox");
+ if (!win) {
+ throw new Error("No browser window");
+ }
+ win.focus();
+
+ // The profiler frontend currently doesn't support being loaded in a private
+ // window, because it does some storage writes in IndexedDB. That's why we
+ // force the opening of the tab in a non-private window. This might open a new
+ // non-private window if the only currently opened window is a private window.
+ const contentBrowser = await new Promise(resolveOnContentBrowserCreated =>
+ win.openWebLinkIn(urlToLoad, "tab", {
+ forceNonPrivate: true,
+ resolveOnContentBrowserCreated,
+ userContextId: win.gBrowser?.contentPrincipal.userContextId,
+ })
+ );
+ return contentBrowser;
+}
+
+/**
+ * Flatten all the sharedLibraries of the different processes in the profile
+ * into one list of libraries.
+ * @param {MinimallyTypedGeckoProfile} profile - The profile JSON object
+ * @returns {Library[]}
+ */
+function sharedLibrariesFromProfile(profile) {
+ /**
+ * @param {MinimallyTypedGeckoProfile} processProfile
+ * @returns {Library[]}
+ */
+ function getLibsRecursive(processProfile) {
+ return processProfile.libs.concat(
+ ...processProfile.processes.map(getLibsRecursive)
+ );
+ }
+
+ return getLibsRecursive(profile);
+}
+
+/**
+ * Restarts the browser with a given environment variable set to a value.
+ *
+ * @type {RestartBrowserWithEnvironmentVariable}
+ */
+function restartBrowserWithEnvironmentVariable(envName, value) {
+ Services.env.set(envName, value);
+
+ Services.startup.quit(
+ Services.startup.eForceQuit | Services.startup.eRestart
+ );
+}
+
+/**
+ * @param {Window} window
+ * @param {string[]} objdirs
+ * @param {(objdirs: string[]) => unknown} changeObjdirs
+ */
+function openFilePickerForObjdir(window, objdirs, changeObjdirs) {
+ const FilePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
+ Ci.nsIFilePicker
+ );
+ FilePicker.init(window, "Pick build directory", FilePicker.modeGetFolder);
+ FilePicker.open(rv => {
+ if (rv == FilePicker.returnOK) {
+ const path = FilePicker.file.path;
+ if (path && !objdirs.includes(path)) {
+ const newObjdirs = [...objdirs, path];
+ changeObjdirs(newObjdirs);
+ }
+ }
+ });
+}
+
+module.exports = {
+ openProfilerTab,
+ sharedLibrariesFromProfile,
+ restartBrowserWithEnvironmentVariable,
+ openFilePickerForObjdir,
+};
diff --git a/devtools/client/performance-new/shared/moz.build b/devtools/client/performance-new/shared/moz.build
new file mode 100644
index 0000000000..b98771ab72
--- /dev/null
+++ b/devtools/client/performance-new/shared/moz.build
@@ -0,0 +1,17 @@
+# 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",
+ "browser.js",
+ "profiler_get_symbols.js",
+ "symbolication-worker.js",
+ "symbolication.jsm.js",
+ "typescript-lazy-load.jsm.js",
+ "utils.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Performance Tools (Profiler/Timeline)")
diff --git a/devtools/client/performance-new/shared/profiler_get_symbols.js b/devtools/client/performance-new/shared/profiler_get_symbols.js
new file mode 100644
index 0000000000..dc190853ba
--- /dev/null
+++ b/devtools/client/performance-new/shared/profiler_get_symbols.js
@@ -0,0 +1,498 @@
+/* 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/. */
+
+//
+// THIS FILE IS AUTOGENERATED by wasm-bindgen.
+//
+// Generated from:
+// https://github.com/mstange/profiler-get-symbols/commit/0373708893e45e8299e58ca692764be448e3457d
+// by following the instructions in that repository's Readme.md
+//
+
+let wasm_bindgen;
+(function() {
+ const __exports = {};
+ let wasm;
+
+ const heap = new Array(32).fill(undefined);
+
+ heap.push(undefined, null, true, false);
+
+function getObject(idx) { return heap[idx]; }
+
+let heap_next = heap.length;
+
+function dropObject(idx) {
+ if (idx < 36) return;
+ heap[idx] = heap_next;
+ heap_next = idx;
+}
+
+function takeObject(idx) {
+ const ret = getObject(idx);
+ dropObject(idx);
+ return ret;
+}
+
+let WASM_VECTOR_LEN = 0;
+
+let cachedUint8Memory0 = new Uint8Array();
+
+function getUint8Memory0() {
+ if (cachedUint8Memory0.byteLength === 0) {
+ cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
+ }
+ return cachedUint8Memory0;
+}
+
+const cachedTextEncoder = new TextEncoder();
+
+const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
+ ? function (arg, view) {
+ return cachedTextEncoder.encodeInto(arg, view);
+}
+ : function (arg, view) {
+ const buf = cachedTextEncoder.encode(arg);
+ view.set(buf);
+ return {
+ read: arg.length,
+ written: buf.length
+ };
+});
+
+function passStringToWasm0(arg, malloc, realloc) {
+
+ if (realloc === undefined) {
+ const buf = cachedTextEncoder.encode(arg);
+ const ptr = malloc(buf.length);
+ getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
+ WASM_VECTOR_LEN = buf.length;
+ return ptr;
+ }
+
+ let len = arg.length;
+ let ptr = malloc(len);
+
+ const mem = getUint8Memory0();
+
+ let offset = 0;
+
+ for (; offset < len; offset++) {
+ const code = arg.charCodeAt(offset);
+ if (code > 0x7F) break;
+ mem[ptr + offset] = code;
+ }
+
+ if (offset !== len) {
+ if (offset !== 0) {
+ arg = arg.slice(offset);
+ }
+ ptr = realloc(ptr, len, len = offset + arg.length * 3);
+ const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
+ const ret = encodeString(arg, view);
+
+ offset += ret.written;
+ }
+
+ WASM_VECTOR_LEN = offset;
+ return ptr;
+}
+
+function isLikeNone(x) {
+ return x === undefined || x === null;
+}
+
+let cachedInt32Memory0 = new Int32Array();
+
+function getInt32Memory0() {
+ if (cachedInt32Memory0.byteLength === 0) {
+ cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
+ }
+ return cachedInt32Memory0;
+}
+
+function addHeapObject(obj) {
+ if (heap_next === heap.length) heap.push(heap.length + 1);
+ const idx = heap_next;
+ heap_next = heap[idx];
+
+ heap[idx] = obj;
+ return idx;
+}
+
+const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+
+cachedTextDecoder.decode();
+
+function getStringFromWasm0(ptr, len) {
+ return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
+}
+
+function makeMutClosure(arg0, arg1, dtor, f) {
+ const state = { a: arg0, b: arg1, cnt: 1, dtor };
+ const real = (...args) => {
+ // First up with a closure we increment the internal reference
+ // count. This ensures that the Rust closure environment won't
+ // be deallocated while we're invoking it.
+ state.cnt++;
+ const a = state.a;
+ state.a = 0;
+ try {
+ return f(a, state.b, ...args);
+ } finally {
+ if (--state.cnt === 0) {
+ wasm.__wbindgen_export_2.get(state.dtor)(a, state.b);
+
+ } else {
+ state.a = a;
+ }
+ }
+ };
+ real.original = state;
+
+ return real;
+}
+function __wbg_adapter_16(arg0, arg1, arg2) {
+ wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h19297bd79b0d78f6(arg0, arg1, addHeapObject(arg2));
+}
+
+function handleError(f, args) {
+ try {
+ return f.apply(this, args);
+ } catch (e) {
+ wasm.__wbindgen_exn_store(addHeapObject(e));
+ }
+}
+function __wbg_adapter_33(arg0, arg1, arg2, arg3) {
+ wasm.wasm_bindgen__convert__closures__invoke2_mut__h80dea6fd01e77b95(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
+}
+
+/**
+* Usage:
+*
+* ```js
+* async function getSymbolTable(debugName, breakpadId, libKeyToPathMap) {
+* const helper = {
+* getCandidatePathsForDebugFile: (info) => {
+* const path = libKeyToPathMap.get(`${info.debugName}/${info.breakpadId}`);
+* if (path !== undefined) {
+* return [path];
+* }
+* return [];
+* },
+* getCandidatePathsForBinary: (info) => [],
+* readFile: async (filename) => {
+* const byteLength = await getFileSizeInBytes(filename);
+* const fileHandle = getFileHandle(filename);
+* return {
+* size: byteLength,
+* readBytesInto: (array, offset) => {
+* syncReadFilePartIntoBuffer(fileHandle, array, offset);
+* },
+* close: () => {},
+* };
+* },
+* };
+*
+* const [addr, index, buffer] = await getCompactSymbolTable(debugName, breakpadId, helper);
+* return [addr, index, buffer];
+* }
+* ```
+* @param {string} debug_name
+* @param {string} breakpad_id
+* @param {any} helper
+* @returns {Promise<any>}
+*/
+__exports.getCompactSymbolTable = function(debug_name, breakpad_id, helper) {
+ const ptr0 = passStringToWasm0(debug_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passStringToWasm0(breakpad_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len1 = WASM_VECTOR_LEN;
+ const ret = wasm.getCompactSymbolTable(ptr0, len0, ptr1, len1, addHeapObject(helper));
+ return takeObject(ret);
+};
+
+/**
+* Usage:
+*
+* ```js
+* async function queryAPIWrapper(url, requestJSONString, libKeyToPathMap) {
+* const helper = {
+* getCandidatePathsForDebugFile: (info) => {
+* const path = libKeyToPathMap.get(`${info.debugName}/${info.breakpadId}`);
+* if (path !== undefined) {
+* return [path];
+* }
+* return [];
+* },
+* getCandidatePathsForBinary: (info) => [],
+* readFile: async (filename) => {
+* const byteLength = await getFileSizeInBytes(filename);
+* const fileHandle = getFileHandle(filename);
+* return {
+* size: byteLength,
+* readBytesInto: (array, offset) => {
+* syncReadFilePartIntoBuffer(fileHandle, array, offset);
+* },
+* close: () => {},
+* };
+* },
+* };
+*
+* const responseJSONString = await queryAPI(url, requestJSONString, helper);
+* return responseJSONString;
+* }
+* ```
+* @param {string} url
+* @param {string} request_json
+* @param {any} helper
+* @returns {Promise<any>}
+*/
+__exports.queryAPI = function(url, request_json, helper) {
+ const ptr0 = passStringToWasm0(url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len1 = WASM_VECTOR_LEN;
+ const ret = wasm.queryAPI(ptr0, len0, ptr1, len1, addHeapObject(helper));
+ return takeObject(ret);
+};
+
+async function load(module, imports) {
+ if (typeof Response === 'function' && module instanceof Response) {
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
+ try {
+ return await WebAssembly.instantiateStreaming(module, imports);
+
+ } catch (e) {
+ if (module.headers.get('Content-Type') != 'application/wasm') {
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ const bytes = await module.arrayBuffer();
+ return await WebAssembly.instantiate(bytes, imports);
+
+ } else {
+ const instance = await WebAssembly.instantiate(module, imports);
+
+ if (instance instanceof WebAssembly.Instance) {
+ return { instance, module };
+
+ } else {
+ return instance;
+ }
+ }
+}
+
+function getImports() {
+ const imports = {};
+ imports.wbg = {};
+ imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
+ takeObject(arg0);
+ };
+ imports.wbg.__wbindgen_cb_drop = function(arg0) {
+ const obj = takeObject(arg0).original;
+ if (obj.cnt-- == 1) {
+ obj.a = 0;
+ return true;
+ }
+ const ret = false;
+ return ret;
+ };
+ imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
+ const obj = getObject(arg1);
+ const ret = typeof(obj) === 'string' ? obj : undefined;
+ var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ var len0 = WASM_VECTOR_LEN;
+ getInt32Memory0()[arg0 / 4 + 1] = len0;
+ getInt32Memory0()[arg0 / 4 + 0] = ptr0;
+ };
+ imports.wbg.__wbg_close_334601fc6c36b53e = function() { return handleError(function (arg0) {
+ getObject(arg0).close();
+ }, arguments) };
+ imports.wbg.__wbg_get_57245cc7d7c7619d = function(arg0, arg1) {
+ const ret = getObject(arg0)[arg1 >>> 0];
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_memory = function() {
+ const ret = wasm.memory;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_buffer_3f3d764d4747d564 = function(arg0) {
+ const ret = getObject(arg0).buffer;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_newwithbyteoffsetandlength_d9aa266703cb98be = function(arg0, arg1, arg2) {
+ const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_readBytesInto_3ec3b16ea5839a95 = function() { return handleError(function (arg0, arg1, arg2) {
+ getObject(arg0).readBytesInto(takeObject(arg1), arg2);
+ }, arguments) };
+ imports.wbg.__wbg_name_48eda3ae6aa697ca = function(arg0) {
+ const ret = getObject(arg0).name;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_message_fe2af63ccc8985bc = function(arg0) {
+ const ret = getObject(arg0).message;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_new_0b9bfdd97583284e = function() {
+ const ret = new Object();
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
+ const ret = getStringFromWasm0(arg0, arg1);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_set_20cbc34131e76824 = function(arg0, arg1, arg2) {
+ getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
+ };
+ imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
+ const ret = getObject(arg0);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_new_9962f939219f1820 = function(arg0, arg1) {
+ try {
+ var state0 = {a: arg0, b: arg1};
+ var cb0 = (arg0, arg1) => {
+ const a = state0.a;
+ state0.a = 0;
+ try {
+ return __wbg_adapter_33(a, state0.b, arg0, arg1);
+ } finally {
+ state0.a = a;
+ }
+ };
+ const ret = new Promise(cb0);
+ return addHeapObject(ret);
+ } finally {
+ state0.a = state0.b = 0;
+ }
+ };
+ imports.wbg.__wbg_newwithbyteoffsetandlength_9cc9adccd861aa26 = function(arg0, arg1, arg2) {
+ const ret = new Uint32Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_new_949418f5ed1e29f7 = function(arg0) {
+ const ret = new Uint32Array(getObject(arg0));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_new_8c3f0052272a457a = function(arg0) {
+ const ret = new Uint8Array(getObject(arg0));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_of_22ee6ea02403744c = function(arg0, arg1, arg2) {
+ const ret = Array.of(getObject(arg0), getObject(arg1), getObject(arg2));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_call_168da88779e35f61 = function() { return handleError(function (arg0, arg1, arg2) {
+ const ret = getObject(arg0).call(getObject(arg1), getObject(arg2));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_getCandidatePathsForDebugFile_19de1ea293153630 = function() { return handleError(function (arg0, arg1) {
+ const ret = getObject(arg0).getCandidatePathsForDebugFile(takeObject(arg1));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_from_7ce3cb27cb258569 = function(arg0) {
+ const ret = Array.from(getObject(arg0));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_length_6e3bbe7c8bd4dbd8 = function(arg0) {
+ const ret = getObject(arg0).length;
+ return ret;
+ };
+ imports.wbg.__wbg_getCandidatePathsForBinary_8311cb7aeae90263 = function() { return handleError(function (arg0, arg1) {
+ const ret = getObject(arg0).getCandidatePathsForBinary(takeObject(arg1));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_readFile_33b95391c6839d48 = function(arg0, arg1, arg2) {
+ const ret = getObject(arg0).readFile(getStringFromWasm0(arg1, arg2));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_then_cedad20fbbd9418a = function(arg0, arg1, arg2) {
+ const ret = getObject(arg0).then(getObject(arg1), getObject(arg2));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_size_c5375fc90958b38d = function() { return handleError(function (arg0) {
+ const ret = getObject(arg0).size;
+ return ret;
+ }, arguments) };
+ imports.wbg.__wbindgen_throw = function(arg0, arg1) {
+ throw new Error(getStringFromWasm0(arg0, arg1));
+ };
+ imports.wbg.__wbg_then_11f7a54d67b4bfad = function(arg0, arg1) {
+ const ret = getObject(arg0).then(getObject(arg1));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_resolve_99fe17964f31ffc0 = function(arg0) {
+ const ret = Promise.resolve(getObject(arg0));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_closure_wrapper1912 = function(arg0, arg1, arg2) {
+ const ret = makeMutClosure(arg0, arg1, 143, __wbg_adapter_16);
+ return addHeapObject(ret);
+ };
+
+ return imports;
+}
+
+function initMemory(imports, maybe_memory) {
+
+}
+
+function finalizeInit(instance, module) {
+ wasm = instance.exports;
+ init.__wbindgen_wasm_module = module;
+ cachedInt32Memory0 = new Int32Array();
+ cachedUint8Memory0 = new Uint8Array();
+
+
+ return wasm;
+}
+
+function initSync(module) {
+ const imports = getImports();
+
+ initMemory(imports);
+
+ if (!(module instanceof WebAssembly.Module)) {
+ module = new WebAssembly.Module(module);
+ }
+
+ const instance = new WebAssembly.Instance(module, imports);
+
+ return finalizeInit(instance, module);
+}
+
+async function init(input) {
+ if (typeof input === 'undefined') {
+ let src;
+ if (typeof document === 'undefined') {
+ src = location.href;
+ } else {
+ src = document.currentScript.src;
+ }
+ input = src.replace(/\.js$/, '_bg.wasm');
+ }
+ const imports = getImports();
+
+ if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+ input = fetch(input);
+ }
+
+ initMemory(imports);
+
+ const { instance, module } = await load(await input, imports);
+
+ return finalizeInit(instance, module);
+}
+
+wasm_bindgen = Object.assign(init, { initSync }, __exports);
+
+})();
diff --git a/devtools/client/performance-new/shared/symbolication-worker.js b/devtools/client/performance-new/shared/symbolication-worker.js
new file mode 100644
index 0000000000..4f2ee02f29
--- /dev/null
+++ b/devtools/client/performance-new/shared/symbolication-worker.js
@@ -0,0 +1,279 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* 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/. */
+
+/* eslint-env mozilla/chrome-worker */
+
+// FIXME: This file is currently not covered by TypeScript, there is no "@ts-check" comment.
+// We should fix this once we know how to deal with the module imports below.
+// (Maybe once Firefox supports worker module? Bug 1247687)
+
+"use strict";
+
+/* import-globals-from profiler_get_symbols.js */
+importScripts(
+ "resource://devtools/client/performance-new/shared/profiler_get_symbols.js"
+);
+
+/**
+ * @typedef {import("../@types/perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage
+ * @typedef {import("../@types/perf").FileHandle} FileHandle
+ */
+
+// This worker uses the wasm module that was generated from https://github.com/mstange/profiler-get-symbols.
+// See ProfilerGetSymbols.jsm for more information.
+//
+// The worker instantiates the module, reads the binary into wasm memory, runs
+// the wasm code, and returns the symbol table or an error. Then it shuts down
+// itself.
+
+/* eslint camelcase: 0*/
+const { getCompactSymbolTable, queryAPI } = wasm_bindgen;
+
+// Returns a plain object that is Structured Cloneable and has name and
+// message properties.
+function createPlainErrorObject(e) {
+ // Regular errors: just rewrap the object.
+ const { name, message, fileName, lineNumber } = e;
+ return { name, message, fileName, lineNumber };
+}
+
+/**
+ * A FileAndPathHelper object is passed to getCompactSymbolTable, which calls
+ * the methods `getCandidatePathsForBinaryOrPdb` and `readFile` on it.
+ */
+class FileAndPathHelper {
+ constructor(libInfoMap, objdirs) {
+ this._libInfoMap = libInfoMap;
+ this._objdirs = objdirs;
+ }
+
+ /**
+ * Enumerate all paths at which we could find files with symbol information.
+ * This method is called by wasm code (via the bindings).
+ *
+ * @param {LibraryInfo} libraryInfo
+ * @returns {Array<string>}
+ */
+ getCandidatePathsForDebugFile(libraryInfo) {
+ const { debugName, breakpadId } = libraryInfo;
+ const key = `${debugName}:${breakpadId}`;
+ const lib = this._libInfoMap.get(key);
+ if (!lib) {
+ throw new Error(
+ `Could not find the library for "${debugName}", "${breakpadId}".`
+ );
+ }
+
+ const { name, path, debugPath, arch } = lib;
+ const candidatePaths = [];
+
+ // First, try to find a binary with a matching file name and breakpadId
+ // in one of the manually specified objdirs.
+ // This is needed if the debuggee is a build running on a remote machine that
+ // was compiled by the developer on *this* machine (the "host machine"). In
+ // that case, the objdir will contain the compiled binary with full symbol and
+ // debug information, whereas the binary on the device may not exist in
+ // uncompressed form or may have been stripped of debug information and some
+ // symbol information.
+ // An objdir, or "object directory", is a directory on the host machine that's
+ // used to store build artifacts ("object files") from the compilation process.
+ // This only works if the binary is one of the Gecko binaries and not
+ // a system library.
+ for (const objdirPath of this._objdirs) {
+ try {
+ // Binaries are usually expected to exist at objdir/dist/bin/filename.
+ candidatePaths.push(PathUtils.join(objdirPath, "dist", "bin", name));
+
+ // Also search in the "objdir" directory itself (not just in dist/bin).
+ // If, for some unforeseen reason, the relevant binary is not inside the
+ // objdirs dist/bin/ directory, this provides a way out because it lets the
+ // user specify the actual location.
+ candidatePaths.push(PathUtils.join(objdirPath, name));
+ } catch (e) {
+ // PathUtils.join throws if objdirPath is not an absolute path.
+ // Ignore those invalid objdir paths.
+ }
+ }
+
+ // Check the absolute paths of the library last.
+ // We do this after the objdir search because the library's path may point
+ // to a stripped binary, which will have fewer symbols than the original
+ // binaries in the objdir.
+ if (debugPath !== path) {
+ // We're on Windows, and debugPath points to a PDB file.
+ // On non-Windows, path and debugPath are always the same.
+
+ // Check the PDB file before the binary because the PDB has the symbol
+ // information. The binary is only used as a last-ditch fallback
+ // for things like Windows system libraries (e.g. graphics drivers).
+ candidatePaths.push(debugPath);
+ }
+
+ // The location of the binary. If the profile was obtained on this machine
+ // (and not, for example, on an Android device), this file should always
+ // exist.
+ candidatePaths.push(path);
+
+ // On macOS, for system libraries, add a final fallback for the dyld shared
+ // cache. Starting with macOS 11, most system libraries are located in this
+ // system-wide cache file and not present as individual files.
+ if (arch && (path.startsWith("/usr/") || path.startsWith("/System/"))) {
+ // Use the special syntax `dyldcache:<dyldcachepath>:<librarypath>`.
+
+ // Dyld cache location used on macOS 13+:
+ candidatePaths.push(
+ `dyldcache:/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_${arch}:${path}`
+ );
+ // Dyld cache location used on macOS 11 and 12:
+ candidatePaths.push(
+ `dyldcache:/System/Library/dyld/dyld_shared_cache_${arch}:${path}`
+ );
+ }
+
+ return candidatePaths;
+ }
+
+ /**
+ * Enumerate all paths at which we could find the binary which matches the
+ * given libraryInfo, in order to disassemble machine code.
+ * This method is called by wasm code (via the bindings).
+ *
+ * @param {LibraryInfo} libraryInfo
+ * @returns {Array<string>}
+ */
+ getCandidatePathsForBinary(libraryInfo) {
+ const { debugName, breakpadId } = libraryInfo;
+ const key = `${debugName}:${breakpadId}`;
+ const lib = this._libInfoMap.get(key);
+ if (!lib) {
+ throw new Error(
+ `Could not find the library for "${debugName}", "${breakpadId}".`
+ );
+ }
+
+ const { name, path, arch } = lib;
+ const candidatePaths = [];
+
+ // The location of the binary. If the profile was obtained on this machine
+ // (and not, for example, on an Android device), this file should always
+ // exist.
+ candidatePaths.push(path);
+
+ // Fall back to searching in the manually specified objdirs.
+ // This is needed if the debuggee is a build running on a remote machine that
+ // was compiled by the developer on *this* machine (the "host machine"). In
+ // that case, the objdir will contain the compiled binary.
+ for (const objdirPath of this._objdirs) {
+ try {
+ // Binaries are usually expected to exist at objdir/dist/bin/filename.
+ candidatePaths.push(PathUtils.join(objdirPath, "dist", "bin", name));
+
+ // Also search in the "objdir" directory itself (not just in dist/bin).
+ // If, for some unforeseen reason, the relevant binary is not inside the
+ // objdirs dist/bin/ directory, this provides a way out because it lets the
+ // user specify the actual location.
+ candidatePaths.push(PathUtils.join(objdirPath, name));
+ } catch (e) {
+ // PathUtils.join throws if objdirPath is not an absolute path.
+ // Ignore those invalid objdir paths.
+ }
+ }
+
+ // On macOS, for system libraries, add a final fallback for the dyld shared
+ // cache. Starting with macOS 11, most system libraries are located in this
+ // system-wide cache file and not present as individual files.
+ if (arch && (path.startsWith("/usr/") || path.startsWith("/System/"))) {
+ // Use the special syntax `dyldcache:<dyldcachepath>:<librarypath>`.
+
+ // Dyld cache location used on macOS 13+:
+ candidatePaths.push(
+ `dyldcache:/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_${arch}:${path}`
+ );
+ // Dyld cache location used on macOS 11 and 12:
+ candidatePaths.push(
+ `dyldcache:/System/Library/dyld/dyld_shared_cache_${arch}:${path}`
+ );
+ }
+
+ return candidatePaths;
+ }
+
+ /**
+ * Asynchronously prepare the file at `path` for synchronous reading.
+ * This method is called by wasm code (via the bindings).
+ *
+ * @param {string} path
+ * @returns {FileHandle}
+ */
+ async readFile(path) {
+ const info = await IOUtils.stat(path);
+ if (info.type === "directory") {
+ throw new Error(`Path "${path}" is a directory.`);
+ }
+
+ return IOUtils.openFileForSyncReading(path);
+ }
+}
+
+/** @param {MessageEvent<SymbolicationWorkerInitialMessage>} e */
+onmessage = async e => {
+ try {
+ const { request, libInfoMap, objdirs, module } = e.data;
+
+ if (!(module instanceof WebAssembly.Module)) {
+ throw new Error("invalid WebAssembly module");
+ }
+
+ // Instantiate the WASM module.
+ await wasm_bindgen(module);
+
+ const helper = new FileAndPathHelper(libInfoMap, objdirs);
+
+ switch (request.type) {
+ case "GET_SYMBOL_TABLE": {
+ const { debugName, breakpadId } = request;
+ const result = await getCompactSymbolTable(
+ debugName,
+ breakpadId,
+ helper
+ );
+ postMessage(
+ { result },
+ result.map(r => r.buffer)
+ );
+ break;
+ }
+ case "QUERY_SYMBOLICATION_API": {
+ const { path, requestJson } = request;
+ const result = await queryAPI(path, requestJson, helper);
+ postMessage({ result });
+ break;
+ }
+ default:
+ throw new Error(`Unexpected request type ${request.type}`);
+ }
+ } catch (error) {
+ postMessage({ error: createPlainErrorObject(error) });
+ }
+ close();
+};
+
+onunhandledrejection = e => {
+ // Unhandled rejections can happen if the WASM code throws a
+ // "RuntimeError: unreachable executed" exception, which can happen
+ // if the Rust code panics or runs out of memory.
+ // These panics currently are not propagated to the promise reject
+ // callback, see https://github.com/rustwasm/wasm-bindgen/issues/2724 .
+ // Ideally, the Rust code should never panic and handle all error
+ // cases gracefully.
+ e.preventDefault();
+ postMessage({ error: createPlainErrorObject(e.reason) });
+};
+
+// Catch any other unhandled errors, just to be sure.
+onerror = e => {
+ postMessage({ error: createPlainErrorObject(e) });
+};
diff --git a/devtools/client/performance-new/shared/symbolication.jsm.js b/devtools/client/performance-new/shared/symbolication.jsm.js
new file mode 100644
index 0000000000..f79cffe6cb
--- /dev/null
+++ b/devtools/client/performance-new/shared/symbolication.jsm.js
@@ -0,0 +1,364 @@
+/* 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";
+
+/** @type {any} */
+const lazy = {};
+
+/**
+ * @typedef {import("../@types/perf").Library} Library
+ * @typedef {import("../@types/perf").PerfFront} PerfFront
+ * @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
+ * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService
+ * @typedef {import("../@types/perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage
+ */
+
+/**
+ * @template R
+ * @typedef {import("../@types/perf").SymbolicationWorkerReplyData<R>} SymbolicationWorkerReplyData<R>
+ */
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+/** @type {any} */
+const global = globalThis;
+
+// This module obtains symbol tables for binaries.
+// It does so with the help of a WASM module which gets pulled in from the
+// internet on demand. We're doing this purely for the purposes of saving on
+// code size. The contents of the WASM module are expected to be static, they
+// are checked against the hash specified below.
+// The WASM code is run on a ChromeWorker thread. It takes the raw byte
+// contents of the to-be-dumped binary (and of an additional optional pdb file
+// on Windows) as its input, and returns a set of typed arrays which make up
+// the symbol table.
+
+// Don't let the strange looking URLs and strings below scare you.
+// The hash check ensures that the contents of the wasm module are what we
+// expect them to be.
+// The source code is at https://github.com/mstange/profiler-get-symbols/ .
+// Documentation is at https://docs.rs/samply-api/ .
+// The sha384 sum can be computed with the following command (tested on macOS):
+// shasum -b -a 384 profiler_get_symbols_wasm_bg.wasm | awk '{ print $1 }' | xxd -r -p | base64
+
+// Generated from https://github.com/mstange/profiler-get-symbols/commit/0373708893e45e8299e58ca692764be448e3457d
+const WASM_MODULE_URL =
+ "https://storage.googleapis.com/firefox-profiler-get-symbols/0373708893e45e8299e58ca692764be448e3457d.wasm";
+const WASM_MODULE_INTEGRITY =
+ "sha384-rUGgHTg1eAKP2MB4JcX/HGROSBlRUmvpm6FFIihH0gGQ74zfJE2p7P8cxR86faQ7";
+
+const EXPIRY_TIME_IN_MS = 5 * 60 * 1000; // 5 minutes
+
+/** @type {Promise<WebAssembly.Module> | null} */
+let gCachedWASMModulePromise = null;
+let gCachedWASMModuleExpiryTimer = 0;
+
+// Keep active workers alive (see bug 1592227).
+const gActiveWorkers = new Set();
+
+function clearCachedWASMModule() {
+ gCachedWASMModulePromise = null;
+ gCachedWASMModuleExpiryTimer = 0;
+}
+
+function getWASMProfilerGetSymbolsModule() {
+ if (!gCachedWASMModulePromise) {
+ gCachedWASMModulePromise = (async function () {
+ const request = new Request(WASM_MODULE_URL, {
+ integrity: WASM_MODULE_INTEGRITY,
+ credentials: "omit",
+ });
+ return WebAssembly.compileStreaming(fetch(request));
+ })();
+ }
+
+ // Reset expiry timer.
+ lazy.clearTimeout(gCachedWASMModuleExpiryTimer);
+ gCachedWASMModuleExpiryTimer = lazy.setTimeout(
+ clearCachedWASMModule,
+ EXPIRY_TIME_IN_MS
+ );
+
+ return gCachedWASMModulePromise;
+}
+
+/**
+ * Handle the entire life cycle of a worker, and report its result.
+ * This method creates a new worker, sends the initial message to it, handles
+ * any errors, and accepts the result.
+ * Returns a promise that resolves with the contents of the (singular) result
+ * message or rejects with an error.
+ *
+ * @template M
+ * @template R
+ * @param {string} workerURL
+ * @param {M} initialMessageToWorker
+ * @returns {Promise<R>}
+ */
+async function getResultFromWorker(workerURL, initialMessageToWorker) {
+ return new Promise((resolve, reject) => {
+ const worker = new ChromeWorker(workerURL);
+ gActiveWorkers.add(worker);
+
+ /** @param {MessageEvent<SymbolicationWorkerReplyData<R>>} msg */
+ worker.onmessage = msg => {
+ gActiveWorkers.delete(worker);
+ if ("error" in msg.data) {
+ const error = msg.data.error;
+ if (error.name) {
+ // Turn the JSON error object into a real Error object.
+ const { name, message, fileName, lineNumber } = error;
+ const ErrorObjConstructor =
+ name in global && Error.isPrototypeOf(global[name])
+ ? global[name]
+ : Error;
+ const e = new ErrorObjConstructor(message, fileName, lineNumber);
+ e.name = name;
+ reject(e);
+ } else {
+ reject(error);
+ }
+ return;
+ }
+ resolve(msg.data.result);
+ };
+
+ // Handle uncaught errors from the worker script. onerror is called if
+ // there's a syntax error in the worker script, for example, or when an
+ // unhandled exception is thrown, but not for unhandled promise
+ // rejections. Without this handler, mistakes during development such as
+ // syntax errors can be hard to track down.
+ worker.onerror = errorEvent => {
+ gActiveWorkers.delete(worker);
+ worker.terminate();
+ if (ErrorEvent.isInstance(errorEvent)) {
+ const { message, filename, lineno } = errorEvent;
+ const error = new Error(`${message} at ${filename}:${lineno}`);
+ error.name = "WorkerError";
+ reject(error);
+ } else {
+ reject(new Error("Error in worker"));
+ }
+ };
+
+ // Handle errors from messages that cannot be deserialized. I'm not sure
+ // how to get into such a state, but having this handler seems like a good
+ // idea.
+ worker.onmessageerror = () => {
+ gActiveWorkers.delete(worker);
+ worker.terminate();
+ reject(new Error("Error in worker"));
+ };
+
+ worker.postMessage(initialMessageToWorker);
+ });
+}
+
+/**
+ * @param {PerfFront} perfFront
+ * @param {string} path
+ * @param {string} breakpadId
+ * @returns {Promise<SymbolTableAsTuple>}
+ */
+async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
+ const [addresses, index, buffer] = await perfFront.getSymbolTable(
+ path,
+ breakpadId
+ );
+ // The protocol transmits these arrays as plain JavaScript arrays of
+ // numbers, but we want to pass them on as typed arrays. Convert them now.
+ return [
+ new Uint32Array(addresses),
+ new Uint32Array(index),
+ new Uint8Array(buffer),
+ ];
+}
+
+/**
+ * Profiling through the DevTools remote debugging protocol supports multiple
+ * different modes. This class is specialized to handle various profiling
+ * modes such as:
+ *
+ * 1) Profiling the same browser on the same machine.
+ * 2) Profiling a remote browser on the same machine.
+ * 3) Profiling a remote browser on a different device.
+ *
+ * It's also built to handle symbolication requests for both Gecko libraries and
+ * system libraries. However, it only handles cases where symbol information
+ * can be found in a local file on this machine. There is one case that is not
+ * covered by that restriction: Android system libraries. That case requires
+ * the help of the perf actor and is implemented in
+ * LocalSymbolicationServiceWithRemoteSymbolTableFallback.
+ */
+class LocalSymbolicationService {
+ /**
+ * @param {Library[]} sharedLibraries - Information about the shared libraries.
+ * This allows mapping (debugName, breakpadId) pairs to the absolute path of
+ * the binary and/or PDB file, and it ensures that these absolute paths come
+ * from a trusted source and not from the profiler UI.
+ * @param {string[]} objdirs - An array of objdir paths
+ * on the host machine that should be searched for relevant build artifacts.
+ */
+ constructor(sharedLibraries, objdirs) {
+ this._libInfoMap = new Map(
+ sharedLibraries.map(lib => {
+ const { debugName, breakpadId } = lib;
+ const key = `${debugName}:${breakpadId}`;
+ return [key, lib];
+ })
+ );
+ this._objdirs = objdirs;
+ }
+
+ /**
+ * @param {string} debugName
+ * @param {string} breakpadId
+ * @returns {Promise<SymbolTableAsTuple>}
+ */
+ async getSymbolTable(debugName, breakpadId) {
+ const module = await getWASMProfilerGetSymbolsModule();
+ /** @type {SymbolicationWorkerInitialMessage} */
+ const initialMessage = {
+ request: {
+ type: "GET_SYMBOL_TABLE",
+ debugName,
+ breakpadId,
+ },
+ libInfoMap: this._libInfoMap,
+ objdirs: this._objdirs,
+ module,
+ };
+ return getResultFromWorker(
+ "resource://devtools/client/performance-new/shared/symbolication-worker.js",
+ initialMessage
+ );
+ }
+
+ /**
+ * @param {string} path
+ * @param {string} requestJson
+ * @returns {Promise<string>}
+ */
+ async querySymbolicationApi(path, requestJson) {
+ const module = await getWASMProfilerGetSymbolsModule();
+ /** @type {SymbolicationWorkerInitialMessage} */
+ const initialMessage = {
+ request: {
+ type: "QUERY_SYMBOLICATION_API",
+ path,
+ requestJson,
+ },
+ libInfoMap: this._libInfoMap,
+ objdirs: this._objdirs,
+ module,
+ };
+ return getResultFromWorker(
+ "resource://devtools/client/performance-new/shared/symbolication-worker.js",
+ initialMessage
+ );
+ }
+}
+
+/**
+ * An implementation of the SymbolicationService interface which also
+ * covers the Android system library case.
+ * We first try to get symbols from the wrapped SymbolicationService.
+ * If that fails, we try to get the symbol table through the perf actor.
+ */
+class LocalSymbolicationServiceWithRemoteSymbolTableFallback {
+ /**
+ * @param {SymbolicationService} symbolicationService - The regular symbolication service.
+ * @param {Library[]} sharedLibraries - Information about the shared libraries
+ * @param {PerfFront} perfFront - A perf actor, to obtain symbol
+ * tables from remote targets
+ */
+ constructor(symbolicationService, sharedLibraries, perfFront) {
+ this._symbolicationService = symbolicationService;
+ this._libs = sharedLibraries;
+ this._perfFront = perfFront;
+ }
+
+ /**
+ * @param {string} debugName
+ * @param {string} breakpadId
+ * @returns {Promise<SymbolTableAsTuple>}
+ */
+ async getSymbolTable(debugName, breakpadId) {
+ try {
+ return await this._symbolicationService.getSymbolTable(
+ debugName,
+ breakpadId
+ );
+ } catch (errorFromLocalFiles) {
+ // Try to obtain the symbol table on the debuggee. We get into this
+ // branch in the following cases:
+ // - Android system libraries
+ // - Firefox binaries that have no matching equivalent on the host
+ // machine, for example because the user didn't point us at the
+ // corresponding objdir, or if the build was compiled somewhere
+ // else, or if the build on the device is outdated.
+ // For now, the "debuggee" is never a Windows machine, which is why we don't
+ // need to pass the library's debugPath. (path and debugPath are always the
+ // same on non-Windows.)
+ const lib = this._libs.find(
+ l => l.debugName === debugName && l.breakpadId === breakpadId
+ );
+ if (!lib) {
+ throw new Error(
+ `Could not find the library for "${debugName}", "${breakpadId}" after falling ` +
+ `back to remote symbol table querying because regular getSymbolTable failed ` +
+ `with error: ${errorFromLocalFiles.message}.`
+ );
+ }
+ return getSymbolTableFromDebuggee(this._perfFront, lib.path, breakpadId);
+ }
+ }
+
+ /**
+ * @param {string} path
+ * @param {string} requestJson
+ * @returns {Promise<string>}
+ */
+ async querySymbolicationApi(path, requestJson) {
+ return this._symbolicationService.querySymbolicationApi(path, requestJson);
+ }
+}
+
+/**
+ * Return an object that implements the SymbolicationService interface.
+ *
+ * @param {Library[]} sharedLibraries - Information about the shared libraries
+ * @param {string[]} objdirs - An array of objdir paths
+ * on the host machine that should be searched for relevant build artifacts.
+ * @param {PerfFront} [perfFront] - An optional perf actor, to obtain symbol
+ * tables from remote targets
+ * @return {SymbolicationService}
+ */
+function createLocalSymbolicationService(sharedLibraries, objdirs, perfFront) {
+ const service = new LocalSymbolicationService(sharedLibraries, objdirs);
+ if (perfFront) {
+ return new LocalSymbolicationServiceWithRemoteSymbolTableFallback(
+ service,
+ sharedLibraries,
+ perfFront
+ );
+ }
+ return service;
+}
+
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */
+var module = {};
+
+module.exports = {
+ createLocalSymbolicationService,
+};
+
+// 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/shared/typescript-lazy-load.jsm.js b/devtools/client/performance-new/shared/typescript-lazy-load.jsm.js
new file mode 100644
index 0000000000..c640572c53
--- /dev/null
+++ b/devtools/client/performance-new/shared/typescript-lazy-load.jsm.js
@@ -0,0 +1,55 @@
+/* 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";
+
+/**
+ * TypeScript can't understand the lazyRequireGetter mechanism, due to how it defines
+ * properties as a getter. This function, instead provides lazy loading in a
+ * TypeScript-friendly manner. It applies the lazy load memoization to each property
+ * of the provided object.
+ *
+ * Example usage:
+ *
+ * const lazy = createLazyLoaders({
+ * moduleA: () => require("module/a"),
+ * moduleB: () => require("module/b"),
+ * });
+ *
+ * Later:
+ *
+ * const moduleA = lazy.moduleA();
+ * const { objectInModuleB } = lazy.moduleB();
+ *
+ * @template T
+ * @param {T} definition - An object where each property has a function that loads a module.
+ * @returns {T} - The load memoized version of T.
+ */
+function createLazyLoaders(definition) {
+ /** @type {any} */
+ const result = {};
+ for (const [key, callback] of Object.entries(definition)) {
+ /** @type {any} */
+ let cache;
+ result[key] = () => {
+ if (cache === undefined) {
+ cache = callback();
+ }
+ return cache;
+ };
+ }
+ return result;
+}
+
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */
+var module = {};
+
+module.exports = {
+ createLazyLoaders,
+};
+
+// 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/shared/utils.js b/devtools/client/performance-new/shared/utils.js
new file mode 100644
index 0000000000..034e572186
--- /dev/null
+++ b/devtools/client/performance-new/shared/utils.js
@@ -0,0 +1,566 @@
+/* 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
+/**
+ * @typedef {import("../@types/perf").NumberScaler} NumberScaler
+ * @typedef {import("../@types/perf").ScaleFunctions} ScaleFunctions
+ * @typedef {import("../@types/perf").FeatureDescription} FeatureDescription
+ */
+"use strict";
+
+const UNITS = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
+
+const AppConstants = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+).AppConstants;
+
+/**
+ * Linearly interpolate between values.
+ * https://en.wikipedia.org/wiki/Linear_interpolation
+ *
+ * @param {number} frac - Value ranged 0 - 1 to interpolate between the range start and range end.
+ * @param {number} rangeStart - The value to start from.
+ * @param {number} rangeEnd - The value to interpolate to.
+ * @returns {number}
+ */
+function lerp(frac, rangeStart, rangeEnd) {
+ return (1 - frac) * rangeStart + frac * rangeEnd;
+}
+
+/**
+ * Make sure a value is clamped between a min and max value.
+ *
+ * @param {number} val - The value to clamp.
+ * @param {number} min - The minimum value.
+ * @param {number} max - The max value.
+ * @returns {number}
+ */
+function clamp(val, min, max) {
+ return Math.max(min, Math.min(max, val));
+}
+
+/**
+ * Formats a file size.
+ * @param {number} num - The number (in bytes) to format.
+ * @returns {string} e.g. "10 B", "100 MiB"
+ */
+function formatFileSize(num) {
+ if (!Number.isFinite(num)) {
+ throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
+ }
+
+ const neg = num < 0;
+
+ if (neg) {
+ num = -num;
+ }
+
+ if (num < 1) {
+ return (neg ? "-" : "") + num + " B";
+ }
+
+ const exponent = Math.min(
+ Math.floor(Math.log2(num) / Math.log2(1024)),
+ UNITS.length - 1
+ );
+ const numStr = Number((num / Math.pow(1024, exponent)).toPrecision(3));
+ const unit = UNITS[exponent];
+
+ return (neg ? "-" : "") + numStr + " " + unit;
+}
+
+/**
+ * Creates numbers that increment linearly within a base 10 scale:
+ * 0.1, 0.2, 0.3, ..., 0.8, 0.9, 1, 2, 3, ..., 9, 10, 20, 30, etc.
+ *
+ * @param {number} rangeStart
+ * @param {number} rangeEnd
+ *
+ * @returns {ScaleFunctions}
+ */
+function makeLinear10Scale(rangeStart, rangeEnd) {
+ const start10 = Math.log10(rangeStart);
+ const end10 = Math.log10(rangeEnd);
+
+ if (!Number.isInteger(start10)) {
+ throw new Error(`rangeStart is not a power of 10: ${rangeStart}`);
+ }
+
+ if (!Number.isInteger(end10)) {
+ throw new Error(`rangeEnd is not a power of 10: ${rangeEnd}`);
+ }
+
+ // Intervals are base 10 intervals:
+ // - [0.01 .. 0.09]
+ // - [0.1 .. 0.9]
+ // - [1 .. 9]
+ // - [10 .. 90]
+ const intervals = end10 - start10;
+
+ // Note that there are only 9 steps per interval, not 10:
+ // 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9
+ const STEP_PER_INTERVAL = 9;
+
+ const steps = intervals * STEP_PER_INTERVAL;
+
+ /** @type {NumberScaler} */
+ const fromFractionToValue = frac => {
+ const step = Math.round(frac * steps);
+ const base = Math.floor(step / STEP_PER_INTERVAL);
+ const factor = (step % STEP_PER_INTERVAL) + 1;
+ return Math.pow(10, base) * factor * rangeStart;
+ };
+
+ /** @type {NumberScaler} */
+ const fromValueToFraction = value => {
+ const interval = Math.floor(Math.log10(value / rangeStart));
+ const base = rangeStart * Math.pow(10, interval);
+ return (interval * STEP_PER_INTERVAL + value / base - 1) / steps;
+ };
+
+ /** @type {NumberScaler} */
+ const fromFractionToSingleDigitValue = frac => {
+ return +fromFractionToValue(frac).toPrecision(1);
+ };
+
+ return {
+ // Takes a number ranged 0-1 and returns it within the range.
+ fromFractionToValue,
+ // Takes a number in the range, and returns a value between 0-1
+ fromValueToFraction,
+ // Takes a number ranged 0-1 and returns a value in the range, but with
+ // a single digit value.
+ fromFractionToSingleDigitValue,
+ // The number of steps available on this scale.
+ steps,
+ };
+}
+
+/**
+ * Creates numbers that scale exponentially as powers of 2.
+ *
+ * @param {number} rangeStart
+ * @param {number} rangeEnd
+ *
+ * @returns {ScaleFunctions}
+ */
+function makePowerOf2Scale(rangeStart, rangeEnd) {
+ const startExp = Math.log2(rangeStart);
+ const endExp = Math.log2(rangeEnd);
+
+ if (!Number.isInteger(startExp)) {
+ throw new Error(`rangeStart is not a power of 2: ${rangeStart}`);
+ }
+
+ if (!Number.isInteger(endExp)) {
+ throw new Error(`rangeEnd is not a power of 2: ${rangeEnd}`);
+ }
+
+ const steps = endExp - startExp;
+
+ /** @type {NumberScaler} */
+ const fromFractionToValue = frac =>
+ Math.pow(2, Math.round((1 - frac) * startExp + frac * endExp));
+
+ /** @type {NumberScaler} */
+ const fromValueToFraction = value =>
+ (Math.log2(value) - startExp) / (endExp - startExp);
+
+ /** @type {NumberScaler} */
+ const fromFractionToSingleDigitValue = frac => {
+ // fromFractionToValue returns an exact power of 2, we don't want to change
+ // its precision. Note that formatFileSize will display it in a nice binary
+ // unit with up to 3 digits.
+ return fromFractionToValue(frac);
+ };
+
+ return {
+ // Takes a number ranged 0-1 and returns it within the range.
+ fromFractionToValue,
+ // Takes a number in the range, and returns a value between 0-1
+ fromValueToFraction,
+ // Takes a number ranged 0-1 and returns a value in the range, but with
+ // a single digit value.
+ fromFractionToSingleDigitValue,
+ // The number of steps available on this scale.
+ steps,
+ };
+}
+
+/**
+ * Scale a source range to a destination range, but clamp it within the
+ * destination range.
+ * @param {number} val - The source range value to map to the destination range,
+ * @param {number} sourceRangeStart,
+ * @param {number} sourceRangeEnd,
+ * @param {number} destRangeStart,
+ * @param {number} destRangeEnd
+ */
+function scaleRangeWithClamping(
+ val,
+ sourceRangeStart,
+ sourceRangeEnd,
+ destRangeStart,
+ destRangeEnd
+) {
+ const frac = clamp(
+ (val - sourceRangeStart) / (sourceRangeEnd - sourceRangeStart),
+ 0,
+ 1
+ );
+ return lerp(frac, destRangeStart, destRangeEnd);
+}
+
+/**
+ * Use some heuristics to guess at the overhead of the recording settings.
+ *
+ * TODO - Bug 1597383. The UI for this has been removed, but it needs to be reworked
+ * for new overhead calculations. Keep it for now in tree.
+ *
+ * @param {number} interval
+ * @param {number} bufferSize
+ * @param {string[]} features - List of the selected features.
+ */
+function calculateOverhead(interval, bufferSize, features) {
+ // NOT "nostacksampling" (double negative) means periodic sampling is on.
+ const periodicSampling = !features.includes("nostacksampling");
+ const overheadFromSampling = periodicSampling
+ ? scaleRangeWithClamping(
+ Math.log(interval),
+ Math.log(0.05),
+ Math.log(1),
+ 1,
+ 0
+ ) +
+ scaleRangeWithClamping(
+ Math.log(interval),
+ Math.log(1),
+ Math.log(100),
+ 0.1,
+ 0
+ )
+ : 0;
+ const overheadFromBuffersize = scaleRangeWithClamping(
+ Math.log(bufferSize),
+ Math.log(10),
+ Math.log(1000000),
+ 0,
+ 0.1
+ );
+ const overheadFromStackwalk =
+ features.includes("stackwalk") && periodicSampling ? 0.05 : 0;
+ const overheadFromJavaScript =
+ features.includes("js") && periodicSampling ? 0.05 : 0;
+ const overheadFromJSTracer = features.includes("jstracer") ? 0.05 : 0;
+ const overheadFromJSAllocations = features.includes("jsallocations")
+ ? 0.05
+ : 0;
+ const overheadFromNativeAllocations = features.includes("nativeallocations")
+ ? 0.5
+ : 0;
+
+ return clamp(
+ overheadFromSampling +
+ overheadFromBuffersize +
+ overheadFromStackwalk +
+ overheadFromJavaScript +
+ overheadFromJSTracer +
+ overheadFromJSAllocations +
+ overheadFromNativeAllocations,
+ 0,
+ 1
+ );
+}
+
+/**
+ * Given an array of absolute paths on the file system, return an array that
+ * doesn't contain the common prefix of the paths; in other words, if all paths
+ * share a common ancestor directory, cut off the path to that ancestor
+ * directory and only leave the path components that differ.
+ * This makes some lists look a little nicer. For example, this turns the list
+ * ["/Users/foo/code/obj-m-android-opt", "/Users/foo/code/obj-m-android-debug"]
+ * into the list ["obj-m-android-opt", "obj-m-android-debug"].
+ *
+ * @param {string[]} pathArray The array of absolute paths.
+ * @returns {string[]} A new array with the described adjustment.
+ */
+function withCommonPathPrefixRemoved(pathArray) {
+ if (pathArray.length === 0) {
+ return [];
+ }
+
+ const firstPath = pathArray[0];
+ const isWin = /^[A-Za-z]:/.test(firstPath);
+ const firstWinDrive = getWinDrive(firstPath);
+ for (const path of pathArray) {
+ const winDrive = getWinDrive(path);
+
+ if (!PathUtils.isAbsolute(path) || winDrive !== firstWinDrive) {
+ // We expect all paths to be absolute and on Windows we expect all
+ // paths to be on the same disk. If this is not the case return the
+ // original array.
+ return pathArray;
+ }
+ }
+
+ // At this point we're either not on Windows or all paths are on the same
+ // Windows disk and all paths are absolute.
+ // Find the common prefix. Start by assuming the entire path except for the
+ // last folder is shared.
+ const splitPaths = pathArray.map(path => PathUtils.split(path));
+ const [firstSplitPath, ...otherSplitPaths] = splitPaths;
+ const prefix = firstSplitPath.slice(0, -1);
+ for (const sp of otherSplitPaths) {
+ prefix.length = Math.min(prefix.length, sp.length - 1);
+ for (let i = 0; i < prefix.length; i++) {
+ if (prefix[i] !== sp[i]) {
+ prefix.length = i;
+ break;
+ }
+ }
+ }
+ if (
+ prefix.length === 0 ||
+ (prefix.length === 1 && (prefix[0] === firstWinDrive || prefix[0] === "/"))
+ ) {
+ // There is no shared prefix.
+ // We treat a prefix of ["/"] as "no prefix", too: Absolute paths on
+ // non-Windows start with a slash, so PathUtils.split(path) always returns
+ // an array whose first element is "/" on those platforms.
+ // Stripping off a prefix of ["/"] from the split paths would simply remove
+ // the leading slash from the un-split paths, which is not useful.
+ return pathArray;
+ }
+
+ // Strip the common prefix from all paths.
+ return splitPaths.map(sp => {
+ return sp.slice(prefix.length).join(isWin ? "\\" : "/");
+ });
+}
+
+/**
+ * This method has been copied from `ospath_win.jsm` as part of the migration
+ * from `OS.Path` to `PathUtils`.
+ *
+ * Return the windows drive name of a path, or |null| if the path does
+ * not contain a drive name.
+ *
+ * Drive name appear either as "DriveName:..." (the return drive
+ * name includes the ":") or "\\\\DriveName..." (the returned drive name
+ * includes "\\\\").
+ *
+ * @param {string} path The path from which we are to return the Windows drive name.
+ * @returns {?string} Windows drive name e.g. "C:" or null if path is not a Windows path.
+ */
+function getWinDrive(path) {
+ if (path == null) {
+ throw new TypeError("path is invalid");
+ }
+
+ if (path.startsWith("\\\\")) {
+ // UNC path
+ if (path.length == 2) {
+ return null;
+ }
+ const index = path.indexOf("\\", 2);
+ if (index == -1) {
+ return path;
+ }
+ return path.slice(0, index);
+ }
+ // Non-UNC path
+ const index = path.indexOf(":");
+ if (index <= 0) {
+ return null;
+ }
+ return path.slice(0, index + 1);
+}
+
+class UnhandledCaseError extends Error {
+ /**
+ * @param {never} value - Check that
+ * @param {string} typeName - A friendly type name.
+ */
+ constructor(value, typeName) {
+ super(`There was an unhandled case for "${typeName}": ${value}`);
+ this.name = "UnhandledCaseError";
+ }
+}
+
+/**
+ * @type {FeatureDescription[]}
+ */
+const featureDescriptions = [
+ {
+ name: "Native Stacks",
+ value: "stackwalk",
+ title:
+ "Record native stacks (C++ and Rust). This is not available on all platforms.",
+ recommended: true,
+ disabledReason: "Native stack walking is not supported on this platform.",
+ },
+ {
+ name: "JavaScript",
+ value: "js",
+ title:
+ "Record JavaScript stack information, and interleave it with native stacks.",
+ recommended: true,
+ },
+ {
+ name: "CPU Utilization",
+ value: "cpu",
+ title:
+ "Record how much CPU has been used between samples by each profiled thread.",
+ recommended: true,
+ },
+ {
+ name: "Java",
+ value: "java",
+ title: "Profile Java code",
+ disabledReason: "This feature is only available on Android.",
+ },
+ {
+ name: "No Periodic Sampling",
+ value: "nostacksampling",
+ title: "Disable interval-based stack sampling",
+ },
+ {
+ name: "Main Thread File IO",
+ value: "mainthreadio",
+ title: "Record main thread File I/O markers.",
+ },
+ {
+ name: "Profiled Threads File IO",
+ value: "fileio",
+ title: "Record File I/O markers from only profiled threads.",
+ },
+ {
+ name: "All File IO",
+ value: "fileioall",
+ title:
+ "Record File I/O markers from all threads, even unregistered threads.",
+ },
+ {
+ name: "No Marker Stacks",
+ value: "nomarkerstacks",
+ title: "Do not capture stacks when recording markers, to reduce overhead.",
+ },
+ {
+ name: "Sequential Styling",
+ value: "seqstyle",
+ title: "Disable parallel traversal in styling.",
+ },
+ {
+ name: "Screenshots",
+ value: "screenshots",
+ title: "Record screenshots of all browser windows.",
+ },
+ {
+ name: "JSTracer",
+ value: "jstracer",
+ title: "Trace JS engine",
+ experimental: true,
+ disabledReason:
+ "JS Tracer is currently disabled due to crashes. See Bug 1565788.",
+ },
+ {
+ name: "IPC Messages",
+ value: "ipcmessages",
+ title: "Track IPC messages.",
+ },
+ {
+ name: "JS Allocations",
+ value: "jsallocations",
+ title: "Track JavaScript allocations",
+ },
+ {
+ name: "Native Allocations",
+ value: "nativeallocations",
+ title: "Track native allocations",
+ },
+ {
+ name: "Audio Callback Tracing",
+ value: "audiocallbacktracing",
+ title: "Trace real-time audio callbacks.",
+ },
+ {
+ name: "No Timer Resolution Change",
+ value: "notimerresolutionchange",
+ title:
+ "Do not enhance the timer resolution for sampling intervals < 10ms, to " +
+ "avoid affecting timer-sensitive code. Warning: Sampling interval may " +
+ "increase in some processes.",
+ disabledReason: "Windows only.",
+ },
+ {
+ name: "CPU Utilization - All Threads",
+ value: "cpuallthreads",
+ title:
+ "Record how much CPU has been used between samples by ALL registered thread.",
+ experimental: true,
+ },
+ {
+ name: "Periodic Sampling - All Threads",
+ value: "samplingallthreads",
+ title: "Capture stack samples in ALL registered thread.",
+ experimental: true,
+ },
+ {
+ name: "Markers - All Threads",
+ value: "markersallthreads",
+ title: "Record markers in ALL registered threads.",
+ experimental: true,
+ },
+ {
+ name: "Unregistered Threads",
+ value: "unregisteredthreads",
+ title:
+ "Periodically discover unregistered threads and record them and their " +
+ "CPU utilization as markers in the main thread -- Beware: expensive!",
+ experimental: true,
+ },
+ {
+ name: "Process CPU Utilization",
+ value: "processcpu",
+ title:
+ "Record how much CPU has been used between samples by each process. " +
+ "To see graphs: When viewing the profile, open the JS console and run: " +
+ "experimental.enableProcessCPUTracks()",
+ experimental: true,
+ },
+ {
+ name: "Power Use",
+ value: "power",
+ title: (() => {
+ switch (AppConstants.platform) {
+ case "win":
+ return (
+ "Record the value of every energy meter available on the system with " +
+ "each sample. Only available on Windows 11 with Intel CPUs."
+ );
+ case "linux":
+ return (
+ "Record the power used by the entire system with each sample. " +
+ "Only available with Intel CPUs and requires setting the sysctl kernel.perf_event_paranoid to 0."
+ );
+ case "macosx":
+ return "Record the power used by the entire system (Intel) or each process (Apple Silicon) with each sample.";
+ default:
+ return "Not supported on this platform.";
+ }
+ })(),
+ experimental: true,
+ },
+];
+
+module.exports = {
+ formatFileSize,
+ makeLinear10Scale,
+ makePowerOf2Scale,
+ scaleRangeWithClamping,
+ calculateOverhead,
+ withCommonPathPrefixRemoved,
+ UnhandledCaseError,
+ featureDescriptions,
+};