gecko-dev/devtools/client/performance-new/popup/background.jsm.js

621 строка
19 KiB
JavaScript

/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { createLazyLoaders } = ChromeUtils.import(
"resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
);
// For some reason TypeScript was giving me an error when de-structuring AppConstants. I
// suspect a bug in TypeScript was at play.
const AppConstants = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
).AppConstants;
/**
* @typedef {import("../@types/perf").RecordingStateFromPreferences} RecordingStateFromPreferences
* @typedef {import("../@types/perf").PopupBackgroundFeatures} PopupBackgroundFeatures
* @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").MessageFromFrontend} MessageFromFrontend
* @typedef {import("../@types/perf").PageContext} PageContext
* @typedef {import("../@types/perf").Presets} Presets
* @typedef {import("../@types/perf").ProfilerViewMode} ProfilerViewMode
*/
/** @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";
// Lazily load the require function, when it's needed.
ChromeUtils.defineModuleGetter(
this,
"require",
"resource://devtools/shared/Loader.jsm"
);
// 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({
OS: () => ChromeUtils.import("resource://gre/modules/osfile.jsm"),
Utils: () => require("devtools/client/performance-new/utils"),
BrowserModule: () => require("devtools/client/performance-new/browser"),
RecordingUtils: () =>
require("devtools/shared/performance-new/recording-utils"),
CustomizableUI: () =>
ChromeUtils.import("resource:///modules/CustomizableUI.jsm"),
PerfSymbolication: () =>
ChromeUtils.import(
"resource://devtools/client/performance-new/symbolication.jsm.js"
),
PreferenceManagement: () =>
require("devtools/client/performance-new/preference-management"),
ProfilerMenuButton: () =>
ChromeUtils.import(
"resource://devtools/client/performance-new/popup/menu-button.jsm.js"
),
});
// TODO - Bug 1681539. The presets still need to be localized.
/** @type {Presets} */
const presets = {
"web-developer": {
label: "Web Developer",
description:
"Recommended preset for most web app debugging, with low overhead.",
entries: 128 * 1024 * 1024,
interval: 1,
features: ["screenshots", "js"],
threads: ["GeckoMain", "Compositor", "Renderer", "DOM Worker"],
duration: 0,
profilerViewMode: "active-tab",
},
"firefox-platform": {
label: "Firefox Platform",
description: "Recommended preset for internal Firefox platform debugging.",
entries: 128 * 1024 * 1024,
interval: 1,
features: ["screenshots", "js", "leaf", "stackwalk", "java"],
threads: ["GeckoMain", "Compositor", "Renderer", "SwComposite"],
duration: 0,
},
"firefox-front-end": {
label: "Firefox Front-End",
description: "Recommended preset for internal Firefox front-end debugging.",
entries: 128 * 1024 * 1024,
interval: 1,
features: ["screenshots", "js", "leaf", "stackwalk", "java"],
threads: ["GeckoMain", "Compositor", "Renderer", "DOM Worker"],
duration: 0,
},
graphics: {
label: "Firefox Graphics",
description:
"Recommended preset for Firefox graphics performance investigation.",
entries: 128 * 1024 * 1024,
interval: 1,
features: ["leaf", "stackwalk", "js", "java"],
threads: [
"GeckoMain",
"Compositor",
"Renderer",
"SwComposite",
"RenderBackend",
"SceneBuilder",
"WrWorker",
],
duration: 0,
},
media: {
label: "Media",
description: "Recommended preset for diagnosing audio and video problems.",
entries: 128 * 1024 * 1024,
interval: 1,
features: ["js", "leaf", "stackwalk", "audiocallbacktracing"],
threads: [
"AsyncCubebTask",
"AudioIPC",
"Compositor",
"GeckoMain",
"GraphRunner",
"MediaDecoderStateMachine",
"MediaPDecoder",
"MediaSupervisor",
"MediaTimer",
"NativeAudioCallback",
"RenderBackend",
"Renderer",
"SwComposite",
],
duration: 0,
},
};
/**
* This Map caches the symbols from the shared libraries.
* @type {Map<string, Library>}
*/
const symbolCache = new Map();
/**
* @param {PageContext} pageContext
* @param {string} debugName
* @param {string} breakpadId
*/
async function getSymbolsFromThisBrowser(pageContext, debugName, breakpadId) {
if (symbolCache.size === 0) {
// Prime the symbols cache.
for (const lib of Services.profiler.sharedLibraries) {
symbolCache.set(`${lib.debugName}/${lib.breakpadId}`, lib);
}
}
const cachedLib = symbolCache.get(`${debugName}/${breakpadId}`);
if (!cachedLib) {
throw new Error(
`The library ${debugName} ${breakpadId} is not in the ` +
"Services.profiler.sharedLibraries list, so the local path for it is not known " +
"and symbols for it can not be obtained. This usually happens if a content " +
"process uses a library that's not used in the parent process - " +
"Services.profiler.sharedLibraries only knows about libraries in the " +
"parent process."
);
}
const lib = cachedLib;
const objdirs = getObjdirPrefValue(pageContext);
const { getSymbolTableMultiModal } = lazy.PerfSymbolication();
return getSymbolTableMultiModal(lib, objdirs);
}
/**
* 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 postfix = getPrefPostfix(pageContext);
const presetName = Services.prefs.getCharPref(PRESET_PREF + postfix);
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 directly by devtools/startup/DevToolsStartup.jsm when
* using the shortcut keys to capture a profile.
* @param {PageContext} pageContext
* @return {Promise<void>}
*/
async function captureProfile(pageContext) {
if (!Services.profiler.IsActive()) {
// The profiler is not active, ignore this shortcut.
return;
}
if (Services.profiler.IsPaused()) {
// The profiler is already paused for capture, ignore this shortcut.
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();
const profile = await Services.profiler
.getProfileDataAsGzippedArrayBuffer()
.catch(
/** @type {(e: any) => {}} */ e => {
console.error(e);
return {};
}
);
const profilerViewMode = getProfilerViewModeForCurrentPreset(pageContext);
const receiveProfile = lazy.BrowserModule().receiveProfile;
receiveProfile(profile, profilerViewMode, (debugName, breakpadId) => {
return getSymbolsFromThisBrowser(pageContext, debugName, breakpadId);
});
Services.profiler.StopProfiler();
}
/**
* This function is only called by devtools/startup/DevToolsStartup.jsm when
* starting the profiler using the shortcut keys, through toggleProfiler below.
* @param {PageContext} pageContext
*/
function startProfiler(pageContext) {
const { translatePreferencesToState } = lazy.PreferenceManagement();
const {
entries,
interval,
features,
threads,
duration,
} = translatePreferencesToState(
getRecordingPreferences(pageContext, Services.profiler.GetFeatures())
);
// Get the active BrowsingContext ID from browser.
const { getActiveBrowsingContextID } = lazy.RecordingUtils();
const activeBrowsingContextID = getActiveBrowsingContextID();
Services.profiler.StartProfiler(
entries,
interval,
features,
threads,
activeBrowsingContextID,
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);
}
/**
* @param {string} prefName
* @return {string[]}
*/
function _getArrayOfStringsHostPref(prefName) {
const text = Services.prefs.getStringPref(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 {string}
*/
function getPrefPostfix(pageContext) {
switch (pageContext) {
case "devtools":
case "aboutprofiling":
// 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 {PageContext} pageContext
* @returns {string[]}
*/
function getObjdirPrefValue(pageContext) {
const postfix = getPrefPostfix(pageContext);
return _getArrayOfStringsHostPref(OBJDIRS_PREF + postfix);
}
/**
* @param {PageContext} pageContext
* @param {string[]} supportedFeatures
* @returns {RecordingStateFromPreferences}
*/
function getRecordingPreferences(pageContext, supportedFeatures) {
const postfix = getPrefPostfix(pageContext);
// If you add a new preference here, please do not forget to update
// `revertRecordingPreferences` as well.
const objdirs = getObjdirPrefValue(pageContext);
const presetName = Services.prefs.getCharPref(PRESET_PREF + postfix);
// First try to get the values from a preset.
const recordingPrefs = getRecordingPrefsFromPreset(
presetName,
supportedFeatures,
objdirs
);
if (recordingPrefs) {
return recordingPrefs;
}
// Next use the preferences to get the values.
const entries = Services.prefs.getIntPref(ENTRIES_PREF + postfix);
const interval = Services.prefs.getIntPref(INTERVAL_PREF + postfix);
const features = _getArrayOfStringsPref(FEATURES_PREF + postfix);
const threads = _getArrayOfStringsPref(THREADS_PREF + postfix);
const duration = Services.prefs.getIntPref(DURATION_PREF + postfix);
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 {string} presetName
* @param {string[]} supportedFeatures
* @param {string[]} objdirs
* @return {RecordingStateFromPreferences | null}
*/
function getRecordingPrefsFromPreset(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,
// The interval is stored in preferences as microseconds, but the preset
// defines it in terms of milliseconds. Make the conversion here.
interval: preset.interval * 1000,
// 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 {PageContext} pageContext
* @param {RecordingStateFromPreferences} prefs
*/
function setRecordingPreferences(pageContext, prefs) {
const postfix = getPrefPostfix(pageContext);
Services.prefs.setCharPref(PRESET_PREF + postfix, prefs.presetName);
Services.prefs.setIntPref(ENTRIES_PREF + postfix, prefs.entries);
// The interval pref stores the value in microseconds for extra precision.
Services.prefs.setIntPref(INTERVAL_PREF + postfix, prefs.interval);
Services.prefs.setCharPref(
FEATURES_PREF + postfix,
JSON.stringify(prefs.features)
);
Services.prefs.setCharPref(
THREADS_PREF + postfix,
JSON.stringify(prefs.threads)
);
Services.prefs.setCharPref(
OBJDIRS_PREF + postfix,
JSON.stringify(prefs.objdirs)
);
}
const platform = AppConstants.platform;
/**
* Revert the recording prefs for both local and remote profiling.
* @return {void}
*/
function revertRecordingPreferences() {
for (const postfix of ["", ".remote"]) {
Services.prefs.clearUserPref(PRESET_PREF + postfix);
Services.prefs.clearUserPref(ENTRIES_PREF + postfix);
Services.prefs.clearUserPref(INTERVAL_PREF + postfix);
Services.prefs.clearUserPref(FEATURES_PREF + postfix);
Services.prefs.clearUserPref(THREADS_PREF + postfix);
Services.prefs.clearUserPref(OBJDIRS_PREF + postfix);
Services.prefs.clearUserPref(DURATION_PREF + postfix);
}
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 postfix = getPrefPostfix(pageContext);
const objdirs = _getArrayOfStringsHostPref(OBJDIRS_PREF + postfix);
let recordingPrefs = getRecordingPrefsFromPreset(
presetName,
supportedFeatures,
objdirs
);
if (!recordingPrefs) {
// No recordingPrefs 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 + postfix, presetName);
recordingPrefs = getRecordingPreferences(pageContext, supportedFeatures);
}
setRecordingPreferences(pageContext, recordingPrefs);
}
/**
* 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
*/
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;
switch (messageFromFrontend.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();
channel.send(
{
type: "STATUS_RESPONSE",
menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(),
requestId,
},
target
);
break;
}
case "ENABLE_MENU_BUTTON": {
const { ownerDocument } = target.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);
// 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);
// Respond back that we've done it.
channel.send(
{
type: "ENABLE_MENU_BUTTON_DONE",
requestId,
},
target
);
break;
}
default:
console.error(
"An unknown message type was received by the profiler's WebChannel handler.",
message
);
}
}
// Provide a fake module.exports for the JSM to be properly read by TypeScript.
/** @type {any} */ (this).module = { exports: {} };
module.exports = {
presets,
captureProfile,
startProfiler,
stopProfiler,
restartProfiler,
toggleProfiler,
platform,
getSymbolsFromThisBrowser,
getRecordingPreferences,
setRecordingPreferences,
revertRecordingPreferences,
changePreset,
handleWebChannelMessage,
};
// Object.keys() confuses the linting which expects a static array expression.
// eslint-disable-next-line
var EXPORTED_SYMBOLS = Object.keys(module.exports);