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

230 lines
8.1 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").SymbolicationService} SymbolicationService
* @typedef {import("./@types/perf").PreferenceFront} PreferenceFront
* @typedef {import("./@types/perf").PerformancePref} PerformancePref
* @typedef {import("./@types/perf").RecordingSettings} RecordingSettings
* @typedef {import("./@types/perf").RestartBrowserWithEnvironmentVariable} RestartBrowserWithEnvironmentVariable
* @typedef {import("./@types/perf").GetEnvironmentVariable} GetEnvironmentVariable
* @typedef {import("./@types/perf").GetActiveBrowserID} GetActiveBrowserID
* @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"),
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 | ArrayBuffer | {}} 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 {SymbolicationService} symbolicationService - An object which implements the
* SymbolicationService interface, whose getSymbolTable method will be invoked
* when profiler.firefox.com sends SYMBOL_TABLE_REQUEST_EVENT messages to us. This
* method should obtain a symbol table for the requested binary and resolve the
* returned promise with it.
*/
function openProfilerAndDisplayProfile(
profile,
profilerViewMode,
symbolicationService
) {
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.
let viewModeQueryString = "";
if (profilerViewMode === "active-tab") {
viewModeQueryString = "?view=active-tab&implementation=js";
} else if (profilerViewMode !== undefined && profilerViewMode !== "full") {
viewModeQueryString = `?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;
symbolicationService.getSymbolTable(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 },
});
}
);
});
}
/**
* Flatten all the sharedLibraries of the different processes in the profile
* into one list of libraries.
* @param {MinimallyTypedGeckoProfile} profile - The profile JSON object
* @returns {Library[]}
*/
function sharedLibrariesFromProfile(profile) {
/**
* @param {MinimallyTypedGeckoProfile} processProfile
* @returns {Library[]}
*/
function getLibsRecursive(processProfile) {
return processProfile.libs.concat(
...processProfile.processes.map(getLibsRecursive)
);
}
return getLibsRecursive(profile);
}
/**
* 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 = {
openProfilerAndDisplayProfile,
sharedLibrariesFromProfile,
restartBrowserWithEnvironmentVariable,
getEnvironmentVariable,
openFilePickerForObjdir,
};