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/. */
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").RestartBrowserWithEnvironmentVariable} RestartBrowserWithEnvironmentVariable
18 * @typedef {import("../@types/perf").GetActiveBrowserID} GetActiveBrowserID
19 * @typedef {import("../@types/perf").MinimallyTypedGeckoProfile} MinimallyTypedGeckoProfile
20 * @typedef {import("../@types/perf").ProfilerViewMode} ProfilerViewMode
21 * @typedef {import("../@types/perf").ProfilerPanel} ProfilerPanel
26 } = require("resource://devtools/client/framework/devtools.js");
28 /** @type {PerformancePref["UIBaseUrl"]} */
29 const UI_BASE_URL_PREF
= "devtools.performance.recording.ui-base-url";
30 /** @type {PerformancePref["UIBaseUrlPathPref"]} */
31 const UI_BASE_URL_PATH_PREF
= "devtools.performance.recording.ui-base-url-path";
33 /** @type {PerformancePref["UIEnableActiveTabView"]} */
34 const UI_ENABLE_ACTIVE_TAB_PREF
=
35 "devtools.performance.recording.active-tab-view.enabled";
37 const UI_BASE_URL_DEFAULT
= "https://profiler.firefox.com";
38 const UI_BASE_URL_PATH_DEFAULT
= "/from-browser";
41 * This file contains all of the privileged browser-specific functionality. This helps
42 * keep a clear separation between the privileged and non-privileged client code. It
43 * is also helpful in being able to mock out browser behavior for tests, without
44 * worrying about polluting the browser environment.
48 * Once a profile is received from the actor, it needs to be opened up in
49 * profiler.firefox.com to be analyzed. This function opens up profiler.firefox.com
50 * into a new browser tab.
52 * @typedef {Object} OpenProfilerOptions
53 * @property {ProfilerViewMode | undefined} [profilerViewMode] - View mode for the Firefox Profiler
54 * front-end timeline. While opening the url, we should append a query string
55 * if a view other than "full" needs to be displayed.
56 * @property {ProfilerPanel} [defaultPanel] Allows to change the default opened panel.
58 * @param {OpenProfilerOptions} options
59 * @returns {Promise<MockedExports.Browser>} The browser for the opened tab.
61 async
function openProfilerTab({ profilerViewMode
, defaultPanel
}) {
62 // Allow the user to point to something other than profiler.firefox.com.
63 const baseUrl
= Services
.prefs
.getStringPref(
67 // Allow tests to override the path.
68 const baseUrlPath
= Services
.prefs
.getStringPref(
69 UI_BASE_URL_PATH_PREF
,
70 UI_BASE_URL_PATH_DEFAULT
72 const additionalPath
= defaultPanel
? `/${defaultPanel}/` : "";
73 // This controls whether we enable the active tab view when capturing in web
75 const enableActiveTab
= Services
.prefs
.getBoolPref(
76 UI_ENABLE_ACTIVE_TAB_PREF
,
80 // We automatically open up the "full" mode if no query string is present.
81 // `undefined` also means nothing is specified, and it should open the "full"
82 // timeline view in that case.
83 let viewModeQueryString
= "";
84 if (profilerViewMode
=== "active-tab") {
85 // We're not enabling the active-tab view in all environments until we
86 // iron out all its issues.
87 if (enableActiveTab
) {
88 viewModeQueryString
= "?view=active-tab&implementation=js";
90 viewModeQueryString
= "?implementation=js";
92 } else if (profilerViewMode
!== undefined && profilerViewMode
!== "full") {
93 viewModeQueryString
= `?view=${profilerViewMode}`;
96 const urlToLoad
= `${baseUrl}${baseUrlPath}${additionalPath}${viewModeQueryString}`;
98 // Find the most recently used window, as the DevTools client could be in a variety
100 // Note that when running from the browser toolbox, there won't be the browser window,
101 // but only the browser toolbox document.
103 Services
.wm
.getMostRecentWindow("navigator:browser") ||
104 Services
.wm
.getMostRecentWindow("devtools:toolbox");
106 throw new Error("No browser window");
110 // The profiler frontend currently doesn't support being loaded in a private
111 // window, because it does some storage writes in IndexedDB. That's why we
112 // force the opening of the tab in a non-private window. This might open a new
113 // non-private window if the only currently opened window is a private window.
114 const contentBrowser
= await
new Promise(resolveOnContentBrowserCreated
=>
115 win
.openWebLinkIn(urlToLoad
, "tab", {
116 forceNonPrivate
: true,
117 resolveOnContentBrowserCreated
,
118 userContextId
: win
.gBrowser
?.contentPrincipal
.userContextId
,
119 relatedToCurrent
: true,
122 return contentBrowser
;
126 * Flatten all the sharedLibraries of the different processes in the profile
127 * into one list of libraries.
128 * @param {MinimallyTypedGeckoProfile} profile - The profile JSON object
129 * @returns {Library[]}
131 function sharedLibrariesFromProfile(profile
) {
133 * @param {MinimallyTypedGeckoProfile} processProfile
134 * @returns {Library[]}
136 function getLibsRecursive(processProfile
) {
137 return processProfile
.libs
.concat(
138 ...processProfile
.processes
.map(getLibsRecursive
)
142 return getLibsRecursive(profile
);
146 * Restarts the browser with a given environment variable set to a value.
148 * @type {RestartBrowserWithEnvironmentVariable}
150 function restartBrowserWithEnvironmentVariable(envName
, value
) {
151 Services
.env
.set(envName
, value
);
153 Services
.startup
.quit(
154 Services
.startup
.eForceQuit
| Services
.startup
.eRestart
159 * @param {Window} window
160 * @param {string[]} objdirs
161 * @param {(objdirs: string[]) => unknown} changeObjdirs
163 function openFilePickerForObjdir(window
, objdirs
, changeObjdirs
) {
164 const FilePicker
= Cc
["@mozilla.org/filepicker;1"].createInstance(
168 window
.browsingContext
,
169 "Pick build directory",
170 FilePicker
.modeGetFolder
172 FilePicker
.open(rv
=> {
173 if (rv
== FilePicker
.returnOK
) {
174 const path
= FilePicker
.file
.path
;
175 if (path
&& !objdirs
.includes(path
)) {
176 const newObjdirs
= [...objdirs
, path
];
177 changeObjdirs(newObjdirs
);
184 * Try to open the given script with line and column in the tab.
186 * If the profiled tab is not alive anymore, returns without doing anything.
188 * @param {number} tabId
189 * @param {string} scriptUrl
190 * @param {number} line
191 * @param {number} columnOneBased
193 async
function openScriptInDebugger(tabId
, scriptUrl
, line
, columnOneBased
) {
194 const win
= Services
.wm
.getMostRecentWindow("navigator:browser");
196 // Iterate through all tabs in the current window and find the tab that we want.
197 const foundTab
= win
.gBrowser
.tabs
.find(
198 tab
=> tab
.linkedBrowser
.browserId
=== tabId
202 console
.log(`No tab found with the tab id: ${tabId}`);
206 // If a matching tab was found, switch to it.
207 win
.gBrowser
.selectedTab
= foundTab
;
209 // And open the devtools debugger with script.
210 const toolbox
= await gDevTools
.showToolboxForTab(foundTab
, {
211 toolId
: "jsdebugger",
216 // In case profiler backend can't retrieve the column number, it can return zero.
217 const columnZeroBased
= columnOneBased
> 0 ? columnOneBased
- 1 : 0;
218 await toolbox
.viewSourceInDebugger(
222 /* sourceId = */ null,
229 sharedLibrariesFromProfile
,
230 restartBrowserWithEnvironmentVariable
,
231 openFilePickerForObjdir
,
232 openScriptInDebugger
,