123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742 |
- /**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @flow
- * @format
- */
- 'use strict';
- const SourceMetadataMapConsumer = require('./SourceMetadataMapConsumer');
- const fs = require('fs');
- const invariant = require('invariant');
- const path = require('path');
- import type {MixedSourceMap, HermesFunctionOffsets} from 'metro-source-map';
- // flowlint-next-line untyped-type-import:off
- import {typeof SourceMapConsumer} from 'source-map';
- type SingleMapModuleIds = {
- segmentId: number,
- localId: ?number,
- ...
- };
- type ContextOptionsInput = {
- +nameSource?: 'function_names' | 'identifier_names',
- +inputLineStart?: number,
- +inputColumnStart?: number,
- +outputLineStart?: number,
- +outputColumnStart?: number,
- ...
- };
- // TODO (T46584006): Write the real types for these.
- // eslint-disable-next-line lint/no-unclear-flowtypes
- type SizeAttributionMap = Object;
- // eslint-disable-next-line lint/no-unclear-flowtypes
- type ChromeTrace = Object;
- // eslint-disable-next-line lint/no-unclear-flowtypes
- type ChromeTraceEntry = Object;
- type HermesMinidumpCrashInfo = {
- +callstack: $ReadOnlyArray<HermesMinidumpStackFrame | NativeCodeStackFrame>,
- ...
- };
- type HermesMinidumpStackFrame = $ReadOnly<{|
- ByteCodeOffset: number,
- FunctionID: number,
- CJSModuleOffset: number,
- SourceURL: string,
- StackFrameRegOffs: string,
- SourceLocation?: string,
- |}>;
- type NativeCodeStackFrame = $ReadOnly<{|
- NativeCode: true,
- StackFrameRegOffs: string,
- |}>;
- type SymbolicatedStackTrace = $ReadOnlyArray<
- SymbolicatedStackFrame | NativeCodeStackFrame,
- >;
- type SymbolicatedStackFrame = $ReadOnly<{|
- line: ?number,
- column: ?number,
- source: ?string,
- functionName: ?string,
- name: ?string,
- |}>;
- const UNKNOWN_MODULE_IDS: SingleMapModuleIds = {
- segmentId: 0,
- localId: undefined,
- };
- class SymbolicationContext<ModuleIdsT> {
- +options: {
- +nameSource: 'function_names' | 'identifier_names',
- +inputLineStart: number,
- +inputColumnStart: number,
- +outputLineStart: number,
- +outputColumnStart: number,
- ...
- };
- constructor(options: ContextOptionsInput) {
- this.options = {
- inputLineStart: 1,
- inputColumnStart: 0,
- outputLineStart: 1,
- outputColumnStart: 0,
- nameSource: 'function_names',
- };
- if (options) {
- for (const option of [
- 'inputLineStart',
- 'inputColumnStart',
- 'outputLineStart',
- 'outputColumnStart',
- ]) {
- if (options[option] != null) {
- this.options[option] = options[option];
- }
- }
- if (options.nameSource != null) {
- this.options.nameSource = options.nameSource;
- }
- }
- }
- // parse stack trace with String.replace
- // replace the matched part of stack trace to symbolicated result
- // sample stack trace:
- // IOS: foo@4:18131, Android: bar:4:18063
- // sample stack trace with module id:
- // IOS: foo@123.js:4:18131, Android: bar:123.js:4:18063
- // sample stack trace without function name:
- // 123.js:4:18131
- // sample result:
- // IOS: foo.js:57:foo, Android: bar.js:75:bar
- symbolicate(stackTrace: string): string {
- return stackTrace.replace(
- /(?:([^@: \n(]+)(@|:))?(?:(?:([^@: \n(]+):)?(\d+):(\d+)|\[native code\])/g,
- (match, func, delimiter, fileName, line, column) => {
- if (delimiter === ':' && func && !fileName) {
- fileName = func;
- func = null;
- }
- const original = this.getOriginalPositionFor(
- line,
- column,
- this.parseFileName(fileName || ''),
- );
- return (
- (original.source ?? 'null') +
- ':' +
- (original.line ?? 'null') +
- ':' +
- (original.name ?? 'null')
- );
- },
- );
- }
- // Taking in a map like
- // trampoline offset (optional js function name)
- // JS_0158_xxxxxxxxxxxxxxxxxxxxxx fe 91081
- // JS_0159_xxxxxxxxxxxxxxxxxxxxxx Ft 68651
- // JS_0160_xxxxxxxxxxxxxxxxxxxxxx value 50700
- // JS_0161_xxxxxxxxxxxxxxxxxxxxxx setGapAtCursor 0
- // JS_0162_xxxxxxxxxxxxxxxxxxxxxx (unknown) 50818
- // JS_0163_xxxxxxxxxxxxxxxxxxxxxx value 108267
- symbolicateProfilerMap(mapFile: string): string {
- return fs
- .readFileSync(mapFile, 'utf8')
- .split('\n')
- .slice(0, -1)
- .map(line => {
- const line_list = line.split(' ');
- const trampoline = line_list[0];
- const js_name = line_list[1];
- const offset = parseInt(line_list[2], 10);
- if (!offset) {
- return trampoline + ' ' + trampoline;
- }
- const original = this.getOriginalPositionFor(
- this.options.inputLineStart,
- offset,
- );
- return (
- trampoline +
- ' ' +
- (original.name || js_name) +
- '::' +
- [original.source, original.line, original.column].join(':')
- );
- })
- .join('\n');
- }
- symbolicateAttribution(obj: SizeAttributionMap): SizeAttributionMap {
- const loc = obj.location;
- const line = loc.line != null ? loc.line : this.options.inputLineStart;
- let column = loc.column != null ? loc.column : loc.virtualOffset;
- const file = loc.filename ? this.parseFileName(loc.filename) : null;
- let original = this.getOriginalPositionFor(line, column, file);
- const isBytecodeRange =
- loc.bytecodeSize != null &&
- loc.virtualOffset != null &&
- !loc.column != null;
- // Functions compiled from Metro-bundled modules will often have a little bit
- // of unmapped wrapper code right at the beginning - which is where we query.
- // Let's attribute them to where the inner module code originates instead.
- // This loop is O(n*log(n)) in the size of the function, but we will generally
- // either:
- // 1. Find a non-null mapping within one or two iterations; or
- // 2. Reach the end of the function without encountering mappings - this might
- // happen for function bodies that never throw (generally very short).
- while (
- isBytecodeRange &&
- original.source == null &&
- ++column < loc.virtualOffset + loc.bytecodeSize
- ) {
- original = this.getOriginalPositionFor(line, column, file);
- }
- obj.location = {
- file: original.source,
- line: original.line,
- column: original.column,
- };
- }
- // Symbolicate chrome trace "stackFrames" section.
- // Each frame in it has three fields: name, funcVirtAddr(optional), offset(optional).
- // funcVirtAddr and offset are only available if trace is generated from
- // hbc bundle without debug info.
- symbolicateChromeTrace(
- traceFile: string,
- {
- stdout,
- stderr,
- }: {
- stdout: stream$Writable,
- stderr: stream$Writable,
- ...
- },
- ): void {
- const contentJson: ChromeTrace = JSON.parse(
- fs.readFileSync(traceFile, 'utf8'),
- );
- if (contentJson.stackFrames == null) {
- throw new Error('Unable to locate `stackFrames` section in trace.');
- }
- stdout.write(
- 'Processing ' + Object.keys(contentJson.stackFrames).length + ' frames\n',
- );
- Object.values(contentJson.stackFrames).forEach(
- (entry: ChromeTraceEntry) => {
- let line;
- let column;
- // Function entrypoint line/column; used for symbolicating function name
- // with legacy source maps (or when --no-function-names is set).
- let funcLine;
- let funcColumn;
- if (entry.funcVirtAddr != null && entry.offset != null) {
- // Without debug information.
- const funcVirtAddr = parseInt(entry.funcVirtAddr, 10);
- const offsetInFunction = parseInt(entry.offset, 10);
- // Main bundle always use hard-coded line value 1.
- // TODO: support multiple bundle/module.
- line = this.options.inputLineStart;
- column = funcVirtAddr + offsetInFunction;
- funcLine = this.options.inputLineStart;
- funcColumn = funcVirtAddr;
- } else if (entry.line != null && entry.column != null) {
- // For hbc bundle with debug info, name field may already have source
- // information for the bundle; we still can use the Metro
- // source map to symbolicate the bundle frame addresses further to its
- // original source code.
- line = entry.line;
- column = entry.column;
- funcLine = entry.funcLine;
- funcColumn = entry.funcColumn;
- } else {
- // Native frames.
- return;
- }
- // Symbolicate original file/line/column.
- const addressOriginal = this.getOriginalPositionDetailsFor(
- line,
- column,
- );
- let frameName;
- if (addressOriginal.functionName) {
- frameName = addressOriginal.functionName;
- } else {
- frameName = entry.name;
- // Symbolicate function name.
- if (funcLine != null && funcColumn != null) {
- const funcOriginal = this.getOriginalPositionFor(
- funcLine,
- funcColumn,
- );
- if (funcOriginal.name != null) {
- frameName = funcOriginal.name;
- }
- } else {
- // No function line/column info.
- (stderr || stdout).write(
- 'Warning: no function prolog line/column info; name may be wrong\n',
- );
- }
- }
- // Output format is: funcName(file:line:column)
- entry.name = [
- frameName,
- '(',
- [
- addressOriginal.source ?? 'null',
- addressOriginal.line ?? 'null',
- addressOriginal.column ?? 'null',
- ].join(':'),
- ')',
- ].join('');
- },
- );
- stdout.write('Writing to ' + traceFile + '\n');
- fs.writeFileSync(traceFile, JSON.stringify(contentJson));
- }
- /*
- * A helper function to return a mapping {line, column} object for a given input
- * line and column, and optionally a module ID.
- */
- getOriginalPositionFor(
- lineNumber: ?number,
- columnNumber: ?number,
- moduleIds: ?ModuleIdsT,
- ): {|
- line: ?number,
- column: ?number,
- source: ?string,
- name: ?string,
- |} {
- const position = this.getOriginalPositionDetailsFor(
- lineNumber,
- columnNumber,
- moduleIds,
- );
- return {
- line: position.line,
- column: position.column,
- source: position.source,
- name: position.functionName ? position.functionName : position.name,
- };
- }
- /*
- * Symbolicates the JavaScript stack trace extracted from the minidump
- * produced by hermes
- */
- symbolicateHermesMinidumpTrace(
- crashInfo: HermesMinidumpCrashInfo,
- ): SymbolicatedStackTrace {
- throw new Error('Not implemented');
- }
- /*
- * An internal helper function similar to getOriginalPositionFor. This one
- * returns both `name` and `functionName` fields so callers can distinguish the
- * source of the name.
- */
- getOriginalPositionDetailsFor(
- lineNumber: ?number,
- columnNumber: ?number,
- moduleIds: ?ModuleIdsT,
- ): SymbolicatedStackFrame {
- throw new Error('Not implemented');
- }
- parseFileName(str: string): ModuleIdsT {
- throw new Error('Not implemented');
- }
- }
- class SingleMapSymbolicationContext extends SymbolicationContext<SingleMapModuleIds> {
- +_segments: {
- +[id: string]: {|
- +consumer: SourceMapConsumer,
- +moduleOffsets: $ReadOnlyArray<number>,
- +sourceFunctionsConsumer: ?SourceMetadataMapConsumer,
- +hermesOffsets: ?HermesFunctionOffsets,
- |},
- ...,
- };
- +_hasLegacySegments: boolean;
- constructor(
- SourceMapConsumer: SourceMapConsumer,
- sourceMapContent: string | MixedSourceMap,
- options: ContextOptionsInput = {},
- ) {
- super(options);
- const useFunctionNames = this.options.nameSource === 'function_names';
- const sourceMapJson: MixedSourceMap =
- typeof sourceMapContent === 'string'
- ? JSON.parse(sourceMapContent.replace(/^\)\]\}'/, ''))
- : sourceMapContent;
- const {x_hermes_function_offsets} = sourceMapJson;
- const segments = {
- '0': {
- consumer: new SourceMapConsumer(sourceMapJson),
- moduleOffsets: sourceMapJson.x_facebook_offsets || [],
- sourceFunctionsConsumer: useFunctionNames
- ? new SourceMetadataMapConsumer(sourceMapJson)
- : null,
- hermesOffsets: x_hermes_function_offsets,
- },
- };
- if (sourceMapJson.x_facebook_segments) {
- for (const key of Object.keys(sourceMapJson.x_facebook_segments)) {
- const map = sourceMapJson.x_facebook_segments[key];
- segments[key] = {
- consumer: new SourceMapConsumer(map),
- moduleOffsets: map.x_facebook_offsets || [],
- sourceFunctionsConsumer: useFunctionNames
- ? new SourceMetadataMapConsumer(map)
- : null,
- hermesOffsets: map.x_hermes_function_offsets,
- };
- }
- }
- this._hasLegacySegments = sourceMapJson.x_facebook_segments != null;
- this._segments = segments;
- }
- symbolicateHermesMinidumpTrace(
- crashInfo: HermesMinidumpCrashInfo,
- ): SymbolicatedStackTrace {
- const symbolicatedTrace = [];
- const {callstack} = crashInfo;
- if (callstack != null) {
- for (const stackItem of callstack) {
- if (stackItem.NativeCode) {
- symbolicatedTrace.push(stackItem);
- } else {
- const {
- CJSModuleOffset,
- SourceURL,
- FunctionID,
- ByteCodeOffset: localOffset,
- } = stackItem;
- const moduleInformation = this._hasLegacySegments
- ? this.parseFileName(SourceURL)
- : UNKNOWN_MODULE_IDS;
- const generatedLine = CJSModuleOffset + this.options.inputLineStart;
- const segment = this._segments[
- moduleInformation.segmentId.toString()
- ];
- const hermesOffsets = segment?.hermesOffsets;
- if (!hermesOffsets) {
- symbolicatedTrace.push({
- line: null,
- column: null,
- source: null,
- functionName: null,
- name: null,
- });
- } else {
- const segmentOffsets = hermesOffsets[Number(CJSModuleOffset)];
- const generatedColumn =
- segmentOffsets[FunctionID] +
- localOffset +
- this.options.inputColumnStart;
- const originalPosition = this.getOriginalPositionDetailsFor(
- generatedLine,
- generatedColumn,
- moduleInformation,
- );
- 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
- * source of the name.
- */
- getOriginalPositionDetailsFor(
- lineNumber: ?number,
- columnNumber: ?number,
- moduleIds: ?SingleMapModuleIds,
- ): SymbolicatedStackFrame {
- // Adjust arguments to source-map's input coordinates
- lineNumber =
- lineNumber != null
- ? lineNumber - this.options.inputLineStart + 1
- : lineNumber;
- columnNumber =
- columnNumber != null
- ? columnNumber - this.options.inputColumnStart + 0
- : columnNumber;
- if (!moduleIds) {
- moduleIds = UNKNOWN_MODULE_IDS;
- }
- let moduleLineOffset = 0;
- const metadata = this._segments[moduleIds.segmentId + ''];
- const {localId} = moduleIds;
- if (localId != null) {
- const {moduleOffsets} = metadata;
- if (!moduleOffsets) {
- throw new Error(
- 'Module ID given for a source map that does not have ' +
- 'an x_facebook_offsets field',
- );
- }
- if (moduleOffsets[localId] == null) {
- throw new Error('Unknown module ID: ' + localId);
- }
- moduleLineOffset = moduleOffsets[localId];
- }
- const original = metadata.consumer.originalPositionFor({
- line: Number(lineNumber) + moduleLineOffset,
- column: Number(columnNumber),
- });
- if (metadata.sourceFunctionsConsumer) {
- original.functionName =
- metadata.sourceFunctionsConsumer.functionNameFor(original) || null;
- } else {
- original.functionName = null;
- }
- return {
- ...original,
- line:
- original.line != null
- ? original.line - 1 + this.options.outputLineStart
- : original.line,
- column:
- original.column != null
- ? original.column - 0 + this.options.outputColumnStart
- : original.column,
- };
- }
- parseFileName(str: string): SingleMapModuleIds {
- return parseSingleMapFileName(str);
- }
- }
- class DirectorySymbolicationContext extends SymbolicationContext<string> {
- +_fileMaps: Map<string, SingleMapSymbolicationContext>;
- +_rootDir: string;
- +_SourceMapConsumer: SourceMapConsumer;
- constructor(
- SourceMapConsumer: SourceMapConsumer,
- rootDir: string,
- options: ContextOptionsInput = {},
- ) {
- super(options);
- this._fileMaps = new Map();
- this._rootDir = rootDir;
- this._SourceMapConsumer = SourceMapConsumer;
- }
- _loadMap(mapFilename: string): SingleMapSymbolicationContext {
- invariant(
- fs.existsSync(mapFilename),
- `Could not read source map from '${mapFilename}'`,
- );
- let fileMap = this._fileMaps.get(mapFilename);
- if (fileMap == null) {
- fileMap = new SingleMapSymbolicationContext(
- this._SourceMapConsumer,
- fs.readFileSync(mapFilename, 'utf8'),
- this.options,
- );
- this._fileMaps.set(mapFilename, fileMap);
- }
- return fileMap;
- }
- /*
- * An internal helper function similar to getOriginalPositionFor. This one
- * returns both `name` and `functionName` fields so callers can distinguish the
- * source of the name.
- */
- getOriginalPositionDetailsFor(
- lineNumber: ?number,
- columnNumber: ?number,
- filename: ?string,
- ): SymbolicatedStackFrame {
- invariant(
- filename != null,
- 'filename is required for DirectorySymbolicationContext',
- );
- const mapFilename = path.join(this._rootDir, filename + '.map');
- if (!fs.existsSync(mapFilename)) {
- // Adjust arguments to the output coordinates
- lineNumber =
- lineNumber != null
- ? lineNumber -
- this.options.inputLineStart +
- this.options.outputLineStart
- : lineNumber;
- columnNumber =
- columnNumber != null
- ? columnNumber -
- this.options.inputColumnStart +
- this.options.outputColumnStart
- : columnNumber;
- return {
- line: lineNumber,
- column: columnNumber,
- source: filename,
- name: null,
- functionName: null,
- };
- }
- return this._loadMap(mapFilename).getOriginalPositionDetailsFor(
- lineNumber,
- columnNumber,
- );
- }
- parseFileName(str: string): string {
- return str;
- }
- }
- /*
- * If the file name of a stack frame is numeric (+ ".js"), we assume it's a
- * lazily injected module coming from a "random access bundle". We are using
- * special source maps for these bundles, so that we can symbolicate stack
- * traces for multiple injected files with a single source map.
- *
- * There is also a convention for callsites that are in split segments of a
- * bundle, named either `seg-3.js` for segment #3 for example, or `seg-3_5.js`
- * for module #5 of segment #3 of a segmented RAM bundle.
- */
- function parseSingleMapFileName(str: string): SingleMapModuleIds {
- const modMatch = str.match(/^(\d+).js$/);
- if (modMatch != null) {
- return {segmentId: 0, localId: Number(modMatch[1])};
- }
- const segMatch = str.match(/^seg-(\d+)(?:_(\d+))?.js$/);
- if (segMatch != null) {
- return {
- segmentId: Number(segMatch[1]),
- localId: segMatch[2] ? Number(segMatch[2]) : null,
- };
- }
- return UNKNOWN_MODULE_IDS;
- }
- function createContext(
- SourceMapConsumer: SourceMapConsumer,
- sourceMapContent: string | MixedSourceMap,
- options: ContextOptionsInput = {},
- ): SingleMapSymbolicationContext {
- return new SingleMapSymbolicationContext(
- SourceMapConsumer,
- sourceMapContent,
- options,
- );
- }
- function unstable_createDirectoryContext(
- SourceMapConsumer: SourceMapConsumer,
- rootDir: string,
- options: ContextOptionsInput = {},
- ): DirectorySymbolicationContext {
- return new DirectorySymbolicationContext(SourceMapConsumer, rootDir, options);
- }
- function getOriginalPositionFor<ModuleIdsT>(
- lineNumber: ?number,
- columnNumber: ?number,
- moduleIds: ?ModuleIdsT,
- context: SymbolicationContext<ModuleIdsT>,
- ): {|
- line: ?number,
- column: ?number,
- source: ?string,
- name: ?string,
- |} {
- return context.getOriginalPositionFor(lineNumber, columnNumber, moduleIds);
- }
- function symbolicate<ModuleIdsT>(
- stackTrace: string,
- context: SymbolicationContext<ModuleIdsT>,
- ): string {
- return context.symbolicate(stackTrace);
- }
- function symbolicateProfilerMap<ModuleIdsT>(
- mapFile: string,
- context: SymbolicationContext<ModuleIdsT>,
- ): string {
- return context.symbolicateProfilerMap(mapFile);
- }
- function symbolicateAttribution<ModuleIdsT>(
- obj: SizeAttributionMap,
- context: SymbolicationContext<ModuleIdsT>,
- ): SizeAttributionMap {
- return context.symbolicateAttribution(obj);
- }
- function symbolicateChromeTrace<ModuleIdsT>(
- traceFile: string,
- {
- stdout,
- stderr,
- }: {
- stdout: stream$Writable,
- stderr: stream$Writable,
- ...
- },
- context: SymbolicationContext<ModuleIdsT>,
- ): void {
- return context.symbolicateChromeTrace(traceFile, {stdout, stderr});
- }
- module.exports = {
- createContext,
- unstable_createDirectoryContext,
- getOriginalPositionFor,
- parseFileName: parseSingleMapFileName,
- symbolicate,
- symbolicateProfilerMap,
- symbolicateAttribution,
- symbolicateChromeTrace,
- SourceMetadataMapConsumer,
- };
|