1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const { XPCOMUtils } = ChromeUtils.importESModule(
7 "resource://gre/modules/XPCOMUtils.sys.mjs"
9 const gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService(
12 const gDirServ = Cc["@mozilla.org/file/directory_service;1"].getService(
13 Ci.nsIDirectoryServiceProvider
16 const { ProfilerMenuButton } = ChromeUtils.importESModule(
17 "resource://devtools/client/performance-new/popup/menu-button.sys.mjs"
19 const { CustomizableUI } = ChromeUtils.importESModule(
20 "resource:///modules/CustomizableUI.sys.mjs"
23 ChromeUtils.defineLazyGetter(this, "ProfilerPopupBackground", function () {
24 return ChromeUtils.importESModule(
25 "resource://devtools/client/performance-new/shared/background.sys.mjs"
29 const $ = document.querySelector.bind(document);
30 const $$ = document.querySelectorAll.bind(document);
32 function fileEnvVarPresent() {
33 return Services.env.get("MOZ_LOG_FILE") || Services.env.get("NSPR_LOG_FILE");
36 function moduleEnvVarPresent() {
37 return Services.env.get("MOZ_LOG") || Services.env.get("NSPR_LOG");
41 * All the information associated with a logging presets:
42 * - `modules` is the list of log modules and option, the same that would have
43 * been set as a MOZ_LOG environment variable
44 * - l10nIds.label and l10nIds.description are the Ids of the strings that
45 * appear in the dropdown selector, and a one-liner describing the purpose of
46 * a particular logging preset
47 * - profilerPreset is the name of a Firefox Profiler preset [1]. In general,
48 * the profiler preset will have the correct set of threads for a particular
49 * logging preset, so that all logging statements are recorded in the profile
52 * [1]: The keys of the `presets` object defined in
53 * https://searchfox.org/mozilla-central/source/devtools/client/performance-new/shared/background.sys.mjs
56 const gOsSpecificLoggingPresets = (() => {
58 if (navigator.platform.startsWith("Win")) {
62 "timestamp,sync,Widget:5,BaseWidget:5,WindowsEvent:4,TaskbarConcealer:5,FileDialog:5",
64 label: "about-logging-preset-windows-label",
65 description: "about-logging-preset-windows-description",
74 const gLoggingPresets = {
77 "timestamp,sync,nsHttp:5,cache2:5,nsSocketTransport:5,nsHostResolver:5,EarlyHint:5",
79 label: "about-logging-preset-networking-label",
80 description: "about-logging-preset-networking-description",
82 profilerPreset: "networking",
85 modules: "timestamp,sync,nsHttp:5,cache2:5,cookie:5",
87 label: "about-logging-preset-networking-cookie-label",
88 description: "about-logging-preset-networking-cookie-description",
93 "timestamp,sync,nsHttp:5,nsWebSocket:5,nsSocketTransport:5,nsHostResolver:5",
95 label: "about-logging-preset-networking-websocket-label",
96 description: "about-logging-preset-networking-websocket-description",
101 "timestamp,sync,nsHttp:5,nsSocketTransport:5,nsHostResolver:5,neqo_http3::*:5,neqo_transport::*:5",
103 label: "about-logging-preset-networking-http3-label",
104 description: "about-logging-preset-networking-http3-description",
107 "http3-upload-speed": {
108 modules: "timestamp,neqo_transport::*:3",
110 label: "about-logging-preset-networking-http3-upload-speed-label",
112 "about-logging-preset-networking-http3-upload-speed-description",
117 "HTMLMediaElement:4,HTMLMediaElementEvents:4,cubeb:5,PlatformDecoderModule:5,AudioSink:5,AudioSinkWrapper:5,MediaDecoderStateMachine:4,MediaDecoder:4,MediaFormatReader:5,GMP:5",
119 label: "about-logging-preset-media-playback-label",
120 description: "about-logging-preset-media-playback-description",
122 profilerPreset: "media",
126 "jsep:5,sdp:5,signaling:5,mtransport:5,RTCRtpReceiver:5,RTCRtpSender:5,RTCDMTFSender:5,VideoFrameConverter:5,WebrtcTCPSocket:5,CamerasChild:5,CamerasParent:5,VideoEngine:5,ShmemPool:5,TabShare:5,MediaChild:5,MediaParent:5,MediaManager:5,MediaTrackGraph:5,cubeb:5,MediaStream:5,MediaStreamTrack:5,DriftCompensator:5,ForwardInputTrack:5,MediaRecorder:5,MediaEncoder:5,TrackEncoder:5,VP8TrackEncoder:5,Muxer:5,GetUserMedia:5,MediaPipeline:5,PeerConnectionImpl:5,WebAudioAPI:5,webrtc_trace:5,RTCRtpTransceiver:5,ForwardedInputTrack:5,HTMLMediaElement:5,HTMLMediaElementEvents:5",
128 label: "about-logging-preset-webrtc-label",
129 description: "about-logging-preset-webrtc-description",
131 profilerPreset: "media",
135 "wgpu_core::*:5,wgpu_hal::*:5,wgpu_types::*:5,naga::*:5,wgpu_bindings::*:5,WebGPU:5",
137 label: "about-logging-preset-webgpu-label",
138 description: "about-logging-preset-webgpu-description",
143 "webrender::*:5,webrender_bindings::*:5,webrender_types::*:5,gfx2d:5,WebRenderBridgeParent:5,DcompSurface:5,apz.displayport:5,layout:5,dl.content:5,dl.parent:5,nsRefreshDriver:5,fontlist:5,fontinit:5,textrun:5,textrunui:5,textperf:5",
145 label: "about-logging-preset-gfx-label",
146 description: "about-logging-preset-gfx-description",
148 // The graphics profiler preset enables the threads we want but loses the screenshots.
149 // We could add an extra preset for that if we miss it.
150 profilerPreset: "graphics",
152 ...gOsSpecificLoggingPresets,
156 label: "about-logging-preset-custom-label",
157 description: "about-logging-preset-custom-description",
162 const gLoggingSettings = {
163 // Possible values: "profiler" and "file".
164 loggingOutputType: "profiler",
166 // If non-null, the profiler preset to use. If null, the preset selected in
167 // the dropdown is going to be used. It is also possible to use a "custom"
168 // preset and an explicit list of modules.
170 // If non-null, the profiler preset to use. If a logging preset is being used,
171 // and this is null, the profiler preset associated to the logging preset is
172 // going to be used. Otherwise, a generic profiler preset is going to be used
173 // ("firefox-platform").
174 profilerPreset: null,
175 // If non-null, the threads that will be recorded by the Firefox Profiler. If
176 // null, the threads from the profiler presets are going to be used.
177 profilerThreads: null,
178 // If non-null, stack traces will be recorded for MOZ_LOG profiler markers.
179 // This is set only when coming from the URL, not when the user changes the UI.
180 profilerStacks: null,
183 // When the profiler has been started, this holds the promise the
184 // Services.profiler.StartProfiler returns, to ensure the profiler has
185 // effectively started.
186 let gProfilerPromise = null;
190 return gLoggingPresets;
194 function settings() {
195 return gLoggingSettings;
199 function profilerPromise() {
200 return gProfilerPromise;
203 function populatePresets() {
204 let dropdown = $("#logging-preset-dropdown");
205 for (let presetName in gLoggingPresets) {
206 let preset = gLoggingPresets[presetName];
207 let option = document.createElement("option");
208 document.l10n.setAttributes(option, preset.l10nIds.label);
209 option.value = presetName;
210 dropdown.appendChild(option);
211 if (option.value === gLoggingSettings.loggingPreset) {
212 option.setAttribute("selected", true);
216 function setPresetAndDescription(preset) {
217 document.l10n.setAttributes(
218 $("#logging-preset-description"),
219 gLoggingPresets[preset].l10nIds.description
221 gLoggingSettings.loggingPreset = preset;
224 dropdown.onchange = function () {
225 // When switching to custom, leave the existing module list, to allow
227 if (dropdown.value != "custom") {
228 $("#log-modules").value = gLoggingPresets[dropdown.value].modules;
230 setPresetAndDescription(dropdown.value);
231 Services.prefs.setCharPref("logging.config.preset", dropdown.value);
234 $("#log-modules").value = gLoggingPresets[dropdown.value].modules;
235 setPresetAndDescription(dropdown.value);
236 // When changing the list switch to custom.
237 $("#log-modules").oninput = e => {
238 dropdown.value = "custom";
242 function updateLoggingOutputType(profilerOutputType) {
243 gLoggingSettings.loggingOutputType = profilerOutputType;
244 Services.prefs.setCharPref("logging.config.output_type", profilerOutputType);
245 $(`input[type=radio][value=${profilerOutputType}]`).checked = true;
247 switch (profilerOutputType) {
249 if (!gLoggingSettings.profilerStacks) {
250 // If this value is set from the URL, do not allow to change it.
251 $("#with-profiler-stacks-checkbox").disabled = false;
253 // hide options related to file output for clarity
254 $("#log-file-configuration").hidden = true;
257 $("#with-profiler-stacks-checkbox").disabled = true;
258 $("#log-file-configuration").hidden = false;
259 $("#no-log-file").hidden = !!$("#current-log-file").innerText.length;
264 function displayErrorMessage(error) {
265 var err = $("#error");
268 var errorDescription = $("#error-description");
269 document.l10n.setAttributes(errorDescription, error.l10nId, {
275 class ParseError extends Error {
276 constructor(l10nId, key, value) {
278 this.l10nId = l10nId;
288 function parseURL() {
289 let options = new URL(document.location.href).searchParams;
295 let modulesOverriden = null,
296 outputTypeOverriden = null,
297 loggingPresetOverriden = null,
298 threadsOverriden = null,
299 profilerPresetOverriden = null,
300 profilerStacksOverriden = null;
302 for (let [k, v] of options) {
306 modulesOverriden = v;
310 if (v !== "profiler" && v !== "file") {
311 throw new ParseError("about-logging-invalid-output", k, v);
313 outputTypeOverriden = v;
316 case "logging-preset":
317 if (!Object.keys(gLoggingPresets).includes(v)) {
318 throw new ParseError("about-logging-unknown-logging-preset", k, v);
320 loggingPresetOverriden = v;
324 threadsOverriden = v;
326 case "profiler-preset":
327 if (!Object.keys(ProfilerPopupBackground.presets).includes(v)) {
328 throw new Error(["about-logging-unknown-profiler-preset", k, v]);
330 profilerPresetOverriden = v;
332 case "profilerstacks":
333 profilerStacksOverriden = true;
336 throw new ParseError("about-logging-unknown-option", k, v);
340 displayErrorMessage(e);
344 // Detect combinations that don't make sense
346 (profilerPresetOverriden || threadsOverriden) &&
347 outputTypeOverriden == "file"
350 new ParseError("about-logging-file-and-profiler-override")
355 // Configuration is deemed at least somewhat valid, override each setting in
357 let someElementsDisabled = false;
359 if (modulesOverriden || loggingPresetOverriden) {
360 // Don't allow changing those if set by the URL
361 let logModules = $("#log-modules");
362 var dropdown = $("#logging-preset-dropdown");
363 if (loggingPresetOverriden) {
364 dropdown.value = loggingPresetOverriden;
367 if (modulesOverriden) {
368 logModules.value = modulesOverriden;
369 dropdown.value = "custom";
371 dropdown.disabled = true;
372 someElementsDisabled = true;
374 logModules.disabled = true;
375 $("#set-log-modules-button").disabled = true;
376 $("#logging-preset-dropdown").disabled = true;
377 someElementsDisabled = true;
380 if (outputTypeOverriden) {
381 $$("input[type=radio]").forEach(e => (e.disabled = true));
382 someElementsDisabled = true;
383 updateLoggingOutputType(outputTypeOverriden);
385 if (profilerStacksOverriden) {
386 const checkbox = $("#with-profiler-stacks-checkbox");
387 checkbox.disabled = true;
388 someElementsDisabled = true;
389 Services.prefs.setBoolPref("logging.config.profilerstacks", true);
390 gLoggingSettings.profilerStacks = true;
393 if (loggingPresetOverriden) {
394 gLoggingSettings.loggingPreset = loggingPresetOverriden;
396 if (profilerPresetOverriden) {
397 gLoggingSettings.profilerPreset = profilerPresetOverriden;
399 if (threadsOverriden) {
400 gLoggingSettings.profilerThreads = threadsOverriden;
403 $("#some-elements-unavailable").hidden = !someElementsDisabled;
412 gDashboard.enableLogging = true;
417 $("#log-file-configuration").addEventListener("submit", e => {
422 $("#log-modules-form").addEventListener("submit", e => {
427 let toggleLoggingButton = $("#toggle-logging-button");
428 toggleLoggingButton.addEventListener("click", startStopLogging);
430 $$("input[type=radio]").forEach(radio => {
431 radio.onchange = e => {
432 updateLoggingOutputType(e.target.value);
436 $("#with-profiler-stacks-checkbox").addEventListener("change", e => {
437 Services.prefs.setBoolPref(
438 "logging.config.profilerstacks",
444 let loggingOutputType = Services.prefs.getCharPref(
445 "logging.config.output_type",
448 if (loggingOutputType.length) {
449 updateLoggingOutputType(loggingOutputType);
452 $("#with-profiler-stacks-checkbox").checked = Services.prefs.getBoolPref(
453 "logging.config.profilerstacks",
458 let loggingPreset = Services.prefs.getCharPref("logging.config.preset");
459 gLoggingSettings.loggingPreset = loggingPreset;
463 let running = Services.prefs.getBoolPref("logging.config.running");
464 gLoggingSettings.running = running;
465 document.l10n.setAttributes(
466 $("#toggle-logging-button"),
467 `about-logging-${gLoggingSettings.running ? "stop" : "start"}-logging`
472 let file = gDirServ.getFile("TmpD", {});
473 file.append("log.txt");
474 $("#log-file").value = file.path;
479 // Update the value of the log file.
482 // Update the active log modules
485 // If we can't set the file and the modules at runtime,
486 // the start and stop buttons wouldn't really do anything.
488 ($("#set-log-file-button").disabled ||
489 $("#set-log-modules-button").disabled) &&
490 moduleEnvVarPresent()
492 $("#buttons-disabled").hidden = false;
493 toggleLoggingButton.disabled = true;
497 function updateLogFile(file) {
500 // Try to get the environment variable for the log file
502 Services.env.get("MOZ_LOG_FILE") || Services.env.get("NSPR_LOG_FILE");
503 let currentLogFile = $("#current-log-file");
504 let setLogFileButton = $("#set-log-file-button");
506 // If the log file was set from an env var, we disable the ability to set it
508 if (logPath.length) {
509 currentLogFile.innerText = logPath;
510 setLogFileButton.disabled = true;
511 } else if (gDashboard.getLogPath() != ".moz_log") {
512 // There may be a value set by a pref.
513 currentLogFile.innerText = gDashboard.getLogPath();
514 } else if (file !== undefined) {
515 currentLogFile.innerText = file;
518 let file = gDirServ.getFile("TmpD", {});
519 file.append("log.txt");
520 $("#log-file").value = file.path;
524 // Fall back to the temp dir
525 currentLogFile.innerText = $("#log-file").value;
528 let openLogFileButton = $("#open-log-file-button");
529 openLogFileButton.disabled = true;
531 if (currentLogFile.innerText.length) {
532 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
533 file.initWithPath(currentLogFile.innerText);
536 openLogFileButton.disabled = false;
537 openLogFileButton.onclick = function (e) {
542 $("#no-log-file").hidden = !!currentLogFile.innerText.length;
543 $("#current-log-file").hidden = !currentLogFile.innerText.length;
546 function updateLogModules() {
547 // Try to get the environment variable for the log file
549 Services.env.get("MOZ_LOG") ||
550 Services.env.get("MOZ_LOG_MODULES") ||
551 Services.env.get("NSPR_LOG_MODULES");
552 let currentLogModules = $("#current-log-modules");
553 let setLogModulesButton = $("#set-log-modules-button");
554 if (logModules.length) {
555 currentLogModules.innerText = logModules;
556 // If the log modules are set by an environment variable at startup, do not
557 // allow changing them throught a pref. It would be difficult to figure out
558 // which ones are enabled and which ones are not. The user probably knows
559 // what he they are doing.
560 setLogModulesButton.disabled = true;
562 let activeLogModules = [];
563 let children = Services.prefs.getBranch("logging.").getChildList("");
565 for (let pref of children) {
566 if (pref.startsWith("config.")) {
571 let value = Services.prefs.getIntPref(`logging.${pref}`);
572 activeLogModules.push(`${pref}:${value}`);
578 if (activeLogModules.length) {
579 // Add some options only if some modules are present.
580 if (Services.prefs.getBoolPref("logging.config.add_timestamp", false)) {
581 activeLogModules.push("timestamp");
583 if (Services.prefs.getBoolPref("logging.config.sync", false)) {
584 activeLogModules.push("sync");
586 if (Services.prefs.getBoolPref("logging.config.profilerstacks", false)) {
587 activeLogModules.push("profilerstacks");
591 if (activeLogModules.length !== 0) {
592 currentLogModules.innerText = activeLogModules.join(",");
593 currentLogModules.hidden = false;
594 $("#no-log-modules").hidden = true;
596 currentLogModules.innerText = "";
597 currentLogModules.hidden = true;
598 $("#no-log-modules").hidden = false;
603 function setLogFile() {
604 let setLogButton = $("#set-log-file-button");
605 if (setLogButton.disabled) {
606 // There's no point trying since it wouldn't work anyway.
609 let logFile = $("#log-file").value.trim();
610 Services.prefs.setCharPref("logging.config.LOG_FILE", logFile);
611 updateLogFile(logFile);
614 function clearLogModules() {
615 // Turn off all the modules.
616 let children = Services.prefs.getBranch("logging.").getChildList("");
617 for (let pref of children) {
618 if (!pref.startsWith("config.")) {
619 Services.prefs.clearUserPref(`logging.${pref}`);
622 Services.prefs.clearUserPref("logging.config.add_timestamp");
623 Services.prefs.clearUserPref("logging.config.sync");
627 function setLogModules() {
628 if (moduleEnvVarPresent()) {
629 // The modules were set via env var, so we shouldn't try to change them.
633 let modules = $("#log-modules").value.trim();
635 // Clear previously set log modules.
638 if (modules.length !== 0) {
639 let logModules = modules.split(",");
640 for (let module of logModules) {
641 if (module == "timestamp") {
642 Services.prefs.setBoolPref("logging.config.add_timestamp", true);
643 } else if (module == "rotate") {
644 // XXX: rotate is not yet supported.
645 } else if (module == "append") {
646 // XXX: append is not yet supported.
647 } else if (module == "sync") {
648 Services.prefs.setBoolPref("logging.config.sync", true);
649 } else if (module == "profilerstacks") {
650 Services.prefs.setBoolPref("logging.config.profilerstacks", true);
652 let lastColon = module.lastIndexOf(":");
653 let key = module.slice(0, lastColon);
654 let value = parseInt(module.slice(lastColon + 1), 10);
655 Services.prefs.setIntPref(`logging.${key}`, value);
663 function isLogging() {
665 return Services.prefs.getBoolPref("logging.config.running");
671 function startStopLogging() {
673 document.l10n.setAttributes(
674 $("#toggle-logging-button"),
675 "about-logging-start-logging"
679 document.l10n.setAttributes(
680 $("#toggle-logging-button"),
681 "about-logging-stop-logging"
687 function startLogging() {
689 if (gLoggingSettings.loggingOutputType === "profiler") {
690 const pageContext = "aboutlogging";
691 const supportedFeatures = Services.profiler.GetFeatures();
692 if (gLoggingSettings.loggingPreset != "custom") {
693 // Change the preset before starting the profiler, so that the
694 // underlying profiler code picks up the right configuration.
695 const profilerPreset =
696 gLoggingPresets[gLoggingSettings.loggingPreset].profilerPreset;
697 ProfilerPopupBackground.changePreset(
703 // a baseline set of threads, and possibly others, overriden by the URL
704 ProfilerPopupBackground.changePreset(
710 let { entries, interval, features, threads, duration } =
711 ProfilerPopupBackground.getRecordingSettings(
713 Services.profiler.GetFeatures()
716 if (gLoggingSettings.profilerThreads) {
717 threads.push(...gLoggingSettings.profilerThreads.split(","));
718 // Small hack: if cubeb is being logged, it's almost always necessary (and
719 // never harmful) to enable audio callback tracing, otherwise, no log
720 // statements will be recorded from real-time threads.
721 if (gLoggingSettings.profilerThreads.includes("cubeb")) {
722 features.push("audiocallbacktracing");
725 const win = Services.wm.getMostRecentWindow("navigator:browser");
726 const windowid = win?.gBrowser?.selectedBrowser?.browsingContext?.browserId;
728 // Force displaying the profiler button in the navbar if not preset, so
729 // that there is a visual indication profiling is in progress.
730 if (!ProfilerMenuButton.isInNavbar()) {
731 // Ensure the widget is enabled.
732 Services.prefs.setBoolPref(
733 "devtools.performance.popup.feature-flag",
736 // Enable the profiler menu button.
737 ProfilerMenuButton.addToNavbar();
738 // Dispatch the change event manually, so that the shortcuts will also be
740 CustomizableUI.dispatchToolboxEvent("customizationchange");
743 gProfilerPromise = Services.profiler.StartProfiler(
754 Services.prefs.setBoolPref("logging.config.running", true);
757 async function stopLogging() {
758 if (gLoggingSettings.loggingOutputType === "profiler") {
759 await ProfilerPopupBackground.captureProfile("aboutlogging");
761 Services.prefs.clearUserPref("logging.config.LOG_FILE");
764 Services.prefs.setBoolPref("logging.config.running", false);
768 // We use the pageshow event instead of onload. This is needed because sometimes
769 // the page is loaded via session-restore/bfcache. In such cases we need to call
770 // init() to keep the page behaviour consistent with the ticked checkboxes.
771 // Mostly the issue is with the autorefresh checkbox.
772 window.addEventListener("pageshow", function () {