UNPKG

10.5 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', {
4 value: true
5});
6exports.default = void 0;
7
8function _os() {
9 const data = require('os');
10
11 _os = function () {
12 return data;
13 };
14
15 return data;
16}
17
18function _worker_threads() {
19 const data = require('worker_threads');
20
21 _worker_threads = function () {
22 return data;
23 };
24
25 return data;
26}
27
28function _mergeStream() {
29 const data = _interopRequireDefault(require('merge-stream'));
30
31 _mergeStream = function () {
32 return data;
33 };
34
35 return data;
36}
37
38var _types = require('../types');
39
40var _WorkerAbstract = _interopRequireDefault(require('./WorkerAbstract'));
41
42function _interopRequireDefault(obj) {
43 return obj && obj.__esModule ? obj : {default: obj};
44}
45
46/**
47 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
48 *
49 * This source code is licensed under the MIT license found in the
50 * LICENSE file in the root directory of this source tree.
51 */
52class ExperimentalWorker extends _WorkerAbstract.default {
53 _worker;
54 _options;
55 _request;
56 _retries;
57 _onProcessEnd;
58 _onCustomMessage;
59 _stdout;
60 _stderr;
61 _memoryUsagePromise;
62 _resolveMemoryUsage;
63 _childWorkerPath;
64 _childIdleMemoryUsage;
65 _childIdleMemoryUsageLimit;
66 _memoryUsageCheck = false;
67
68 constructor(options) {
69 super(options);
70 this._options = options;
71 this._request = null;
72 this._stdout = null;
73 this._stderr = null;
74 this._childWorkerPath =
75 options.childWorkerPath || require.resolve('./threadChild');
76 this._childIdleMemoryUsage = null;
77 this._childIdleMemoryUsageLimit = options.idleMemoryLimit || null;
78 this.initialize();
79 }
80
81 initialize() {
82 if (
83 this.state === _types.WorkerStates.OUT_OF_MEMORY ||
84 this.state === _types.WorkerStates.SHUTTING_DOWN ||
85 this.state === _types.WorkerStates.SHUT_DOWN
86 ) {
87 return;
88 }
89
90 if (this._worker) {
91 this._worker.terminate();
92 }
93
94 this.state = _types.WorkerStates.STARTING;
95 this._worker = new (_worker_threads().Worker)(this._childWorkerPath, {
96 eval: false,
97 resourceLimits: this._options.resourceLimits,
98 stderr: true,
99 stdout: true,
100 workerData: this._options.workerData,
101 ...this._options.forkOptions
102 });
103
104 if (this._worker.stdout) {
105 if (!this._stdout) {
106 // We need to add a permanent stream to the merged stream to prevent it
107 // from ending when the subprocess stream ends
108 this._stdout = (0, _mergeStream().default)(this._getFakeStream());
109 }
110
111 this._stdout.add(this._worker.stdout);
112 }
113
114 if (this._worker.stderr) {
115 if (!this._stderr) {
116 // We need to add a permanent stream to the merged stream to prevent it
117 // from ending when the subprocess stream ends
118 this._stderr = (0, _mergeStream().default)(this._getFakeStream());
119 }
120
121 this._stderr.add(this._worker.stderr);
122 } // This can be useful for debugging.
123
124 if (!(this._options.silent ?? true)) {
125 this._worker.stdout.setEncoding('utf8'); // eslint-disable-next-line no-console
126
127 this._worker.stdout.on('data', console.log);
128
129 this._worker.stderr.setEncoding('utf8');
130
131 this._worker.stderr.on('data', console.error);
132 }
133
134 this._worker.on('message', this._onMessage.bind(this));
135
136 this._worker.on('exit', this._onExit.bind(this));
137
138 this._worker.on('error', this._onError.bind(this));
139
140 this._worker.postMessage([
141 _types.CHILD_MESSAGE_INITIALIZE,
142 false,
143 this._options.workerPath,
144 this._options.setupArgs,
145 String(this._options.workerId + 1) // 0-indexed workerId, 1-indexed JEST_WORKER_ID
146 ]);
147
148 this._retries++; // If we exceeded the amount of retries, we will emulate an error reply
149 // coming from the child. This avoids code duplication related with cleaning
150 // the queue, and scheduling the next call.
151
152 if (this._retries > this._options.maxRetries) {
153 const error = new Error('Call retries were exceeded');
154
155 this._onMessage([
156 _types.PARENT_MESSAGE_CLIENT_ERROR,
157 error.name,
158 error.message,
159 error.stack,
160 {
161 type: 'WorkerError'
162 }
163 ]);
164 }
165
166 this.state = _types.WorkerStates.OK;
167
168 if (this._resolveWorkerReady) {
169 this._resolveWorkerReady();
170 }
171 }
172
173 _onError(error) {
174 if (error.message.includes('heap out of memory')) {
175 this.state = _types.WorkerStates.OUT_OF_MEMORY; // Threads don't behave like processes, they don't crash when they run out of
176 // memory. But for consistency we want them to behave like processes so we call
177 // terminate to simulate a crash happening that was not planned
178
179 this._worker.terminate();
180 }
181 }
182
183 _onMessage(response) {
184 let error;
185
186 switch (response[0]) {
187 case _types.PARENT_MESSAGE_OK:
188 this._onProcessEnd(null, response[1]);
189
190 break;
191
192 case _types.PARENT_MESSAGE_CLIENT_ERROR:
193 error = response[4];
194
195 if (error != null && typeof error === 'object') {
196 const extra = error; // @ts-expect-error: no index
197
198 const NativeCtor = globalThis[response[1]];
199 const Ctor = typeof NativeCtor === 'function' ? NativeCtor : Error;
200 error = new Ctor(response[2]);
201 error.type = response[1];
202 error.stack = response[3];
203
204 for (const key in extra) {
205 // @ts-expect-error: no index
206 error[key] = extra[key];
207 }
208 }
209
210 this._onProcessEnd(error, null);
211
212 break;
213
214 case _types.PARENT_MESSAGE_SETUP_ERROR:
215 error = new Error(`Error when calling setup: ${response[2]}`); // @ts-expect-error: adding custom properties to errors.
216
217 error.type = response[1];
218 error.stack = response[3];
219
220 this._onProcessEnd(error, null);
221
222 break;
223
224 case _types.PARENT_MESSAGE_CUSTOM:
225 this._onCustomMessage(response[1]);
226
227 break;
228
229 case _types.PARENT_MESSAGE_MEM_USAGE:
230 this._childIdleMemoryUsage = response[1];
231
232 if (this._resolveMemoryUsage) {
233 this._resolveMemoryUsage(response[1]);
234
235 this._resolveMemoryUsage = undefined;
236 this._memoryUsagePromise = undefined;
237 }
238
239 this._performRestartIfRequired();
240
241 break;
242
243 default:
244 throw new TypeError(`Unexpected response from worker: ${response[0]}`);
245 }
246 }
247
248 _onExit(exitCode) {
249 this._workerReadyPromise = undefined;
250 this._resolveWorkerReady = undefined;
251
252 if (exitCode !== 0 && this.state === _types.WorkerStates.OUT_OF_MEMORY) {
253 this._onProcessEnd(
254 new Error('Jest worker ran out of memory and crashed'),
255 null
256 );
257
258 this._shutdown();
259 } else if (
260 (exitCode !== 0 &&
261 this.state !== _types.WorkerStates.SHUTTING_DOWN &&
262 this.state !== _types.WorkerStates.SHUT_DOWN) ||
263 this.state === _types.WorkerStates.RESTARTING
264 ) {
265 this.initialize();
266
267 if (this._request) {
268 this._worker.postMessage(this._request);
269 }
270 } else {
271 this._shutdown();
272 }
273 }
274
275 waitForExit() {
276 return this._exitPromise;
277 }
278
279 forceExit() {
280 this.state = _types.WorkerStates.SHUTTING_DOWN;
281
282 this._worker.terminate();
283 }
284
285 send(request, onProcessStart, onProcessEnd, onCustomMessage) {
286 onProcessStart(this);
287
288 this._onProcessEnd = (...args) => {
289 const hasRequest = !!this._request; // Clean the request to avoid sending past requests to workers that fail
290 // while waiting for a new request (timers, unhandled rejections...)
291
292 this._request = null;
293
294 if (this._childIdleMemoryUsageLimit && hasRequest) {
295 this.checkMemoryUsage();
296 }
297
298 const res = onProcessEnd?.(...args); // Clean up the reference so related closures can be garbage collected.
299
300 onProcessEnd = null;
301 return res;
302 };
303
304 this._onCustomMessage = (...arg) => onCustomMessage(...arg);
305
306 this._request = request;
307 this._retries = 0;
308
309 this._worker.postMessage(request);
310 }
311
312 getWorkerId() {
313 return this._options.workerId;
314 }
315
316 getStdout() {
317 return this._stdout;
318 }
319
320 getStderr() {
321 return this._stderr;
322 }
323
324 _performRestartIfRequired() {
325 if (this._memoryUsageCheck) {
326 this._memoryUsageCheck = false;
327 let limit = this._childIdleMemoryUsageLimit; // TODO: At some point it would make sense to make use of
328 // stringToBytes found in jest-config, however as this
329 // package does not have any dependencies on an other jest
330 // packages that can wait until some other time.
331
332 if (limit && limit > 0 && limit <= 1) {
333 limit = Math.floor((0, _os().totalmem)() * limit);
334 } else if (limit) {
335 limit = Math.floor(limit);
336 }
337
338 if (
339 limit &&
340 this._childIdleMemoryUsage &&
341 this._childIdleMemoryUsage > limit
342 ) {
343 this.state = _types.WorkerStates.RESTARTING;
344
345 this._worker.terminate();
346 }
347 }
348 }
349 /**
350 * Gets the last reported memory usage.
351 *
352 * @returns Memory usage in bytes.
353 */
354
355 getMemoryUsage() {
356 if (!this._memoryUsagePromise) {
357 let rejectCallback;
358 const promise = new Promise((resolve, reject) => {
359 this._resolveMemoryUsage = resolve;
360 rejectCallback = reject;
361 });
362 this._memoryUsagePromise = promise;
363
364 if (!this._worker.threadId) {
365 rejectCallback(new Error('Child process is not running.'));
366 this._memoryUsagePromise = undefined;
367 this._resolveMemoryUsage = undefined;
368 return promise;
369 }
370
371 try {
372 this._worker.postMessage([_types.CHILD_MESSAGE_MEM_USAGE]);
373 } catch (err) {
374 this._memoryUsagePromise = undefined;
375 this._resolveMemoryUsage = undefined;
376 rejectCallback(err);
377 }
378
379 return promise;
380 }
381
382 return this._memoryUsagePromise;
383 }
384 /**
385 * Gets updated memory usage and restarts if required
386 */
387
388 checkMemoryUsage() {
389 if (this._childIdleMemoryUsageLimit) {
390 this._memoryUsageCheck = true;
391
392 this._worker.postMessage([_types.CHILD_MESSAGE_MEM_USAGE]);
393 } else {
394 console.warn(
395 'Memory usage of workers can only be checked if a limit is set'
396 );
397 }
398 }
399 /**
400 * Gets the thread id of the worker.
401 *
402 * @returns Thread id.
403 */
404
405 getWorkerSystemId() {
406 return this._worker.threadId;
407 }
408
409 isWorkerRunning() {
410 return this._worker.threadId >= 0;
411 }
412}
413
414exports.default = ExperimentalWorker;