Skip to content

Commit 0849f84

Browse files
davidaureliofacebook-github-bot
authored andcommitted
create better debuggable source maps
Summary: Introduces a new mechanism to build source maps that allows us to use real mapping segments instead of just mapping line-by-line. This mechanism is only used when building development bundles to improve the debugging experience in Chrome. The new mechanism takes advantage of a new feature in babel-generator that exposes raw mapping objects. These raw mapping objects are converted to arrays with 2, 4, or 5 for the most compact representation possible. We no longer generate a source map for the bundle that maps each line to itself in conjunction with configuring babel generator to retain lines. Instead, we create a source map with a large mappings object produced from the mappings of each individual file in conjunction with a “carry over” – the number of preceding lines in the bundle. The implementation makes a couple of assumptions that hold true for babel transform results, e.g. mappings being in the order of the generated code, and that a block of mappings always belongs to the same source file. In addition, the implementation avoids allocation of objects and strings at all costs. All calculations are purely numeric, and base64 vlq produces numeric ascii character codes. These are written to a preallocated buffer objects, which is turned to a string only at the end of the building process. This implementation is ~5x faster than using the source-map library. In addition to providing development source maps that work better, we can now also produce individual high-quality source maps for production builds and combine them to an “index source map”. This approach is unfeasable for development source maps, because index source map consistently crash Chrome. Better production source maps are useful to get precise information about source location and symbol names when symbolicating stack traces from crashes in production. Reviewed By: jeanlauliac Differential Revision: D4382290 fbshipit-source-id: 365a176fa142729d0a4cef43edeb81084361e54d
1 parent 4969f26 commit 0849f84

File tree

20 files changed

+181
-436
lines changed

20 files changed

+181
-436
lines changed

flow/babel.js.flow

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ type __TransformOptions = {
8787

8888
type _TransformOptions =
8989
__TransformOptions & {env?: {[key: string]: __TransformOptions}};
90-
declare class _Ast {};
90+
declare class _Ast {}
9191
type TransformResult = {
9292
ast: _Ast,
9393
code: ?string,
@@ -119,9 +119,17 @@ declare module 'babel-core' {
119119
): TransformResult;
120120
}
121121

122+
type RawMapping = {
123+
generated: {column: number, line: number},
124+
name?: string,
125+
original?: {column: number, line: number},
126+
source?: string,
127+
};
128+
122129
declare module 'babel-generator' {
130+
declare type RawMapping = RawMapping;
123131
declare function exports(
124132
ast: _Ast,
125133
options?: GeneratorOptions,
126-
): TransformResult;
134+
): TransformResult & {rawMappings: ?Array<RawMapping>};
127135
}

jest/preprocessor.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ const babel = require('babel-core');
1212
const babelRegisterOnly = require('../packager/babelRegisterOnly');
1313
const createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction');
1414
const path = require('path');
15-
const transformer = require('../packager/transformer.js');
1615

1716
const nodeFiles = RegExp([
1817
'/local-cli/',
1918
'/packager/(?!react-packager/src/Resolver/polyfills/)',
2019
].join('|'));
2120
const nodeOptions = babelRegisterOnly.config([nodeFiles]);
2221

22+
babelRegisterOnly([]);
23+
// has to be required after setting up babelRegisterOnly
24+
const transformer = require('../packager/transformer.js');
25+
2326
module.exports = {
2427
process(src, file) {
2528
// Don't transform node_modules, except react-tools which includes the

local-cli/bundle/output/bundle.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ function buildBundle(packagerClient: Server, requestOptions: RequestOptions) {
2626
}
2727

2828
function createCodeWithMap(bundle: Bundle, dev: boolean, sourceMapSourcesRoot?: string): * {
29-
const sourceMap = relativizeSourceMap(bundle.getSourceMap({dev}), sourceMapSourcesRoot);
29+
const map = bundle.getSourceMap({dev});
30+
const sourceMap = relativizeSourceMap(
31+
typeof map === 'string' ? JSON.parse(map) : map,
32+
sourceMapSourcesRoot);
3033
return {
3134
code: bundle.getSource({dev}),
3235
map: JSON.stringify(sourceMap),

local-cli/bundle/output/unbundle/util.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
*/
1111
'use strict';
1212

13+
const invariant = require('fbjs/lib/invariant');
14+
1315
import type {ModuleGroups, ModuleTransportLike, SourceMap} from '../../types.flow';
1416

1517
const newline = /\r\n?|\n|\u2028|\u2029/g;
@@ -99,6 +101,10 @@ function combineSourceMaps({
99101
column = wrapperEnd(code);
100102
}
101103

104+
invariant(
105+
!Array.isArray(map),
106+
'Random Access Bundle source maps cannot be built from raw mappings',
107+
);
102108
sections.push(Section(line, column, map || lineToLineSourceMap(code, name)));
103109
if (hasOffset) {
104110
offsets[id] = line;

local-cli/bundle/types.flow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export type ModuleGroups = {|
2626
export type ModuleTransportLike = {
2727
code: string,
2828
id: number,
29-
map?: ?MixedSourceMap,
29+
map?: $PropertyType<ModuleTransport, 'map'>,
3030
+name?: string,
3131
};
3232

packager/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.4.0",
2+
"version": "0.5.0",
33
"name": "react-native-packager",
44
"description": "Build native apps with React!",
55
"repository": {

packager/react-packager/src/Bundler/Bundle.js

Lines changed: 70 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ const BundleBase = require('./BundleBase');
1515
const ModuleTransport = require('../lib/ModuleTransport');
1616

1717
const _ = require('lodash');
18-
const base64VLQ = require('./base64-vlq');
1918
const crypto = require('crypto');
19+
const debug = require('debug')('RNP:Bundle');
20+
const invariant = require('fbjs/lib/invariant');
21+
22+
const {fromRawMappings} = require('./source-map');
2023

2124
import type {SourceMap, CombinedSourceMap, MixedSourceMap} from '../lib/SourceMap';
2225
import type {GetSourceOptions, FinalizeOptions} from './BundleBase';
@@ -27,6 +30,8 @@ export type Unbundle = {
2730
groups: Map<number, Set<number>>,
2831
};
2932

33+
type SourceMapFormat = 'undetermined' | 'indexed' | 'flattened';
34+
3035
const SOURCEMAPPING_URL = '\n\/\/# sourceMappingURL=';
3136

3237
class Bundle extends BundleBase {
@@ -37,8 +42,8 @@ class Bundle extends BundleBase {
3742
_numRequireCalls: number;
3843
_ramBundle: Unbundle | null;
3944
_ramGroups: Array<string> | void;
40-
_shouldCombineSourceMaps: boolean;
41-
_sourceMap: boolean;
45+
_sourceMap: string | null;
46+
_sourceMapFormat: SourceMapFormat;
4247
_sourceMapUrl: string | void;
4348

4449
constructor({sourceMapUrl, dev, minify, ramGroups}: {
@@ -48,9 +53,9 @@ class Bundle extends BundleBase {
4853
ramGroups?: Array<string>,
4954
} = {}) {
5055
super();
51-
this._sourceMap = false;
56+
this._sourceMap = null;
57+
this._sourceMapFormat = 'undetermined';
5258
this._sourceMapUrl = sourceMapUrl;
53-
this._shouldCombineSourceMaps = false;
5459
this._numRequireCalls = 0;
5560
this._dev = dev;
5661
this._minify = minify;
@@ -86,8 +91,22 @@ class Bundle extends BundleBase {
8691
}).then(({code, map}) => {
8792
// If we get a map from the transformer we'll switch to a mode
8893
// were we're combining the source maps as opposed to
89-
if (!this._shouldCombineSourceMaps && map != null) {
90-
this._shouldCombineSourceMaps = true;
94+
if (map) {
95+
const usesRawMappings = isRawMappings(map);
96+
97+
if (this._sourceMapFormat === 'undetermined') {
98+
this._sourceMapFormat = usesRawMappings ? 'flattened' : 'indexed';
99+
} else if (usesRawMappings && this._sourceMapFormat === 'indexed') {
100+
throw new Error(
101+
`Got at least one module with a full source map, but ${
102+
moduleTransport.sourcePath} has raw mappings`
103+
);
104+
} else if (!usesRawMappings && this._sourceMapFormat === 'flattened') {
105+
throw new Error(
106+
`Got at least one module with raw mappings, but ${
107+
moduleTransport.sourcePath} has a full source map`
108+
);
109+
}
91110
}
92111

93112
this.replaceModuleAt(
@@ -103,7 +122,7 @@ class Bundle extends BundleBase {
103122
options.runBeforeMainModule.forEach(this._addRequireCall, this);
104123
/* $FlowFixMe: this is unsound, as nothing enforces the module ID to have
105124
* been set beforehand. */
106-
this._addRequireCall(super.getMainModuleId());
125+
this._addRequireCall(this.getMainModuleId());
107126
}
108127

109128
super.finalize(options);
@@ -126,16 +145,16 @@ class Bundle extends BundleBase {
126145
127146
_getInlineSourceMap(dev) {
128147
if (this._inlineSourceMap == null) {
129-
const sourceMap = this.getSourceMap({excludeSource: true, dev});
148+
const sourceMap = this.getSourceMapString({excludeSource: true, dev});
130149
/*eslint-env node*/
131-
const encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64');
150+
const encoded = new Buffer(sourceMap).toString('base64');
132151
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
133152
}
134153
return this._inlineSourceMap;
135154
}
136155
137156
getSource(options: GetSourceOptions) {
138-
super.assertFinalized();
157+
this.assertFinalized();
139158
140159
options = options || {};
141160
@@ -175,6 +194,12 @@ class Bundle extends BundleBase {
175194
return this._ramBundle;
176195
}
177196
197+
invalidateSource() {
198+
debug('invalidating bundle');
199+
super.invalidateSource();
200+
this._sourceMap = null;
201+
}
202+
178203
/**
179204
* Combine each of the sourcemaps multiple modules have into a single big
180205
* one. This works well thanks to a neat trick defined on the sourcemap spec
@@ -190,23 +215,22 @@ class Bundle extends BundleBase {
190215
191216
let line = 0;
192217
this.getModules().forEach(module => {
193-
let map = module.map;
218+
let map = module.map == null || module.virtual
219+
? generateSourceMapForVirtualModule(module)
220+
: module.map;
194221
195-
if (module.virtual) {
196-
map = generateSourceMapForVirtualModule(module);
197-
}
222+
invariant(
223+
!Array.isArray(map),
224+
`Unexpected raw mappings for ${module.sourcePath}`,
225+
);
198226
199-
if (options.excludeSource) {
200-
/* $FlowFixMe: assume the map is not empty if we got here. */
201-
if (map.sourcesContent && map.sourcesContent.length) {
202-
map = Object.assign({}, map, {sourcesContent: []});
203-
}
227+
if (options.excludeSource && 'sourcesContent' in map) {
228+
map = {...map, sourcesContent: []};
204229
}
205230
206231
result.sections.push({
207232
offset: { line: line, column: 0 },
208-
/* $FlowFixMe: assume the map is not empty if we got here. */
209-
map: map,
233+
map: (map: MixedSourceMap),
210234
});
211235
line += module.code.split('\n').length;
212236
});
@@ -215,23 +239,30 @@ class Bundle extends BundleBase {
215239
}
216240
217241
getSourceMap(options: {excludeSource?: boolean}): MixedSourceMap {
218-
super.assertFinalized();
242+
this.assertFinalized();
243+
244+
return this._sourceMapFormat === 'indexed'
245+
? this._getCombinedSourceMaps(options)
246+
: fromRawMappings(this.getModules()).toMap();
247+
}
219248
220-
if (this._shouldCombineSourceMaps) {
221-
return this._getCombinedSourceMaps(options);
249+
getSourceMapString(options: {excludeSource?: boolean}): string {
250+
if (this._sourceMapFormat === 'indexed') {
251+
return JSON.stringify(this.getSourceMap(options));
222252
}
223253
224-
const mappings = this._getMappings();
225-
const modules = this.getModules();
226-
const map = {
227-
file: this._getSourceMapFile(),
228-
sources: modules.map(module => module.sourcePath),
229-
version: 3,
230-
names: [],
231-
mappings: mappings,
232-
sourcesContent: options.excludeSource
233-
? [] : modules.map(module => module.sourceCode),
234-
};
254+
// The following code is an optimization specific to the development server:
255+
// 1. generator.toSource() is faster than JSON.stringify(generator.toMap()).
256+
// 2. caching the source map unless there are changes saves time in
257+
// development settings.
258+
let map = this._sourceMap;
259+
if (map == null) {
260+
debug('Start building flat source map');
261+
map = this._sourceMap = fromRawMappings(this.getModules()).toString();
262+
debug('End building flat source map');
263+
} else {
264+
debug('Returning cached source map');
265+
}
235266
return map;
236267
}
237268
@@ -248,53 +279,6 @@ class Bundle extends BundleBase {
248279
: 'bundle.js';
249280
}
250281
251-
_getMappings() {
252-
const modules = super.getModules();
253-
254-
// The first line mapping in our package is basically the base64vlq code for
255-
// zeros (A).
256-
const firstLine = 'AAAA';
257-
258-
// Most other lines in our mappings are all zeros (for module, column etc)
259-
// except for the lineno mappinp: curLineno - prevLineno = 1; Which is C.
260-
const line = 'AACA';
261-
262-
const moduleLines = Object.create(null);
263-
let mappings = '';
264-
for (let i = 0; i < modules.length; i++) {
265-
const module = modules[i];
266-
const code = module.code;
267-
let lastCharNewLine = false;
268-
moduleLines[module.sourcePath] = 0;
269-
for (let t = 0; t < code.length; t++) {
270-
if (t === 0 && i === 0) {
271-
mappings += firstLine;
272-
} else if (t === 0) {
273-
mappings += 'AC';
274-
275-
// This is the only place were we actually don't know the mapping ahead
276-
// of time. When it's a new module (and not the first) the lineno
277-
// mapping is 0 (current) - number of lines in prev module.
278-
mappings += base64VLQ.encode(
279-
0 - moduleLines[modules[i - 1].sourcePath]
280-
);
281-
mappings += 'A';
282-
} else if (lastCharNewLine) {
283-
moduleLines[module.sourcePath]++;
284-
mappings += line;
285-
}
286-
lastCharNewLine = code[t] === '\n';
287-
if (lastCharNewLine) {
288-
mappings += ';';
289-
}
290-
}
291-
if (i !== modules.length - 1) {
292-
mappings += ';';
293-
}
294-
}
295-
return mappings;
296-
}
297-
298282
getJSModulePaths() {
299283
return this.getModules()
300284
// Filter out non-js files. Like images etc.
@@ -305,7 +289,7 @@ class Bundle extends BundleBase {
305289
getDebugInfo() {
306290
return [
307291
/* $FlowFixMe: this is unsound as the module ID could be unset. */
308-
'<div><h3>Main Module:</h3> ' + super.getMainModuleId() + '</div>',
292+
'<div><h3>Main Module:</h3> ' + this.getMainModuleId() + '</div>',
309293
'<style>',
310294
'pre.collapsed {',
311295
' height: 10px;',
@@ -328,30 +312,6 @@ class Bundle extends BundleBase {
328312
setRamGroups(ramGroups: Array<string>) {
329313
this._ramGroups = ramGroups;
330314
}
331-
332-
toJSON() {
333-
this.assertFinalized('Cannot serialize bundle unless finalized');
334-
335-
return {
336-
...super.toJSON(),
337-
sourceMapUrl: this._sourceMapUrl,
338-
numRequireCalls: this._numRequireCalls,
339-
shouldCombineSourceMaps: this._shouldCombineSourceMaps,
340-
};
341-
}
342-
343-
static fromJSON(json) {
344-
const bundle = new Bundle({sourceMapUrl: json.sourceMapUrl});
345-
346-
bundle._sourceMapUrl = json.sourceMapUrl;
347-
bundle._numRequireCalls = json.numRequireCalls;
348-
bundle._shouldCombineSourceMaps = json.shouldCombineSourceMaps;
349-
350-
BundleBase.fromJSON(bundle, json);
351-
352-
/* $FlowFixMe: this modifies BundleBase#fromJSON() signature. */
353-
return bundle;
354-
}
355315
}
356316
357317
function generateSourceMapForVirtualModule(module): SourceMap {
@@ -472,4 +432,6 @@ function createGroups(ramGroups: Array<string>, lazyModules) {
472432
return result;
473433
}
474434

435+
const isRawMappings = Array.isArray;
436+
475437
module.exports = Bundle;

0 commit comments

Comments
 (0)