feat: add support for parallelization && caching (options.parallel) - #77
Conversation
|
Awesome 😊 I will review as soon as possible 😛 |
| "find-cache-dir": "^1.0.0", | ||
| "source-map": "^0.5.6", | ||
| "uglify-es": "^3.0.21", | ||
| "uglify-es": "^3.0.24", |
| "test": "jest", | ||
| "test:coverage": "jest --collectCoverageFrom='src/**/*.js' --coverage", | ||
| "test:watch": "jest --watch", | ||
| "test": "jest --env node", |
There was a problem hiding this comment.
Why is that needed and please revert it in any case here, since we handle that in a project called webpack-defaults separately across the organisation
There was a problem hiding this comment.
Jest default configuration is --env = jsdom, it can simulate the browser environment, I worry that it takes up extra memory
There was a problem hiding this comment.
Actually, Jest uses less memory than Karma does, by quite a bit. That said, this change if needed belongs in webpack-defaults.
All of our libs run the exact same projects setup. So for the scope of this pull request, this needs to be set back to what it was.
If you feel that strongly about this particular configuration, open a pull request in https://github.com/webpack-contrib/webpack-defaults/issues so the change can be applied to all of our libs in the next patch release of defaults.
| ie8, | ||
| }; | ||
| this.options.uglifyOptions = this.options.uglifyOptions || {}; | ||
| this.options.maxWorkers = this.options.maxWorkers || os.cpus().length; |
There was a problem hiding this comment.
this.options.parallel = {
cache: '{Boolean}' || 'path/to/cache',
workers: '{Number}'
}parallelmight not be the best name, open for suggestions here :)
There was a problem hiding this comment.
@mikesherov Should this be a +1 for options.cpus ? 🙃
| import crypto from 'crypto'; | ||
| import cacache from 'cacache'; | ||
|
|
||
| const hashAlgorithm = 'sha512'; |
There was a problem hiding this comment.
hashAlgorithm =>hash || algorithm || sha512
parallelization && caching (options.parallel`)
parallelization && caching (options.parallel`)parallelization && caching (options.parallel)
|
|
|
i agree with @michael-ciniawsky, i really like |
| }; | ||
| }; | ||
|
|
||
| const buildCommentsFunction = (options, uglifyOptions, extractedComments) => { |
There was a problem hiding this comment.
buildCommentsFunction => buildComments || comments
| @@ -0,0 +1,108 @@ | |||
| import uglify from 'uglify-es'; | |||
|
|
|||
| const defaultUglifyOptions = { | |||
There was a problem hiding this comment.
move the {Object} to L21 please :)
| }, | ||
| }; | ||
|
|
||
| const buildDefaultUglifyOptions = ({ ecma, warnings, parse = {}, compress = {}, mangle, output, toplevel, ie8 }) => { |
There was a problem hiding this comment.
buildDefaultUglifyOptions => buildOptions || defaults
| import ComputeCluster from 'compute-cluster'; | ||
| import { get, put } from './cache'; | ||
| import { encode } from './serialization'; // eslint-disable-line import/newline-after-import | ||
| const uglifyVersion = require(require.resolve('uglify-es/package.json')).version; // eslint-disable-line import/no-dynamic-require |
There was a problem hiding this comment.
const version = {
uglify: require(require.resolve('uglify-es/package.json')).version,
plugin: require('../../package.json').version // require.resolve() here aswell ?
}Maybe consider a separate file src/uglify/versions.js ?
| @@ -0,0 +1,46 @@ | |||
| const toType = value => (Object.prototype.toString.call(value).slice(8, -1)); | |||
There was a problem hiding this comment.
Please give an example, why this is needed here
There was a problem hiding this comment.
Since RegExp or Function can not be serialized, they need special handling:
worker.send({ extractComments: /foo/ });// worker
process.on('message', (message) => {
console.log(message); // -> { extractComments: {} } 😢
})Use serialization :
worker.send(encode({ extractComments: /foo/ }));// worker
process.on('message', (message) => {
message = decode(message);
console.log(message); // -> { extractComments: /foo/ } 😊
})There was a problem hiding this comment.
I don't think we can legitimately do this for functions, though.
var a = 1;
function toBeSerialized() {
console.log(a);
}
in the case above, this results in non-obvious errors when serializing and deserializing in a different context. Is there any way we can avoid passing functions or regexes down to the worker and instead post process once comments are sent back to us?
There was a problem hiding this comment.
Thanks for reminding me. I now think of three viable options:
- When
parallel.workers> 0, users are not allowed to useFunctionorRegExp - Before serializing, use ESlint to check the
Functionthat contains context-dependent - At the run-time stage, catch the error and tell the user why the error occurred
I am currently inclined to the third program because it is friendly and simple. :-)
There was a problem hiding this comment.
@mikesherov I refactored the serialization.js, and now the user can know the details of the error :-)
Error: options serialization failed: "test" parse failed: 'console' is not defined.
1| function toBeSerialized() {
>> 2| console.log(a);
3| }
|
Great work here, a couple of notes:
|
| compilerEnv.context = ''; | ||
|
|
||
| const plugin = new UglifyJsPlugin(); | ||
| const plugin = new UglifyJsPlugin({ parallel: { cache: false, workers: 0 } }); |
There was a problem hiding this comment.
Please move all options related changes in this test to it's own test file, or to parallel-option.test.js. The purpose of this test is to literally have empty options.
| }); | ||
|
|
||
| new UglifyJsPlugin({ | ||
| parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
Same comment I made for empty-options.test> It's important that these options for these tests are not changed in order for us to really understand that it is not breaking those use cases.
| compilerEnv.context = ''; | ||
|
|
||
| const plugin = new UglifyJsPlugin({ | ||
| parallel: { cache: false, workers: 0 }, |
| it('matches snapshot', () => { | ||
| const compiler = createCompiler(); | ||
| new UglifyJsPlugin({ | ||
| parallel: { cache: false, workers: 0 }, |
| compilerEnv.context = ''; | ||
|
|
||
| const plugin = new UglifyJsPlugin({ | ||
| parallel: { cache: false, workers: 0 }, |
| Array [ | ||
| "Error: main.0c220ec66316af2c1b24.js from UglifyJs | ||
| DefaultsError: \`invalid-option\` is not a supported option", | ||
| \`invalid-option\` is not a supported option", |
There was a problem hiding this comment.
Where did DefaultsError: go?
There was a problem hiding this comment.
uglify-es error object does not have a msg field.
src/index.js > buildError() :
} else if (err.msg) {
return new Error(`${file} from UglifyJs\n${err.msg}`);
}
return new Error(`${file} from UglifyJs\n${err.stack}`); // -> ...DefaultsError: `invalid-option` is not a supported option"} else if (err.message) {
return new Error(`${file} from UglifyJs\n${err.message}`); // -> ...`invalid-option` is not a supported option"
}
return new Error(`${file} from UglifyJs\n${err.stack}`);There was a problem hiding this comment.
can you try changing this line to:
const str = exports.removeCWD(error.message)| \`invalid-option\` is not a supported option", | ||
| "Error: manifest.6afe1bc6685e9ab36c1c.js from UglifyJs | ||
| DefaultsError: \`invalid-option\` is not a supported option", | ||
| \`invalid-option\` is not a supported option", |
| compilerEnv.context = ''; | ||
|
|
||
| const plugin = new UglifyJsPlugin({ | ||
| parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
This one's fine, since this is an all-options test.
| }); | ||
|
|
||
| it('should get all warnings', () => { | ||
| it('should get all warnings', (done) => { |
There was a problem hiding this comment.
Remove, because this is a synchronous test (ensured via PluginEnvironment). This should not use a callback. or return a promise. This will also help to make sure the parallel feature being added does not break this expected behavior.
You can instead basically replicate this test inside parallel-option.test.js - this way we can have coverage for both single-core and multicore environments.
| }); | ||
|
|
||
| it('should get no warnings', () => { | ||
| it('should get no warnings', (done) => { |
There was a problem hiding this comment.
Please remove all of these for any existiing tests. So that we can keep visibility on the synchronous behavior.
| files: ['test', 'test.js'], | ||
| }], () => { | ||
| expect(Object.keys(compilation.assets).length).toBe(4); | ||
| done(); |
There was a problem hiding this comment.
Remove these too, please.
| compilerEnv.context = ''; | ||
|
|
||
| const plugin = new UglifyJsPlugin({ | ||
| parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
This one is ok, because it's helpful to test this against es2015 feature.
| compilerEnv.context = ''; | ||
|
|
||
| const plugin = new UglifyJsPlugin({ | ||
| parallel: { cache: false, workers: 0 }, |
| it('matches snapshot', () => { | ||
| const compiler = createCompiler(); | ||
| new UglifyJsPlugin().apply(compiler); | ||
| new UglifyJsPlugin({ parallel: { cache: false } }).apply(compiler); |
There was a problem hiding this comment.
Please remove this change here and just make a new test inside parallel-option.test.ja. Something like:
it('matches snapshot', () => {
const compiler = createCompiler();
new UglifyJsPlugin({ parallel: { cache: false, workers: 0 } }).apply(compiler);
return compile(compiler).then((stats) => {
const errors = stats.compilation.errors.map(cleanErrorStack);
const warnings = stats.compilation.warnings.map(cleanErrorStack);
expect(errors).toMatchSnapshot('errors');
expect(warnings).toMatchSnapshot('warnings');
for (const file in stats.compilation.assets) {
if (Object.prototype.hasOwnProperty.call(stats.compilation.assets, file)) {
expect(stats.compilation.assets[file].source()).toMatchSnapshot(file);
}
}
});
});
mikesherov
left a comment
There was a problem hiding this comment.
I think we're finally ready to land this!
hulkish
left a comment
There was a problem hiding this comment.
Looks great, good stuff.
| }], () => { | ||
| expect(compilation.errors.length).toBe(1); | ||
| expect(compilation.errors[0]).toBeInstanceOf(Error); | ||
| expect(compilation.errors[0].message).toEqual(expect.stringContaining('TypeError')); |
There was a problem hiding this comment.
Just making note of this for when #80 is rebased on top of this. This should remain a TypeError - but I think the options schema validation should cover this. Nothing needing done for this pr i believe.
michael-ciniawsky
left a comment
There was a problem hiding this comment.
@aui Thx very much fo putting up this PR, this is very good work :)
|
Wooo! |
|
Benchmarks vs webpack-parallel-uglify-plugin webpack-parallel-uglify-plugin uglifyjs-webpack-plugin uglifyjs-webpack-plugin (parallel: false) 👍 👍 👍 |
Build time contrast:
Demo: https://travis-ci.org/aui/uglifyjs-webpack-plugin-demo