-
-
Notifications
You must be signed in to change notification settings - Fork 179
feat: add support for parallelization
&& caching
(options.parallel
)
#77
Conversation
Awesome 😊 I will review as soon as possible 😛 |
package.json
Outdated
"source-map": "^0.5.6", | ||
"uglify-es": "^3.0.21", | ||
"uglify-es": "^3.0.24", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already included in #74 I will merge that PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#74 merged, this should also update some of the snapshots you updated aswell :)
package.json
Outdated
"test": "jest", | ||
"test:coverage": "jest --collectCoverageFrom='src/**/*.js' --coverage", | ||
"test:watch": "jest --watch", | ||
"test": "jest --env node", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/index.js
Outdated
ie8, | ||
}; | ||
this.options.uglifyOptions = this.options.uglifyOptions || {}; | ||
this.options.maxWorkers = this.options.maxWorkers || os.cpus().length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.options.parallel = {
cache: '{Boolean}' || 'path/to/cache',
workers: '{Number}'
}
parallel
might not be the best name, open for suggestions here :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 1 for cpus
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikesherov Should this be a +1 for options.cpus
? 🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
length - 1
src/uglify/cache.js
Outdated
import crypto from 'crypto'; | ||
import cacache from 'cacache'; | ||
|
||
const hashAlgorithm = 'sha512'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hashAlgorithm
=>hash || algorithm || sha512
parallelization
&& caching (
options.parallel`)
parallelization
&& caching (
options.parallel`)parallelization
&& caching
(options.parallel
)
|
i agree with @michael-ciniawsky, i really like |
src/uglify/minify.js
Outdated
}; | ||
}; | ||
|
||
const buildCommentsFunction = (options, uglifyOptions, extractedComments) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildCommentsFunction
=> buildComments || comments
src/uglify/minify.js
Outdated
@@ -0,0 +1,108 @@ | |||
import uglify from 'uglify-es'; | |||
|
|||
const defaultUglifyOptions = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move the {Object}
to L21 please :)
src/uglify/minify.js
Outdated
}, | ||
}; | ||
|
||
const buildDefaultUglifyOptions = ({ ecma, warnings, parse = {}, compress = {}, mangle, output, toplevel, ie8 }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildDefaultUglifyOptions
=> buildOptions || defaults
src/uglify/index.js
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please give an example, why this is needed here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reminding me. I now think of three viable options:
- When
parallel.workers> 0
, users are not allowed to useFunction
orRegExp
- Before serializing, use ESlint to check the
Function
that 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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:
|
test/empty-options.test.js
Outdated
@@ -16,14 +16,14 @@ describe('when applied with no options', () => { | |||
const compilerEnv = pluginEnvironment.getEnvironmentStub(); | |||
compilerEnv.context = ''; | |||
|
|||
const plugin = new UglifyJsPlugin(); | |||
const plugin = new UglifyJsPlugin({ parallel: { cache: false, workers: 0 } }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
test/es2015.test.js
Outdated
@@ -17,6 +17,7 @@ describe('when applied with uglifyOptions.ecma', () => { | |||
}); | |||
|
|||
new UglifyJsPlugin({ | |||
parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -11,6 +11,7 @@ describe('when applied with extract option set to a single file', () => { | |||
compilerEnv.context = ''; | |||
|
|||
const plugin = new UglifyJsPlugin({ | |||
parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as #77 (comment)
test/invalid-options.test.js
Outdated
@@ -10,6 +10,7 @@ describe('when applied with invalid options', () => { | |||
it('matches snapshot', () => { | |||
const compiler = createCompiler(); | |||
new UglifyJsPlugin({ | |||
parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as #77 (comment)
test/invalid-options.test.js
Outdated
const pluginEnvironment = new PluginEnvironment(); | ||
const compilerEnv = pluginEnvironment.getEnvironmentStub(); | ||
compilerEnv.context = ''; | ||
|
||
const plugin = new UglifyJsPlugin({ | ||
parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as #77 (comment)
@@ -3,9 +3,9 @@ | |||
exports[`errors 1`] = ` | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where did DefaultsError:
go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you try changing this line to:
const str = exports.removeCWD(error.message)
"Error: manifest.6afe1bc6685e9ab36c1c.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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again about DefaultsError
test/all-options.test.js
Outdated
@@ -17,6 +17,7 @@ describe('when applied with all options', () => { | |||
compilerEnv.context = ''; | |||
|
|||
const plugin = new UglifyJsPlugin({ | |||
parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one's fine, since this is an all-options
test.
test/all-options.test.js
Outdated
@@ -311,13 +325,14 @@ describe('when applied with all options', () => { | |||
compilationEventBindings = chunkPluginEnvironment.getEventBindings(); | |||
}); | |||
|
|||
it('should get all warnings', () => { | |||
it('should get all warnings', (done) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
test/all-options.test.js
Outdated
@@ -365,28 +380,16 @@ describe('when applied with all options', () => { | |||
compilationEventBindings = chunkPluginEnvironment.getEventBindings(); | |||
}); | |||
|
|||
it('should get no warnings', () => { | |||
it('should get no warnings', (done) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove all of these for any existiing tests. So that we can keep visibility on the synchronous behavior.
test/empty-options.test.js
Outdated
compilationEventBinding.handler([{ | ||
files: ['test', 'test.js'], | ||
}], () => { | ||
expect(Object.keys(compilation.assets).length).toBe(4); | ||
done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove these too, please.
const pluginEnvironment = new PluginEnvironment(); | ||
const compilerEnv = pluginEnvironment.getEnvironmentStub(); | ||
compilerEnv.context = ''; | ||
|
||
const plugin = new UglifyJsPlugin({ | ||
parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is ok, because it's helpful to test this against es2015 feature.
const pluginEnvironment = new PluginEnvironment(); | ||
const compilerEnv = pluginEnvironment.getEnvironmentStub(); | ||
compilerEnv.context = ''; | ||
|
||
const plugin = new UglifyJsPlugin({ | ||
parallel: { cache: false, workers: 0 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove please.
test/empty-options.test.js
Outdated
plugin.apply(compilerEnv); | ||
eventBindings = pluginEnvironment.getEventBindings(); | ||
}); | ||
|
||
it('matches snapshot', () => { | ||
const compiler = createCompiler(); | ||
new UglifyJsPlugin().apply(compiler); | ||
new UglifyJsPlugin({ parallel: { cache: false } }).apply(compiler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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);
}
}
});
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're finally ready to land this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, good stuff.
@@ -121,7 +123,7 @@ describe('when applied with no options', () => { | |||
}], () => { | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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