diff --git a/devtools/client/jar.mn b/devtools/client/jar.mn index 486c6bddafea8..8cd81b6b94c53 100644 --- a/devtools/client/jar.mn +++ b/devtools/client/jar.mn @@ -341,7 +341,6 @@ devtools.jar: # Perfomance content/performance/index.xhtml (performance/index.xhtml) content/performance-new/index.xhtml (performance-new/index.xhtml) - content/performance-new/frame-script.js (performance-new/frame-script.js) # Memory content/memory/index.xhtml (memory/index.xhtml) diff --git a/devtools/client/performance-new/@types/frame-script.d.ts b/devtools/client/performance-new/@types/frame-script.d.ts deleted file mode 100644 index 4271a8222eef6..0000000000000 --- a/devtools/client/performance-new/@types/frame-script.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* 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/. */ - -/** - * This file contains the globals for the Gecko Profiler frame script environment. - */ - -interface ContentWindow { - wrappedJSObject: { - connectToGeckoProfiler?: ( - interface: import("./perf").GeckoProfilerFrameScriptInterface - ) => void; - }; -} - -declare var content: ContentWindow; diff --git a/devtools/client/performance-new/@types/gecko.d.ts b/devtools/client/performance-new/@types/gecko.d.ts index 19805f1003c0a..9323ca9591e23 100644 --- a/devtools/client/performance-new/@types/gecko.d.ts +++ b/devtools/client/performance-new/@types/gecko.d.ts @@ -291,7 +291,6 @@ declare namespace MockedExports { * Then add the file path to the KnownModules above. */ import: (module: S) => KnownModules[S]; - createObjectIn: (content: ContentWindow) => object; exportFunction: (fn: Function, scope: object, options?: object) => void; cloneInto: (value: any, scope: object, options?: object) => void; isInAutomation: boolean; diff --git a/devtools/client/performance-new/@types/perf.d.ts b/devtools/client/performance-new/@types/perf.d.ts index be43e042c0f69..76ce2115c7b10 100644 --- a/devtools/client/performance-new/@types/perf.d.ts +++ b/devtools/client/performance-new/@types/perf.d.ts @@ -452,26 +452,85 @@ export interface Presets { [presetName: string]: PresetDefinition; } -export type MessageFromFrontend = - | { - type: "STATUS_QUERY"; - requestId: number; - } - | { - type: "ENABLE_MENU_BUTTON"; - requestId: number; - }; +// Should be kept in sync with the types in https://github.com/firefox-devtools/profiler/blob/main/src/app-logic/web-channel.js . +// Compatibility is handled as follows: +// - The front-end needs to worry about compatibility and handle older browser versions. +// - The browser can require the latest front-end version and does not need to keep any legacy functionality for older front-end versions. + +type MessageFromFrontend = { + requestId: number; +} & RequestFromFrontend; + +export type RequestFromFrontend = + | StatusQueryRequest + | EnableMenuButtonRequest + | GetProfileRequest + | GetSymbolTableRequest + | QuerySymbolicationApiRequest; + +type StatusQueryRequest = { type: "STATUS_QUERY" }; +type EnableMenuButtonRequest = { type: "ENABLE_MENU_BUTTON" }; +type GetProfileRequest = { type: "GET_PROFILE" }; +type GetSymbolTableRequest = { + type: "GET_SYMBOL_TABLE"; + debugName: string; + breakpadId: string; +}; +type QuerySymbolicationApiRequest = { + type: "QUERY_SYMBOLICATION_API"; + path: string; + requestJson: string; +}; -export type MessageToFrontend = - | { - type: "STATUS_RESPONSE"; - menuButtonIsEnabled: boolean; - requestId: number; - } - | { - type: "ENABLE_MENU_BUTTON_DONE"; - requestId: number; - }; +export type MessageToFrontend = + | OutOfBandErrorMessageToFrontend + | ErrorResponseMessageToFrontend + | SuccessResponseMessageToFrontend; + +type OutOfBandErrorMessageToFrontend = { + errno: number; + error: string; +}; + +type ErrorResponseMessageToFrontend = { + type: "ERROR_RESPONSE"; + requestId: number; + error: string; +}; + +type SuccessResponseMessageToFrontend = { + type: "SUCCESS_RESPONSE"; + requestId: number; + response: R; +}; + +export type ResponseToFrontend = + | StatusQueryResponse + | EnableMenuButtonResponse + | GetProfileResponse + | GetSymbolTableResponse + | QuerySymbolicationApiResponse; + +type StatusQueryResponse = { + menuButtonIsEnabled: boolean; + // The version indicates which message types are supported by the browser. + // No version: + // Shipped in Firefox 76. + // Supports the following message types: + // - STATUS_QUERY + // - ENABLE_MENU_BUTTON + // Version 1: + // Shipped in Firefox 93. + // Adds support for the following message types: + // - GET_PROFILE + // - GET_SYMBOL_TABLE + // - QUERY_SYMBOLICATION_API + version: number; +}; +type EnableMenuButtonResponse = void; +type GetProfileResponse = ArrayBuffer | MinimallyTypedGeckoProfile; +type GetSymbolTableResponse = SymbolTableAsTuple; +type QuerySymbolicationApiResponse = string; /** * This represents an event channel that can talk to a content page on the web. @@ -484,7 +543,7 @@ export type MessageToFrontend = export class ProfilerWebChannel { constructor(id: string, url: MockedExports.nsIURI); send: ( - message: MessageToFrontend, + message: MessageToFrontend, target: MockedExports.WebChannelTarget ) => void; listen: ( @@ -496,6 +555,26 @@ export class ProfilerWebChannel { ) => void; } +/** + * The per-tab information that is stored when a new profile is captured + * and a profiler tab is opened, to serve the correct profile to the tab + * that sends the WebChannel message. + */ +export type ProfilerBrowserInfo = { + profileCaptureResult: ProfileCaptureResult; + symbolicationService: SymbolicationService; +}; + +export type ProfileCaptureResult = + | { + type: "SUCCESS"; + profile: MinimallyTypedGeckoProfile | ArrayBuffer; + } + | { + type: "ERROR"; + error: Error; + }; + /** * Describes all of the profiling features that can be turned on and * off in about:profiling. diff --git a/devtools/client/performance-new/browser.js b/devtools/client/performance-new/browser.js index 055eda66e7529..68b9c1c9bbdfc 100644 --- a/devtools/client/performance-new/browser.js +++ b/devtools/client/performance-new/browser.js @@ -36,10 +36,6 @@ const lazy = createLazyLoaders({ ), }); -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"]} */ @@ -58,23 +54,13 @@ const UI_BASE_URL_PATH_DEFAULT = "/from-addon"; /** * 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. + * into a new browser tab. * @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. + * @returns {MockedExports.Browser} The browser for the opened tab. */ -function openProfilerAndDisplayProfile( - profile, - profilerViewMode, - symbolicationService -) { +function openProfilerTab(profilerViewMode) { const Services = lazy.Services(); // Find the most recently used window, as the DevTools client could be in a variety // of hosts. @@ -115,36 +101,7 @@ function openProfilerAndDisplayProfile( } ); 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 }, - }); - } - ); - }); + return tab.linkedBrowser; } /** @@ -221,7 +178,7 @@ function openFilePickerForObjdir(window, objdirs, changeObjdirs) { } module.exports = { - openProfilerAndDisplayProfile, + openProfilerTab, sharedLibrariesFromProfile, restartBrowserWithEnvironmentVariable, getEnvironmentVariable, diff --git a/devtools/client/performance-new/frame-script.js b/devtools/client/performance-new/frame-script.js deleted file mode 100644 index e1bafe12d1918..0000000000000 --- a/devtools/client/performance-new/frame-script.js +++ /dev/null @@ -1,200 +0,0 @@ -/* 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 -/// -/* global content */ -"use strict"; - -/** - * @typedef {import("./@types/perf").GetSymbolTableCallback} GetSymbolTableCallback - * @typedef {import("./@types/perf").ContentFrameMessageManager} ContentFrameMessageManager - * @typedef {import("./@types/perf").MinimallyTypedGeckoProfile} MinimallyTypedGeckoProfile - */ - -/** - * This frame script injects itself into profiler.firefox.com and injects the profile - * into the page. It is mostly taken from the Gecko Profiler Addon implementation. - */ - -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 {null | MinimallyTypedGeckoProfile} */ -let gProfile = null; -const symbolReplyPromiseMap = new Map(); - -/** - * TypeScript wants to use the DOM library definition, which conflicts with our - * own definitions for the frame message manager. Instead, coerce the `this` - * variable into the proper interface. - * - * @type {ContentFrameMessageManager} - */ -let frameScript; -{ - const any = /** @type {any} */ (this); - frameScript = any; -} - -frameScript.addMessageListener(TRANSFER_EVENT, e => { - gProfile = e.data; - // Eagerly try and see if the framescript was evaluated after perf loaded its scripts. - connectToPage(); - // If not try again at DOMContentLoaded which should be called after the script - // tag was synchronously loaded in. - frameScript.addEventListener("DOMContentLoaded", connectToPage); -}); - -frameScript.addMessageListener(SYMBOL_TABLE_RESPONSE_EVENT, e => { - const { debugName, breakpadId, status, result, error } = e.data; - const promiseKey = [debugName, breakpadId].join(":"); - const { resolve, reject } = symbolReplyPromiseMap.get(promiseKey); - symbolReplyPromiseMap.delete(promiseKey); - - if (status === "success") { - const [addresses, index, buffer] = result; - resolve([addresses, index, buffer]); - } else { - reject(error); - } -}); - -function connectToPage() { - const unsafeWindow = content.wrappedJSObject; - if (unsafeWindow.connectToGeckoProfiler) { - unsafeWindow.connectToGeckoProfiler( - makeAccessibleToPage( - { - getProfile: () => - gProfile - ? Promise.resolve(gProfile) - : Promise.reject( - new Error("No profile was available to inject into the page.") - ), - getSymbolTable: (debugName, breakpadId) => - getSymbolTable(debugName, breakpadId), - }, - unsafeWindow - ) - ); - } -} - -/** @type {GetSymbolTableCallback} */ -function getSymbolTable(debugName, breakpadId) { - return new Promise((resolve, reject) => { - frameScript.sendAsyncMessage(SYMBOL_TABLE_REQUEST_EVENT, { - debugName, - breakpadId, - }); - symbolReplyPromiseMap.set([debugName, breakpadId].join(":"), { - resolve, - reject, - }); - }); -} - -// The following functions handle the security of cloning the object into the page. -// The code was taken from the original Gecko Profiler Add-on to maintain -// compatibility with the existing profile importing mechanism: -// See: https://github.com/firefox-devtools/Gecko-Profiler-Addon/blob/78138190b42565f54ce4022a5b28583406489ed2/data/tab-framescript.js - -/** - * Create a promise that can be used in the page. - * - * @template T - * @param {(resolve: Function, reject: Function) => Promise} fun - * @param {any} contentGlobal - * @returns Promise - */ -function createPromiseInPage(fun, contentGlobal) { - /** - * Use the any type here, as this is pretty dynamic, and probably not worth typing. - * @param {any} resolve - * @param {any} reject - */ - function funThatClonesObjects(resolve, reject) { - return fun( - /** @type {(result: any) => any} */ - result => resolve(Cu.cloneInto(result, contentGlobal)), - /** @type {(result: any) => any} */ - error => { - if (error.name) { - // Turn the JSON error object into a real Error object. - const { name, message, fileName, lineNumber } = error; - const ErrorObjConstructor = - name in contentGlobal && - contentGlobal.Error.isPrototypeOf(contentGlobal[name]) - ? contentGlobal[name] - : contentGlobal.Error; - const e = new ErrorObjConstructor(message, fileName, lineNumber); - e.name = name; - reject(e); - } else { - reject(Cu.cloneInto(error, contentGlobal)); - } - } - ); - } - return new contentGlobal.Promise( - Cu.exportFunction(funThatClonesObjects, contentGlobal) - ); -} - -/** - * Returns a function that calls the original function and tries to make the - * return value available to the page. - * @param {Function} fun - * @param {any} contentGlobal - * @return {Function} - */ -function wrapFunction(fun, contentGlobal) { - return function() { - // @ts-ignore - Ignore the use of `this`. - const result = fun.apply(this, arguments); - if (typeof result === "object") { - if ("then" in result && typeof result.then === "function") { - // fun returned a promise. - return createPromiseInPage( - (resolve, reject) => result.then(resolve, reject), - contentGlobal - ); - } - return Cu.cloneInto(result, contentGlobal); - } - return result; - }; -} - -/** - * Pass a simple object containing values that are objects or functions. - * The objects or functions are wrapped in such a way that they can be - * consumed by the page. - * @template T - * @param {T} obj - * @param {any} contentGlobal - * @return {T} - */ -function makeAccessibleToPage(obj, contentGlobal) { - /** @type {any} - This value is probably too dynamic to type. */ - const result = Cu.createObjectIn(contentGlobal); - for (const field in obj) { - switch (typeof obj[field]) { - case "function": - // @ts-ignore - Ignore the obj[field] call. This code is too dynamic. - Cu.exportFunction(wrapFunction(obj[field], contentGlobal), result, { - defineAs: field, - }); - break; - case "object": - Cu.cloneInto(obj[field], result, { defineAs: field }); - break; - default: - result[field] = obj[field]; - break; - } - } - return result; -} diff --git a/devtools/client/performance-new/initializer.js b/devtools/client/performance-new/initializer.js index a917180857ba7..1bff90cdd0219 100644 --- a/devtools/client/performance-new/initializer.js +++ b/devtools/client/performance-new/initializer.js @@ -12,6 +12,7 @@ * @typedef {import("./@types/perf").PanelWindow} PanelWindow * @typedef {import("./@types/perf").Store} Store * @typedef {import("./@types/perf").MinimallyTypedGeckoProfile} MinimallyTypedGeckoProfile + * @typedef {import("./@types/perf").ProfileCaptureResult} ProfileCaptureResult * @typedef {import("./@types/perf").ProfilerViewMode} ProfilerViewMode */ "use strict"; @@ -64,13 +65,17 @@ const selectors = require("devtools/client/performance-new/store/selectors"); const reducers = require("devtools/client/performance-new/store/reducers"); const actions = require("devtools/client/performance-new/store/actions"); const { - openProfilerAndDisplayProfile, + openProfilerTab, sharedLibrariesFromProfile, } = require("devtools/client/performance-new/browser"); const { createLocalSymbolicationService } = ChromeUtils.import( "resource://devtools/client/performance-new/symbolication.jsm.js" ); -const { presets, getProfilerViewModeForCurrentPreset } = ChromeUtils.import( +const { + presets, + getProfilerViewModeForCurrentPreset, + registerProfileCaptureForBrowser, +} = ChromeUtils.import( "resource://devtools/client/performance-new/popup/background.jsm.js" ); @@ -140,9 +145,16 @@ async function gInit(perfFront, pageContext, openAboutProfiling) { objdirs, perfFront ); - openProfilerAndDisplayProfile( - profile, - profilerViewMode, + const browser = openProfilerTab(profilerViewMode); + + /** + * @type {ProfileCaptureResult} + */ + const profileCaptureResult = { type: "SUCCESS", profile }; + + registerProfileCaptureForBrowser( + browser, + profileCaptureResult, symbolicationService ); }; diff --git a/devtools/client/performance-new/popup/background.jsm.js b/devtools/client/performance-new/popup/background.jsm.js index c1d5aa8ac95f1..b76c2c954f93d 100644 --- a/devtools/client/performance-new/popup/background.jsm.js +++ b/devtools/client/performance-new/popup/background.jsm.js @@ -29,12 +29,17 @@ const AppConstants = ChromeUtils.import( * @typedef {import("../@types/perf").Library} Library * @typedef {import("../@types/perf").PerformancePref} PerformancePref * @typedef {import("../@types/perf").ProfilerWebChannel} ProfilerWebChannel - * @typedef {import("../@types/perf").MessageFromFrontend} MessageFromFrontend * @typedef {import("../@types/perf").PageContext} PageContext * @typedef {import("../@types/perf").PrefObserver} PrefObserver * @typedef {import("../@types/perf").PrefPostfix} PrefPostfix * @typedef {import("../@types/perf").Presets} Presets * @typedef {import("../@types/perf").ProfilerViewMode} ProfilerViewMode + * @typedef {import("../@types/perf").MessageFromFrontend} MessageFromFrontend + * @typedef {import("../@types/perf").RequestFromFrontend} RequestFromFrontend + * @typedef {import("../@types/perf").ResponseToFrontend} ResponseToFrontend + * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService + * @typedef {import("../@types/perf").ProfilerBrowserInfo} ProfilerBrowserInfo + * @typedef {import("../@types/perf").ProfileCaptureResult} ProfileCaptureResult */ /** @type {PerformancePref["Entries"]} */ @@ -56,6 +61,13 @@ const POPUP_FEATURE_FLAG_PREF = "devtools.performance.popup.feature-flag"; /* This will be used to observe all profiler-related prefs. */ const PREF_PREFIX = "devtools.performance.recording."; +// The version of the profiler WebChannel. +// This is reported from the STATUS_QUERY message, and identifies the +// capabilities of the WebChannel. The front-end can handle old WebChannel +// versions and has a full list of versions and capabilities here: +// https://github.com/firefox-devtools/profiler/blob/main/src/app-logic/web-channel.js +const CURRENT_WEBCHANNEL_VERSION = 1; + // Lazily load the require function, when it's needed. ChromeUtils.defineModuleGetter( this, @@ -264,12 +276,16 @@ async function captureProfile(pageContext) { // more samples while the parent process waits for subprocess profiles. Services.profiler.Pause(); - const profile = await Services.profiler + /** + * @type {ProfileCaptureResult} + */ + const profileCaptureResult = await Services.profiler .getProfileDataAsGzippedArrayBuffer() - .catch( - /** @type {(e: any) => {}} */ e => { - console.error(e); - return {}; + .then( + profile => ({ type: "SUCCESS", profile }), + error => { + console.error(error); + return { type: "ERROR", error }; } ); @@ -283,10 +299,11 @@ async function captureProfile(pageContext) { objdirs ); - const { openProfilerAndDisplayProfile } = lazy.BrowserModule(); - openProfilerAndDisplayProfile( - profile, - profilerViewMode, + const { openProfilerTab } = lazy.BrowserModule(); + const browser = openProfilerTab(profilerViewMode); + registerProfileCaptureForBrowser( + browser, + profileCaptureResult, symbolicationService ); @@ -609,40 +626,48 @@ function removePrefObserver(observer) { } /** - * This handler handles any messages coming from the WebChannel from profiler.firefox.com. + * This map stores information that is associated with a "profile capturing" + * action, so that we can look up this information for WebChannel messages + * from the profiler tab. + * Most importantly, this stores the captured profile. When the profiler tab + * requests the profile, we can respond to the message with the correct profile. + * This works even if the request happens long after the tab opened. It also + * works for an "old" tab even if new profiles have been captured since that + * tab was opened. + * Supporting tab refresh is important because the tab sometimes reloads itself: + * If an old version of the front-end is cached in the service worker, and the + * browser supplies a profile with a newer format version, then the front-end + * updates its service worker and reloads itself, so that the updated version + * can parse the profile. * - * @param {ProfilerWebChannel} channel - * @param {string} id - * @param {any} message - * @param {MockedExports.WebChannelTarget} target + * This is a WeakMap so that the profile can be garbage-collected when the tab + * is closed. + * + * @type {WeakMap} */ -function handleWebChannelMessage(channel, id, message, target) { - if (typeof message !== "object" || typeof message.type !== "string") { - console.error( - "An malformed message was received by the profiler's WebChannel handler.", - message - ); - return; - } - const messageFromFrontend = /** @type {MessageFromFrontend} */ (message); - const { requestId } = messageFromFrontend; - switch (messageFromFrontend.type) { +const infoForBrowserMap = new WeakMap(); + +/** + * This handler computes the response for any messages coming + * from the WebChannel from profiler.firefox.com. + * + * @param {RequestFromFrontend} request + * @param {MockedExports.Browser} browser - The tab's browser. + * @return {Promise} + */ +async function getResponseForMessage(request, browser) { + switch (request.type) { case "STATUS_QUERY": { // The content page wants to know if this channel exists. It does, so respond // back to the ping. const { ProfilerMenuButton } = lazy.ProfilerMenuButton(); - channel.send( - { - type: "STATUS_RESPONSE", - menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(), - requestId, - }, - target - ); - break; + return { + version: CURRENT_WEBCHANNEL_VERSION, + menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(), + }; } case "ENABLE_MENU_BUTTON": { - const { ownerDocument } = target.browser; + const { ownerDocument } = browser; if (!ownerDocument) { throw new Error( "Could not find the owner document for the current browser while enabling " + @@ -664,24 +689,124 @@ function handleWebChannelMessage(channel, id, message, target) { // Open the popup with a message. ProfilerMenuButton.openPopup(ownerDocument); - // Respond back that we've done it. - channel.send( - { - type: "ENABLE_MENU_BUTTON_DONE", - requestId, - }, - target + // There is no response data for this message. + return undefined; + } + case "GET_PROFILE": { + const infoForBrowser = infoForBrowserMap.get(browser); + if (infoForBrowser === undefined) { + throw new Error("Could not find a profile for this tab."); + } + const { profileCaptureResult } = infoForBrowser; + switch (profileCaptureResult.type) { + case "SUCCESS": + return profileCaptureResult.profile; + case "ERROR": + throw profileCaptureResult.error; + default: + const { UnhandledCaseError } = lazy.Utils(); + throw new UnhandledCaseError( + profileCaptureResult, + "profileCaptureResult" + ); + } + } + case "GET_SYMBOL_TABLE": { + const infoForBrowser = infoForBrowserMap.get(browser); + if (infoForBrowser === undefined) { + throw new Error("Could not find a symbolication service for this tab."); + } + const { debugName, breakpadId } = request; + return infoForBrowser.symbolicationService.getSymbolTable( + debugName, + breakpadId + ); + } + case "QUERY_SYMBOLICATION_API": { + const infoForBrowser = infoForBrowserMap.get(browser); + if (infoForBrowser === undefined) { + throw new Error("Could not find a symbolication service for this tab."); + } + const { path, requestJson } = request; + return infoForBrowser.symbolicationService.querySymbolicationApi( + path, + requestJson ); - break; } default: console.error( "An unknown message type was received by the profiler's WebChannel handler.", - message + request ); + const { UnhandledCaseError } = lazy.Utils(); + throw new UnhandledCaseError(request, "WebChannel request"); } } +/** + * This handler handles any messages coming from the WebChannel from profiler.firefox.com. + * + * @param {ProfilerWebChannel} channel + * @param {string} id + * @param {any} message + * @param {MockedExports.WebChannelTarget} target + */ +async function handleWebChannelMessage(channel, id, message, target) { + if (typeof message !== "object" || typeof message.type !== "string") { + console.error( + "An malformed message was received by the profiler's WebChannel handler.", + message + ); + return; + } + const messageFromFrontend = /** @type {MessageFromFrontend} */ (message); + const { requestId } = messageFromFrontend; + + try { + const response = await getResponseForMessage( + messageFromFrontend, + target.browser + ); + channel.send( + { + type: "SUCCESS_RESPONSE", + requestId, + response, + }, + target + ); + } catch (error) { + channel.send( + { + type: "ERROR_RESPONSE", + requestId, + error: `${error.name}: ${error.message}`, + }, + target + ); + } +} + +/** + * @param {MockedExports.Browser} browser - The tab's browser. + * @param {ProfileCaptureResult} profileCaptureResult - The Gecko profile. + * @param {SymbolicationService} symbolicationService - An object which implements the + * SymbolicationService interface, whose getSymbolTable method will be invoked + * when profiler.firefox.com sends GET_SYMBOL_TABLE WebChannel messages to us. This + * method should obtain a symbol table for the requested binary and resolve the + * returned promise with it. + */ +function registerProfileCaptureForBrowser( + browser, + profileCaptureResult, + symbolicationService +) { + infoForBrowserMap.set(browser, { + profileCaptureResult, + symbolicationService, + }); +} + // Provide a fake module.exports for the JSM to be properly read by TypeScript. /** @type {any} */ (this).module = { exports: {} }; @@ -698,6 +823,7 @@ module.exports = { revertRecordingSettings, changePreset, handleWebChannelMessage, + registerProfileCaptureForBrowser, addPrefObserver, removePrefObserver, getProfilerViewModeForCurrentPreset, diff --git a/devtools/client/performance-new/store/actions.js b/devtools/client/performance-new/store/actions.js index 3f00cee7439b5..6174081e9b47a 100644 --- a/devtools/client/performance-new/store/actions.js +++ b/devtools/client/performance-new/store/actions.js @@ -19,6 +19,7 @@ const { * @typedef {import("../@types/perf").RecordingSettings} RecordingSettings * @typedef {import("../@types/perf").Presets} Presets * @typedef {import("../@types/perf").PanelWindow} PanelWindow + * @typedef {import("../@types/perf").MinimallyTypedGeckoProfile} MinimallyTypedGeckoProfile */ /**