|
| 1 | +/** |
| 2 | + * Copyright (c) Facebook, Inc. and its affiliates. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + * |
| 7 | + * @format |
| 8 | + */ |
| 9 | + |
| 10 | +'use strict'; |
| 11 | + |
| 12 | +/* eslint-disable no-console */ |
| 13 | + |
| 14 | +const fs = require('fs'); |
| 15 | + |
| 16 | +const UNKNOWN_MODULE_IDS = { |
| 17 | + segmentId: 0, |
| 18 | + localId: undefined, |
| 19 | +}; |
| 20 | + |
| 21 | +/* |
| 22 | + * If the file name of a stack frame is numeric (+ ".js"), we assume it's a |
| 23 | + * lazily injected module coming from a "random access bundle". We are using |
| 24 | + * special source maps for these bundles, so that we can symbolicate stack |
| 25 | + * traces for multiple injected files with a single source map. |
| 26 | + * |
| 27 | + * There is also a convention for callsites that are in split segments of a |
| 28 | + * bundle, named either `seg-3.js` for segment #3 for example, or `seg-3_5.js` |
| 29 | + * for module #5 of segment #3 of a segmented RAM bundle. |
| 30 | + */ |
| 31 | +function parseFileName(str) { |
| 32 | + const modMatch = str.match(/^(\d+).js$/); |
| 33 | + if (modMatch != null) { |
| 34 | + return {segmentId: 0, localId: Number(modMatch[1])}; |
| 35 | + } |
| 36 | + const segMatch = str.match(/^seg-(\d+)(?:_(\d+))?.js$/); |
| 37 | + if (segMatch != null) { |
| 38 | + return { |
| 39 | + segmentId: Number(segMatch[1]), |
| 40 | + localId: segMatch[2] && Number(segMatch[2]), |
| 41 | + }; |
| 42 | + } |
| 43 | + return UNKNOWN_MODULE_IDS; |
| 44 | +} |
| 45 | + |
| 46 | +/* |
| 47 | + * A helper function to return a mapping {line, column} object for a given input |
| 48 | + * line and column, and optionally a module ID. |
| 49 | + */ |
| 50 | +function getOriginalPositionFor(lineNumber, columnNumber, moduleIds, context) { |
| 51 | + var moduleLineOffset = 0; |
| 52 | + var metadata = context.segments[moduleIds.segmentId]; |
| 53 | + const {localId} = moduleIds; |
| 54 | + if (localId != null) { |
| 55 | + const {moduleOffsets} = metadata; |
| 56 | + if (!moduleOffsets) { |
| 57 | + throw new Error( |
| 58 | + 'Module ID given for a source map that does not have ' + |
| 59 | + 'an x_facebook_offsets field', |
| 60 | + ); |
| 61 | + } |
| 62 | + if (moduleOffsets[localId] == null) { |
| 63 | + throw new Error('Unknown module ID: ' + localId); |
| 64 | + } |
| 65 | + moduleLineOffset = moduleOffsets[localId]; |
| 66 | + } |
| 67 | + return metadata.consumer.originalPositionFor({ |
| 68 | + line: Number(lineNumber) + moduleLineOffset, |
| 69 | + column: Number(columnNumber), |
| 70 | + }); |
| 71 | +} |
| 72 | + |
| 73 | +function createContext(SourceMapConsumer, sourceMapContent) { |
| 74 | + var sourceMapJson = JSON.parse(sourceMapContent.replace(/^\)\]\}'/, '')); |
| 75 | + return { |
| 76 | + segments: Object.entries(sourceMapJson.x_facebook_segments || {}).reduce( |
| 77 | + (acc, seg) => { |
| 78 | + acc[seg[0]] = { |
| 79 | + consumer: new SourceMapConsumer(seg[1]), |
| 80 | + moduleOffsets: seg[1].x_facebook_offsets || {}, |
| 81 | + }; |
| 82 | + return acc; |
| 83 | + }, |
| 84 | + { |
| 85 | + '0': { |
| 86 | + consumer: new SourceMapConsumer(sourceMapJson), |
| 87 | + moduleOffsets: sourceMapJson.x_facebook_offsets || {}, |
| 88 | + }, |
| 89 | + }, |
| 90 | + ), |
| 91 | + }; |
| 92 | +} |
| 93 | + |
| 94 | +// parse stack trace with String.replace |
| 95 | +// replace the matched part of stack trace to symbolicated result |
| 96 | +// sample stack trace: |
| 97 | +// IOS: foo@4:18131, Android: bar:4:18063 |
| 98 | +// sample stack trace with module id: |
| 99 | +// IOS: foo@123.js:4:18131, Android: bar:123.js:4:18063 |
| 100 | +// sample result: |
| 101 | +// IOS: foo.js:57:foo, Android: bar.js:75:bar |
| 102 | +function symbolicate(stackTrace, context) { |
| 103 | + return stackTrace.replace( |
| 104 | + /(?:([^@: \n]+)(@|:))?(?:(?:([^@: \n]+):)?(\d+):(\d+)|\[native code\])/g, |
| 105 | + function(match, func, delimiter, fileName, line, column) { |
| 106 | + var original = getOriginalPositionFor( |
| 107 | + line, |
| 108 | + column, |
| 109 | + parseFileName(fileName || ''), |
| 110 | + context, |
| 111 | + ); |
| 112 | + return original.source + ':' + original.line + ':' + original.name; |
| 113 | + }, |
| 114 | + ); |
| 115 | +} |
| 116 | + |
| 117 | +// Taking in a map like |
| 118 | +// trampoline offset (optional js function name) |
| 119 | +// JS_0158_xxxxxxxxxxxxxxxxxxxxxx fe 91081 |
| 120 | +// JS_0159_xxxxxxxxxxxxxxxxxxxxxx Ft 68651 |
| 121 | +// JS_0160_xxxxxxxxxxxxxxxxxxxxxx value 50700 |
| 122 | +// JS_0161_xxxxxxxxxxxxxxxxxxxxxx setGapAtCursor 0 |
| 123 | +// JS_0162_xxxxxxxxxxxxxxxxxxxxxx (unknown) 50818 |
| 124 | +// JS_0163_xxxxxxxxxxxxxxxxxxxxxx value 108267 |
| 125 | + |
| 126 | +function symbolicateProfilerMap(mapFile, context) { |
| 127 | + return fs |
| 128 | + .readFileSync(mapFile, 'utf8') |
| 129 | + .split('\n') |
| 130 | + .slice(0, -1) |
| 131 | + .map(function(line) { |
| 132 | + const line_list = line.split(' '); |
| 133 | + const trampoline = line_list[0]; |
| 134 | + const js_name = line_list[1]; |
| 135 | + const offset = parseInt(line_list[2], 10); |
| 136 | + |
| 137 | + if (!offset) { |
| 138 | + return trampoline + ' ' + trampoline; |
| 139 | + } |
| 140 | + |
| 141 | + var original = getOriginalPositionFor( |
| 142 | + 1, |
| 143 | + offset, |
| 144 | + UNKNOWN_MODULE_IDS, |
| 145 | + context, |
| 146 | + ); |
| 147 | + |
| 148 | + return ( |
| 149 | + trampoline + |
| 150 | + ' ' + |
| 151 | + (original.name || js_name) + |
| 152 | + '::' + |
| 153 | + [original.source, original.line, original.column].join(':') |
| 154 | + ); |
| 155 | + }) |
| 156 | + .join('\n'); |
| 157 | +} |
| 158 | + |
| 159 | +function symbolicateAttribution(obj, context) { |
| 160 | + var loc = obj.location; |
| 161 | + var line = loc.line || 1; |
| 162 | + var column = loc.column || loc.virtualOffset; |
| 163 | + var file = loc.filename ? parseFileName(loc.filename) : UNKNOWN_MODULE_IDS; |
| 164 | + var original = getOriginalPositionFor(line, column, file, context); |
| 165 | + obj.location = { |
| 166 | + file: original.source, |
| 167 | + line: original.line, |
| 168 | + column: original.column, |
| 169 | + }; |
| 170 | +} |
| 171 | + |
| 172 | +// Symbolicate chrome trace "stackFrames" section. |
| 173 | +// Each frame in it has three fields: name, funcVirtAddr(optional), offset(optional). |
| 174 | +// funcVirtAddr and offset are only available if trace is generated from |
| 175 | +// hbc bundle without debug info. |
| 176 | +function symbolicateChromeTrace(traceFile, context) { |
| 177 | + const contentJson = JSON.parse(fs.readFileSync(traceFile, 'utf8')); |
| 178 | + if (contentJson.stackFrames == null) { |
| 179 | + console.error('Unable to locate `stackFrames` section in trace.'); |
| 180 | + process.exit(1); |
| 181 | + } |
| 182 | + console.log( |
| 183 | + 'Processing ' + Object.keys(contentJson.stackFrames).length + ' frames', |
| 184 | + ); |
| 185 | + Object.values(contentJson.stackFrames).forEach(function(entry) { |
| 186 | + let line; |
| 187 | + let column; |
| 188 | + |
| 189 | + // Function entrypoint line/column; used for symbolicating function name. |
| 190 | + let funcLine; |
| 191 | + let funcColumn; |
| 192 | + |
| 193 | + if (entry.funcVirtAddr != null && entry.offset != null) { |
| 194 | + // Without debug information. |
| 195 | + const funcVirtAddr = parseInt(entry.funcVirtAddr, 10); |
| 196 | + const offsetInFunction = parseInt(entry.offset, 10); |
| 197 | + // Main bundle always use hard-coded line value 1. |
| 198 | + // TODO: support multiple bundle/module. |
| 199 | + line = 1; |
| 200 | + column = funcVirtAddr + offsetInFunction; |
| 201 | + funcLine = 1; |
| 202 | + funcColumn = funcVirtAddr; |
| 203 | + } else if (entry.line != null && entry.column != null) { |
| 204 | + // For hbc bundle with debug info, name field may already have source |
| 205 | + // information for the bundle; we still can use babel/metro/prepack |
| 206 | + // source map to symbolicate the bundle frame addresses further to its |
| 207 | + // original source code. |
| 208 | + line = entry.line; |
| 209 | + column = entry.column; |
| 210 | + |
| 211 | + funcLine = entry.funcLine; |
| 212 | + funcColumn = entry.funcColumn; |
| 213 | + } else { |
| 214 | + // Native frames. |
| 215 | + return; |
| 216 | + } |
| 217 | + |
| 218 | + // Symbolicate original file/line/column. |
| 219 | + const addressOriginal = getOriginalPositionFor( |
| 220 | + line, |
| 221 | + column, |
| 222 | + UNKNOWN_MODULE_IDS, |
| 223 | + context, |
| 224 | + ); |
| 225 | + |
| 226 | + let frameName = entry.name; |
| 227 | + // Symbolicate function name. |
| 228 | + if (funcLine != null && funcColumn != null) { |
| 229 | + const funcOriginal = getOriginalPositionFor( |
| 230 | + funcLine, |
| 231 | + funcColumn, |
| 232 | + UNKNOWN_MODULE_IDS, |
| 233 | + context, |
| 234 | + ); |
| 235 | + if (funcOriginal.name != null) { |
| 236 | + frameName = funcOriginal.name; |
| 237 | + } |
| 238 | + } else { |
| 239 | + // No function line/column info. |
| 240 | + console.warn( |
| 241 | + 'Warning: no function prolog line/column info; name may be wrong', |
| 242 | + ); |
| 243 | + } |
| 244 | + |
| 245 | + // Output format is: funcName(file:line:column) |
| 246 | + const sourceLocation = `(${addressOriginal.source}:${ |
| 247 | + addressOriginal.line |
| 248 | + }:${addressOriginal.column})`; |
| 249 | + entry.name = frameName + sourceLocation; |
| 250 | + }); |
| 251 | + console.log('Writing to ' + traceFile); |
| 252 | + fs.writeFileSync(traceFile, JSON.stringify(contentJson)); |
| 253 | +} |
| 254 | + |
| 255 | +module.exports = { |
| 256 | + createContext, |
| 257 | + getOriginalPositionFor, |
| 258 | + parseFileName, |
| 259 | + symbolicate, |
| 260 | + symbolicateProfilerMap, |
| 261 | + symbolicateAttribution, |
| 262 | + symbolicateChromeTrace, |
| 263 | +}; |
0 commit comments