Skip to content

Commit b6ca952

Browse files
davidaureliofacebook-github-bot
authored andcommitted
Symbolicate stack traces off the main process
Summary: Moves stack trace symbolication to a worker process. The worker process is spawned laziliy, and is treated as an exclusive resource (requests are queued). This helps keeping the server process responsive when symbolicating. Reviewed By: cpojer Differential Revision: D4602722 fbshipit-source-id: 5da97e53afd9a1ab981c5ba4b02a7d1d869dee71
1 parent 17c175a commit b6ca952

File tree

11 files changed

+685
-91
lines changed

11 files changed

+685
-91
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
"bser": "^1.0.2",
154154
"chalk": "^1.1.1",
155155
"commander": "^2.9.0",
156+
"concat-stream": "^1.6.0",
156157
"connect": "^2.8.3",
157158
"core-js": "^2.2.2",
158159
"debug": "^2.2.0",
@@ -207,6 +208,7 @@
207208
"ws": "^1.1.0",
208209
"xcode": "^0.8.9",
209210
"xmldoc": "^0.4.0",
211+
"xpipe": "^1.0.5",
210212
"yargs": "^6.4.0"
211213
},
212214
"devDependencies": {

packager/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"babel-register": "^6.18.0",
1818
"babylon": "^6.14.1",
1919
"chalk": "^1.1.1",
20+
"concat-stream": "^1.6.0",
2021
"core-js": "^2.2.2",
2122
"debug": "^2.2.0",
2223
"denodeify": "^1.2.1",
@@ -38,6 +39,7 @@
3839
"throat": "^3.0.0",
3940
"uglify-js": "^2.6.2",
4041
"worker-farm": "^1.3.1",
41-
"write-file-atomic": "^1.2.0"
42+
"write-file-atomic": "^1.2.0",
43+
"xpipe": "^1.0.5"
4244
}
4345
}

packager/src/Server/__tests__/Server-test.js

+33-52
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010

1111
jest.disableAutomock();
1212

13-
jest.setMock('worker-farm', function() { return () => {}; })
14-
.setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) })
15-
.setMock('uglify-js')
16-
.setMock('crypto')
17-
.setMock('source-map', { SourceMapConsumer: function(fn) {}})
13+
jest.mock('worker-farm', () => () => () => {})
14+
.mock('timers', () => ({ setImmediate: (fn) => setTimeout(fn, 0) }))
15+
.mock('uglify-js')
16+
.mock('crypto')
17+
.mock(
18+
'../symbolicate',
19+
() => ({createWorker: jest.fn().mockReturnValue(jest.fn())}),
20+
)
1821
.mock('../../Bundler')
1922
.mock('../../AssetServer')
2023
.mock('../../lib/declareOpts')
@@ -23,14 +26,14 @@ jest.setMock('worker-farm', function() { return () => {}; })
2326
.mock('../../lib/GlobalTransformCache');
2427

2528
describe('processRequest', () => {
26-
let SourceMapConsumer, Bundler, Server, AssetServer, Promise;
29+
let Bundler, Server, AssetServer, Promise, symbolicate;
2730
beforeEach(() => {
2831
jest.resetModules();
29-
SourceMapConsumer = require('source-map').SourceMapConsumer;
3032
Bundler = require('../../Bundler');
3133
Server = require('../');
3234
AssetServer = require('../../AssetServer');
3335
Promise = require('promise');
36+
symbolicate = require('../symbolicate');
3437
});
3538

3639
let server;
@@ -69,7 +72,7 @@ describe('processRequest', () => {
6972
Promise.resolve({
7073
getModules: () => [],
7174
getSource: () => 'this is the source',
72-
getSourceMap: () => {},
75+
getSourceMap: () => ({version: 3}),
7376
getSourceMapString: () => 'this is the source map',
7477
getEtag: () => 'this is an etag',
7578
}));
@@ -464,60 +467,38 @@ describe('processRequest', () => {
464467
});
465468

466469
describe('/symbolicate endpoint', () => {
470+
let symbolicationWorker;
471+
beforeEach(() => {
472+
symbolicationWorker = symbolicate.createWorker();
473+
symbolicationWorker.mockReset();
474+
});
475+
467476
it('should symbolicate given stack trace', () => {
468-
const body = JSON.stringify({stack: [{
477+
const inputStack = [{
469478
file: 'http://foo.bundle?platform=ios',
470479
lineNumber: 2100,
471480
column: 44,
472481
customPropShouldBeLeftUnchanged: 'foo',
473-
}]});
474-
475-
SourceMapConsumer.prototype.originalPositionFor = jest.fn((frame) => {
476-
expect(frame.line).toEqual(2100);
477-
expect(frame.column).toEqual(44);
478-
return {
479-
source: 'foo.js',
480-
line: 21,
481-
column: 4,
482-
};
483-
});
484-
485-
return makeRequest(
486-
requestHandler,
487-
'/symbolicate',
488-
{ rawBody: body }
489-
).then(response => {
490-
expect(JSON.parse(response.body)).toEqual({
491-
stack: [{
492-
file: 'foo.js',
493-
lineNumber: 21,
494-
column: 4,
495-
customPropShouldBeLeftUnchanged: 'foo',
496-
}]
497-
});
482+
}];
483+
const outputStack = [{
484+
source: 'foo.js',
485+
line: 21,
486+
column: 4,
487+
}];
488+
const body = JSON.stringify({stack: inputStack});
489+
490+
expect.assertions(2);
491+
symbolicationWorker.mockImplementation(stack => {
492+
expect(stack).toEqual(inputStack);
493+
return outputStack;
498494
});
499-
});
500-
501-
it('ignores `/debuggerWorker.js` stack frames', () => {
502-
const body = JSON.stringify({stack: [{
503-
file: 'http://localhost:8081/debuggerWorker.js',
504-
lineNumber: 123,
505-
column: 456,
506-
}]});
507495

508496
return makeRequest(
509497
requestHandler,
510498
'/symbolicate',
511-
{ rawBody: body }
512-
).then(response => {
513-
expect(JSON.parse(response.body)).toEqual({
514-
stack: [{
515-
file: 'http://localhost:8081/debuggerWorker.js',
516-
lineNumber: 123,
517-
column: 456,
518-
}]
519-
});
520-
});
499+
{rawBody: body},
500+
).then(response =>
501+
expect(JSON.parse(response.body)).toEqual({stack: outputStack}));
521502
});
522503
});
523504

packager/src/Server/index.js

+33-38
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ const AssetServer = require('../AssetServer');
1515
const getPlatformExtension = require('../node-haste').getPlatformExtension;
1616
const Bundler = require('../Bundler');
1717
const MultipartResponse = require('./MultipartResponse');
18-
const SourceMapConsumer = require('source-map').SourceMapConsumer;
1918

2019
const declareOpts = require('../lib/declareOpts');
2120
const defaults = require('../../defaults');
2221
const mime = require('mime-types');
2322
const path = require('path');
23+
const symbolicate = require('./symbolicate');
2424
const terminal = require('../lib/terminal');
2525
const url = require('url');
2626

@@ -34,6 +34,7 @@ import type Bundle from '../Bundler/Bundle';
3434
import type {Reporter} from '../lib/reporting';
3535
import type {GetTransformOptions} from '../Bundler';
3636
import type GlobalTransformCache from '../lib/GlobalTransformCache';
37+
import type {SourceMap, Symbolicate} from './symbolicate';
3738

3839
const {
3940
createActionStartEntry,
@@ -201,6 +202,7 @@ class Server {
201202
_debouncedFileChangeHandler: (filePath: string) => mixed;
202203
_hmrFileChangeListener: (type: string, filePath: string) => mixed;
203204
_reporter: Reporter;
205+
_symbolicateInWorker: Symbolicate;
204206

205207
constructor(options: Options) {
206208
this._opts = {
@@ -281,6 +283,8 @@ class Server {
281283
}
282284
this._informChangeWatchers();
283285
}, 50);
286+
287+
this._symbolicateInWorker = symbolicate.createWorker();
284288
}
285289

286290
end(): mixed {
@@ -802,45 +806,27 @@ class Server {
802806

803807
// In case of multiple bundles / HMR, some stack frames can have
804808
// different URLs from others
805-
const urlIndexes = {};
806-
const uniqueUrls = [];
809+
const urls = new Set();
807810
stack.forEach(frame => {
808811
const sourceUrl = frame.file;
809812
// Skip `/debuggerWorker.js` which drives remote debugging because it
810813
// does not need to symbolication.
811814
// Skip anything except http(s), because there is no support for that yet
812-
if (!urlIndexes.hasOwnProperty(sourceUrl) &&
815+
if (!urls.has(sourceUrl) &&
813816
!sourceUrl.endsWith('/debuggerWorker.js') &&
814817
sourceUrl.startsWith('http')) {
815-
urlIndexes[sourceUrl] = uniqueUrls.length;
816-
uniqueUrls.push(sourceUrl);
818+
urls.add(sourceUrl);
817819
}
818820
});
819821

820-
const sourceMaps = uniqueUrls.map(
821-
sourceUrl => this._sourceMapForURL(sourceUrl)
822-
);
823-
return Promise.all(sourceMaps).then(consumers => {
824-
return stack.map(frame => {
825-
const sourceUrl = frame.file;
826-
if (!urlIndexes.hasOwnProperty(sourceUrl)) {
827-
return frame;
828-
}
829-
const idx = urlIndexes[sourceUrl];
830-
const consumer = consumers[idx];
831-
const original = consumer.originalPositionFor({
832-
line: frame.lineNumber,
833-
column: frame.column,
834-
});
835-
if (!original) {
836-
return frame;
837-
}
838-
return Object.assign({}, frame, {
839-
file: original.source,
840-
lineNumber: original.line,
841-
column: original.column,
842-
});
843-
});
822+
const mapPromises =
823+
Array.from(urls.values()).map(this._sourceMapForURL, this);
824+
825+
debug('Getting source maps for symbolication');
826+
return Promise.all(mapPromises).then(maps => {
827+
debug('Sending stacks and maps to symbolication worker');
828+
const urlsToMaps = zip(urls.values(), maps);
829+
return this._symbolicateInWorker(stack, urlsToMaps);
844830
});
845831
}).then(
846832
stack => {
@@ -858,16 +844,13 @@ class Server {
858844
);
859845
}
860846

861-
_sourceMapForURL(reqUrl: string): Promise<SourceMapConsumer> {
847+
_sourceMapForURL(reqUrl: string): Promise<SourceMap> {
862848
const options = this._getOptionsFromUrl(reqUrl);
863849
const building = this._useCachedOrUpdateOrCreateBundle(options);
864-
return building.then(p => {
865-
const sourceMap = p.getSourceMap({
866-
minify: options.minify,
867-
dev: options.dev,
868-
});
869-
return new SourceMapConsumer(sourceMap);
870-
});
850+
return building.then(p => p.getSourceMap({
851+
minify: options.minify,
852+
dev: options.dev,
853+
}));
871854
}
872855

873856
_handleError(res: ServerResponse, bundleID: string, error: {
@@ -990,4 +973,16 @@ function contentsEqual(array: Array<mixed>, set: Set<mixed>): boolean {
990973
return array.length === set.size && array.every(set.has, set);
991974
}
992975

976+
function* zip<X, Y>(xs: Iterable<X>, ys: Iterable<Y>): Iterable<[X, Y]> {
977+
//$FlowIssue #9324959
978+
const ysIter: Iterator<Y> = ys[Symbol.iterator]();
979+
for (const x of xs) {
980+
const y = ysIter.next();
981+
if (y.done) {
982+
return;
983+
}
984+
yield [x, y.value];
985+
}
986+
}
987+
993988
module.exports = Server;

0 commit comments

Comments
 (0)