Skip to content

Commit

Permalink
Limit concurrency to the number of CPU cores (#1467)
Browse files Browse the repository at this point in the history
Fixes #966
  • Loading branch information
sindresorhus committed Jul 30, 2017
1 parent 4eea226 commit 465fcec
Show file tree
Hide file tree
Showing 6 changed files with 15 additions and 118 deletions.
89 changes: 9 additions & 80 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const EventEmitter = require('events');
const path = require('path');
const fs = require('fs');
const os = require('os');
const commonPathPrefix = require('common-path-prefix');
const uniqueTempDir = require('unique-temp-dir');
const findCacheDir = require('find-cache-dir');
Expand Down Expand Up @@ -160,16 +161,17 @@ class Api extends EventEmitter {
this._setupTimeout(runStatus);
}

let overwatch;
let concurrency = os.cpus().length;

if (this.options.concurrency > 0) {
const concurrency = this.options.serial ? 1 : this.options.concurrency;
overwatch = this._runWithPool(files, runStatus, concurrency);
} else {
// _runWithoutPool exists to preserve legacy behavior, specifically around `.only`
overwatch = this._runWithoutPool(files, runStatus);
concurrency = this.options.concurrency;
}

if (this.options.serial) {
concurrency = 1;
}

return overwatch;
return this._runWithPool(files, runStatus, concurrency);
});
}
_computeForkExecArgs(files) {
Expand Down Expand Up @@ -223,79 +225,6 @@ class Api extends EventEmitter {
file: err.file ? path.relative(process.cwd(), err.file) : undefined
});
}
_runWithoutPool(files, runStatus) {
const tests = [];
let execArgvList;

// TODO: This should be cleared at the end of the run
runStatus.on('timeout', () => {
tests.forEach(fork => {
fork.exit();
});
});

return this._computeForkExecArgs(files)
.then(argvList => {
execArgvList = argvList;
})
.return(files)
.each((file, index) => {
return new Promise(resolve => {
const forkArgs = execArgvList[index];
const test = this._runFile(file, runStatus, forkArgs);
tests.push(test);
test.on('stats', resolve);
test.catch(resolve);
}).catch(err => {
err.results = [];
err.file = file;
return Promise.reject(err);
});
})
.then(() => {
if (this.options.match.length > 0 && !runStatus.hasExclusive) {
const err = new AvaError('Couldn\'t find any matching tests');
err.file = undefined;
err.results = [];
return Promise.reject(err);
}

const method = this.options.serial ? 'mapSeries' : 'map';
const options = {
runOnlyExclusive: runStatus.hasExclusive
};

return Promise[method](files, (file, index) => {
return tests[index].run(options).catch(err => {
err.file = file;
this._handleError(runStatus, err);
return getBlankResults();
});
});
})
.catch(err => {
this._handleError(runStatus, err);
return err.results;
})
.tap(results => {
// If no tests ran, make sure to tear down the child processes
if (results.length === 0) {
tests.forEach(test => {
test.send('teardown');
});
}
})
.then(results => {
// Cancel debounced _onTimeout() from firing
if (this.options.timeout) {
this._cancelTimeout(runStatus);
}

runStatus.processResults(results);

return runStatus;
});
}
_runWithPool(files, runStatus, concurrency) {
const tests = [];
let execArgvList;
Expand Down
2 changes: 2 additions & 0 deletions docs/common-pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ AVA uses [is-ci](https://github.com/watson/is-ci) to decide if it's in a CI envi

You may be using a service that only allows a limited number of concurrent connections. For example, many database-as-a-service businesses offer a free plan with a limit on how many clients can be using it at the same time. AVA can hit those limits as it runs multiple processes, but well-written services should emit an error or throttle in those cases. If the one you're using doesn't, the tests will hang.

By default, AVA will use as many processes as there are CPU cores in your machine.

Use the `concurrency` flag to limit the number of processes ran. For example, if your service plan allows 5 clients, you should run AVA with `concurrency=5` or less.

## Asynchronous operations
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes/precompiling-with-webpack.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ npm scripts:
"precompile-src": "cross-env NODE_ENV=test babel src --out-dir _src",
"precompile-tests": "cross-env NODE_ENV=test webpack --config webpack.config.test.js",
"pretest": "npm run precompile-src && npm run precompile-tests",
"test": "cross-env NODE_ENV=test nyc --cache ava _build --concurrency 3"
"test": "cross-env NODE_ENV=test nyc --cache ava _build"
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ exports.run = () => {
--match, -m Only run tests with matching title (Can be repeated)
--watch, -w Re-run tests when tests and source files change
--timeout, -T Set global timeout
--concurrency, -c Maximum number of test files running at the same time (EXPERIMENTAL)
--concurrency, -c Max number of test files running at the same time (Default: CPU cores)
--update-snapshots, -u Update snapshots
Examples
Expand Down
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ $ ava --help
--match, -m Only run tests with matching title (Can be repeated)
--watch, -w Re-run tests when tests and source files change
--timeout, -T Set global timeout
--concurrency, -c Maximum number of test files running at the same time (EXPERIMENTAL)
--concurrency, -c Max number of test files running at the same time (Default: CPU cores)
--update-snapshots, -u Update snapshots

Examples
Expand Down Expand Up @@ -400,7 +400,7 @@ test.only('will be run', t => {
});
```

`.only` applies across all test files, so if you use it in one file, no tests from the other file will run.
*Note:* The `.only` modifier applies to the test file it's defined in, so if you run multiple test files, tests in other files will still run. If you want to only run the `test.only` test, provide just that test file to AVA.

### Running tests with matching titles

Expand Down
34 changes: 0 additions & 34 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,6 @@ function apiCreator(options) {
return instance;
}

generateTests('Without Pool:', options => apiCreator(options || {}));

// The following two tests are only run against "Without Pool" behavior as they test the exclusive test features. These features are currently not expected to work correctly in the limited process pool. When the limited process pool behavior is finalized this test file will be updated. See: https://github.com/avajs/ava/pull/791#issuecomment-216293302
test('Without Pool: test file with exclusive tests causes non-exclusive tests in other files to be ignored', t => {
const files = [
path.join(__dirname, 'fixture/exclusive.js'),
path.join(__dirname, 'fixture/exclusive-nonexclusive.js'),
path.join(__dirname, 'fixture/one-pass-one-fail.js')
];

const api = apiCreator({});

return api.run(files)
.then(result => {
t.ok(result.hasExclusive);
t.is(result.testCount, 5);
t.is(result.passCount, 2);
t.is(result.failCount, 0);
});
});

test('Without Pool: test files can be forced to run in exclusive mode', t => {
const api = apiCreator();
return api.run(
[path.join(__dirname, 'fixture/es2015.js')],
{runOnlyExclusive: true}
).then(result => {
t.ok(result.hasExclusive);
t.is(result.testCount, 1);
t.is(result.passCount, 0);
t.is(result.failCount, 0);
});
});

generateTests('With Pool:', options => {
options = options || {};
options.concurrency = 2;
Expand Down

0 comments on commit 465fcec

Please sign in to comment.