Skip to content

Commit

Permalink
Symbolicate JS stacktrace using RN Packager
Browse files Browse the repository at this point in the history
Summary:
The way we currently symbolicate JS stack traces in RN during development time
(e.g. inside the RedBox) is the following: we download the source map from RN,
parse it and use `source-map` find original file/line numbers. All happens
inside running JSC VM in a simulator.

The problem with this approach is that the source map size is pretty big and it
is very expensive to load/parse.

Before we load sourcemaps:
{F60869250}

After we load sourcemaps:
{F60869249}

In the past it wasn't a big problem, however the sourcemap file is only getting
larger and soon we will be loading it for yellow boxes too: facebook#7459

Moving stack trace symbolication to server side will let us:
- save a bunch of memory on device
- improve performance (no need to JSON serialize/deserialize and transfer sourcemap via HTTP and bridge)
- remove ugly workaround with `RCTExceptionsManager.updateExceptionMessage`
- we will be able to symbolicate from native by simply sending HTTP request, which means symbolication
  can be more robust (no need to depend on crashed JS to do symbolication) and we can pause JSC to
  avoid getting too many redboxes that hide original error.
- reduce the bundle by ~65KB (the size of source-map parsing library we ship, see SourceMap module)

Reviewed By: davidaurelio

Differential Revision: D3291793

fbshipit-source-id: 29dce5f40100259264f57254e6715ace8ea70174
  • Loading branch information
frantic authored and Facebook Github Bot 8 committed May 20, 2016
1 parent e475386 commit 62e74f3
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 0 deletions.
57 changes: 57 additions & 0 deletions packager/react-packager/src/Server/__tests__/Server-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ jest.setMock('worker-farm', function() { return () => {}; })
.setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) })
.setMock('uglify-js')
.setMock('crypto')
.setMock('source-map', { SourceMapConsumer: (fn) => {}})
.mock('../../Bundler')
.mock('../../AssetServer')
.mock('../../lib/declareOpts')
.mock('node-haste')
.mock('../../Activity');

const Promise = require('promise');
const SourceMapConsumer = require('source-map').SourceMapConsumer;

const Bundler = require('../../Bundler');
const Server = require('../');
Expand Down Expand Up @@ -392,4 +394,59 @@ describe('processRequest', () => {
);
});
});

describe('/symbolicate endpoint', () => {
pit('should symbolicate given stack trace', () => {
const body = JSON.stringify({stack: [{
file: 'foo.bundle?platform=ios',
lineNumber: 2100,
column: 44,
customPropShouldBeLeftUnchanged: 'foo',
}]});

SourceMapConsumer.prototype.originalPositionFor = jest.fn((frame) => {
expect(frame.line).toEqual(2100);
expect(frame.column).toEqual(44);
return {
source: 'foo.js',
line: 21,
column: 4,
};
});

return makeRequest(
requestHandler,
'/symbolicate',
{ rawBody: body }
).then(response => {
expect(JSON.parse(response.body)).toEqual({
stack: [{
file: 'foo.js',
lineNumber: 21,
column: 4,
customPropShouldBeLeftUnchanged: 'foo',
}]
});
});
});
});

describe('/symbolicate handles errors', () => {
pit('should symbolicate given stack trace', () => {
const body = 'clearly-not-json';
console.error = jest.fn();

return makeRequest(
requestHandler,
'/symbolicate',
{ rawBody: body }
).then(response => {
expect(response.statusCode).toEqual(500);
expect(JSON.parse(response.body)).toEqual({
error: jasmine.any(String),
});
expect(console.error).toBeCalled();
});
});
});
});
62 changes: 62 additions & 0 deletions packager/react-packager/src/Server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const FileWatcher = require('node-haste').FileWatcher;
const getPlatformExtension = require('node-haste').getPlatformExtension;
const Bundler = require('../Bundler');
const Promise = require('promise');
const SourceMapConsumer = require('source-map').SourceMapConsumer;

const _ = require('lodash');
const declareOpts = require('../lib/declareOpts');
Expand Down Expand Up @@ -428,6 +429,9 @@ class Server {
} else if (pathname.match(/^\/assets\//)) {
this._processAssetsRequest(req, res);
return;
} else if (pathname === '/symbolicate') {
this._symbolicate(req, res);
return;
} else {
next();
return;
Expand Down Expand Up @@ -480,6 +484,64 @@ class Server {
).done();
}

_symbolicate(req, res) {
const startReqEventId = Activity.startEvent('symbolicate');
new Promise.resolve(req.rawBody).then(body => {
const stack = JSON.parse(body).stack;

// In case of multiple bundles / HMR, some stack frames can have
// different URLs from others
const urls = stack.map(frame => frame.file);
const uniqueUrls = urls.filter((elem, idx) => urls.indexOf(elem) === idx);

const sourceMaps = uniqueUrls.map(sourceUrl => this._sourceMapForURL(sourceUrl));
return Promise.all(sourceMaps).then(consumers => {
return stack.map(frame => {
const idx = uniqueUrls.indexOf(frame.file);
const consumer = consumers[idx];

const original = consumer.originalPositionFor({
line: frame.lineNumber,
column: frame.column,
});

if (!original) {
return frame;
}

return Object.assign({}, frame, {
file: original.source,
lineNumber: original.line,
column: original.column,
});
});
});
}).then(
stack => res.end(JSON.stringify({stack: stack})),
error => {
console.error(error.stack || error);
res.statusCode = 500;
res.end(JSON.stringify({error: error.message}));
}
).done(() => {
Activity.endEvent(startReqEventId);
});
}

_sourceMapForURL(reqUrl) {
const options = this._getOptionsFromUrl(reqUrl);
const optionsJson = JSON.stringify(options);
const building = this._bundles[optionsJson] || this.buildBundle(options);
this._bundles[optionsJson] = building;
return building.then(p => {
const sourceMap = p.getSourceMap({
minify: options.minify,
dev: options.dev,
});
return new SourceMapConsumer(sourceMap);
});
}

_handleError(res, bundleID, error) {
res.writeHead(error.status || 500, {
'Content-Type': 'application/json; charset=UTF-8',
Expand Down

0 comments on commit 62e74f3

Please sign in to comment.