diff --git a/packages/metro-symbolicate/src/Symbolication.js b/packages/metro-symbolicate/src/Symbolication.js index e7d2ef8ad3..d2512db818 100644 --- a/packages/metro-symbolicate/src/Symbolication.js +++ b/packages/metro-symbolicate/src/Symbolication.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - const SourceMetadataMapConsumer = require('./SourceMetadataMapConsumer'); const fs = require('fs'); @@ -81,6 +79,15 @@ type HermesMinidumpStackFrame = $ReadOnly<{| SourceLocation?: string, |}>; +type HermesCoverageInfo = { + +executedFunctions: $ReadOnlyArray, +}; + +type HermesCoverageStackFrame = $ReadOnly<{ + SegmentID: number, + VirtualOffset: number, +}>; + type NativeCodeStackFrame = $ReadOnly<{| NativeCode: true, StackFrameRegOffs: string, @@ -439,6 +446,32 @@ class SymbolicationContext { return snapshotData; } + /* + * Symbolicates the JavaScript stack trace extracted from the coverage information + * produced by HermesRuntime::getExecutedFunctions. + */ + symbolicateHermesCoverageTrace( + coverageInfo: HermesCoverageInfo, + ): SymbolicatedStackTrace { + const symbolicatedTrace = []; + const {executedFunctions} = coverageInfo; + + if (executedFunctions != null) { + for (const stackItem of executedFunctions) { + const {SegmentID, VirtualOffset: localOffset} = stackItem; + const generatedLine = SegmentID + this.options.inputLineStart; + const generatedColumn = localOffset + this.options.inputColumnStart; + + const originalPosition = this.getOriginalPositionDetailsFor( + generatedLine, + generatedColumn, + ); + symbolicatedTrace.push(originalPosition); + } + } + return symbolicatedTrace; + } + /* * An internal helper function similar to getOriginalPositionFor. This one * returns both `name` and `functionName` fields so callers can distinguish the @@ -576,6 +609,28 @@ class SingleMapSymbolicationContext extends SymbolicationContext", + "line": 16, + "name": null, + "source": "/js/react-native-github/Libraries/Utilities/createPerformanceLogger.js", + }, + Object { + "column": 2, + "functionName": "", + "line": 25, + "name": null, + "source": "/js/RKJSModules/Libraries/MobileConfig/MobileConfig.js", + }, +] +`; + exports[`directory context does not read source maps outside the root dir, with relative or absolute paths 1`] = ` "/__fixtures__/testfile.js:1:null ../testfile.js:1:null @@ -20,7 +39,7 @@ exports[`directory context symbolicating a stack trace with absolute paths 1`] = " `; -exports[`symbolicating a hermes stack trace 1`] = ` +exports[`hermes-crash option symbolicating a hermes stack trace 1`] = ` Array [ Object { "column": 21, @@ -63,7 +82,7 @@ Array [ ] `; -exports[`symbolicating a hermes stack trace CJS 1`] = ` +exports[`hermes-crash option symbolicating a hermes stack trace CJS 1`] = ` Array [ Object { "column": 36, @@ -99,7 +118,7 @@ Array [ ] `; -exports[`symbolicating a hermes stack trace CJS with SegmentID 1`] = ` +exports[`hermes-crash option symbolicating a hermes stack trace CJS with SegmentID 1`] = ` Array [ Object { "column": 36, diff --git a/packages/metro-symbolicate/src/__tests__/symbolicate-test.js b/packages/metro-symbolicate/src/__tests__/symbolicate-test.js index d56ddec619..fd2c0e5608 100644 --- a/packages/metro-symbolicate/src/__tests__/symbolicate-test.js +++ b/packages/metro-symbolicate/src/__tests__/symbolicate-test.js @@ -62,71 +62,122 @@ const TESTFILE_RAM_MAP = resolve('testfile.ram.js.map'); const HERMES_MAP = resolve('hermes.js.hbc.map'); const HERMES_MAP_CJS = resolve('hermescjs.js.hbc.map'); -test('symbolicating a hermes stack trace', async () => { - const output = JSON.parse( - await execute( - [HERMES_MAP, '--hermes-crash'], - read('hermesStackTrace.json'), - ), - ); - expect(output).toMatchSnapshot(); -}); +describe('hermes-crash option', () => { + test('symbolicating a hermes stack trace', async () => { + const output = JSON.parse( + await execute( + [HERMES_MAP, '--hermes-crash'], + read('hermesStackTrace.json'), + ), + ); + expect(output).toMatchSnapshot(); + }); -test('symbolicating a hermes stack trace with input-line-start and input-column-start', async () => { - const output0Based = JSON.parse( - await execute( - [ - HERMES_MAP, - '--hermes-crash', - '--input-line-start', - '0', - '--input-column-start', - '0', - ], - read('hermesStackTrace.json'), - ), - ); - const output1Based = JSON.parse( - await execute( - [ - HERMES_MAP, - '--hermes-crash', - '--input-line-start', - '1', - '--input-column-start', - '1', - ], - read('hermesStackTrace.json'), - ), - ); - const outputNoFlag = JSON.parse( - await execute( - [HERMES_MAP, '--hermes-crash'], - read('hermesStackTrace.json'), - ), - ); - expect(outputNoFlag).toMatchObject(output0Based); - expect(outputNoFlag).toMatchObject(output1Based); -}); + test('symbolicating a hermes stack trace with input-line-start and input-column-start', async () => { + const output0Based = JSON.parse( + await execute( + [ + HERMES_MAP, + '--hermes-crash', + '--input-line-start', + '0', + '--input-column-start', + '0', + ], + read('hermesStackTrace.json'), + ), + ); + const output1Based = JSON.parse( + await execute( + [ + HERMES_MAP, + '--hermes-crash', + '--input-line-start', + '1', + '--input-column-start', + '1', + ], + read('hermesStackTrace.json'), + ), + ); + const outputNoFlag = JSON.parse( + await execute( + [HERMES_MAP, '--hermes-crash'], + read('hermesStackTrace.json'), + ), + ); + expect(outputNoFlag).toMatchObject(output0Based); + expect(outputNoFlag).toMatchObject(output1Based); + }); -test('symbolicating a hermes stack trace CJS', async () => { - const output = JSON.parse( - await execute( - [HERMES_MAP_CJS, '--hermes-crash'], - read('hermesStackTraceCJS.json'), - ), - ); - expect(output).toMatchSnapshot(); + test('symbolicating a hermes stack trace CJS', async () => { + const output = JSON.parse( + await execute( + [HERMES_MAP_CJS, '--hermes-crash'], + read('hermesStackTraceCJS.json'), + ), + ); + expect(output).toMatchSnapshot(); + }); + + test('symbolicating a hermes stack trace CJS with SegmentID', async () => { + const output = JSON.parse( + await execute( + [HERMES_MAP_CJS, '--hermes-crash'], + read('hermesStackTraceCJS-SegmentID.json'), + ), + ); + expect(output).toMatchSnapshot(); + }); }); -test('symbolicating a hermes stack trace CJS with SegmentID', async () => { - const output = JSON.parse( - await execute( - [HERMES_MAP_CJS, '--hermes-crash'], - read('hermesStackTraceCJS-SegmentID.json'), - ), - ); - expect(output).toMatchSnapshot(); +describe('coverage option', () => { + test('ignores input-line-start and input-column-start', async () => { + const output0Based = JSON.parse( + await execute( + [ + HERMES_MAP_CJS, + '--hermes-coverage', + '--input-line-start', + '0', + '--input-column-start', + '0', + ], + read('coverageStackTraceCJS.json'), + ), + ); + const output1Based = JSON.parse( + await execute( + [ + HERMES_MAP_CJS, + '--hermes-coverage', + '--input-line-start', + '1', + '--input-column-start', + '1', + ], + read('coverageStackTraceCJS.json'), + ), + ); + const outputNoFlag = JSON.parse( + await execute( + [HERMES_MAP_CJS, '--hermes-coverage'], + read('coverageStackTraceCJS.json'), + ), + ); + expect(outputNoFlag).toMatchObject(output0Based); + expect(outputNoFlag).toMatchObject(output1Based); + }); + + test('symbolicating a coverage stack trace CJS', async () => { + const output = JSON.parse( + await execute( + [HERMES_MAP_CJS, '--hermes-coverage'], + read('coverageStackTraceCJS.json'), + ), + ); + expect(output).toMatchSnapshot(); + }); }); test('symbolicating a stack trace', async () => diff --git a/packages/metro-symbolicate/src/symbolicate.js b/packages/metro-symbolicate/src/symbolicate.js index 9ea622f0ca..887cf4fd26 100644 --- a/packages/metro-symbolicate/src/symbolicate.js +++ b/packages/metro-symbolicate/src/symbolicate.js @@ -26,6 +26,28 @@ const fs = require('fs'); // flowlint-next-line untyped-import:off const through2 = require('through2'); +function printHelp() { + const usages = [ + 'Usage: ' + __filename + ' ', + ' ' + __filename + ' [column]', + ' ' + __filename + ' .js [column]', + ' ' + __filename + ' .profmap', + ' ' + + __filename + + ' --attribution < in.jsonl > out.jsonl', + ' ' + __filename + ' .cpuprofile', + ' Optional flags:', + ' --no-function-names', + ' --hermes-crash (mutually exclusive with --hermes-coverage)', + ' --hermes-coverage (mutually exclusive with --hermes-crash)', + ' --input-line-start (default: 1)', + ' --input-column-start (default: 0)', + ' --output-line-start (default: 1)', + ' --output-column-start (default: 0)', + ]; + console.error(usages.join('\n')); +} + async function main( argvInput: Array = process.argv.slice(2), { @@ -57,6 +79,7 @@ async function main( try { const noFunctionNames = checkAndRemoveArg('--no-function-names'); const isHermesCrash = checkAndRemoveArg('--hermes-crash'); + const isCoverage = checkAndRemoveArg('--hermes-coverage'); const inputLineStart = Number.parseInt( checkAndRemoveArgWithValue('--input-line-start') || '1', 10, @@ -76,27 +99,15 @@ async function main( if (argv.length < 1 || argv.length > 4) { /* eslint no-path-concat: "off" */ + printHelp(); + return 1; + } - const usages = [ - 'Usage: ' + __filename + ' ', - ' ' + __filename + ' [column]', - ' ' + - __filename + - ' .js [column]', - ' ' + __filename + ' .profmap', - ' ' + - __filename + - ' --attribution < in.jsonl > out.jsonl', - ' ' + __filename + ' .cpuprofile', - ' Optional flags:', - ' --no-function-names', - ' --hermes-crash', - ' --input-line-start (default: 1)', - ' --input-column-start (default: 0)', - ' --output-line-start (default: 1)', - ' --output-column-start (default: 0)', - ]; - console.error(usages.join('\n')); + if (isHermesCrash && isCoverage) { + console.error( + 'Pass either --hermes-crash or --hermes-coverage, not both', + ); + printHelp(); return 1; } @@ -132,6 +143,12 @@ async function main( stackTraceJSON, ); stdout.write(JSON.stringify(symbolicatedTrace)); + } else if (isCoverage) { + const stackTraceJSON = JSON.parse(stackTrace); + const symbolicatedTrace = context.symbolicateHermesCoverageTrace( + stackTraceJSON, + ); + stdout.write(JSON.stringify(symbolicatedTrace)); } else { stdout.write(context.symbolicate(stackTrace)); }