Skip to content
This repository was archived by the owner on Dec 5, 2019. It is now read-only.

feat: add support for parallelization && caching (options.parallel) - #77

Merged
michael-ciniawsky merged 35 commits into
webpack-contrib:masterfrom
aui:master
Jul 20, 2017
Merged

feat: add support for parallelization && caching (options.parallel)#77
michael-ciniawsky merged 35 commits into
webpack-contrib:masterfrom
aui:master

Conversation

@aui

@aui aui commented Jul 9, 2017

Copy link
Copy Markdown
Contributor
  1. Use multiple processes to perform tasks in parallel.
  2. Use the file caching system to skip files that have not changed.

Build time contrast:

| cache | workers | time(ms) |
| ------| ------- | -------- |
| false |       1 |    35216 |
| false |      31 |    16345 |
| true  |      31 |     3384 |

Demo: https://travis-ci.org/aui/uglifyjs-webpack-plugin-demo

@jsf-clabot

jsf-clabot commented Jul 9, 2017

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@michael-ciniawsky

michael-ciniawsky commented Jul 9, 2017

Copy link
Copy Markdown
Member

Awesome 😊 I will review as soon as possible 😛

Comment thread package.json
"find-cache-dir": "^1.0.0",
"source-map": "^0.5.6",
"uglify-es": "^3.0.21",
"uglify-es": "^3.0.24",

Copy link
Copy Markdown
Member

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

Copy link
Copy Markdown
Member

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 :)

Comment thread package.json Outdated
"test": "jest",
"test:coverage": "jest --collectCoverageFrom='src/**/*.js' --coverage",
"test:watch": "jest --watch",
"test": "jest --env node",

Copy link
Copy Markdown
Member

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

Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor

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.

Comment thread src/index.js Outdated
ie8,
};
this.options.uglifyOptions = this.options.uglifyOptions || {};
this.options.maxWorkers = this.options.maxWorkers || os.cpus().length;

Copy link
Copy Markdown
Member

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}'
}

parallelmight not be the best name, open for suggestions here :)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 1 for cpus

Copy link
Copy Markdown
Member

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 ? 🙃

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

length - 1

Comment thread src/uglify/cache.js Outdated
import crypto from 'crypto';
import cacache from 'cacache';

const hashAlgorithm = 'sha512';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hashAlgorithm =>hash || algorithm || sha512

@michael-ciniawsky michael-ciniawsky changed the title perf: improve the build speed perf: add support parallelization && caching (options.parallel`) Jul 9, 2017
@michael-ciniawsky michael-ciniawsky changed the title perf: add support parallelization && caching (options.parallel`) perf: add support for parallelization && caching (options.parallel) Jul 9, 2017
@michael-ciniawsky

Copy link
Copy Markdown
Member

compute-cluster seems to be fairly 'outdated' and is on v0.0.9 with failing tests. Could you elaborate on your choice here please ? :). What about worker-farm ?

@hulkish

hulkish commented Jul 9, 2017

Copy link
Copy Markdown
Contributor

i agree with @michael-ciniawsky, i really like worker-farm for this also.

Comment thread src/uglify/minify.js Outdated
};
};

const buildCommentsFunction = (options, uglifyOptions, extractedComments) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildCommentsFunction => buildComments || comments

Comment thread src/uglify/minify.js Outdated
@@ -0,0 +1,108 @@
import uglify from 'uglify-es';

const defaultUglifyOptions = {

Copy link
Copy Markdown
Member

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 :)

Comment thread src/uglify/minify.js
},
};

const buildDefaultUglifyOptions = ({ ecma, warnings, parse = {}, compress = {}, mangle, output, toplevel, ie8 }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildDefaultUglifyOptions => buildOptions || defaults

Comment thread 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

Copy link
Copy Markdown
Member

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));

Copy link
Copy Markdown
Member

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

Copy link
Copy Markdown
Contributor Author

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/ } 😊
})

Copy link
Copy Markdown

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?

@aui aui Jul 11, 2017

Copy link
Copy Markdown
Contributor Author

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:

  1. When parallel.workers> 0, users are not allowed to use Function or RegExp
  2. Before serializing, use ESlint to check the Function that contains context-dependent
  3. 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. :-)

Copy link
Copy Markdown
Contributor Author

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|   }

@mikesherov

Copy link
Copy Markdown

Great work here, a couple of notes:

  1. Is workerization faster when CPU count is 1? Same question for when number of files to uglify is less than CPU count?
  2. Separately, we should use worker-farm to manage processes.
  3. Please max sure maxWorkers is maxed out at CPU count - 1. Having more workers than CPUs - 1 will introduce jank and slowdown.

@michael-ciniawsky

michael-ciniawsky commented Jul 19, 2017

Copy link
Copy Markdown
Member

@aui Are there any options definitely known to not work in when options.parallel is enabled? I need to update the docs PR (#61) with the changes here accordingly, so anything thats needs attention in terms of documentation would be appreciated if you could give me a brief sum up on that 😛

Comment thread test/empty-options.test.js Outdated
compilerEnv.context = '';

const plugin = new UglifyJsPlugin();
const plugin = new UglifyJsPlugin({ parallel: { cache: false, workers: 0 } });

Copy link
Copy Markdown
Contributor

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.

Comment thread test/es2015.test.js Outdated
});

new UglifyJsPlugin({
parallel: { cache: false, workers: 0 },

Copy link
Copy Markdown
Contributor

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.

compilerEnv.context = '';

const plugin = new UglifyJsPlugin({
parallel: { cache: false, workers: 0 },

@hulkish hulkish Jul 19, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #77 (comment)

Comment thread test/invalid-options.test.js Outdated
it('matches snapshot', () => {
const compiler = createCompiler();
new UglifyJsPlugin({
parallel: { cache: false, workers: 0 },

@hulkish hulkish Jul 19, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #77 (comment)

Comment thread test/invalid-options.test.js Outdated
compilerEnv.context = '';

const plugin = new UglifyJsPlugin({
parallel: { cache: false, workers: 0 },

@hulkish hulkish Jul 19, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #77 (comment)

Array [
"Error: main.0c220ec66316af2c1b24.js from UglifyJs
DefaultsError: \`invalid-option\` is not a supported option",
\`invalid-option\` is not a supported option",

@hulkish hulkish Jul 20, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did DefaultsError: go?

Copy link
Copy Markdown
Contributor Author

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}`);

@hulkish hulkish Jul 20, 2017

Copy link
Copy Markdown
Contributor

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)

\`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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again about DefaultsError

Comment thread test/all-options.test.js Outdated
compilerEnv.context = '';

const plugin = new UglifyJsPlugin({
parallel: { cache: false, workers: 0 },

Copy link
Copy Markdown
Contributor

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.

Comment thread test/all-options.test.js Outdated
});

it('should get all warnings', () => {
it('should get all warnings', (done) => {

@hulkish hulkish Jul 20, 2017

Copy link
Copy Markdown
Contributor

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.

Comment thread test/all-options.test.js Outdated
});

it('should get no warnings', () => {
it('should get no warnings', (done) => {

@hulkish hulkish Jul 20, 2017

Copy link
Copy Markdown
Contributor

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.

Comment thread test/empty-options.test.js Outdated
files: ['test', 'test.js'],
}], () => {
expect(Object.keys(compilation.assets).length).toBe(4);
done();

@hulkish hulkish Jul 20, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these too, please.

Comment thread test/extract-comments-options.test.js Outdated
compilerEnv.context = '';

const plugin = new UglifyJsPlugin({
parallel: { cache: false, workers: 0 },

Copy link
Copy Markdown
Contributor

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.

Comment thread test/extract-comments-options.test.js Outdated
compilerEnv.context = '';

const plugin = new UglifyJsPlugin({
parallel: { cache: false, workers: 0 },

@hulkish hulkish Jul 20, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove please.

Comment thread test/empty-options.test.js Outdated
it('matches snapshot', () => {
const compiler = createCompiler();
new UglifyJsPlugin().apply(compiler);
new UglifyJsPlugin({ parallel: { cache: false } }).apply(compiler);

@hulkish hulkish Jul 20, 2017

Copy link
Copy Markdown
Contributor

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);
        }
      }
    });
  });

@mikesherov mikesherov left a comment

Copy link
Copy Markdown

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!

@hulkish hulkish left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'));

@hulkish hulkish Jul 20, 2017

Copy link
Copy Markdown
Contributor

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.

@michael-ciniawsky michael-ciniawsky left a comment

Copy link
Copy Markdown
Member

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 :)

@mikesherov

Copy link
Copy Markdown

Wooo!

@scott-thrillist

scott-thrillist commented Jul 21, 2017

Copy link
Copy Markdown

Benchmarks vs webpack-parallel-uglify-plugin

webpack-parallel-uglify-plugin
23.18 seconds
22.61 seconds
22.19 seconds
22.51 seconds

uglifyjs-webpack-plugin
25.96 seconds
18.41 seconds
16.14 seconds
16.18 seconds

uglifyjs-webpack-plugin (parallel: false)
51.89 seconds

👍 👍 👍

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants