173 lines
4.7 KiB
JavaScript
173 lines
4.7 KiB
JavaScript
|
/**
|
||
|
* A wrapper around a third-party child process worker pool implementation.
|
||
|
* Used by {@link module:buffered-runner}.
|
||
|
* @private
|
||
|
* @module buffered-worker-pool
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
const serializeJavascript = require('serialize-javascript');
|
||
|
const workerpool = require('workerpool');
|
||
|
const {deserialize} = require('./serializer');
|
||
|
const debug = require('debug')('mocha:parallel:buffered-worker-pool');
|
||
|
const {createInvalidArgumentTypeError} = require('../errors');
|
||
|
|
||
|
const WORKER_PATH = require.resolve('./worker.js');
|
||
|
|
||
|
/**
|
||
|
* A mapping of Mocha `Options` objects to serialized values.
|
||
|
*
|
||
|
* This is helpful because we tend to same the same options over and over
|
||
|
* over IPC.
|
||
|
* @type {WeakMap<Options,string>}
|
||
|
*/
|
||
|
let optionsCache = new WeakMap();
|
||
|
|
||
|
/**
|
||
|
* These options are passed into the [workerpool](https://npm.im/workerpool) module.
|
||
|
* @type {Partial<WorkerPoolOptions>}
|
||
|
*/
|
||
|
const WORKER_POOL_DEFAULT_OPTS = {
|
||
|
// use child processes, not worker threads!
|
||
|
workerType: 'process',
|
||
|
// ensure the same flags sent to `node` for this `mocha` invocation are passed
|
||
|
// along to children
|
||
|
forkOpts: {execArgv: process.execArgv},
|
||
|
maxWorkers: workerpool.cpus - 1
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* A wrapper around a third-party worker pool implementation.
|
||
|
* @private
|
||
|
*/
|
||
|
class BufferedWorkerPool {
|
||
|
/**
|
||
|
* Creates an underlying worker pool instance; determines max worker count
|
||
|
* @param {Partial<WorkerPoolOptions>} [opts] - Options
|
||
|
*/
|
||
|
constructor(opts = {}) {
|
||
|
const maxWorkers = Math.max(
|
||
|
1,
|
||
|
typeof opts.maxWorkers === 'undefined'
|
||
|
? WORKER_POOL_DEFAULT_OPTS.maxWorkers
|
||
|
: opts.maxWorkers
|
||
|
);
|
||
|
|
||
|
/* istanbul ignore next */
|
||
|
if (workerpool.cpus < 2) {
|
||
|
// TODO: decide whether we should warn
|
||
|
debug(
|
||
|
'not enough CPU cores available to run multiple jobs; avoid --parallel on this machine'
|
||
|
);
|
||
|
} else if (maxWorkers >= workerpool.cpus) {
|
||
|
// TODO: decide whether we should warn
|
||
|
debug(
|
||
|
'%d concurrent job(s) requested, but only %d core(s) available',
|
||
|
maxWorkers,
|
||
|
workerpool.cpus
|
||
|
);
|
||
|
}
|
||
|
/* istanbul ignore next */
|
||
|
debug(
|
||
|
'run(): starting worker pool of max size %d, using node args: %s',
|
||
|
maxWorkers,
|
||
|
process.execArgv.join(' ')
|
||
|
);
|
||
|
|
||
|
this.options = {...WORKER_POOL_DEFAULT_OPTS, opts, maxWorkers};
|
||
|
this._pool = workerpool.pool(WORKER_PATH, this.options);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Terminates all workers in the pool.
|
||
|
* @param {boolean} [force] - Whether to force-kill workers. By default, lets workers finish their current task before termination.
|
||
|
* @private
|
||
|
* @returns {Promise<void>}
|
||
|
*/
|
||
|
async terminate(force = false) {
|
||
|
/* istanbul ignore next */
|
||
|
debug('terminate(): terminating with force = %s', force);
|
||
|
return this._pool.terminate(force);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds a test file run to the worker pool queue for execution by a worker process.
|
||
|
*
|
||
|
* Handles serialization/deserialization.
|
||
|
*
|
||
|
* @param {string} filepath - Filepath of test
|
||
|
* @param {Options} [options] - Options for Mocha instance
|
||
|
* @private
|
||
|
* @returns {Promise<SerializedWorkerResult>}
|
||
|
*/
|
||
|
async run(filepath, options = {}) {
|
||
|
if (!filepath || typeof filepath !== 'string') {
|
||
|
throw createInvalidArgumentTypeError(
|
||
|
'Expected a non-empty filepath',
|
||
|
'filepath',
|
||
|
'string'
|
||
|
);
|
||
|
}
|
||
|
const serializedOptions = BufferedWorkerPool.serializeOptions(options);
|
||
|
const result = await this._pool.exec('run', [filepath, serializedOptions]);
|
||
|
return deserialize(result);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns stats about the state of the worker processes in the pool.
|
||
|
*
|
||
|
* Used for debugging.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
stats() {
|
||
|
return this._pool.stats();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Instantiates a {@link WorkerPool}.
|
||
|
* @private
|
||
|
*/
|
||
|
static create(...args) {
|
||
|
return new BufferedWorkerPool(...args);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given Mocha options object `opts`, serialize into a format suitable for
|
||
|
* transmission over IPC.
|
||
|
*
|
||
|
* @param {Options} [opts] - Mocha options
|
||
|
* @private
|
||
|
* @returns {string} Serialized options
|
||
|
*/
|
||
|
static serializeOptions(opts = {}) {
|
||
|
if (!optionsCache.has(opts)) {
|
||
|
const serialized = serializeJavascript(opts, {
|
||
|
unsafe: true, // this means we don't care about XSS
|
||
|
ignoreFunction: true // do not serialize functions
|
||
|
});
|
||
|
optionsCache.set(opts, serialized);
|
||
|
/* istanbul ignore next */
|
||
|
debug(
|
||
|
'serializeOptions(): serialized options %O to: %s',
|
||
|
opts,
|
||
|
serialized
|
||
|
);
|
||
|
}
|
||
|
return optionsCache.get(opts);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Resets internal cache of serialized options objects.
|
||
|
*
|
||
|
* For testing/debugging
|
||
|
* @private
|
||
|
*/
|
||
|
static resetOptionsCache() {
|
||
|
optionsCache = new WeakMap();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exports.BufferedWorkerPool = BufferedWorkerPool;
|