summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/shared/symbolication.jsm.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance-new/shared/symbolication.jsm.js')
-rw-r--r--devtools/client/performance-new/shared/symbolication.jsm.js364
1 files changed, 364 insertions, 0 deletions
diff --git a/devtools/client/performance-new/shared/symbolication.jsm.js b/devtools/client/performance-new/shared/symbolication.jsm.js
new file mode 100644
index 0000000000..f79cffe6cb
--- /dev/null
+++ b/devtools/client/performance-new/shared/symbolication.jsm.js
@@ -0,0 +1,364 @@
+/* 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";
+
+/** @type {any} */
+const lazy = {};
+
+/**
+ * @typedef {import("../@types/perf").Library} Library
+ * @typedef {import("../@types/perf").PerfFront} PerfFront
+ * @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
+ * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService
+ * @typedef {import("../@types/perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage
+ */
+
+/**
+ * @template R
+ * @typedef {import("../@types/perf").SymbolicationWorkerReplyData<R>} SymbolicationWorkerReplyData<R>
+ */
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+/** @type {any} */
+const global = globalThis;
+
+// This module obtains symbol tables for binaries.
+// It does so with the help of a WASM module which gets pulled in from the
+// internet on demand. We're doing this purely for the purposes of saving on
+// code size. The contents of the WASM module are expected to be static, they
+// are checked against the hash specified below.
+// The WASM code is run on a ChromeWorker thread. It takes the raw byte
+// contents of the to-be-dumped binary (and of an additional optional pdb file
+// on Windows) as its input, and returns a set of typed arrays which make up
+// the symbol table.
+
+// Don't let the strange looking URLs and strings below scare you.
+// The hash check ensures that the contents of the wasm module are what we
+// expect them to be.
+// The source code is at https://github.com/mstange/profiler-get-symbols/ .
+// Documentation is at https://docs.rs/samply-api/ .
+// The sha384 sum can be computed with the following command (tested on macOS):
+// shasum -b -a 384 profiler_get_symbols_wasm_bg.wasm | awk '{ print $1 }' | xxd -r -p | base64
+
+// Generated from https://github.com/mstange/profiler-get-symbols/commit/0373708893e45e8299e58ca692764be448e3457d
+const WASM_MODULE_URL =
+ "https://storage.googleapis.com/firefox-profiler-get-symbols/0373708893e45e8299e58ca692764be448e3457d.wasm";
+const WASM_MODULE_INTEGRITY =
+ "sha384-rUGgHTg1eAKP2MB4JcX/HGROSBlRUmvpm6FFIihH0gGQ74zfJE2p7P8cxR86faQ7";
+
+const EXPIRY_TIME_IN_MS = 5 * 60 * 1000; // 5 minutes
+
+/** @type {Promise<WebAssembly.Module> | null} */
+let gCachedWASMModulePromise = null;
+let gCachedWASMModuleExpiryTimer = 0;
+
+// Keep active workers alive (see bug 1592227).
+const gActiveWorkers = new Set();
+
+function clearCachedWASMModule() {
+ gCachedWASMModulePromise = null;
+ gCachedWASMModuleExpiryTimer = 0;
+}
+
+function getWASMProfilerGetSymbolsModule() {
+ if (!gCachedWASMModulePromise) {
+ gCachedWASMModulePromise = (async function () {
+ const request = new Request(WASM_MODULE_URL, {
+ integrity: WASM_MODULE_INTEGRITY,
+ credentials: "omit",
+ });
+ return WebAssembly.compileStreaming(fetch(request));
+ })();
+ }
+
+ // Reset expiry timer.
+ lazy.clearTimeout(gCachedWASMModuleExpiryTimer);
+ gCachedWASMModuleExpiryTimer = lazy.setTimeout(
+ clearCachedWASMModule,
+ EXPIRY_TIME_IN_MS
+ );
+
+ return gCachedWASMModulePromise;
+}
+
+/**
+ * Handle the entire life cycle of a worker, and report its result.
+ * This method creates a new worker, sends the initial message to it, handles
+ * any errors, and accepts the result.
+ * Returns a promise that resolves with the contents of the (singular) result
+ * message or rejects with an error.
+ *
+ * @template M
+ * @template R
+ * @param {string} workerURL
+ * @param {M} initialMessageToWorker
+ * @returns {Promise<R>}
+ */
+async function getResultFromWorker(workerURL, initialMessageToWorker) {
+ return new Promise((resolve, reject) => {
+ const worker = new ChromeWorker(workerURL);
+ gActiveWorkers.add(worker);
+
+ /** @param {MessageEvent<SymbolicationWorkerReplyData<R>>} msg */
+ worker.onmessage = msg => {
+ gActiveWorkers.delete(worker);
+ if ("error" in msg.data) {
+ const error = msg.data.error;
+ if (error.name) {
+ // Turn the JSON error object into a real Error object.
+ const { name, message, fileName, lineNumber } = error;
+ const ErrorObjConstructor =
+ name in global && Error.isPrototypeOf(global[name])
+ ? global[name]
+ : Error;
+ const e = new ErrorObjConstructor(message, fileName, lineNumber);
+ e.name = name;
+ reject(e);
+ } else {
+ reject(error);
+ }
+ return;
+ }
+ resolve(msg.data.result);
+ };
+
+ // Handle uncaught errors from the worker script. onerror is called if
+ // there's a syntax error in the worker script, for example, or when an
+ // unhandled exception is thrown, but not for unhandled promise
+ // rejections. Without this handler, mistakes during development such as
+ // syntax errors can be hard to track down.
+ worker.onerror = errorEvent => {
+ gActiveWorkers.delete(worker);
+ worker.terminate();
+ if (ErrorEvent.isInstance(errorEvent)) {
+ const { message, filename, lineno } = errorEvent;
+ const error = new Error(`${message} at ${filename}:${lineno}`);
+ error.name = "WorkerError";
+ reject(error);
+ } else {
+ reject(new Error("Error in worker"));
+ }
+ };
+
+ // Handle errors from messages that cannot be deserialized. I'm not sure
+ // how to get into such a state, but having this handler seems like a good
+ // idea.
+ worker.onmessageerror = () => {
+ gActiveWorkers.delete(worker);
+ worker.terminate();
+ reject(new Error("Error in worker"));
+ };
+
+ worker.postMessage(initialMessageToWorker);
+ });
+}
+
+/**
+ * @param {PerfFront} perfFront
+ * @param {string} path
+ * @param {string} breakpadId
+ * @returns {Promise<SymbolTableAsTuple>}
+ */
+async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
+ const [addresses, index, buffer] = await perfFront.getSymbolTable(
+ path,
+ breakpadId
+ );
+ // The protocol transmits these arrays as plain JavaScript arrays of
+ // numbers, but we want to pass them on as typed arrays. Convert them now.
+ return [
+ new Uint32Array(addresses),
+ new Uint32Array(index),
+ new Uint8Array(buffer),
+ ];
+}
+
+/**
+ * Profiling through the DevTools remote debugging protocol supports multiple
+ * different modes. This class is specialized to handle various profiling
+ * modes such as:
+ *
+ * 1) Profiling the same browser on the same machine.
+ * 2) Profiling a remote browser on the same machine.
+ * 3) Profiling a remote browser on a different device.
+ *
+ * It's also built to handle symbolication requests for both Gecko libraries and
+ * system libraries. However, it only handles cases where symbol information
+ * can be found in a local file on this machine. There is one case that is not
+ * covered by that restriction: Android system libraries. That case requires
+ * the help of the perf actor and is implemented in
+ * LocalSymbolicationServiceWithRemoteSymbolTableFallback.
+ */
+class LocalSymbolicationService {
+ /**
+ * @param {Library[]} sharedLibraries - Information about the shared libraries.
+ * This allows mapping (debugName, breakpadId) pairs to the absolute path of
+ * the binary and/or PDB file, and it ensures that these absolute paths come
+ * from a trusted source and not from the profiler UI.
+ * @param {string[]} objdirs - An array of objdir paths
+ * on the host machine that should be searched for relevant build artifacts.
+ */
+ constructor(sharedLibraries, objdirs) {
+ this._libInfoMap = new Map(
+ sharedLibraries.map(lib => {
+ const { debugName, breakpadId } = lib;
+ const key = `${debugName}:${breakpadId}`;
+ return [key, lib];
+ })
+ );
+ this._objdirs = objdirs;
+ }
+
+ /**
+ * @param {string} debugName
+ * @param {string} breakpadId
+ * @returns {Promise<SymbolTableAsTuple>}
+ */
+ async getSymbolTable(debugName, breakpadId) {
+ const module = await getWASMProfilerGetSymbolsModule();
+ /** @type {SymbolicationWorkerInitialMessage} */
+ const initialMessage = {
+ request: {
+ type: "GET_SYMBOL_TABLE",
+ debugName,
+ breakpadId,
+ },
+ libInfoMap: this._libInfoMap,
+ objdirs: this._objdirs,
+ module,
+ };
+ return getResultFromWorker(
+ "resource://devtools/client/performance-new/shared/symbolication-worker.js",
+ initialMessage
+ );
+ }
+
+ /**
+ * @param {string} path
+ * @param {string} requestJson
+ * @returns {Promise<string>}
+ */
+ async querySymbolicationApi(path, requestJson) {
+ const module = await getWASMProfilerGetSymbolsModule();
+ /** @type {SymbolicationWorkerInitialMessage} */
+ const initialMessage = {
+ request: {
+ type: "QUERY_SYMBOLICATION_API",
+ path,
+ requestJson,
+ },
+ libInfoMap: this._libInfoMap,
+ objdirs: this._objdirs,
+ module,
+ };
+ return getResultFromWorker(
+ "resource://devtools/client/performance-new/shared/symbolication-worker.js",
+ initialMessage
+ );
+ }
+}
+
+/**
+ * An implementation of the SymbolicationService interface which also
+ * covers the Android system library case.
+ * We first try to get symbols from the wrapped SymbolicationService.
+ * If that fails, we try to get the symbol table through the perf actor.
+ */
+class LocalSymbolicationServiceWithRemoteSymbolTableFallback {
+ /**
+ * @param {SymbolicationService} symbolicationService - The regular symbolication service.
+ * @param {Library[]} sharedLibraries - Information about the shared libraries
+ * @param {PerfFront} perfFront - A perf actor, to obtain symbol
+ * tables from remote targets
+ */
+ constructor(symbolicationService, sharedLibraries, perfFront) {
+ this._symbolicationService = symbolicationService;
+ this._libs = sharedLibraries;
+ this._perfFront = perfFront;
+ }
+
+ /**
+ * @param {string} debugName
+ * @param {string} breakpadId
+ * @returns {Promise<SymbolTableAsTuple>}
+ */
+ async getSymbolTable(debugName, breakpadId) {
+ try {
+ return await this._symbolicationService.getSymbolTable(
+ debugName,
+ breakpadId
+ );
+ } catch (errorFromLocalFiles) {
+ // Try to obtain the symbol table on the debuggee. We get into this
+ // branch in the following cases:
+ // - Android system libraries
+ // - Firefox binaries that have no matching equivalent on the host
+ // machine, for example because the user didn't point us at the
+ // corresponding objdir, or if the build was compiled somewhere
+ // else, or if the build on the device is outdated.
+ // For now, the "debuggee" is never a Windows machine, which is why we don't
+ // need to pass the library's debugPath. (path and debugPath are always the
+ // same on non-Windows.)
+ const lib = this._libs.find(
+ l => l.debugName === debugName && l.breakpadId === breakpadId
+ );
+ if (!lib) {
+ throw new Error(
+ `Could not find the library for "${debugName}", "${breakpadId}" after falling ` +
+ `back to remote symbol table querying because regular getSymbolTable failed ` +
+ `with error: ${errorFromLocalFiles.message}.`
+ );
+ }
+ return getSymbolTableFromDebuggee(this._perfFront, lib.path, breakpadId);
+ }
+ }
+
+ /**
+ * @param {string} path
+ * @param {string} requestJson
+ * @returns {Promise<string>}
+ */
+ async querySymbolicationApi(path, requestJson) {
+ return this._symbolicationService.querySymbolicationApi(path, requestJson);
+ }
+}
+
+/**
+ * Return an object that implements the SymbolicationService interface.
+ *
+ * @param {Library[]} sharedLibraries - Information about the shared libraries
+ * @param {string[]} objdirs - An array of objdir paths
+ * on the host machine that should be searched for relevant build artifacts.
+ * @param {PerfFront} [perfFront] - An optional perf actor, to obtain symbol
+ * tables from remote targets
+ * @return {SymbolicationService}
+ */
+function createLocalSymbolicationService(sharedLibraries, objdirs, perfFront) {
+ const service = new LocalSymbolicationService(sharedLibraries, objdirs);
+ if (perfFront) {
+ return new LocalSymbolicationServiceWithRemoteSymbolTableFallback(
+ service,
+ sharedLibraries,
+ perfFront
+ );
+ }
+ return service;
+}
+
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */
+var module = {};
+
+module.exports = {
+ createLocalSymbolicationService,
+};
+
+// Object.keys() confuses the linting which expects a static array expression.
+// eslint-disable-next-line
+var EXPORTED_SYMBOLS = Object.keys(module.exports);