Skip to content

Commit 3f504ec

Browse files
davidaurelioFacebook Github Bot 7
authored andcommitted
Make reloads faster for simple file changes
Summary: This is a very hacky solution to make reloads from packager faster for simple file changes. Simple means that no dependencies have changed, otherwise packager will abort the attempt to update and fall back to the usual rebuilding method. In principle, this change avoids re-walking and analyzing the whole dependency tree, and just updates modules in existing bundles. Reviewed By: bestander Differential Revision: D3703896 fbshipit-source-id: 671206618dc093965822aed7161e3a99db69a529
1 parent 8edb952 commit 3f504ec

File tree

6 files changed

+192
-27
lines changed

6 files changed

+192
-27
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class Bundle extends BundleBase {
5959
this._addRequireCall(super.getMainModuleId());
6060
}
6161

62-
super.finalize();
62+
super.finalize(options);
6363
}
6464

6565
_addRequireCall(moduleId) {

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ class BundleBase {
5959
}
6060

6161
finalize(options) {
62-
Object.freeze(this._modules);
63-
Object.freeze(this._assets);
62+
if (!options.allowUpdates) {
63+
Object.freeze(this._modules);
64+
Object.freeze(this._assets);
65+
}
6466

6567
this._finalized = true;
6668
}
@@ -76,6 +78,10 @@ class BundleBase {
7678
return this._source;
7779
}
7880

81+
invalidateSource() {
82+
this._source = null;
83+
}
84+
7985
assertFinalized(message) {
8086
if (!this._finalized) {
8187
throw new Error(message || 'Bundle needs to be finalized before getting any source');

packager/react-packager/src/Bundler/__tests__/Bundler-test.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ describe('Bundler', function() {
129129
dependencies: modules,
130130
transformOptions,
131131
getModuleId: () => 123,
132+
getResolvedDependencyPairs: () => [],
132133
})
133134
);
134135

@@ -141,7 +142,7 @@ describe('Bundler', function() {
141142
});
142143
});
143144

144-
pit('create a bundle', function() {
145+
it('create a bundle', function() {
145146
assetServer.getAssetData.mockImpl(() => {
146147
return {
147148
scales: [1,2,3],
@@ -170,9 +171,11 @@ describe('Bundler', function() {
170171
expect(ithAddedModule(3)).toEqual('/root/img/new_image.png');
171172
expect(ithAddedModule(4)).toEqual('/root/file.json');
172173

173-
expect(bundle.finalize.mock.calls[0]).toEqual([
174-
{runMainModule: true, runBeforeMainModule: []}
175-
]);
174+
expect(bundle.finalize.mock.calls[0]).toEqual([{
175+
runMainModule: true,
176+
runBeforeMainModule: [],
177+
allowUpdates: false,
178+
}]);
176179

177180
expect(bundle.addAsset.mock.calls[0]).toEqual([{
178181
__packager_asset: true,

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ const validateOpts = declareOpts({
8989
type: 'boolean',
9090
default: false,
9191
},
92+
allowBundleUpdates: {
93+
type: 'boolean',
94+
default: false,
95+
},
9296
});
9397

9498
const assetPropertyBlacklist = new Set([
@@ -280,6 +284,7 @@ class Bundler {
280284
bundle.finalize({
281285
runMainModule,
282286
runBeforeMainModule: runBeforeMainModuleIds,
287+
allowUpdates: this._opts.allowBundleUpdates,
283288
});
284289
return bundle;
285290
});
@@ -402,6 +407,7 @@ class Bundler {
402407
entryFilePath,
403408
transformOptions: response.transformOptions,
404409
getModuleId: response.getModuleId,
410+
dependencyPairs: response.getResolvedDependencyPairs(module),
405411
}).then(transformed => {
406412
modulesByName[transformed.name] = module;
407413
onModuleTransformed({
@@ -533,7 +539,14 @@ class Bundler {
533539
);
534540
}
535541

536-
_toModuleTransport({module, bundle, entryFilePath, transformOptions, getModuleId}) {
542+
_toModuleTransport({
543+
module,
544+
bundle,
545+
entryFilePath,
546+
transformOptions,
547+
getModuleId,
548+
dependencyPairs,
549+
}) {
537550
let moduleTransport;
538551
const moduleId = getModuleId(module);
539552

@@ -566,7 +579,7 @@ class Bundler {
566579
id: moduleId,
567580
code,
568581
map,
569-
meta: {dependencies, dependencyOffsets, preloaded},
582+
meta: {dependencies, dependencyOffsets, preloaded, dependencyPairs},
570583
sourceCode: source,
571584
sourcePath: module.path
572585
});

packager/react-packager/src/Server/index.js

Lines changed: 155 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@ const mime = require('mime-types');
2222
const path = require('path');
2323
const url = require('url');
2424

25-
function debounce(fn, delay) {
26-
var timeout;
27-
return () => {
25+
const debug = require('debug')('ReactNativePackager:Server');
26+
27+
function debounceAndBatch(fn, delay) {
28+
let timeout, args = [];
29+
return (value) => {
30+
args.push(value);
2831
clearTimeout(timeout);
29-
timeout = setTimeout(fn, delay);
32+
timeout = setTimeout(() => {
33+
const a = args;
34+
args = [];
35+
fn(a);
36+
}, delay);
3037
};
3138
}
3239

@@ -139,7 +146,10 @@ const bundleOpts = declareOpts({
139146
isolateModuleIDs: {
140147
type: 'boolean',
141148
default: false
142-
}
149+
},
150+
resolutionResponse: {
151+
type: 'object',
152+
},
143153
});
144154

145155
const dependencyOpts = declareOpts({
@@ -163,8 +173,14 @@ const dependencyOpts = declareOpts({
163173
type: 'boolean',
164174
default: false,
165175
},
176+
minify: {
177+
type: 'boolean',
178+
default: undefined,
179+
},
166180
});
167181

182+
const bundleDeps = new WeakMap();
183+
168184
class Server {
169185
constructor(options) {
170186
const opts = validateOpts(options);
@@ -209,12 +225,27 @@ class Server {
209225
const bundlerOpts = Object.create(opts);
210226
bundlerOpts.fileWatcher = this._fileWatcher;
211227
bundlerOpts.assetServer = this._assetServer;
228+
bundlerOpts.allowBundleUpdates = !options.nonPersistent;
212229
this._bundler = new Bundler(bundlerOpts);
213230

214231
this._fileWatcher.on('all', this._onFileChange.bind(this));
215232

216-
this._debouncedFileChangeHandler = debounce(filePath => {
217-
this._clearBundles();
233+
this._debouncedFileChangeHandler = debounceAndBatch(filePaths => {
234+
// only clear bundles for non-JS changes
235+
if (filePaths.every(RegExp.prototype.test, /\.js(?:on)?$/i)) {
236+
for (const key in this._bundles) {
237+
this._bundles[key].then(bundle => {
238+
const deps = bundleDeps.get(bundle);
239+
filePaths.forEach(filePath => {
240+
if (deps.files.has(filePath)) {
241+
deps.outdated.add(filePath);
242+
}
243+
});
244+
});
245+
}
246+
} else {
247+
this._clearBundles();
248+
}
218249
this._informChangeWatchers();
219250
}, 50);
220251
}
@@ -243,7 +274,25 @@ class Server {
243274
}
244275

245276
const opts = bundleOpts(options);
246-
return this._bundler.bundle(opts);
277+
const building = this._bundler.bundle(opts);
278+
building.then(bundle => {
279+
const modules = bundle.getModules().filter(m => !m.virtual);
280+
bundleDeps.set(bundle, {
281+
files: new Map(
282+
modules
283+
.map(({sourcePath, meta = {dependencies: []}}) =>
284+
[sourcePath, meta.dependencies])
285+
),
286+
idToIndex: new Map(modules.map(({id}, i) => [id, i])),
287+
dependencyPairs: new Map(
288+
modules
289+
.filter(({meta}) => meta && meta.dependencyPairs)
290+
.map(m => [m.sourcePath, m.meta.dependencyPairs])
291+
),
292+
outdated: new Set(),
293+
});
294+
});
295+
return building;
247296
});
248297
}
249298

@@ -428,6 +477,91 @@ class Server {
428477
).done(() => Activity.endEvent(assetEvent));
429478
}
430479

480+
_useCachedOrUpdateOrCreateBundle(options) {
481+
const optionsJson = JSON.stringify(options);
482+
const bundleFromScratch = () => {
483+
const building = this.buildBundle(options);
484+
this._bundles[optionsJson] = building;
485+
return building;
486+
};
487+
488+
if (optionsJson in this._bundles) {
489+
return this._bundles[optionsJson].then(bundle => {
490+
const deps = bundleDeps.get(bundle);
491+
const {dependencyPairs, files, idToIndex, outdated} = deps;
492+
if (outdated.size) {
493+
debug('Attempt to update existing bundle');
494+
const changedModules =
495+
Array.from(outdated, this.getModuleForPath, this);
496+
deps.outdated = new Set();
497+
498+
const opts = bundleOpts(options);
499+
const {platform, dev, minify, hot} = opts;
500+
501+
// Need to create a resolution response to pass to the bundler
502+
// to process requires after transform. By providing a
503+
// specific response we can compute a non recursive one which
504+
// is the least we need and improve performance.
505+
const bundlePromise = this._bundles[optionsJson] =
506+
this.getDependencies({
507+
platform, dev, hot, minify,
508+
entryFile: options.entryFile,
509+
recursive: false,
510+
}).then(response => {
511+
debug('Update bundle: rebuild shallow bundle');
512+
513+
changedModules.forEach(m => {
514+
response.setResolvedDependencyPairs(
515+
m,
516+
dependencyPairs.get(m.path),
517+
{ignoreFinalized: true},
518+
);
519+
});
520+
521+
return this.buildBundle({
522+
...options,
523+
resolutionResponse: response.copy({
524+
dependencies: changedModules,
525+
})
526+
}).then(updateBundle => {
527+
const oldModules = bundle.getModules();
528+
const newModules = updateBundle.getModules();
529+
for (let i = 0, n = newModules.length; i < n; i++) {
530+
const moduleTransport = newModules[i];
531+
const {meta, sourcePath} = moduleTransport;
532+
if (outdated.has(sourcePath)) {
533+
if (!contentsEqual(meta.dependencies, new Set(files.get(sourcePath)))) {
534+
// bail out if any dependencies changed
535+
return Promise.reject(Error(
536+
`Dependencies of ${sourcePath} changed from [${
537+
files.get(sourcePath).join(', ')
538+
}] to [${meta.dependencies.join(', ')}]`
539+
));
540+
}
541+
542+
oldModules[idToIndex.get(moduleTransport.id)] = moduleTransport;
543+
}
544+
}
545+
546+
bundle.invalidateSource();
547+
debug('Successfully updated existing bundle');
548+
return bundle;
549+
});
550+
}).catch(e => {
551+
debug('Failed to update existing bundle, rebuilding...', e.stack || e.message);
552+
return bundleFromScratch();
553+
});
554+
return bundlePromise;
555+
} else {
556+
debug('Using cached bundle');
557+
return bundle;
558+
}
559+
});
560+
}
561+
562+
return bundleFromScratch();
563+
}
564+
431565
processRequest(req, res, next) {
432566
const urlObj = url.parse(req.url, true);
433567
const pathname = urlObj.pathname;
@@ -458,26 +592,29 @@ class Server {
458592

459593
const startReqEventId = Activity.startEvent('request:' + req.url);
460594
const options = this._getOptionsFromUrl(req.url);
461-
const optionsJson = JSON.stringify(options);
462-
const building = this._bundles[optionsJson] || this.buildBundle(options);
463-
464-
this._bundles[optionsJson] = building;
595+
debug('Getting bundle for request');
596+
const building = this._useCachedOrUpdateOrCreateBundle(options);
465597
building.then(
466598
p => {
467599
if (requestType === 'bundle') {
600+
debug('Generating source code');
468601
const bundleSource = p.getSource({
469602
inlineSourceMap: options.inlineSourceMap,
470603
minify: options.minify,
471604
dev: options.dev,
472605
});
606+
debug('Writing response headers');
473607
res.setHeader('Content-Type', 'application/javascript');
474608
res.setHeader('ETag', p.getEtag());
475609
if (req.headers['if-none-match'] === res.getHeader('ETag')){
610+
debug('Responding with 304');
476611
res.statusCode = 304;
477612
res.end();
478613
} else {
614+
debug('Writing request body');
479615
res.end(bundleSource);
480616
}
617+
debug('Finished response');
481618
Activity.endEvent(startReqEventId);
482619
} else if (requestType === 'map') {
483620
let sourceMap = p.getSourceMap({
@@ -499,7 +636,7 @@ class Server {
499636
Activity.endEvent(startReqEventId);
500637
}
501638
},
502-
this._handleError.bind(this, res, optionsJson)
639+
error => this._handleError(res, JSON.stringify(options), error)
503640
).done();
504641
}
505642

@@ -564,9 +701,7 @@ class Server {
564701

565702
_sourceMapForURL(reqUrl) {
566703
const options = this._getOptionsFromUrl(reqUrl);
567-
const optionsJson = JSON.stringify(options);
568-
const building = this._bundles[optionsJson] || this.buildBundle(options);
569-
this._bundles[optionsJson] = building;
704+
const building = this._useCachedOrUpdateOrCreateBundle(options);
570705
return building.then(p => {
571706
const sourceMap = p.getSourceMap({
572707
minify: options.minify,
@@ -659,4 +794,8 @@ class Server {
659794
}
660795
}
661796

797+
function contentsEqual(array, set) {
798+
return array.length === set.size && array.every(set.has, set);
799+
}
800+
662801
module.exports = Server;

0 commit comments

Comments
 (0)