symbolication.worker.js (10691B)
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set sts=2 sw=2 et tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 // FIXME: This file is currently not covered by TypeScript, there is no "@ts-check" comment. 8 // We should fix this once we know how to deal with the module imports below. 9 // (Maybe once Firefox supports worker module? Bug 1247687) 10 11 "use strict"; 12 13 /* import-globals-from profiler_get_symbols.js */ 14 importScripts( 15 "resource://devtools/shared/performance-new/profiler_get_symbols.js" 16 ); 17 18 /** 19 * @typedef {import("../@types/perf").SymbolicationWorkerInitialMessage} SymbolicationWorkerInitialMessage 20 * @typedef {import("../@types/perf").FileHandle} FileHandle 21 */ 22 23 // This worker uses the wasm module that was generated from https://github.com/mstange/profiler-get-symbols. 24 // See ProfilerGetSymbols.jsm for more information. 25 // 26 // The worker instantiates the module, reads the binary into wasm memory, runs 27 // the wasm code, and returns the symbol table or an error. Then it shuts down 28 // itself. 29 30 /* eslint camelcase: 0*/ 31 const { getCompactSymbolTable, queryAPI } = wasm_bindgen; 32 33 // Returns a plain object that is Structured Cloneable and has name and 34 // message properties. 35 function createPlainErrorObject(e) { 36 // Regular errors: just rewrap the object. 37 // eslint-disable-next-line no-shadow 38 const { name, message, fileName, lineNumber } = e; 39 return { name, message, fileName, lineNumber }; 40 } 41 42 /** 43 * A FileAndPathHelper object is passed to getCompactSymbolTable, which calls 44 * the methods `getCandidatePathsForBinaryOrPdb` and `readFile` on it. 45 */ 46 class FileAndPathHelper { 47 constructor(libInfoMap, objdirs) { 48 this._libInfoMap = libInfoMap; 49 this._objdirs = objdirs; 50 } 51 52 /** 53 * Enumerate all paths at which we could find files with symbol information. 54 * This method is called by wasm code (via the bindings). 55 * 56 * @param {LibraryInfo} libraryInfo 57 * @returns {Array<string>} 58 */ 59 getCandidatePathsForDebugFile(libraryInfo) { 60 const { debugName, breakpadId } = libraryInfo; 61 const key = `${debugName}:${breakpadId}`; 62 const lib = this._libInfoMap.get(key); 63 if (!lib) { 64 throw new Error( 65 `Could not find the library for "${debugName}", "${breakpadId}".` 66 ); 67 } 68 69 // eslint-disable-next-line no-shadow 70 const { name, path, debugPath, arch } = lib; 71 const candidatePaths = []; 72 73 // First, try to find a binary with a matching file name and breakpadId 74 // in one of the manually specified objdirs. 75 // This is needed if the debuggee is a build running on a remote machine that 76 // was compiled by the developer on *this* machine (the "host machine"). In 77 // that case, the objdir will contain the compiled binary with full symbol and 78 // debug information, whereas the binary on the device may not exist in 79 // uncompressed form or may have been stripped of debug information and some 80 // symbol information. 81 // An objdir, or "object directory", is a directory on the host machine that's 82 // used to store build artifacts ("object files") from the compilation process. 83 // This only works if the binary is one of the Gecko binaries and not 84 // a system library. 85 for (const objdirPath of this._objdirs) { 86 try { 87 // Binaries are usually expected to exist at objdir/dist/bin/filename. 88 candidatePaths.push(PathUtils.join(objdirPath, "dist", "bin", name)); 89 90 // Also search in the "objdir" directory itself (not just in dist/bin). 91 // If, for some unforeseen reason, the relevant binary is not inside the 92 // objdirs dist/bin/ directory, this provides a way out because it lets the 93 // user specify the actual location. 94 candidatePaths.push(PathUtils.join(objdirPath, name)); 95 } catch (e) { 96 // PathUtils.join throws if objdirPath is not an absolute path. 97 // Ignore those invalid objdir paths. 98 } 99 } 100 101 // Check the absolute paths of the library last. 102 // We do this after the objdir search because the library's path may point 103 // to a stripped binary, which will have fewer symbols than the original 104 // binaries in the objdir. 105 if (debugPath !== path) { 106 // We're on Windows, and debugPath points to a PDB file. 107 // On non-Windows, path and debugPath are always the same. 108 109 // Check the PDB file before the binary because the PDB has the symbol 110 // information. The binary is only used as a last-ditch fallback 111 // for things like Windows system libraries (e.g. graphics drivers). 112 candidatePaths.push(debugPath); 113 } 114 115 // The location of the binary. If the profile was obtained on this machine 116 // (and not, for example, on an Android device), this file should always 117 // exist. 118 candidatePaths.push(path); 119 120 // On macOS, for system libraries, add a final fallback for the dyld shared 121 // cache. Starting with macOS 11, most system libraries are located in this 122 // system-wide cache file and not present as individual files. 123 if (arch && (path.startsWith("/usr/") || path.startsWith("/System/"))) { 124 // Use the special syntax `dyldcache:<dyldcachepath>:<librarypath>`. 125 126 // Dyld cache location used on macOS 13+: 127 candidatePaths.push( 128 `dyldcache:/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_${arch}:${path}` 129 ); 130 // Dyld cache location used on macOS 11 and 12: 131 candidatePaths.push( 132 `dyldcache:/System/Library/dyld/dyld_shared_cache_${arch}:${path}` 133 ); 134 } 135 136 return candidatePaths; 137 } 138 139 /** 140 * Enumerate all paths at which we could find the binary which matches the 141 * given libraryInfo, in order to disassemble machine code. 142 * This method is called by wasm code (via the bindings). 143 * 144 * @param {LibraryInfo} libraryInfo 145 * @returns {Array<string>} 146 */ 147 getCandidatePathsForBinary(libraryInfo) { 148 const { debugName, breakpadId } = libraryInfo; 149 const key = `${debugName}:${breakpadId}`; 150 const lib = this._libInfoMap.get(key); 151 if (!lib) { 152 throw new Error( 153 `Could not find the library for "${debugName}", "${breakpadId}".` 154 ); 155 } 156 157 // eslint-disable-next-line no-shadow 158 const { name, path, arch } = lib; 159 const candidatePaths = []; 160 161 // The location of the binary. If the profile was obtained on this machine 162 // (and not, for example, on an Android device), this file should always 163 // exist. 164 candidatePaths.push(path); 165 166 // Fall back to searching in the manually specified objdirs. 167 // This is needed if the debuggee is a build running on a remote machine that 168 // was compiled by the developer on *this* machine (the "host machine"). In 169 // that case, the objdir will contain the compiled binary. 170 for (const objdirPath of this._objdirs) { 171 try { 172 // Binaries are usually expected to exist at objdir/dist/bin/filename. 173 candidatePaths.push(PathUtils.join(objdirPath, "dist", "bin", name)); 174 175 // Also search in the "objdir" directory itself (not just in dist/bin). 176 // If, for some unforeseen reason, the relevant binary is not inside the 177 // objdirs dist/bin/ directory, this provides a way out because it lets the 178 // user specify the actual location. 179 candidatePaths.push(PathUtils.join(objdirPath, name)); 180 } catch (e) { 181 // PathUtils.join throws if objdirPath is not an absolute path. 182 // Ignore those invalid objdir paths. 183 } 184 } 185 186 // On macOS, for system libraries, add a final fallback for the dyld shared 187 // cache. Starting with macOS 11, most system libraries are located in this 188 // system-wide cache file and not present as individual files. 189 if (arch && (path.startsWith("/usr/") || path.startsWith("/System/"))) { 190 // Use the special syntax `dyldcache:<dyldcachepath>:<librarypath>`. 191 192 // Dyld cache location used on macOS 13+: 193 candidatePaths.push( 194 `dyldcache:/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_${arch}:${path}` 195 ); 196 // Dyld cache location used on macOS 11 and 12: 197 candidatePaths.push( 198 `dyldcache:/System/Library/dyld/dyld_shared_cache_${arch}:${path}` 199 ); 200 } 201 202 return candidatePaths; 203 } 204 205 /** 206 * Asynchronously prepare the file at `path` for synchronous reading. 207 * This method is called by wasm code (via the bindings). 208 * 209 * @param {string} path 210 * @returns {FileHandle} 211 */ 212 async readFile(path) { 213 const info = await IOUtils.stat(path); 214 if (info.type === "directory") { 215 throw new Error(`Path "${path}" is a directory.`); 216 } 217 218 return IOUtils.openFileForSyncReading(path); 219 } 220 } 221 222 /** @param {MessageEvent<SymbolicationWorkerInitialMessage>} e */ 223 onmessage = async e => { 224 try { 225 const { request, libInfoMap, objdirs, module } = e.data; 226 227 if (!(module instanceof WebAssembly.Module)) { 228 throw new Error("invalid WebAssembly module"); 229 } 230 231 // Instantiate the WASM module. 232 await wasm_bindgen(module); 233 234 const helper = new FileAndPathHelper(libInfoMap, objdirs); 235 236 switch (request.type) { 237 case "GET_SYMBOL_TABLE": { 238 const { debugName, breakpadId } = request; 239 const result = await getCompactSymbolTable( 240 debugName, 241 breakpadId, 242 helper 243 ); 244 postMessage( 245 { result }, 246 result.map(r => r.buffer) 247 ); 248 break; 249 } 250 case "QUERY_SYMBOLICATION_API": { 251 const { path, requestJson } = request; 252 const result = await queryAPI(path, requestJson, helper); 253 postMessage({ result }); 254 break; 255 } 256 default: 257 throw new Error(`Unexpected request type ${request.type}`); 258 } 259 } catch (error) { 260 postMessage({ error: createPlainErrorObject(error) }); 261 } 262 close(); 263 }; 264 265 onunhandledrejection = e => { 266 // Unhandled rejections can happen if the WASM code throws a 267 // "RuntimeError: unreachable executed" exception, which can happen 268 // if the Rust code panics or runs out of memory. 269 // These panics currently are not propagated to the promise reject 270 // callback, see https://github.com/rustwasm/wasm-bindgen/issues/2724 . 271 // Ideally, the Rust code should never panic and handle all error 272 // cases gracefully. 273 e.preventDefault(); 274 postMessage({ error: createPlainErrorObject(e.reason) }); 275 }; 276 277 // Catch any other unhandled errors, just to be sure. 278 onerror = e => { 279 postMessage({ error: createPlainErrorObject(e) }); 280 };