зеркало из https://github.com/mozilla/gecko-dev.git
285 строки
11 KiB
JavaScript
285 строки
11 KiB
JavaScript
/* 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,
|
|
};
|