1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
|
/* 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/. */
// @ts-check
"use strict";
/**
* @typedef {import("./@types/perf").Action} Action
* @typedef {import("./@types/perf").Library} Library
* @typedef {import("./@types/perf").PerfFront} PerfFront
* @typedef {import("./@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
* @typedef {import("./@types/perf").RecordingState} RecordingState
* @typedef {import("./@types/perf").GetSymbolTableCallback} GetSymbolTableCallback
* @typedef {import("./@types/perf").PreferenceFront} PreferenceFront
* @typedef {import("./@types/perf").PerformancePref} PerformancePref
* @typedef {import("./@types/perf").RecordingStateFromPreferences} RecordingStateFromPreferences
* @typedef {import("./@types/perf").RestartBrowserWithEnvironmentVariable} RestartBrowserWithEnvironmentVariable
* @typedef {import("./@types/perf").GetEnvironmentVariable} GetEnvironmentVariable
* @typedef {import("./@types/perf").GetActiveBrowsingContextID} GetActiveBrowsingContextID
* @typedef {import("./@types/perf").MinimallyTypedGeckoProfile} MinimallyTypedGeckoProfile
* * @typedef {import("./@types/perf").ProfilerViewMode} ProfilerViewMode
*/
const ChromeUtils = require("ChromeUtils");
const { createLazyLoaders } = ChromeUtils.import(
"resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
);
const lazy = createLazyLoaders({
Chrome: () => require("chrome"),
Services: () => require("Services"),
OS: () => ChromeUtils.import("resource://gre/modules/osfile.jsm"),
ProfilerGetSymbols: () =>
ChromeUtils.import("resource://gre/modules/ProfilerGetSymbols.jsm"),
PerfSymbolication: () =>
ChromeUtils.import(
"resource://devtools/client/performance-new/symbolication.jsm.js"
),
});
const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
const SYMBOL_TABLE_REQUEST_EVENT = "devtools:perf-html-request-symbol-table";
const SYMBOL_TABLE_RESPONSE_EVENT = "devtools:perf-html-reply-symbol-table";
/** @type {PerformancePref["UIBaseUrl"]} */
const UI_BASE_URL_PREF = "devtools.performance.recording.ui-base-url";
/** @type {PerformancePref["UIBaseUrlPathPref"]} */
const UI_BASE_URL_PATH_PREF = "devtools.performance.recording.ui-base-url-path";
const UI_BASE_URL_DEFAULT = "https://profiler.firefox.com";
const UI_BASE_URL_PATH_DEFAULT = "/from-addon";
/**
* This file contains all of the privileged browser-specific functionality. This helps
* keep a clear separation between the privileged and non-privileged client code. It
* is also helpful in being able to mock out browser behavior for tests, without
* worrying about polluting the browser environment.
*/
/**
* Once a profile is received from the actor, it needs to be opened up in
* profiler.firefox.com to be analyzed. This function opens up profiler.firefox.com
* into a new browser tab, and injects the profile via a frame script.
*
* @param {MinimallyTypedGeckoProfile} profile - The Gecko profile.
* @param {ProfilerViewMode | undefined} profilerViewMode - View mode for the Firefox Profiler
* front-end timeline. While opening the url, we should append a query string
* if a view other than "full" needs to be displayed.
* @param {GetSymbolTableCallback} getSymbolTableCallback - A callback function with the signature
* (debugName, breakpadId) => Promise<SymbolTableAsTuple>, which will be invoked
* when profiler.firefox.com sends SYMBOL_TABLE_REQUEST_EVENT messages to us. This
* function should obtain a symbol table for the requested binary and resolve the
* returned promise with it.
*/
function receiveProfile(profile, profilerViewMode, getSymbolTableCallback) {
const Services = lazy.Services();
// Find the most recently used window, as the DevTools client could be in a variety
// of hosts.
const win = Services.wm.getMostRecentWindow("navigator:browser");
if (!win) {
throw new Error("No browser window");
}
const browser = win.gBrowser;
win.focus();
// Allow the user to point to something other than profiler.firefox.com.
const baseUrl = Services.prefs.getStringPref(
UI_BASE_URL_PREF,
UI_BASE_URL_DEFAULT
);
// Allow tests to override the path.
const baseUrlPath = Services.prefs.getStringPref(
UI_BASE_URL_PATH_PREF,
UI_BASE_URL_PATH_DEFAULT
);
// We automatically open up the "full" mode if no query string is present.
// `undefined` also means nothing is specified, and it should open the "full"
// timeline view in that case.
const viewModeQueryString =
profilerViewMode !== undefined && profilerViewMode !== "full"
? `?view=${profilerViewMode}`
: "";
const tab = browser.addWebTab(
`${baseUrl}${baseUrlPath}${viewModeQueryString}`,
{
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
userContextId: browser.contentPrincipal.userContextId,
}),
}
);
browser.selectedTab = tab;
const mm = tab.linkedBrowser.messageManager;
mm.loadFrameScript(
"chrome://devtools/content/performance-new/frame-script.js",
false
);
mm.sendAsyncMessage(TRANSFER_EVENT, profile);
mm.addMessageListener(SYMBOL_TABLE_REQUEST_EVENT, e => {
const { debugName, breakpadId } = e.data;
getSymbolTableCallback(debugName, breakpadId).then(
result => {
const [addr, index, buffer] = result;
mm.sendAsyncMessage(SYMBOL_TABLE_RESPONSE_EVENT, {
status: "success",
debugName,
breakpadId,
result: [addr, index, buffer],
});
},
error => {
// Re-wrap the error object into an object that is Structured Clone-able.
const { name, message, lineNumber, fileName } = error;
mm.sendAsyncMessage(SYMBOL_TABLE_RESPONSE_EVENT, {
status: "error",
debugName,
breakpadId,
error: { name, message, lineNumber, fileName },
});
}
);
});
}
/**
* Returns a function getDebugPathFor(debugName, breakpadId) => Library which
* resolves a (debugName, breakpadId) pair to the library's information, which
* contains the absolute paths on the file system where the binary and its
* optional pdb file are stored.
*
* This is needed for the following reason:
* - In order to obtain a symbol table for a system library, we need to know
* the library's absolute path on the file system. On Windows, we
* additionally need to know the absolute path to the library's PDB file,
* which we call the binary's "debugPath".
* - Symbol tables are requested asynchronously, by the profiler UI, after the
* profile itself has been obtained.
* - When the symbol tables are requested, we don't want the profiler UI to
* pass us arbitrary absolute file paths, as an extra defense against
* potential information leaks.
* - Instead, when the UI requests symbol tables, it identifies the library
* with a (debugName, breakpadId) pair. We need to map that pair back to the
* absolute paths.
* - We get the "trusted" paths from the "libs" sections of the profile. We
* trust these paths because we just obtained the profile directly from
* Gecko.
* - This function builds the (debugName, breakpadId) => Library mapping and
* retains it on the returned closure so that it can be consulted after the
* profile has been passed to the UI.
*
* @param {MinimallyTypedGeckoProfile} profile - The profile JSON object
* @returns {(debugName: string, breakpadId: string) => Library | undefined}
*/
function createLibraryMap(profile) {
const map = new Map();
/**
* @param {MinimallyTypedGeckoProfile} processProfile
*/
function fillMapForProcessRecursive(processProfile) {
for (const lib of processProfile.libs) {
const { debugName, breakpadId } = lib;
const key = [debugName, breakpadId].join(":");
map.set(key, lib);
}
for (const subprocess of processProfile.processes) {
fillMapForProcessRecursive(subprocess);
}
}
fillMapForProcessRecursive(profile);
return function getLibraryFor(debugName, breakpadId) {
const key = [debugName, breakpadId].join(":");
return map.get(key);
};
}
/**
* Return a function `getSymbolTable` that calls getSymbolTableMultiModal with the
* right arguments.
*
* @param {MinimallyTypedGeckoProfile} profile - The raw profie (not gzipped).
* @param {() => string[]} getObjdirs - A function that returns an array of objdir paths
* on the host machine that should be searched for relevant build artifacts.
* @param {PerfFront} perfFront
* @return {GetSymbolTableCallback}
*/
function createMultiModalGetSymbolTableFn(profile, getObjdirs, perfFront) {
const libraryGetter = createLibraryMap(profile);
return async function getSymbolTable(debugName, breakpadId) {
const lib = libraryGetter(debugName, breakpadId);
if (!lib) {
throw new Error(
`Could not find the library for "${debugName}", "${breakpadId}".`
);
}
const objdirs = getObjdirs();
const { getSymbolTableMultiModal } = lazy.PerfSymbolication();
return getSymbolTableMultiModal(lib, objdirs, perfFront);
};
}
/**
* Restarts the browser with a given environment variable set to a value.
*
* @type {RestartBrowserWithEnvironmentVariable}
*/
function restartBrowserWithEnvironmentVariable(envName, value) {
const Services = lazy.Services();
const { Cc, Ci } = lazy.Chrome();
const env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
env.set(envName, value);
Services.startup.quit(
Services.startup.eForceQuit | Services.startup.eRestart
);
}
/**
* Gets an environment variable from the browser.
*
* @type {GetEnvironmentVariable}
*/
function getEnvironmentVariable(envName) {
const { Cc, Ci } = lazy.Chrome();
const env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
return env.get(envName);
}
/**
* @param {Window} window
* @param {string[]} objdirs
* @param {(objdirs: string[]) => unknown} changeObjdirs
*/
function openFilePickerForObjdir(window, objdirs, changeObjdirs) {
const { Cc, Ci } = lazy.Chrome();
const FilePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
Ci.nsIFilePicker
);
FilePicker.init(window, "Pick build directory", FilePicker.modeGetFolder);
FilePicker.open(rv => {
if (rv == FilePicker.returnOK) {
const path = FilePicker.file.path;
if (path && !objdirs.includes(path)) {
const newObjdirs = [...objdirs, path];
changeObjdirs(newObjdirs);
}
}
});
}
module.exports = {
receiveProfile,
createMultiModalGetSymbolTableFn,
restartBrowserWithEnvironmentVariable,
getEnvironmentVariable,
openFilePickerForObjdir,
};
|