From 844282c37bc8b13b9d199637c2031b1ddf11cd5b Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Tue, 17 Nov 2015 03:36:50 -0800 Subject: [PATCH] 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 --- react-packager/src/Bundler/Bundle.js | 44 ++++-- .../src/Bundler/__tests__/Bundle-test.js | 12 +- react-packager/src/Server/index.js | 3 + .../__tests__/dead-module-elimination-test.js | 106 ++++++++++++++ .../dead-module-elimination.js | 130 ++++++++++++++++++ .../whole-program-optimisations/index.js | 14 ++ 6 files changed, 291 insertions(+), 18 deletions(-) create mode 100644 react-packager/src/transforms/whole-program-optimisations/__tests__/dead-module-elimination-test.js create mode 100644 react-packager/src/transforms/whole-program-optimisations/dead-module-elimination.js create mode 100644 react-packager/src/transforms/whole-program-optimisations/index.js diff --git a/react-packager/src/Bundler/Bundle.js b/react-packager/src/Bundler/Bundle.js index 66b5e704..b6635be9 100644 --- a/react-packager/src/Bundler/Bundle.js +++ b/react-packager/src/Bundler/Bundle.js @@ -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, }; } diff --git a/react-packager/src/Bundler/__tests__/Bundle-test.js b/react-packager/src/Bundler/__tests__/Bundle-test.js index 62ad59d3..46b57741 100644 --- a/react-packager/src/Bundler/__tests__/Bundle-test.js +++ b/react-packager/src/Bundler/__tests__/Bundle-test.js @@ -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, diff --git a/react-packager/src/Server/index.js b/react-packager/src/Server/index.js index e163e261..1abb78cf 100644 --- a/react-packager/src/Server/index.js +++ b/react-packager/src/Server/index.js @@ -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') { diff --git a/react-packager/src/transforms/whole-program-optimisations/__tests__/dead-module-elimination-test.js b/react-packager/src/transforms/whole-program-optimisations/__tests__/dead-module-elimination-test.js new file mode 100644 index 00000000..089f8695 --- /dev/null +++ b/react-packager/src/transforms/whole-program-optimisations/__tests__/dead-module-elimination-test.js @@ -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');` + ); + }); +}); diff --git a/react-packager/src/transforms/whole-program-optimisations/dead-module-elimination.js b/react-packager/src/transforms/whole-program-optimisations/dead-module-elimination.js new file mode 100644 index 00000000..82e9b5da --- /dev/null +++ b/react-packager/src/transforms/whole-program-optimisations/dead-module-elimination.js @@ -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); + } + } + } + }; +}; diff --git a/react-packager/src/transforms/whole-program-optimisations/index.js b/react-packager/src/transforms/whole-program-optimisations/index.js new file mode 100644 index 00000000..f802c0f7 --- /dev/null +++ b/react-packager/src/transforms/whole-program-optimisations/index.js @@ -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'), +];