gecko-dev/devtools/client/performance-new/frame-script.js

201 строка
6.4 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
/// <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;
}