/* 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(); });