diff options
Diffstat (limited to 'devtools/client/performance-new/frame-script.js')
-rw-r--r-- | devtools/client/performance-new/frame-script.js | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/devtools/client/performance-new/frame-script.js b/devtools/client/performance-new/frame-script.js new file mode 100644 index 0000000000..e1bafe12d1 --- /dev/null +++ b/devtools/client/performance-new/frame-script.js @@ -0,0 +1,200 @@ +/* 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 +/// <reference path="./@types/frame-script.d.ts" /> +/* 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<T>} fun + * @param {any} contentGlobal + * @returns Promise<T> + */ +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; +} |