symbolication.sys.mjs (12657B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 // @ts-check 5 6 /** @type {any} */ 7 const lazy = {}; 8 9 /** 10 * @typedef {import("perf").Library} Library 11 * @typedef {import("perf").PerfFront} PerfFront 12 * @typedef {import("perf").SymbolTableAsTuple} SymbolTableAsTuple 13 * @typedef {import("perf").SymbolicationService} SymbolicationService 14 * @typedef {import("perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage 15 */ 16 17 /** 18 * @template R 19 * @typedef {import("perf").SymbolicationWorkerReplyData<R>} SymbolicationWorkerReplyData<R> 20 */ 21 22 ChromeUtils.defineESModuleGetters( 23 lazy, 24 { 25 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 26 setTimeout: "resource://gre/modules/Timer.sys.mjs", 27 }, 28 { global: "contextual" } 29 ); 30 31 /** @type {any} */ 32 const global = globalThis; 33 34 // This module obtains symbol tables for binaries. 35 // It does so with the help of a WASM module which gets pulled in from the 36 // internet on demand. We're doing this purely for the purposes of saving on 37 // code size. The contents of the WASM module are expected to be static, they 38 // are checked against the hash specified below. 39 // The WASM code is run on a ChromeWorker thread. It takes the raw byte 40 // contents of the to-be-dumped binary (and of an additional optional pdb file 41 // on Windows) as its input, and returns a set of typed arrays which make up 42 // the symbol table. 43 44 // Don't let the strange looking URLs and strings below scare you. 45 // The hash check ensures that the contents of the wasm module are what we 46 // expect them to be. 47 // The source code is at https://github.com/mstange/profiler-get-symbols/ . 48 // Documentation is at https://docs.rs/samply-api/ . 49 // The sha384 sum can be computed with the following command (tested on macOS): 50 // shasum -b -a 384 profiler_get_symbols_wasm_bg.wasm | awk '{ print $1 }' | xxd -r -p | base64 51 52 // Generated from https://github.com/mstange/profiler-get-symbols/commit/390b8c4be82c720dd3977ff205fb34bd7d0e00ba 53 const WASM_MODULE_URL = 54 "https://storage.googleapis.com/firefox-profiler-get-symbols/390b8c4be82c720dd3977ff205fb34bd7d0e00ba.wasm"; 55 const WASM_MODULE_INTEGRITY = 56 "sha384-P8j6U9jY+M4zSfJKXb1ECjsTPkzQ0hAvgb4zv3gHvlg+THRtVpOrDSywHJBhin00"; 57 58 const EXPIRY_TIME_IN_MS = 5 * 60 * 1000; // 5 minutes 59 60 /** @type {Promise<WebAssembly.Module> | null} */ 61 let gCachedWASMModulePromise = null; 62 let gCachedWASMModuleExpiryTimer = 0; 63 64 function clearCachedWASMModule() { 65 gCachedWASMModulePromise = null; 66 gCachedWASMModuleExpiryTimer = 0; 67 } 68 69 function getWASMProfilerGetSymbolsModule() { 70 if (!gCachedWASMModulePromise) { 71 gCachedWASMModulePromise = (async function () { 72 const request = new Request(WASM_MODULE_URL, { 73 integrity: WASM_MODULE_INTEGRITY, 74 credentials: "omit", 75 }); 76 return WebAssembly.compileStreaming(fetch(request)); 77 })(); 78 } 79 80 // Reset expiry timer. 81 lazy.clearTimeout(gCachedWASMModuleExpiryTimer); 82 gCachedWASMModuleExpiryTimer = lazy.setTimeout( 83 clearCachedWASMModule, 84 EXPIRY_TIME_IN_MS 85 ); 86 87 return gCachedWASMModulePromise; 88 } 89 90 /** 91 * Handle the entire life cycle of a worker, and report its result. 92 * This method creates a new worker, sends the initial message to it, handles 93 * any errors, and accepts the result. 94 * Returns a promise that resolves with the contents of the (singular) result 95 * message or rejects with an error. 96 * 97 * @template M 98 * @template R 99 * @param {string} workerURL 100 * @param {M} initialMessageToWorker 101 * @returns {Promise<R>} 102 */ 103 async function getResultFromWorker(workerURL, initialMessageToWorker) { 104 return new Promise((resolve, reject) => { 105 const worker = new ChromeWorker(workerURL); 106 107 /** @param {MessageEvent<SymbolicationWorkerReplyData<R>>} msg */ 108 worker.onmessage = msg => { 109 if ("error" in msg.data) { 110 const error = msg.data.error; 111 if (error.name) { 112 // Turn the JSON error object into a real Error object. 113 const { name, message, fileName, lineNumber } = error; 114 const ErrorObjConstructor = 115 name in global && Error.isPrototypeOf(global[name]) 116 ? global[name] 117 : Error; 118 const e = new ErrorObjConstructor(message, fileName, lineNumber); 119 e.name = name; 120 reject(e); 121 } else { 122 reject(error); 123 } 124 return; 125 } 126 resolve(msg.data.result); 127 }; 128 129 // Handle uncaught errors from the worker script. onerror is called if 130 // there's a syntax error in the worker script, for example, or when an 131 // unhandled exception is thrown, but not for unhandled promise 132 // rejections. Without this handler, mistakes during development such as 133 // syntax errors can be hard to track down. 134 worker.onerror = errorEvent => { 135 worker.terminate(); 136 if (ErrorEvent.isInstance(errorEvent)) { 137 const { message, filename, lineno } = errorEvent; 138 const error = new Error(`${message} at ${filename}:${lineno}`); 139 error.name = "WorkerError"; 140 reject(error); 141 } else { 142 reject(new Error("Error in worker " + String(errorEvent))); 143 } 144 }; 145 146 // Handle errors from messages that cannot be deserialized. I'm not sure 147 // how to get into such a state, but having this handler seems like a good 148 // idea. 149 worker.onmessageerror = () => { 150 worker.terminate(); 151 reject(new Error("Error in worker")); 152 }; 153 154 worker.postMessage(initialMessageToWorker); 155 }); 156 } 157 158 /** 159 * @param {PerfFront} perfFront 160 * @param {string} path 161 * @param {string} breakpadId 162 * @returns {Promise<SymbolTableAsTuple>} 163 */ 164 async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) { 165 const [addresses, index, buffer] = await perfFront.getSymbolTable( 166 path, 167 breakpadId 168 ); 169 // The protocol transmits these arrays as plain JavaScript arrays of 170 // numbers, but we want to pass them on as typed arrays. Convert them now. 171 return [ 172 new Uint32Array(addresses), 173 new Uint32Array(index), 174 new Uint8Array(buffer), 175 ]; 176 } 177 178 /** 179 * Profiling through the DevTools remote debugging protocol supports multiple 180 * different modes. This class is specialized to handle various profiling 181 * modes such as: 182 * 183 * 1) Profiling the same browser on the same machine. 184 * 2) Profiling a remote browser on the same machine. 185 * 3) Profiling a remote browser on a different device. 186 * 187 * It's also built to handle symbolication requests for both Gecko libraries and 188 * system libraries. However, it only handles cases where symbol information 189 * can be found in a local file on this machine. There is one case that is not 190 * covered by that restriction: Android system libraries. That case requires 191 * the help of the perf actor and is implemented in 192 * LocalSymbolicationServiceWithRemoteSymbolTableFallback. 193 */ 194 class LocalSymbolicationService { 195 /** 196 * @param {Library[]} sharedLibraries - Information about the shared libraries. 197 * This allows mapping (debugName, breakpadId) pairs to the absolute path of 198 * the binary and/or PDB file, and it ensures that these absolute paths come 199 * from a trusted source and not from the profiler UI. 200 * @param {string[]} objdirs - An array of objdir paths 201 * on the host machine that should be searched for relevant build artifacts. 202 */ 203 constructor(sharedLibraries, objdirs) { 204 this._libInfoMap = new Map( 205 sharedLibraries.map(lib => { 206 const { debugName, breakpadId } = lib; 207 const key = `${debugName}:${breakpadId}`; 208 return [key, lib]; 209 }) 210 ); 211 this._objdirs = objdirs; 212 } 213 214 /** 215 * @param {string} debugName 216 * @param {string} breakpadId 217 * @returns {Promise<SymbolTableAsTuple>} 218 */ 219 async getSymbolTable(debugName, breakpadId) { 220 const module = await getWASMProfilerGetSymbolsModule(); 221 /** @type {SymbolicationWorkerInitialMessage} */ 222 const initialMessage = { 223 request: { 224 type: "GET_SYMBOL_TABLE", 225 debugName, 226 breakpadId, 227 }, 228 libInfoMap: this._libInfoMap, 229 objdirs: this._objdirs, 230 module, 231 }; 232 return getResultFromWorker( 233 "resource://devtools/shared/performance-new/symbolication.worker.js", 234 initialMessage 235 ); 236 } 237 238 /** 239 * @param {string} path 240 * @param {string} requestJson 241 * @returns {Promise<string>} 242 */ 243 async querySymbolicationApi(path, requestJson) { 244 const module = await getWASMProfilerGetSymbolsModule(); 245 /** @type {SymbolicationWorkerInitialMessage} */ 246 const initialMessage = { 247 request: { 248 type: "QUERY_SYMBOLICATION_API", 249 path, 250 requestJson, 251 }, 252 libInfoMap: this._libInfoMap, 253 objdirs: this._objdirs, 254 module, 255 }; 256 return getResultFromWorker( 257 "resource://devtools/shared/performance-new/symbolication.worker.js", 258 initialMessage 259 ); 260 } 261 } 262 263 /** 264 * An implementation of the SymbolicationService interface which also 265 * covers the Android system library case. 266 * We first try to get symbols from the wrapped SymbolicationService. 267 * If that fails, we try to get the symbol table through the perf actor. 268 */ 269 class LocalSymbolicationServiceWithRemoteSymbolTableFallback { 270 /** 271 * @param {SymbolicationService} symbolicationService - The regular symbolication service. 272 * @param {Library[]} sharedLibraries - Information about the shared libraries 273 * @param {PerfFront} perfFront - A perf actor, to obtain symbol 274 * tables from remote targets 275 */ 276 constructor(symbolicationService, sharedLibraries, perfFront) { 277 this._symbolicationService = symbolicationService; 278 this._libs = sharedLibraries; 279 this._perfFront = perfFront; 280 } 281 282 /** 283 * @param {string} debugName 284 * @param {string} breakpadId 285 * @returns {Promise<SymbolTableAsTuple>} 286 */ 287 async getSymbolTable(debugName, breakpadId) { 288 try { 289 return await this._symbolicationService.getSymbolTable( 290 debugName, 291 breakpadId 292 ); 293 } catch (errorFromLocalFiles) { 294 // Try to obtain the symbol table on the debuggee. We get into this 295 // branch in the following cases: 296 // - Android system libraries 297 // - Firefox binaries that have no matching equivalent on the host 298 // machine, for example because the user didn't point us at the 299 // corresponding objdir, or if the build was compiled somewhere 300 // else, or if the build on the device is outdated. 301 // For now, the "debuggee" is never a Windows machine, which is why we don't 302 // need to pass the library's debugPath. (path and debugPath are always the 303 // same on non-Windows.) 304 const lib = this._libs.find( 305 l => l.debugName === debugName && l.breakpadId === breakpadId 306 ); 307 if (!lib) { 308 let errorMessage; 309 if (errorFromLocalFiles instanceof Error) { 310 errorMessage = errorFromLocalFiles.message; 311 } else { 312 errorMessage = `${errorFromLocalFiles}`; 313 } 314 315 throw new Error( 316 `Could not find the library for "${debugName}", "${breakpadId}" after falling ` + 317 `back to remote symbol table querying because regular getSymbolTable failed ` + 318 `with error: ${errorMessage}.` 319 ); 320 } 321 return getSymbolTableFromDebuggee(this._perfFront, lib.path, breakpadId); 322 } 323 } 324 325 /** 326 * @param {string} path 327 * @param {string} requestJson 328 * @returns {Promise<string>} 329 */ 330 async querySymbolicationApi(path, requestJson) { 331 return this._symbolicationService.querySymbolicationApi(path, requestJson); 332 } 333 } 334 335 /** 336 * Return an object that implements the SymbolicationService interface. 337 * 338 * @param {Library[]} sharedLibraries - Information about the shared libraries 339 * @param {string[]} objdirs - An array of objdir paths 340 * on the host machine that should be searched for relevant build artifacts. 341 * @param {PerfFront} [perfFront] - An optional perf actor, to obtain symbol 342 * tables from remote targets 343 * @return {SymbolicationService} 344 */ 345 export function createLocalSymbolicationService( 346 sharedLibraries, 347 objdirs, 348 perfFront 349 ) { 350 const service = new LocalSymbolicationService(sharedLibraries, objdirs); 351 if (perfFront) { 352 return new LocalSymbolicationServiceWithRemoteSymbolTableFallback( 353 service, 354 sharedLibraries, 355 perfFront 356 ); 357 } 358 return service; 359 } 360 361 // This file also exports a named object containing other exports to play well 362 // with defineESModuleGetters. 363 export const Symbolication = { 364 createLocalSymbolicationService, 365 };