Skip to content

Commit

Permalink
Make Metro symbolication non-blocking
Browse files Browse the repository at this point in the history
Reviewed By: cpojer

Differential Revision: D16029386

fbshipit-source-id: 31f9cc1bb1e9c105711e806de62647249d9d1ed5
  • Loading branch information
gaearon authored and facebook-github-bot committed Jun 27, 2019
1 parent 6a99c5a commit 99cd176
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 34 deletions.
103 changes: 87 additions & 16 deletions packages/metro-source-map/src/source-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,44 +58,114 @@ export type MetroSourceMap = IndexMap | BabelSourceMap;
export type FBBasicSourceMap = BabelSourceMap & FBExtensions;
export type FBSourceMap = FBIndexMap | (BabelSourceMap & FBExtensions);

/**
* Creates a source map from modules with "raw mappings", i.e. an array of
* tuples with either 2, 4, or 5 elements:
* generated line, generated column, source line, source line, symbol name.
* Accepts an `offsetLines` argument in case modules' code is to be offset in
* the resulting bundle, e.g. by some prefix code.
*/
function fromRawMappings(
function fromRawMappingsImpl(
isBlocking: boolean,
onDone: Generator => void,
modules: $ReadOnlyArray<{
+map: ?Array<MetroSourceMapSegmentTuple>,
+functionMap: ?FBSourceFunctionMap,
+path: string,
+source: string,
+code: string,
}>,
offsetLines: number = 0,
): Generator {
offsetLines: number,
): void {
const modulesToProcess = modules.slice();
const generator = new Generator();
let carryOver = offsetLines;

for (var j = 0, o = modules.length; j < o; ++j) {
var module = modules[j];
var {code, map} = module;
function processNextModule() {
if (modulesToProcess.length === 0) {
return true;
}

const mod = modulesToProcess.shift();
const {code, map} = mod;
if (Array.isArray(map)) {
addMappingsForFile(generator, map, module, carryOver);
addMappingsForFile(generator, map, mod, carryOver);
} else if (map != null) {
throw new Error(
`Unexpected module with full source map found: ${module.path}`,
`Unexpected module with full source map found: ${mod.path}`,
);
}

carryOver = carryOver + countLines(code);
return false;
}

function workLoop() {
const time = process.hrtime();
while (true) {
const isDone = processNextModule();
if (isDone) {
onDone(generator);
break;
}
if (!isBlocking) {
// Keep the loop running but try to avoid blocking
// for too long because this is not in a worker yet.
const diff = process.hrtime(time);
const NS_IN_MS = 1000000;
if (diff[1] > 50 * NS_IN_MS) {
// We've blocked for more than 50ms.
// This code currently runs on the main thread,
// so let's give Metro an opportunity to handle requests.
setImmediate(workLoop);
break;
}
}
}
}

workLoop();
}

/**
* Creates a source map from modules with "raw mappings", i.e. an array of
* tuples with either 2, 4, or 5 elements:
* generated line, generated column, source line, source line, symbol name.
* Accepts an `offsetLines` argument in case modules' code is to be offset in
* the resulting bundle, e.g. by some prefix code.
*/
function fromRawMappings(
modules: $ReadOnlyArray<{
+map: ?Array<MetroSourceMapSegmentTuple>,
+functionMap: ?FBSourceFunctionMap,
+path: string,
+source: string,
+code: string,
}>,
offsetLines: number = 0,
): Generator {
let generator: void | Generator;
fromRawMappingsImpl(
true,
g => {
generator = g;
},
modules,
offsetLines,
);
if (generator == null) {
throw new Error('Expected fromRawMappingsImpl() to finish synchronously.');
}
return generator;
}

async function fromRawMappingsNonBlocking(
modules: $ReadOnlyArray<{
+map: ?Array<MetroSourceMapSegmentTuple>,
+functionMap: ?FBSourceFunctionMap,
+path: string,
+source: string,
+code: string,
}>,
offsetLines: number = 0,
): Promise<Generator> {
return new Promise(resolve => {
fromRawMappingsImpl(false, resolve, modules, offsetLines);
});
}

/**
* Transforms a standard source map object into a Raw Mappings object, to be
* used across the bundler.
Expand Down Expand Up @@ -185,6 +255,7 @@ module.exports = {
createIndexMap,
generateFunctionMap,
fromRawMappings,
fromRawMappingsNonBlocking,
toBabelSegments,
toSegmentTuple,
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@

'use strict';

const fullSourceMapObject = require('./sourceMapObject');
const getAppendScripts = require('../../lib/getAppendScripts');
const getTransitiveDependencies = require('./helpers/getTransitiveDependencies');
const nullthrows = require('nullthrows');
const path = require('path');

const {createRamBundleGroups} = require('../../Bundler/util');
const {isJsModule, wrapModule} = require('./helpers/js');
const {sourceMapObject} = require('./sourceMapObject');

import type {
ModuleTransportLike,
Expand Down Expand Up @@ -63,7 +63,7 @@ async function getRamBundleInfo(
(module: Module<>): RamModuleTransport => ({
id: options.createModuleId(module.path),
code: wrapModule(module, options),
map: fullSourceMapObject([module], {
map: sourceMapObject([module], {
excludeSource: options.excludeSource,
processModuleFilter: options.processModuleFilter,
}),
Expand Down
94 changes: 87 additions & 7 deletions packages/metro/src/DeltaBundler/Serializers/sourceMapGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,106 @@
const getSourceMapInfo = require('./helpers/getSourceMapInfo');

const {isJsModule} = require('./helpers/js');
const {fromRawMappings} = require('metro-source-map');
const {
fromRawMappings,
fromRawMappingsNonBlocking,
} = require('metro-source-map');

import type {Module} from '../types.flow';

type ReturnType<F> = $Call<<A, R>((...A) => R) => R, F>;

function getSourceMapInfosImpl(
isBlocking: boolean,
onDone: ($ReadOnlyArray<ReturnType<typeof getSourceMapInfo>>) => void,
modules: $ReadOnlyArray<Module<>>,
options: {|
+excludeSource: boolean,
+processModuleFilter: (module: Module<>) => boolean,
|},
): void {
const sourceMapInfos = [];
const modulesToProcess = modules
.filter(isJsModule)
.filter(options.processModuleFilter);

function processNextModule() {
if (modulesToProcess.length === 0) {
return true;
}

const mod = modulesToProcess.shift();
const info = getSourceMapInfo(mod, {
excludeSource: options.excludeSource,
});
sourceMapInfos.push(info);
return false;
}

function workLoop() {
const time = process.hrtime();
while (true) {
const isDone = processNextModule();
if (isDone) {
onDone(sourceMapInfos);
break;
}
if (!isBlocking) {
// Keep the loop running but try to avoid blocking
// for too long because this is not in a worker yet.
const diff = process.hrtime(time);
const NS_IN_MS = 1000000;
if (diff[1] > 50 * NS_IN_MS) {
// We've blocked for more than 50ms.
// This code currently runs on the main thread,
// so let's give Metro an opportunity to handle requests.
setImmediate(workLoop);
break;
}
}
}
}
workLoop();
}

function sourceMapGenerator(
modules: $ReadOnlyArray<Module<>>,
options: {|
+excludeSource: boolean,
+processModuleFilter: (module: Module<>) => boolean,
|},
): ReturnType<typeof fromRawMappings> {
const sourceMapInfos = modules
.filter(isJsModule)
.filter(options.processModuleFilter)
.map((module: Module<>) =>
getSourceMapInfo(module, {excludeSource: options.excludeSource}),
let sourceMapInfos;
getSourceMapInfosImpl(
true,
infos => {
sourceMapInfos = infos;
},
modules,
options,
);
if (sourceMapInfos == null) {
throw new Error(
'Expected getSourceMapInfosImpl() to finish synchronously.',
);
}
return fromRawMappings(sourceMapInfos);
}

module.exports = sourceMapGenerator;
async function sourceMapGeneratorNonBlocking(
modules: $ReadOnlyArray<Module<>>,
options: {|
+excludeSource: boolean,
+processModuleFilter: (module: Module<>) => boolean,
|},
): ReturnType<typeof fromRawMappingsNonBlocking> {
const sourceMapInfos = await new Promise(resolve => {
getSourceMapInfosImpl(false, resolve, modules, options);
});
return fromRawMappingsNonBlocking(sourceMapInfos);
}

module.exports = {
sourceMapGenerator,
sourceMapGeneratorNonBlocking,
};
26 changes: 23 additions & 3 deletions packages/metro/src/DeltaBundler/Serializers/sourceMapObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

'use strict';

const sourceMapGenerator = require('./sourceMapGenerator');
const {
sourceMapGenerator,
sourceMapGeneratorNonBlocking,
} = require('./sourceMapGenerator');

import type {Module} from '../types.flow';
import type {BabelSourceMap} from '@babel/core';
Expand All @@ -22,9 +25,26 @@ function sourceMapObject(
+processModuleFilter: (module: Module<>) => boolean,
|},
): BabelSourceMap {
return sourceMapGenerator(modules, options).toMap(undefined, {
const generator = sourceMapGenerator(modules, options);
return generator.toMap(undefined, {
excludeSource: options.excludeSource,
});
}

module.exports = sourceMapObject;
async function sourceMapObjectNonBlocking(
modules: $ReadOnlyArray<Module<>>,
options: {|
+excludeSource: boolean,
+processModuleFilter: (module: Module<>) => boolean,
|},
): Promise<BabelSourceMap> {
const generator = await sourceMapGeneratorNonBlocking(modules, options);
return generator.toMap(undefined, {
excludeSource: options.excludeSource,
});
}

module.exports = {
sourceMapObject,
sourceMapObjectNonBlocking,
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

'use strict';

const sourceMapGenerator = require('./sourceMapGenerator');
const {sourceMapGenerator} = require('./sourceMapGenerator');

import type {Module} from '../types.flow';

Expand Down
17 changes: 12 additions & 5 deletions packages/metro/src/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const getAllFiles = require('./DeltaBundler/Serializers/getAllFiles');
const getAssets = require('./DeltaBundler/Serializers/getAssets');
const getGraphId = require('./lib/getGraphId');
const getRamBundleInfo = require('./DeltaBundler/Serializers/getRamBundleInfo');
const sourceMapObject = require('./DeltaBundler/Serializers/sourceMapObject');
const sourceMapString = require('./DeltaBundler/Serializers/sourceMapString');
const splitBundleOptions = require('./lib/splitBundleOptions');
const debug = require('debug')('Metro:Server');
Expand All @@ -36,6 +35,9 @@ const ResourceNotFoundError = require('./IncrementalBundler/ResourceNotFoundErro
const RevisionNotFoundError = require('./IncrementalBundler/RevisionNotFoundError');

const {getAsset} = require('./Assets');
const {
sourceMapObjectNonBlocking,
} = require('./DeltaBundler/Serializers/sourceMapObject');

import type {IncomingMessage, ServerResponse} from 'http';
import type {Reporter} from './lib/reporting';
Expand Down Expand Up @@ -1035,10 +1037,15 @@ class Server {

const {prepend, graph} = revision;

return sourceMapObject([...prepend, ...this._getSortedModules(graph)], {
excludeSource: serializerOptions.excludeSource,
processModuleFilter: this._config.serializer.processModuleFilter,
});
// This is a non-blocking version to avoid stalling the server.
// TODO(T46510351): move all of this to a worker.
return sourceMapObjectNonBlocking(
[...prepend, ...this._getSortedModules(graph)],
{
excludeSource: serializerOptions.excludeSource,
processModuleFilter: this._config.serializer.processModuleFilter,
},
);
}

getNewBuildID(): string {
Expand Down

0 comments on commit 99cd176

Please sign in to comment.