browser.js (7547B)
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/. */ 4 // @ts-check 5 "use strict"; 6 7 /** 8 * @typedef {import("../@types/perf").Action} Action 9 * @typedef {import("../@types/perf").Library} Library 10 * @typedef {import("../@types/perf").PerfFront} PerfFront 11 * @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple 12 * @typedef {import("../@types/perf").RecordingState} RecordingState 13 * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService 14 * @typedef {import("../@types/perf").PreferenceFront} PreferenceFront 15 * @typedef {import("../@types/perf").PerformancePref} PerformancePref 16 * @typedef {import("../@types/perf").RecordingSettings} RecordingSettings 17 * @typedef {import("../@types/perf").GetActiveBrowserID} GetActiveBrowserID 18 * @typedef {import("../@types/perf").ProfilerViewMode} ProfilerViewMode 19 * @typedef {import("../@types/perf").ProfilerPanel} ProfilerPanel 20 */ 21 22 const { 23 gDevTools, 24 } = require("resource://devtools/client/framework/devtools.js"); 25 26 /** @type {PerformancePref["UIBaseUrl"]} */ 27 const UI_BASE_URL_PREF = "devtools.performance.recording.ui-base-url"; 28 /** @type {PerformancePref["UIBaseUrlPathPref"]} */ 29 const UI_BASE_URL_PATH_PREF = "devtools.performance.recording.ui-base-url-path"; 30 31 /** @type {PerformancePref["UIEnableActiveTabView"]} */ 32 const UI_ENABLE_ACTIVE_TAB_PREF = 33 "devtools.performance.recording.active-tab-view.enabled"; 34 35 const UI_BASE_URL_DEFAULT = "https://profiler.firefox.com"; 36 const UI_BASE_URL_PATH_DEFAULT = "/from-browser"; 37 38 /** 39 * This file contains all of the privileged browser-specific functionality. This helps 40 * keep a clear separation between the privileged and non-privileged client code. It 41 * is also helpful in being able to mock out browser behavior for tests, without 42 * worrying about polluting the browser environment. 43 */ 44 45 /** 46 * Once a profile is received from the actor, it needs to be opened up in 47 * profiler.firefox.com to be analyzed. This function opens up profiler.firefox.com 48 * into a new browser tab. 49 * 50 * @typedef {object} OpenProfilerOptions 51 * @property {ProfilerViewMode | undefined} [profilerViewMode] - View mode for the Firefox Profiler 52 * front-end timeline. While opening the url, we should append a query string 53 * if a view other than "full" needs to be displayed. 54 * @property {ProfilerPanel} [defaultPanel] Allows to change the default opened panel. 55 * 56 * @param {OpenProfilerOptions} options 57 * @returns {Promise<MockedExports.Browser>} The browser for the opened tab. 58 */ 59 async function openProfilerTab({ profilerViewMode, defaultPanel }) { 60 // Allow the user to point to something other than profiler.firefox.com. 61 const baseUrl = Services.prefs.getStringPref( 62 UI_BASE_URL_PREF, 63 UI_BASE_URL_DEFAULT 64 ); 65 // Allow tests to override the path. 66 const baseUrlPath = Services.prefs.getStringPref( 67 UI_BASE_URL_PATH_PREF, 68 UI_BASE_URL_PATH_DEFAULT 69 ); 70 const additionalPath = defaultPanel ? `/${defaultPanel}/` : ""; 71 // This controls whether we enable the active tab view when capturing in web 72 // developer preset. 73 const enableActiveTab = Services.prefs.getBoolPref( 74 UI_ENABLE_ACTIVE_TAB_PREF, 75 false 76 ); 77 78 // We automatically open up the "full" mode if no query string is present. 79 // `undefined` also means nothing is specified, and it should open the "full" 80 // timeline view in that case. 81 let viewModeQueryString = ""; 82 if (profilerViewMode === "active-tab") { 83 // We're not enabling the active-tab view in all environments until we 84 // iron out all its issues. 85 if (enableActiveTab) { 86 viewModeQueryString = "?view=active-tab&implementation=js"; 87 } else { 88 viewModeQueryString = "?implementation=js"; 89 } 90 } else if (profilerViewMode !== undefined && profilerViewMode !== "full") { 91 viewModeQueryString = `?view=${profilerViewMode}`; 92 } 93 94 const urlToLoad = `${baseUrl}${baseUrlPath}${additionalPath}${viewModeQueryString}`; 95 96 // Find the most recently used window, as the DevTools client could be in a variety 97 // of hosts. 98 // Note that when running from the browser toolbox, there won't be the browser window, 99 // but only the browser toolbox document. 100 const win = 101 Services.wm.getMostRecentBrowserWindow() || 102 Services.wm.getMostRecentWindow("devtools:toolbox"); 103 if (!win) { 104 throw new Error("No browser window"); 105 } 106 win.focus(); 107 108 // The profiler frontend currently doesn't support being loaded in a private 109 // window, because it does some storage writes in IndexedDB. That's why we 110 // force the opening of the tab in a non-private window. This might open a new 111 // non-private window if the only currently opened window is a private window. 112 const contentBrowser = await new Promise(resolveOnContentBrowserCreated => 113 win.openWebLinkIn(urlToLoad, "tab", { 114 forceNonPrivate: true, 115 resolveOnContentBrowserCreated, 116 userContextId: win.gBrowser?.contentPrincipal.userContextId, 117 relatedToCurrent: true, 118 }) 119 ); 120 return contentBrowser; 121 } 122 123 /** 124 * Restarts the browser with a given environment variable set to a value. 125 * 126 * @param {Record<string, string>} env 127 */ 128 function restartBrowserWithEnvironmentVariable(env) { 129 for (const [envName, envValue] of Object.entries(env)) { 130 Services.env.set(envName, envValue); 131 } 132 133 Services.startup.quit( 134 Services.startup.eForceQuit | Services.startup.eRestart 135 ); 136 } 137 138 /** 139 * @param {Window} window 140 * @param {string[]} objdirs 141 * @param {(objdirs: string[]) => unknown} changeObjdirs 142 */ 143 function openFilePickerForObjdir(window, objdirs, changeObjdirs) { 144 const FilePicker = Cc["@mozilla.org/filepicker;1"].createInstance( 145 Ci.nsIFilePicker 146 ); 147 FilePicker.init( 148 window.browsingContext, 149 "Pick build directory", 150 FilePicker.modeGetFolder 151 ); 152 FilePicker.open(rv => { 153 if (rv == FilePicker.returnOK) { 154 const path = FilePicker.file.path; 155 if (path && !objdirs.includes(path)) { 156 const newObjdirs = [...objdirs, path]; 157 changeObjdirs(newObjdirs); 158 } 159 } 160 }); 161 } 162 163 /** 164 * Try to open the given script with line and column in the tab. 165 * 166 * If the profiled tab is not alive anymore, returns without doing anything. 167 * 168 * @param {number} tabId 169 * @param {string} scriptUrl 170 * @param {number} line 171 * @param {number} columnOneBased 172 */ 173 async function openScriptInDebugger(tabId, scriptUrl, line, columnOneBased) { 174 const win = Services.wm.getMostRecentBrowserWindow(); 175 176 // Iterate through all tabs in the current window and find the tab that we want. 177 const foundTab = win.gBrowser.tabs.find( 178 tab => tab.linkedBrowser.browserId === tabId 179 ); 180 181 if (!foundTab) { 182 console.log(`No tab found with the tab id: ${tabId}`); 183 return; 184 } 185 186 // If a matching tab was found, switch to it. 187 win.gBrowser.selectedTab = foundTab; 188 189 // And open the devtools debugger with script. 190 const toolbox = await gDevTools.showToolboxForTab(foundTab, { 191 toolId: "jsdebugger", 192 }); 193 194 toolbox.win.focus(); 195 196 // In case profiler backend can't retrieve the column number, it can return zero. 197 const columnZeroBased = columnOneBased > 0 ? columnOneBased - 1 : 0; 198 await toolbox.viewSourceInDebugger( 199 scriptUrl, 200 line, 201 columnZeroBased, 202 /* sourceId = */ null, 203 "ProfilerOpenScript" 204 ); 205 } 206 207 module.exports = { 208 openProfilerTab, 209 restartBrowserWithEnvironmentVariable, 210 openFilePickerForObjdir, 211 openScriptInDebugger, 212 };