diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/content/aboutLogging.js | 697 |
1 files changed, 697 insertions, 0 deletions
diff --git a/toolkit/content/aboutLogging.js b/toolkit/content/aboutLogging.js new file mode 100644 index 0000000000..15a01c6ce6 --- /dev/null +++ b/toolkit/content/aboutLogging.js @@ -0,0 +1,697 @@ +/* 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/. */ + +"use strict"; +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService( + Ci.nsIDashboard +); +const gDirServ = Cc["@mozilla.org/file/directory_service;1"].getService( + Ci.nsIDirectoryServiceProvider +); + +const { ProfilerMenuButton } = ChromeUtils.import( + "resource://devtools/client/performance-new/popup/menu-button.jsm.js" +); +const { CustomizableUI } = ChromeUtils.importESModule( + "resource:///modules/CustomizableUI.sys.mjs" +); + +XPCOMUtils.defineLazyGetter(this, "ProfilerPopupBackground", function () { + return ChromeUtils.import( + "resource://devtools/client/performance-new/shared/background.jsm.js" + ); +}); + +const $ = document.querySelector.bind(document); +const $$ = document.querySelectorAll.bind(document); + +function fileEnvVarPresent() { + return Services.env.get("MOZ_LOG_FILE") || Services.env.get("NSPR_LOG_FILE"); +} + +function moduleEnvVarPresent() { + return Services.env.get("MOZ_LOG") || Services.env.get("NSPR_LOG"); +} + +/** + * All the information associated with a logging presets: + * - `modules` is the list of log modules and option, the same that would have + * been set as a MOZ_LOG environment variable + * - l10nIds.label and l10nIds.description are the Ids of the strings that + * appear in the dropdown selector, and a one-liner describing the purpose of + * a particular logging preset + * - profilerPreset is the name of a Firefox Profiler preset [1]. In general, + * the profiler preset will have the correct set of threads for a particular + * logging preset, so that all logging statements are recorded in the profile + * as markers. + * + * [1]: The keys of the `presets` object defined in + * https://searchfox.org/mozilla-central/source/devtools/client/performance-new/shared/background.jsm.js + */ +const gLoggingPresets = { + networking: { + modules: + "timestamp,sync,nsHttp:5,cache2:5,nsSocketTransport:5,nsHostResolver:5", + l10nIds: { + label: "about-logging-preset-networking-label", + description: "about-logging-preset-networking-description", + }, + profilerPreset: "networking", + }, + "media-playback": { + modules: + "HTMLMediaElement:4,HTMLMediaElementEvents:4,cubeb:5,PlatformDecoderModule:5,AudioSink:5,AudioSinkWrapper:5,MediaDecoderStateMachine:4,MediaDecoder:4,MediaFormatReader:5", + l10nIds: { + label: "about-logging-preset-media-playback-label", + description: "about-logging-preset-media-playback-description", + }, + profilerPreset: "media", + }, + custom: { + modules: "", + l10nIds: { + label: "about-logging-preset-custom-label", + description: "about-logging-preset-custom-description", + }, + }, +}; + +const gLoggingSettings = { + // Possible values: "profiler" and "file". + loggingOutputType: "profiler", + running: false, + // If non-null, the profiler preset to use. If null, the preset selected in + // the dropdown is going to be used. It is also possible to use a "custom" + // preset and an explicit list of modules. + loggingPreset: null, + // If non-null, the profiler preset to use. If a logging preset is being used, + // and this is null, the profiler preset associated to the logging preset is + // going to be used. Otherwise, a generic profiler preset is going to be used + // ("firefox-platform"). + profilerPreset: null, + // If non-null, the threads that will be recorded by the Firefox Profiler. If + // null, the threads from the profiler presets are going to be used. + profilerThreads: null, + // If non-null, stack traces will be recorded for MOZ_LOG profiler markers. + // This is set only when coming from the URL, not when the user changes the UI. + profilerStacks: null, +}; + +// When the profiler has been started, this holds the promise the +// Services.profiler.StartProfiler returns, to ensure the profiler has +// effectively started. +let gProfilerPromise = null; + +// Used in tests +function presets() { + return gLoggingPresets; +} + +// Used in tests +function settings() { + return gLoggingSettings; +} + +// Used in tests +function profilerPromise() { + return gProfilerPromise; +} + +function populatePresets() { + let dropdown = $("#logging-preset-dropdown"); + for (let presetName in gLoggingPresets) { + let preset = gLoggingPresets[presetName]; + let option = document.createElement("option"); + document.l10n.setAttributes(option, preset.l10nIds.label); + option.value = presetName; + dropdown.appendChild(option); + if (option.value === gLoggingSettings.loggingPreset) { + option.setAttribute("selected", true); + } + } + + function setPresetAndDescription(preset) { + document.l10n.setAttributes( + $("#logging-preset-description"), + gLoggingPresets[preset].l10nIds.description + ); + gLoggingSettings.loggingPreset = preset; + } + + dropdown.onchange = function () { + // When switching to custom, leave the existing module list, to allow + // editing. + if (dropdown.value != "custom") { + $("#log-modules").value = gLoggingPresets[dropdown.value].modules; + } + setPresetAndDescription(dropdown.value); + setLogModules(); + Services.prefs.setCharPref("logging.config.preset", dropdown.value); + }; + + $("#log-modules").value = gLoggingPresets[dropdown.value].modules; + setPresetAndDescription(dropdown.value); + // When changing the list switch to custom. + $("#log-modules").oninput = e => { + dropdown.value = "custom"; + }; +} + +function updateLoggingOutputType(profilerOutputType) { + gLoggingSettings.loggingOutputType = profilerOutputType; + Services.prefs.setCharPref("logging.config.output_type", profilerOutputType); + $(`input[type=radio][value=${profilerOutputType}]`).checked = true; + + switch (profilerOutputType) { + case "profiler": + if (!gLoggingSettings.profilerStacks) { + // If this value is set from the URL, do not allow to change it. + $("#with-profiler-stacks-checkbox").disabled = false; + } + // hide options related to file output for clarity + $("#log-file-configuration").hidden = true; + break; + case "file": + $("#with-profiler-stacks-checkbox").disabled = true; + $("#log-file-configuration").hidden = false; + $("#no-log-file").hidden = !!$("#current-log-file").innerText.length; + break; + } +} + +function displayErrorMessage(error) { + var err = $("#error"); + err.hidden = false; + + var errorDescription = $("#error-description"); + document.l10n.setAttributes(errorDescription, error.l10nId, { + k: error.key, + v: error.value, + }); +} + +class ParseError extends Error { + constructor(l10nId, key, value) { + super(name); + this.l10nId = l10nId; + this.key = key; + this.value = value; + } + name = "ParseError"; + l10nId; + key; + value; +} + +function parseURL() { + let options = new URL(document.location.href).searchParams; + + if (!options) { + return; + } + + let modulesOverriden = null, + outputTypeOverriden = null, + loggingPresetOverriden = null, + threadsOverriden = null, + profilerPresetOverriden = null, + profilerStacksOverriden = null; + try { + for (let [k, v] of options) { + switch (k) { + case "modules": + case "module": + modulesOverriden = v; + break; + case "output": + case "output-type": + if (v !== "profiler" && v !== "file") { + throw new ParseError("about-logging-invalid-output", k, v); + } + outputTypeOverriden = v; + break; + case "preset": + case "logging-preset": + if (!Object.keys(gLoggingPresets).includes(v)) { + throw new ParseError("about-logging-unknown-logging-preset", k, v); + } + loggingPresetOverriden = v; + break; + case "threads": + case "thread": + threadsOverriden = v; + break; + case "profiler-preset": + if (!Object.keys(ProfilerPopupBackground.presets).includes(v)) { + throw new Error(["about-logging-unknown-profiler-preset", k, v]); + } + profilerPresetOverriden = v; + break; + case "profilerstacks": + profilerStacksOverriden = true; + break; + default: + throw new ParseError("about-logging-unknown-option", k, v); + } + } + } catch (e) { + displayErrorMessage(e); + return; + } + + // Detect combinations that don't make sense + if ( + (profilerPresetOverriden || threadsOverriden) && + outputTypeOverriden == "file" + ) { + displayErrorMessage( + new ParseError("about-logging-file-and-profiler-override") + ); + return; + } + + // Configuration is deemed at least somewhat valid, override each setting in + // turn + let someElementsDisabled = false; + + if (modulesOverriden || loggingPresetOverriden) { + // Don't allow changing those if set by the URL + let logModules = $("#log-modules"); + var dropdown = $("#logging-preset-dropdown"); + if (loggingPresetOverriden) { + dropdown.value = loggingPresetOverriden; + dropdown.onchange(); + } + if (modulesOverriden) { + logModules.value = modulesOverriden; + dropdown.value = "custom"; + dropdown.onchange(); + dropdown.disabled = true; + someElementsDisabled = true; + } + logModules.disabled = true; + $("#set-log-modules-button").disabled = true; + $("#logging-preset-dropdown").disabled = true; + someElementsDisabled = true; + setLogModules(); + updateLogModules(); + } + if (outputTypeOverriden) { + $$("input[type=radio]").forEach(e => (e.disabled = true)); + someElementsDisabled = true; + updateLoggingOutputType(outputTypeOverriden); + } + if (profilerStacksOverriden) { + const checkbox = $("#with-profiler-stacks-checkbox"); + checkbox.disabled = true; + someElementsDisabled = true; + Services.prefs.setBoolPref("logging.config.profilerstacks", true); + gLoggingSettings.profilerStacks = true; + } + + if (loggingPresetOverriden) { + gLoggingSettings.loggingPreset = loggingPresetOverriden; + } + if (profilerPresetOverriden) { + gLoggingSettings.profilerPreset = profilerPresetOverriden; + } + if (threadsOverriden) { + gLoggingSettings.profilerThreads = threadsOverriden; + } + + $("#some-elements-unavailable").hidden = !someElementsDisabled; +} + +let gInited = false; +function init() { + if (gInited) { + return; + } + gInited = true; + gDashboard.enableLogging = true; + + populatePresets(); + parseURL(); + + $("#log-file-configuration").addEventListener("submit", e => { + e.preventDefault(); + setLogFile(); + }); + + $("#log-modules-form").addEventListener("submit", e => { + e.preventDefault(); + setLogModules(); + }); + + let toggleLoggingButton = $("#toggle-logging-button"); + toggleLoggingButton.addEventListener("click", startStopLogging); + + $$("input[type=radio]").forEach(radio => { + radio.onchange = e => { + updateLoggingOutputType(e.target.value); + }; + }); + + $("#with-profiler-stacks-checkbox").addEventListener("change", e => { + Services.prefs.setBoolPref( + "logging.config.profilerstacks", + e.target.checked + ); + updateLogModules(); + }); + + let loggingOutputType = Services.prefs.getCharPref( + "logging.config.output_type", + "profiler" + ); + if (loggingOutputType.length) { + updateLoggingOutputType(loggingOutputType); + } + + $("#with-profiler-stacks-checkbox").checked = Services.prefs.getBoolPref( + "logging.config.profilerstacks", + false + ); + + try { + let loggingPreset = Services.prefs.getCharPref("logging.config.preset"); + gLoggingSettings.loggingPreset = loggingPreset; + } catch {} + + try { + let running = Services.prefs.getBoolPref("logging.config.running"); + gLoggingSettings.running = running; + $("#toggle-logging-button").setAttribute( + "data-l10n-id", + `about-logging-${gLoggingSettings.running ? "stop" : "start"}-logging` + ); + } catch {} + + try { + let file = gDirServ.getFile("TmpD", {}); + file.append("log.txt"); + $("#log-file").value = file.path; + } catch (e) { + console.error(e); + } + + // Update the value of the log file. + updateLogFile(); + + // Update the active log modules + updateLogModules(); + + // If we can't set the file and the modules at runtime, + // the start and stop buttons wouldn't really do anything. + if ( + ($("#set-log-file-button").disabled || + $("#set-log-modules-button").disabled) && + moduleEnvVarPresent() + ) { + $("#buttons-disabled").hidden = false; + toggleLoggingButton.disabled = true; + } +} + +function updateLogFile(file) { + let logPath = ""; + + // Try to get the environment variable for the log file + logPath = + Services.env.get("MOZ_LOG_FILE") || Services.env.get("NSPR_LOG_FILE"); + let currentLogFile = $("#current-log-file"); + let setLogFileButton = $("#set-log-file-button"); + + // If the log file was set from an env var, we disable the ability to set it + // at runtime. + if (logPath.length) { + currentLogFile.innerText = logPath; + setLogFileButton.disabled = true; + } else if (gDashboard.getLogPath() != ".moz_log") { + // There may be a value set by a pref. + currentLogFile.innerText = gDashboard.getLogPath(); + } else if (file !== undefined) { + currentLogFile.innerText = file; + } else { + try { + let file = gDirServ.getFile("TmpD", {}); + file.append("log.txt"); + $("#log-file").value = file.path; + } catch (e) { + console.error(e); + } + // Fall back to the temp dir + currentLogFile.innerText = $("#log-file").value; + } + + let openLogFileButton = $("#open-log-file-button"); + openLogFileButton.disabled = true; + + if (currentLogFile.innerText.length) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(currentLogFile.innerText); + + if (file.exists()) { + openLogFileButton.disabled = false; + openLogFileButton.onclick = function (e) { + file.reveal(); + }; + } + } + $("#no-log-file").hidden = !!currentLogFile.innerText.length; + $("#current-log-file").hidden = !currentLogFile.innerText.length; +} + +function updateLogModules() { + // Try to get the environment variable for the log file + let logModules = + Services.env.get("MOZ_LOG") || + Services.env.get("MOZ_LOG_MODULES") || + Services.env.get("NSPR_LOG_MODULES"); + let currentLogModules = $("#current-log-modules"); + let setLogModulesButton = $("#set-log-modules-button"); + if (logModules.length) { + currentLogModules.innerText = logModules; + // If the log modules are set by an environment variable at startup, do not + // allow changing them throught a pref. It would be difficult to figure out + // which ones are enabled and which ones are not. The user probably knows + // what he they are doing. + setLogModulesButton.disabled = true; + } else { + let activeLogModules = []; + let children = Services.prefs.getBranch("logging.").getChildList(""); + + for (let pref of children) { + if (pref.startsWith("config.")) { + continue; + } + + try { + let value = Services.prefs.getIntPref(`logging.${pref}`); + activeLogModules.push(`${pref}:${value}`); + } catch (e) { + console.error(e); + } + } + + if (activeLogModules.length) { + // Add some options only if some modules are present. + if (Services.prefs.getBoolPref("logging.config.add_timestamp", false)) { + activeLogModules.push("timestamp"); + } + if (Services.prefs.getBoolPref("logging.config.sync", false)) { + activeLogModules.push("sync"); + } + if (Services.prefs.getBoolPref("logging.config.profilerstacks", false)) { + activeLogModules.push("profilerstacks"); + } + } + + if (activeLogModules.length !== 0) { + currentLogModules.innerText = activeLogModules.join(","); + currentLogModules.hidden = false; + $("#no-log-modules").hidden = true; + } else { + currentLogModules.innerText = ""; + currentLogModules.hidden = true; + $("#no-log-modules").hidden = false; + } + } +} + +function setLogFile() { + let setLogButton = $("#set-log-file-button"); + if (setLogButton.disabled) { + // There's no point trying since it wouldn't work anyway. + return; + } + let logFile = $("#log-file").value.trim(); + Services.prefs.setCharPref("logging.config.LOG_FILE", logFile); + updateLogFile(logFile); +} + +function clearLogModules() { + // Turn off all the modules. + let children = Services.prefs.getBranch("logging.").getChildList(""); + for (let pref of children) { + if (!pref.startsWith("config.")) { + Services.prefs.clearUserPref(`logging.${pref}`); + } + } + Services.prefs.clearUserPref("logging.config.add_timestamp"); + Services.prefs.clearUserPref("logging.config.sync"); + updateLogModules(); +} + +function setLogModules() { + if (moduleEnvVarPresent()) { + // The modules were set via env var, so we shouldn't try to change them. + return; + } + + let modules = $("#log-modules").value.trim(); + + // Clear previously set log modules. + clearLogModules(); + + if (modules.length !== 0) { + let logModules = modules.split(","); + for (let module of logModules) { + if (module == "timestamp") { + Services.prefs.setBoolPref("logging.config.add_timestamp", true); + } else if (module == "rotate") { + // XXX: rotate is not yet supported. + } else if (module == "append") { + // XXX: append is not yet supported. + } else if (module == "sync") { + Services.prefs.setBoolPref("logging.config.sync", true); + } else if (module == "profilerstacks") { + Services.prefs.setBoolPref("logging.config.profilerstacks", true); + } else { + let lastColon = module.lastIndexOf(":"); + let key = module.slice(0, lastColon); + let value = parseInt(module.slice(lastColon + 1), 10); + Services.prefs.setIntPref(`logging.${key}`, value); + } + } + } + + updateLogModules(); +} + +function isLogging() { + try { + return Services.prefs.getBoolPref("logging.config.running"); + } catch { + return false; + } +} + +function startStopLogging() { + if (isLogging()) { + document.l10n.setAttributes( + $("#toggle-logging-button"), + "about-logging-start-logging" + ); + stopLogging(); + } else { + document.l10n.setAttributes( + $("#toggle-logging-button"), + "about-logging-stop-logging" + ); + startLogging(); + } +} + +function startLogging() { + setLogModules(); + if (gLoggingSettings.loggingOutputType === "profiler") { + const pageContext = "aboutlogging"; + const supportedFeatures = Services.profiler.GetFeatures(); + if (gLoggingSettings.loggingPreset != "custom") { + // Change the preset before starting the profiler, so that the + // underlying profiler code picks up the right configuration. + const profilerPreset = + gLoggingPresets[gLoggingSettings.loggingPreset].profilerPreset; + ProfilerPopupBackground.changePreset( + "aboutlogging", + profilerPreset, + supportedFeatures + ); + } else { + // a baseline set of threads, and possibly others, overriden by the URL + ProfilerPopupBackground.changePreset( + "aboutlogging", + "firefox-platform", + supportedFeatures + ); + } + let { entries, interval, features, threads, duration } = + ProfilerPopupBackground.getRecordingSettings( + pageContext, + Services.profiler.GetFeatures() + ); + + if (gLoggingSettings.profilerThreads) { + threads.push(...gLoggingSettings.profilerThreads.split(",")); + // Small hack: if cubeb is being logged, it's almost always necessary (and + // never harmful) to enable audio callback tracing, otherwise, no log + // statements will be recorded from real-time threads. + if (gLoggingSettings.profilerThreads.includes("cubeb")) { + features.push("audiocallbacktracing"); + } + } + const win = Services.wm.getMostRecentWindow("navigator:browser"); + const windowid = win?.gBrowser?.selectedBrowser?.browsingContext?.browserId; + + // Force displaying the profiler button in the navbar if not preset, so + // that there is a visual indication profiling is in progress. + if (!ProfilerMenuButton.isInNavbar()) { + // Ensure the widget is enabled. + Services.prefs.setBoolPref( + "devtools.performance.popup.feature-flag", + true + ); + // Enable the profiler menu button. + ProfilerMenuButton.addToNavbar(); + // Dispatch the change event manually, so that the shortcuts will also be + // added. + CustomizableUI.dispatchToolboxEvent("customizationchange"); + } + + gProfilerPromise = Services.profiler.StartProfiler( + entries, + interval, + features, + threads, + windowid, + duration + ); + } else { + setLogFile(); + } + Services.prefs.setBoolPref("logging.config.running", true); +} + +async function stopLogging() { + if (gLoggingSettings.loggingOutputType === "profiler") { + await ProfilerPopupBackground.captureProfile("aboutlogging"); + } else { + Services.prefs.clearUserPref("logging.config.LOG_FILE"); + updateLogFile(); + } + Services.prefs.setBoolPref("logging.config.running", false); + clearLogModules(); +} + +// We use the pageshow event instead of onload. This is needed because sometimes +// the page is loaded via session-restore/bfcache. In such cases we need to call +// init() to keep the page behaviour consistent with the ticked checkboxes. +// Mostly the issue is with the autorefresh checkbox. +window.addEventListener("pageshow", function () { + init(); +}); |