diff --git a/index.js b/index.js index 3d115f2..cd57996 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ const fs = require('fs') const stream = require('stream') const { promisify } = require('util') -const breakpad = require('parse-breakpad') +const { symbolicateFrames } = require('@indutny/breakpad'); const got = require('got') const mkdirp = require('mkdirp') const yargs = require('yargs') @@ -14,63 +14,81 @@ const symbolicate = async (options) => { const {force, file} = options const cacheDirectory = path.join(__dirname, 'cache', 'breakpad_symbols') - const symbolCache = new Map const dumpText = await fs.promises.readFile(file, 'utf8') const images = binaryImages(dumpText) const electronImage = images.find(v => /electron/i.test(v.library)) if (electronImage) console.error(`Found Electron ${electronImage.version}`) else console.error('No Electron image found') - let result = [] + const lines = dumpText.split(/\r?\n/); - for (const line of dumpText.split(/\r?\n/)) { + const linesByImage = new Map(); + for (const [lineIndex, line] of lines.entries()) { const parsedLine = parseAddressLine(line) - if (parsedLine) { - const library = parsedLine.libraryBaseName || parsedLine.libraryId - const image = images.find(i => i.library === library || path.basename(i.path) === library) - if (image) { - const offset = parsedLine.address - image.startAddress - const res = await symbolicateOne({ - image, - offset - }) - if (res) { - result.push(line.substr(0, parsedLine.replace.from) + res.func.name + line.substr(parsedLine.replace.from + parsedLine.replace.length)) - continue - } + if (!parsedLine) { + continue; + } + + const library = parsedLine.libraryBaseName || parsedLine.libraryId + const image = images.find(i => i.library === library || i.basename === library) + if (!image) { + continue; + } + + const offset = parsedLine.address - image.startAddress + const imageKey = `${image.debugId}/${image.basename}`; + + let entry = linesByImage.get(imageKey); + if (!entry) { + entry = { image, group: [] }; + linesByImage.set(imageKey, entry); + } + entry.group.push({ + image, + offset, + lineIndex, + parsedLine, + }); + } + + for (const { image, group } of linesByImage.values()) { + const { debugId, basename: moduleBasename, extname: moduleExtname } = image + const suffix = moduleExtname === '.pdb' ? '1' : '0'; + const stream = await getSymbolFile(debugId.replace(/-/g, '') + suffix, moduleBasename) + if (!stream) { + continue; + } + + const frames = group.map(({ offset }) => offset); + const symbolicated = await symbolicateFrames(stream, frames); + + for (const [index, symbol] of symbolicated.entries()) { + if (!symbol) { + continue; } + + const { lineIndex, parsedLine } = group[index]; + const line = lines[lineIndex]; + lines[lineIndex] = line.substr(0, parsedLine.replace.from) + symbol.name + line.substr(parsedLine.replace.from + parsedLine.replace.length); } - result.push(line) } - return result.join('\n') + return lines.join('\n') async function getSymbolFile(moduleId, moduleName) { const pdb = moduleName.replace(/^\//, '') const symbolFileName = pdb.replace(/(\.pdb)?$/, '.sym') const symbolPath = path.join(cacheDirectory, pdb, moduleId, symbolFileName) if (fs.existsSync(symbolPath) && !force) { - return breakpad.parse(fs.createReadStream(symbolPath)) + return fs.createReadStream(symbolPath) } if (!fs.existsSync(symbolPath) && (!fs.existsSync(path.dirname(symbolPath)) || force)) { for (const baseUrl of SYMBOL_BASE_URLS) { if (await fetchSymbol(cacheDirectory, baseUrl, pdb, moduleId, symbolFileName)) - return breakpad.parse(fs.createReadStream(symbolPath)) + return fs.createReadStream(symbolPath) } } } - - async function symbolicateOne({image, offset}) { - const { debugId, path: modulePath } = image - if (!symbolCache.has(debugId)) { - const suffix = path.extname(modulePath) === '.pdb' ? '1' : '0'; - const parsed = await getSymbolFile(debugId.replace(/-/g, '') + suffix, path.basename(modulePath)) - symbolCache.set(debugId, parsed) - } - const parsed = symbolCache.get(debugId) - if (parsed) - return parsed.lookup(offset) - } } const binaryImages = (dumpText) => { @@ -86,7 +104,7 @@ const binaryImages = (dumpText) => { let m const images = [] while (m = re.exec(dumpText)) { - const [, startAddress, endAddress, plus, library, version, debugId, path] = m + const [, startAddress, endAddress, plus, library, version, debugId, modulePath] = m const image = { startAddress: parseInt(startAddress, 16), endAddress: parseInt(endAddress, 16), @@ -94,7 +112,8 @@ const binaryImages = (dumpText) => { library, version, debugId, - path + basename: path.basename(modulePath), + extname: path.extname(modulePath), } const existing = images.find(v => v.library === image.library) if (existing) { diff --git a/package.json b/package.json index 23338cc..9a00846 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ ], "license": "ISC", "dependencies": { + "@indutny/breakpad": "^1.2.0", "got": "^11.8.2", "mkdirp": "^1.0.4", - "parse-breakpad": "^0.1.0", "yargs": "^17.0.1" }, "engines": { diff --git a/yarn.lock b/yarn.lock index fd428a5..735e4e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -408,6 +408,18 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@indutny/breakpad-parser-wasm@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@indutny/breakpad-parser-wasm/-/breakpad-parser-wasm-1.0.0.tgz#bc91c12a6d6a11a4c5366e8a46a3e317f3a7b54f" + integrity sha512-v+/bvaPaz60GJB0vd2trXLu254KWdSB7LIcI2VljZ2eq8Ij478PK+5T7HUPt8uf7TK/5lFfhQZsBdpisGdYwaQ== + +"@indutny/breakpad@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@indutny/breakpad/-/breakpad-1.2.0.tgz#17d19f1210536bd2ed72da91e60a17ba5efc8be8" + integrity sha512-JjG4Y82p710old/xO9jCcFCjoDwR03IWauCuwWD2P5sXQDRzxz3S4CKZAYRDhU7avxf502RMvCQSvI9WLMDQsg== + dependencies: + "@indutny/breakpad-parser-wasm" "^1.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2076,11 +2088,6 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -parse-breakpad@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/parse-breakpad/-/parse-breakpad-0.1.0.tgz#6d3c80c856d35550b6adea374b93ec41d758ee8b" - integrity sha512-xF9F9+LnMzPhXZSExrvyhzormvlR0Co9LJ9Un8SPDMiMNm3PqIwfYiB4UcqG50yc9g42H2Fndpx/5gfNqdgowQ== - parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"