362 lines
12 KiB
JavaScript
362 lines
12 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
|
|
|
|
/** @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/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<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) {
|
|
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<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}
|
|
*/
|
|
export function createLocalSymbolicationService(
|
|
sharedLibraries,
|
|
objdirs,
|
|
perfFront
|
|
) {
|
|
const service = new LocalSymbolicationService(sharedLibraries, objdirs);
|
|
if (perfFront) {
|
|
return new LocalSymbolicationServiceWithRemoteSymbolTableFallback(
|
|
service,
|
|
sharedLibraries,
|
|
perfFront
|
|
);
|
|
}
|
|
return service;
|
|
}
|