/* 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 /** @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} SymbolicationWorkerReplyData */ 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/390b8c4be82c720dd3977ff205fb34bd7d0e00ba const WASM_MODULE_URL = "https://storage.googleapis.com/firefox-profiler-get-symbols/390b8c4be82c720dd3977ff205fb34bd7d0e00ba.wasm"; const WASM_MODULE_INTEGRITY = "sha384-P8j6U9jY+M4zSfJKXb1ECjsTPkzQ0hAvgb4zv3gHvlg+THRtVpOrDSywHJBhin00"; const EXPIRY_TIME_IN_MS = 5 * 60 * 1000; // 5 minutes /** @type {Promise | 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} */ async function getResultFromWorker(workerURL, initialMessageToWorker) { return new Promise((resolve, reject) => { const worker = new ChromeWorker(workerURL); gActiveWorkers.add(worker); /** @param {MessageEvent>} 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} */ 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} */ 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} */ 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} */ 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) { let errorMessage; if (errorFromLocalFiles instanceof Error) { errorMessage = errorFromLocalFiles.message; } else { errorMessage = `${errorFromLocalFiles}`; } 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: ${errorMessage}.` ); } return getSymbolTableFromDebuggee(this._perfFront, lib.path, breakpadId); } } /** * @param {string} path * @param {string} requestJson * @returns {Promise} */ 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} */ export function createLocalSymbolicationService( sharedLibraries, objdirs, perfFront ) { const service = new LocalSymbolicationService(sharedLibraries, objdirs); if (perfFront) { return new LocalSymbolicationServiceWithRemoteSymbolTableFallback( service, sharedLibraries, perfFront ); } return service; }