gecko-dev/devtools/client/performance-new/browser.js

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,
};