Add a naive WPO implementation

Summary: public

RFC: The minifier haven't been stripping dead-code, and it also can't kill unused
modules, so as a temporary solution this inlines `__DEV__`, kill dead branches
and kill dead modules. For now I'm just white-listing the dev variable, but we
could definitely do better than that, but as a temporary fix this should be
helpful.

I also intend to kill some dead variables, so we can kill unused requires,
although inline-requires can also fix it.

Reviewed By: vjeux

Differential Revision: D2605454

fb-gh-sync-id: 50acb9dcbded07a43080b93ac826a5ceda695936
This commit is contained in:
Tadeu Zagallo 2015-11-17 03:36:50 -08:00 committed by facebook-github-bot-5
parent bd1885b5d4
commit 0b46a0c13b
9 changed files with 297 additions and 48 deletions

View File

@ -180,30 +180,6 @@ RCT_EXTERN NSArray<Class> *RCTGetModuleClasses(void);
RCTProfileEndAsyncEvent(0, @"init,download", cookie, @"JavaScript download", nil);
RCTPerformanceLoggerEnd(RCTPLScriptDownload);
// Only override the value of __DEV__ if running in debug mode, and if we
// haven't explicitly overridden the packager dev setting in the bundleURL
BOOL shouldOverrideDev = RCT_DEBUG && ([self.bundleURL isFileURL] ||
[self.bundleURL.absoluteString rangeOfString:@"dev="].location == NSNotFound);
// Force JS __DEV__ value to match RCT_DEBUG
if (shouldOverrideDev) {
NSString *sourceString = [[NSString alloc] initWithData:source encoding:NSUTF8StringEncoding];
NSRange range = [sourceString rangeOfString:@"\\b__DEV__\\s*?=\\s*?(!1|!0|false|true)"
options:NSRegularExpressionSearch];
RCTAssert(range.location != NSNotFound, @"It looks like the implementation"
"of __DEV__ has changed. Update -[RCTBatchedBridge loadSource:].");
NSString *valueString = [sourceString substringWithRange:range];
if ([valueString rangeOfString:@"!1"].length) {
valueString = [valueString stringByReplacingOccurrencesOfString:@"!1" withString:@"!0"];
} else if ([valueString rangeOfString:@"false"].length) {
valueString = [valueString stringByReplacingOccurrencesOfString:@"false" withString:@"true"];
}
source = [[sourceString stringByReplacingCharactersInRange:range withString:valueString]
dataUsingEncoding:NSUTF8StringEncoding];
}
_onSourceLoad(error, source);
};

View File

@ -44,7 +44,7 @@ function buildBundle(args, config) {
client.close();
return outputBundle;
})
.then(outputBundle => processBundle(outputBundle, !args.dev))
.then(outputBundle => processBundle(outputBundle, args.dev))
.then(outputBundle => saveBundleAndMap(
outputBundle,
args.platform,

View File

@ -10,15 +10,15 @@
const log = require('../util/log').out('bundle');
function processBundle(input, shouldMinify) {
function processBundle(input, dev) {
log('start');
let bundle;
if (shouldMinify) {
bundle = input.getMinifiedSourceAndMap();
if (!dev) {
bundle = input.getMinifiedSourceAndMap(dev);
} else {
bundle = {
code: input.getSource(),
map: JSON.stringify(input.getSourceMap()),
code: input.getSource({ dev }),
map: JSON.stringify(input.getSourceMap({ dev })),
};
}
bundle.assets = input.getAssets();

View File

@ -21,6 +21,7 @@ class Bundle {
this._finalized = false;
this._modules = [];
this._assets = [];
this._sourceMap = false;
this._sourceMapUrl = sourceMapUrl;
this._shouldCombineSourceMaps = false;
}
@ -83,16 +84,36 @@ class Bundle {
}
}
_getSource() {
if (this._source == null) {
this._source = _.pluck(this._modules, 'code').join('\n');
_getSource(dev) {
if (this._source) {
return this._source;
}
this._source = _.pluck(this._modules, 'code').join('\n');
if (dev) {
return this._source;
}
const wpoActivity = Activity.startEvent('Whole Program Optimisations');
const result = require('babel-core').transform(this._source, {
retainLines: true,
compact: true,
plugins: require('../transforms/whole-program-optimisations'),
inputSourceMap: this.getSourceMap(),
});
this._source = result.code;
this._sourceMap = result.map;
Activity.endEvent(wpoActivity);
return this._source;
}
_getInlineSourceMap() {
_getInlineSourceMap(dev) {
if (this._inlineSourceMap == null) {
const sourceMap = this.getSourceMap({excludeSource: true});
const sourceMap = this.getSourceMap({excludeSource: true, dev});
/*eslint-env node*/
const encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64');
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
@ -106,13 +127,13 @@ class Bundle {
options = options || {};
if (options.minify) {
return this.getMinifiedSourceAndMap().code;
return this.getMinifiedSourceAndMap(options.dev).code;
}
let source = this._getSource();
let source = this._getSource(options.dev);
if (options.inlineSourceMap) {
source += SOURCEMAPPING_URL + this._getInlineSourceMap();
source += SOURCEMAPPING_URL + this._getInlineSourceMap(options.dev);
} else if (this._sourceMapUrl) {
source += SOURCEMAPPING_URL + this._sourceMapUrl;
}
@ -120,14 +141,14 @@ class Bundle {
return source;
}
getMinifiedSourceAndMap() {
getMinifiedSourceAndMap(dev) {
this._assertFinalized();
if (this._minifiedSourceAndMap) {
return this._minifiedSourceAndMap;
}
const source = this._getSource();
const source = this._getSource(dev);
try {
const minifyActivity = Activity.startEvent('minify');
this._minifiedSourceAndMap = UglifyJS.minify(source, {
@ -203,7 +224,7 @@ class Bundle {
options = options || {};
if (options.minify) {
return this.getMinifiedSourceAndMap().map;
return this.getMinifiedSourceAndMap(options.dev).map;
}
if (this._shouldCombineSourceMaps) {
@ -314,7 +335,6 @@ class Bundle {
modules: this._modules,
assets: this._assets,
sourceMapUrl: this._sourceMapUrl,
shouldCombineSourceMaps: this._shouldCombineSourceMaps,
mainModuleId: this._mainModuleId,
};
}

View File

@ -40,7 +40,7 @@ describe('Bundle', function() {
}));
bundle.finalize({});
expect(bundle.getSource()).toBe([
expect(bundle.getSource({dev: true})).toBe([
'transformed foo;',
'transformed bar;',
'\/\/@ sourceMappingURL=test_url'
@ -61,7 +61,7 @@ describe('Bundle', function() {
}));
p.finalize({});
expect(p.getSource()).toBe([
expect(p.getSource({dev: true})).toBe([
'transformed foo;',
'transformed bar;',
].join('\n'));
@ -85,7 +85,7 @@ describe('Bundle', function() {
runBeforeMainModule: ['bar'],
runMainModule: true,
});
expect(bundle.getSource()).toBe([
expect(bundle.getSource({dev: true})).toBe([
'transformed foo;',
'transformed bar;',
';require("bar");',
@ -110,7 +110,7 @@ describe('Bundle', function() {
sourcePath: 'foo path'
}));
bundle.finalize();
expect(bundle.getMinifiedSourceAndMap()).toBe(minified);
expect(bundle.getMinifiedSourceAndMap({dev: true})).toBe(minified);
});
});
@ -149,7 +149,7 @@ describe('Bundle', function() {
runBeforeMainModule: [],
runMainModule: true,
});
var s = p.getSourceMap();
var s = p.getSourceMap({dev: true});
expect(s).toEqual(genSourceMap(p.getModules()));
});
@ -183,7 +183,7 @@ describe('Bundle', function() {
runMainModule: true,
});
var s = p.getSourceMap();
var s = p.getSourceMap({dev: true});
expect(s).toEqual({
file: 'bundle.js',
version: 3,

View File

@ -240,6 +240,7 @@ class Server {
p.getSource({
inlineSourceMap: options.inlineSourceMap,
minify: options.minify,
dev: options.dev,
});
return p;
});
@ -366,6 +367,7 @@ class Server {
var bundleSource = p.getSource({
inlineSourceMap: options.inlineSourceMap,
minify: options.minify,
dev: options.dev,
});
res.setHeader('Content-Type', 'application/javascript');
res.end(bundleSource);
@ -373,6 +375,7 @@ class Server {
} else if (requestType === 'map') {
var sourceMap = p.getSourceMap({
minify: options.minify,
dev: options.dev,
});
if (typeof sourceMap !== 'string') {

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.autoMockOff();
var deadModuleElimintation = require('../dead-module-elimination');
var babel = require('babel-core');
const compile = (code) =>
babel.transform(code, {
plugins: [deadModuleElimintation],
}).code;
const compare = (source, output) => {
const out = trim(compile(source))
// workaround babel/source map bug
.replace(/^false;/, '');
expect(out).toEqual(trim(output));
};
const trim = (str) =>
str.replace(/\s/g, '');
describe('dead-module-elimination', () => {
it('should inline __DEV__', () => {
compare(
`__DEV__ = false;
var foo = __DEV__;`,
`var foo = false;`
);
});
it('should accept unary operators with literals', () => {
compare(
`__DEV__ = !1;
var foo = __DEV__;`,
`var foo = false;`
);
});
it('should kill dead branches', () => {
compare(
`__DEV__ = false;
if (__DEV__) {
doSomething();
}`,
``
);
});
it('should kill unreferenced modules', () => {
compare(
`__d('foo', function() {})`,
``
);
});
it('should kill unreferenced modules at multiple levels', () => {
compare(
`__d('bar', function() {});
__d('foo', function() { require('bar'); });`,
``
);
});
it('should kill modules referenced only from dead branches', () => {
compare(
`__DEV__ = false;
__d('bar', function() {});
if (__DEV__) { require('bar'); }`,
``
);
});
it('should replace logical expressions with the result', () => {
compare(
`__DEV__ = false;
__d('bar', function() {});
__DEV__ && require('bar');`,
`false;`
);
});
it('should keep if result branch', () => {
compare(
`__DEV__ = false;
__d('bar', function() {});
if (__DEV__) {
killWithFire();
} else {
require('bar');
}`,
`__d('bar', function() {});
require('bar');`
);
});
});

View File

@ -0,0 +1,130 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const t = require('babel-types');
var globals = Object.create(null);
var requires = Object.create(null);
var _requires;
const hasDeadModules = modules =>
Object.keys(modules).some(key => modules[key] === 0);
function CallExpression(path) {
const { node } = path;
const fnName = node.callee.name;
if (fnName === 'require' || fnName === '__d') {
var moduleName = node.arguments[0].value;
if (fnName === '__d' && _requires && !_requires[moduleName]) {
path.remove();
} else if (fnName === '__d'){
requires[moduleName] = requires[moduleName] || 0;
} else {
requires[moduleName] = (requires[moduleName] || 0) + 1;
}
}
}
module.exports = function () {
var firstPass = {
AssignmentExpression(path) {
const { node } = path;
if (node.left.type === 'Identifier' && node.left.name === '__DEV__') {
var value;
if (node.right.type === 'BooleanLiteral') {
value = node.right.value;
} else if (
node.right.type === 'UnaryExpression' &&
node.right.operator === '!' &&
node.right.argument.type === 'NumericLiteral'
) {
value = !node.right.argument.value;
} else {
return;
}
globals[node.left.name] = value;
// workaround babel/source map bug - the minifier should strip it
path.replaceWith(t.booleanLiteral(value));
//path.remove();
//scope.removeBinding(node.left.name);
}
},
IfStatement(path) {
const { node } = path;
if (node.test.type === 'Identifier' && node.test.name in globals) {
if (globals[node.test.name]) {
path.replaceWithMultiple(node.consequent.body);
} else if (node.alternate) {
path.replaceWithMultiple(node.alternate.body);
} else {
path.remove();
}
}
},
Identifier(path) {
const { node } = path;
var parent = path.parent;
if (parent.type === 'AssignmentExpression' && parent.left === node) {
return;
}
if (node.name in globals) {
path.replaceWith(t.booleanLiteral(globals[node.name]));
}
},
CallExpression,
LogicalExpression(path) {
const { node } = path;
if (node.left.type === 'Identifier' && node.left.name in globals) {
const value = globals[node.left.name];
if (node.operator === '&&') {
if (value) {
path.replaceWith(node.right);
} else {
path.replaceWith(t.booleanLiteral(value));
}
} else if (node.operator === '||') {
if (value) {
path.replaceWith(t.booleanLiteral(value));
} else {
path.replaceWith(node.right);
}
}
}
}
};
var secondPass = {
CallExpression,
};
return {
visitor: {
Program(path) {
path.traverse(firstPass);
while (hasDeadModules(requires)) {
_requires = requires;
requires = {};
path.traverse(secondPass);
}
}
}
};
};

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// Return the list of plugins use for Whole Program Optimisations
module.exports = [
require('./dead-module-elimination'),
];