index.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. /**
  2. * Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. *
  8. */
  9. 'use strict';
  10. Object.defineProperty(exports, '__esModule', {
  11. value: true
  12. });
  13. var _mergeStream;
  14. function _load_mergeStream() {
  15. return (_mergeStream = _interopRequireDefault(require('merge-stream')));
  16. }
  17. var _os;
  18. function _load_os() {
  19. return (_os = _interopRequireDefault(require('os')));
  20. }
  21. var _path;
  22. function _load_path() {
  23. return (_path = _interopRequireDefault(require('path')));
  24. }
  25. var _types;
  26. function _load_types() {
  27. return (_types = require('./types'));
  28. }
  29. var _worker;
  30. function _load_worker() {
  31. return (_worker = _interopRequireDefault(require('./worker')));
  32. }
  33. function _interopRequireDefault(obj) {
  34. return obj && obj.__esModule ? obj : {default: obj};
  35. }
  36. /* istanbul ignore next */
  37. const emptyMethod = () => {};
  38. /**
  39. * The Jest farm (publicly called "Worker") is a class that allows you to queue
  40. * methods across multiple child processes, in order to parallelize work. This
  41. * is done by providing an absolute path to a module that will be loaded on each
  42. * of the child processes, and bridged to the main process.
  43. *
  44. * Bridged methods are specified by using the "exposedMethods" property of the
  45. * options "object". This is an array of strings, where each of them corresponds
  46. * to the exported name in the loaded module.
  47. *
  48. * You can also control the amount of workers by using the "numWorkers" property
  49. * of the "options" object, and the settings passed to fork the process through
  50. * the "forkOptions" property. The amount of workers defaults to the amount of
  51. * CPUS minus one.
  52. *
  53. * Queueing calls can be done in two ways:
  54. * - Standard method: calls will be redirected to the first available worker,
  55. * so they will get executed as soon as they can.
  56. *
  57. * - Sticky method: if a "computeWorkerKey" method is provided within the
  58. * config, the resulting string of this method will be used as a key.
  59. * Everytime this key is returned, it is guaranteed that your job will be
  60. * processed by the same worker. This is specially useful if your workers are
  61. * caching results.
  62. */
  63. exports.default = class {
  64. constructor(workerPath) {
  65. let options =
  66. arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  67. const numWorkers =
  68. options.numWorkers || (_os || _load_os()).default.cpus().length - 1;
  69. const workers = new Array(numWorkers);
  70. const stdout = (0, (_mergeStream || _load_mergeStream()).default)();
  71. const stderr = (0, (_mergeStream || _load_mergeStream()).default)();
  72. if (!(_path || _load_path()).default.isAbsolute(workerPath)) {
  73. workerPath = require.resolve(workerPath);
  74. }
  75. const sharedWorkerOptions = {
  76. forkOptions: options.forkOptions || {},
  77. maxRetries: options.maxRetries || 3,
  78. workerPath
  79. };
  80. for (let i = 0; i < numWorkers; i++) {
  81. const workerOptions = Object.assign({}, sharedWorkerOptions, {
  82. workerId: i + 1
  83. });
  84. const worker = new (_worker || _load_worker()).default(workerOptions);
  85. const workerStdout = worker.getStdout();
  86. const workerStderr = worker.getStderr();
  87. if (workerStdout) {
  88. stdout.add(workerStdout);
  89. }
  90. if (workerStderr) {
  91. stderr.add(workerStderr);
  92. }
  93. workers[i] = worker;
  94. }
  95. let exposedMethods = options.exposedMethods;
  96. // If no methods list is given, try getting it by auto-requiring the module.
  97. if (!exposedMethods) {
  98. // $FlowFixMe: This has to be a dynamic require.
  99. const child = require(workerPath);
  100. exposedMethods = Object.keys(child).filter(
  101. name => typeof child[name] === 'function'
  102. );
  103. if (typeof child === 'function') {
  104. exposedMethods.push('default');
  105. }
  106. }
  107. exposedMethods.forEach(name => {
  108. if (name.startsWith('_')) {
  109. return;
  110. }
  111. if (this.constructor.prototype.hasOwnProperty(name)) {
  112. throw new TypeError('Cannot define a method called ' + name);
  113. }
  114. // $FlowFixMe: dynamic extension of the class instance is expected.
  115. this[name] = this._makeCall.bind(this, name);
  116. });
  117. this._stdout = stdout;
  118. this._stderr = stderr;
  119. this._ending = false;
  120. this._cacheKeys = Object.create(null);
  121. this._options = options;
  122. this._workers = workers;
  123. this._offset = 0;
  124. }
  125. getStdout() {
  126. return this._stdout;
  127. }
  128. getStderr() {
  129. return this._stderr;
  130. }
  131. end() {
  132. if (this._ending) {
  133. throw new Error('Farm is ended, no more calls can be done to it');
  134. }
  135. const workers = this._workers;
  136. // We do not cache the request object here. If so, it would only be only
  137. // processed by one of the workers, and we want them all to close.
  138. for (let i = 0; i < workers.length; i++) {
  139. workers[i].send(
  140. [(_types || _load_types()).CHILD_MESSAGE_END, false],
  141. emptyMethod,
  142. emptyMethod
  143. );
  144. }
  145. this._ending = true;
  146. }
  147. // eslint-disable-next-line no-unclear-flowtypes
  148. _makeCall(method) {
  149. for (
  150. var _len = arguments.length,
  151. args = Array(_len > 1 ? _len - 1 : 0),
  152. _key = 1;
  153. _key < _len;
  154. _key++
  155. ) {
  156. args[_key - 1] = arguments[_key];
  157. }
  158. if (this._ending) {
  159. throw new Error('Farm is ended, no more calls can be done to it');
  160. }
  161. return new Promise((resolve, reject) => {
  162. const computeWorkerKey = this._options.computeWorkerKey;
  163. const workers = this._workers;
  164. const length = workers.length;
  165. const cacheKeys = this._cacheKeys;
  166. const request = [
  167. (_types || _load_types()).CHILD_MESSAGE_CALL,
  168. false,
  169. method,
  170. args
  171. ];
  172. let worker = null;
  173. let hash = null;
  174. if (computeWorkerKey) {
  175. hash = computeWorkerKey.apply(this, [method].concat(args));
  176. worker = hash == null ? null : cacheKeys[hash];
  177. }
  178. // Do not use a fat arrow since we need the "this" value, which points to
  179. // the worker that executed the call.
  180. const onProcessStart = worker => {
  181. if (hash != null) {
  182. cacheKeys[hash] = worker;
  183. }
  184. };
  185. const onProcessEnd = (error, result) => {
  186. if (error) {
  187. reject(error);
  188. } else {
  189. resolve(result);
  190. }
  191. };
  192. // If a worker is pre-selected, use it...
  193. if (worker) {
  194. worker.send(request, onProcessStart, onProcessEnd);
  195. return;
  196. }
  197. // ... otherwise use all workers, so the first one available will pick it.
  198. for (let i = 0; i < length; i++) {
  199. workers[(i + this._offset) % length].send(
  200. request,
  201. onProcessStart,
  202. onProcessEnd
  203. );
  204. }
  205. this._offset++;
  206. });
  207. }
  208. };