@atlaspack/workers 2.12.1-canary.3354
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/index.d.ts +23 -0
- package/lib/Handle.js +45 -0
- package/lib/Worker.js +188 -0
- package/lib/WorkerFarm.js +563 -0
- package/lib/backend.js +34 -0
- package/lib/bus.js +31 -0
- package/lib/child.js +294 -0
- package/lib/childState.js +14 -0
- package/lib/core-worker.browser.js +4 -0
- package/lib/core-worker.js +4 -0
- package/lib/cpuCount.js +79 -0
- package/lib/index.js +75 -0
- package/lib/process/ProcessChild.js +58 -0
- package/lib/process/ProcessWorker.js +83 -0
- package/lib/threads/ThreadsChild.js +49 -0
- package/lib/threads/ThreadsWorker.js +61 -0
- package/lib/types.js +1 -0
- package/lib/web/WebChild.js +44 -0
- package/lib/web/WebWorker.js +85 -0
- package/package.json +36 -0
- package/src/Handle.js +48 -0
- package/src/Worker.js +227 -0
- package/src/WorkerFarm.js +707 -0
- package/src/backend.js +33 -0
- package/src/bus.js +26 -0
- package/src/child.js +322 -0
- package/src/childState.js +10 -0
- package/src/core-worker.browser.js +3 -0
- package/src/core-worker.js +2 -0
- package/src/cpuCount.js +75 -0
- package/src/index.js +43 -0
- package/src/process/ProcessChild.js +56 -0
- package/src/process/ProcessWorker.js +91 -0
- package/src/threads/ThreadsChild.js +42 -0
- package/src/threads/ThreadsWorker.js +66 -0
- package/src/types.js +68 -0
- package/src/web/WebChild.js +53 -0
- package/src/web/WebWorker.js +88 -0
- package/test/cpuCount.test.js +19 -0
- package/test/integration/workerfarm/console.js +15 -0
- package/test/integration/workerfarm/echo.js +5 -0
- package/test/integration/workerfarm/ipc-pid.js +18 -0
- package/test/integration/workerfarm/ipc.js +10 -0
- package/test/integration/workerfarm/logging.js +19 -0
- package/test/integration/workerfarm/master-process-id.js +3 -0
- package/test/integration/workerfarm/master-sum.js +3 -0
- package/test/integration/workerfarm/ping.js +5 -0
- package/test/integration/workerfarm/resolve-shared-reference.js +5 -0
- package/test/integration/workerfarm/reverse-handle.js +5 -0
- package/test/integration/workerfarm/shared-reference.js +6 -0
- package/test/workerfarm.js +362 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type {ErrorWithCode, FilePath} from '@atlaspack/types-internal';
|
|
4
|
+
import type {
|
|
5
|
+
CallRequest,
|
|
6
|
+
HandleCallRequest,
|
|
7
|
+
WorkerRequest,
|
|
8
|
+
WorkerDataResponse,
|
|
9
|
+
WorkerErrorResponse,
|
|
10
|
+
BackendType,
|
|
11
|
+
} from './types';
|
|
12
|
+
import type {HandleFunction} from './Handle';
|
|
13
|
+
|
|
14
|
+
import * as coreWorker from './core-worker';
|
|
15
|
+
import * as bus from './bus';
|
|
16
|
+
import invariant from 'assert';
|
|
17
|
+
import nullthrows from 'nullthrows';
|
|
18
|
+
import EventEmitter from 'events';
|
|
19
|
+
import {
|
|
20
|
+
deserialize,
|
|
21
|
+
prepareForSerialization,
|
|
22
|
+
restoreDeserializedObject,
|
|
23
|
+
serialize,
|
|
24
|
+
} from '@atlaspack/core';
|
|
25
|
+
import ThrowableDiagnostic, {anyToDiagnostic, md} from '@atlaspack/diagnostic';
|
|
26
|
+
import Worker, {type WorkerCall} from './Worker';
|
|
27
|
+
import cpuCount from './cpuCount';
|
|
28
|
+
import Handle from './Handle';
|
|
29
|
+
import {child} from './childState';
|
|
30
|
+
import {detectBackend} from './backend';
|
|
31
|
+
import {SamplingProfiler, Trace} from '@atlaspack/profiler';
|
|
32
|
+
import fs from 'fs';
|
|
33
|
+
import logger from '@atlaspack/logger';
|
|
34
|
+
|
|
35
|
+
let referenceId = 1;
|
|
36
|
+
|
|
37
|
+
export opaque type SharedReference = number;
|
|
38
|
+
|
|
39
|
+
export type FarmOptions = {|
|
|
40
|
+
maxConcurrentWorkers: number,
|
|
41
|
+
maxConcurrentCallsPerWorker: number,
|
|
42
|
+
forcedKillTime: number,
|
|
43
|
+
useLocalWorker: boolean,
|
|
44
|
+
warmWorkers: boolean,
|
|
45
|
+
workerPath?: FilePath,
|
|
46
|
+
backend: BackendType,
|
|
47
|
+
shouldPatchConsole?: boolean,
|
|
48
|
+
shouldTrace?: boolean,
|
|
49
|
+
|};
|
|
50
|
+
|
|
51
|
+
type WorkerModule = {
|
|
52
|
+
+[string]: (...args: Array<mixed>) => Promise<mixed>,
|
|
53
|
+
...
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type WorkerApi = {|
|
|
57
|
+
callMaster(CallRequest, ?boolean): Promise<mixed>,
|
|
58
|
+
createReverseHandle(fn: HandleFunction): Handle,
|
|
59
|
+
getSharedReference(ref: SharedReference): mixed,
|
|
60
|
+
resolveSharedReference(value: mixed): ?SharedReference,
|
|
61
|
+
callChild?: (childId: number, request: HandleCallRequest) => Promise<mixed>,
|
|
62
|
+
|};
|
|
63
|
+
|
|
64
|
+
export {Handle};
|
|
65
|
+
|
|
66
|
+
const DEFAULT_MAX_CONCURRENT_CALLS: number = 30;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* workerPath should always be defined inside farmOptions
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
export default class WorkerFarm extends EventEmitter {
|
|
73
|
+
callQueue: Array<WorkerCall> = [];
|
|
74
|
+
ending: boolean = false;
|
|
75
|
+
localWorker: WorkerModule;
|
|
76
|
+
localWorkerInit: ?Promise<void>;
|
|
77
|
+
options: FarmOptions;
|
|
78
|
+
run: HandleFunction;
|
|
79
|
+
warmWorkers: number = 0;
|
|
80
|
+
readyWorkers: number = 0;
|
|
81
|
+
workers: Map<number, Worker> = new Map();
|
|
82
|
+
handles: Map<number, Handle> = new Map();
|
|
83
|
+
sharedReferences: Map<SharedReference, mixed> = new Map();
|
|
84
|
+
sharedReferencesByValue: Map<mixed, SharedReference> = new Map();
|
|
85
|
+
serializedSharedReferences: Map<SharedReference, ?ArrayBuffer> = new Map();
|
|
86
|
+
profiler: ?SamplingProfiler;
|
|
87
|
+
|
|
88
|
+
constructor(farmOptions: $Shape<FarmOptions> = {}) {
|
|
89
|
+
super();
|
|
90
|
+
this.options = {
|
|
91
|
+
maxConcurrentWorkers: WorkerFarm.getNumWorkers(),
|
|
92
|
+
maxConcurrentCallsPerWorker: WorkerFarm.getConcurrentCallsPerWorker(
|
|
93
|
+
farmOptions.shouldTrace ? 1 : DEFAULT_MAX_CONCURRENT_CALLS,
|
|
94
|
+
),
|
|
95
|
+
forcedKillTime: 500,
|
|
96
|
+
warmWorkers: false,
|
|
97
|
+
useLocalWorker: true, // TODO: setting this to false makes some tests fail, figure out why
|
|
98
|
+
backend: detectBackend(),
|
|
99
|
+
...farmOptions,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (!this.options.workerPath) {
|
|
103
|
+
throw new Error('Please provide a worker path!');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// $FlowFixMe
|
|
107
|
+
if (process.browser) {
|
|
108
|
+
if (this.options.workerPath === '@atlaspack/core/src/worker.js') {
|
|
109
|
+
this.localWorker = coreWorker;
|
|
110
|
+
} else {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'No dynamic require possible: ' + this.options.workerPath,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
// $FlowFixMe this must be dynamic
|
|
117
|
+
this.localWorker = require(this.options.workerPath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.localWorkerInit =
|
|
121
|
+
this.localWorker.childInit != null ? this.localWorker.childInit() : null;
|
|
122
|
+
|
|
123
|
+
this.run = this.createHandle('run');
|
|
124
|
+
|
|
125
|
+
// Worker thread stdout is by default piped into the process stdout, if there are enough worker
|
|
126
|
+
// threads to exceed the default listener limit, then anything else piping into stdout will trigger
|
|
127
|
+
// the `MaxListenersExceededWarning`, so we should ensure the max listeners is at least equal to the
|
|
128
|
+
// number of workers + 1 for the main thread.
|
|
129
|
+
//
|
|
130
|
+
// Note this can't be fixed easily where other things pipe into stdout - even after starting > 10 worker
|
|
131
|
+
// threads `process.stdout.getMaxListeners()` will still return 10, however adding another pipe into `stdout`
|
|
132
|
+
// will give the warning with `<worker count + 1>` as the number of listeners.
|
|
133
|
+
process.stdout?.setMaxListeners(
|
|
134
|
+
Math.max(
|
|
135
|
+
process.stdout.getMaxListeners(),
|
|
136
|
+
WorkerFarm.getNumWorkers() + 1,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
this.startMaxWorkers();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
workerApi: {|
|
|
144
|
+
callChild: (childId: number, request: HandleCallRequest) => Promise<mixed>,
|
|
145
|
+
callMaster: (
|
|
146
|
+
request: CallRequest,
|
|
147
|
+
awaitResponse?: ?boolean,
|
|
148
|
+
) => Promise<mixed>,
|
|
149
|
+
createReverseHandle: (fn: HandleFunction) => Handle,
|
|
150
|
+
getSharedReference: (ref: SharedReference) => mixed,
|
|
151
|
+
resolveSharedReference: (value: mixed) => void | SharedReference,
|
|
152
|
+
runHandle: (handle: Handle, args: Array<any>) => Promise<mixed>,
|
|
153
|
+
|} = {
|
|
154
|
+
callMaster: async (
|
|
155
|
+
request: CallRequest,
|
|
156
|
+
awaitResponse: ?boolean = true,
|
|
157
|
+
): Promise<mixed> => {
|
|
158
|
+
// $FlowFixMe
|
|
159
|
+
let result = await this.processRequest({
|
|
160
|
+
...request,
|
|
161
|
+
awaitResponse,
|
|
162
|
+
});
|
|
163
|
+
return deserialize(serialize(result));
|
|
164
|
+
},
|
|
165
|
+
createReverseHandle: (fn: HandleFunction): Handle =>
|
|
166
|
+
this.createReverseHandle(fn),
|
|
167
|
+
callChild: (childId: number, request: HandleCallRequest): Promise<mixed> =>
|
|
168
|
+
new Promise((resolve, reject) => {
|
|
169
|
+
nullthrows(this.workers.get(childId)).call({
|
|
170
|
+
...request,
|
|
171
|
+
resolve,
|
|
172
|
+
reject,
|
|
173
|
+
retries: 0,
|
|
174
|
+
});
|
|
175
|
+
}),
|
|
176
|
+
runHandle: (handle: Handle, args: Array<any>): Promise<mixed> =>
|
|
177
|
+
this.workerApi.callChild(nullthrows(handle.childId), {
|
|
178
|
+
handle: handle.id,
|
|
179
|
+
args,
|
|
180
|
+
}),
|
|
181
|
+
getSharedReference: (ref: SharedReference) =>
|
|
182
|
+
this.sharedReferences.get(ref),
|
|
183
|
+
resolveSharedReference: (value: mixed) =>
|
|
184
|
+
this.sharedReferencesByValue.get(value),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
warmupWorker(method: string, args: Array<any>): void {
|
|
188
|
+
// Workers are already stopping
|
|
189
|
+
if (this.ending) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Workers are not warmed up yet.
|
|
194
|
+
// Send the job to a remote worker in the background,
|
|
195
|
+
// but use the result from the local worker - it will be faster.
|
|
196
|
+
let promise = this.addCall(method, [...args, true]);
|
|
197
|
+
if (promise) {
|
|
198
|
+
promise
|
|
199
|
+
.then(() => {
|
|
200
|
+
this.warmWorkers++;
|
|
201
|
+
if (this.warmWorkers >= this.workers.size) {
|
|
202
|
+
this.emit('warmedup');
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
.catch(() => {});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
shouldStartRemoteWorkers(): boolean {
|
|
210
|
+
return (
|
|
211
|
+
this.options.maxConcurrentWorkers > 0 || !this.options.useLocalWorker
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
createHandle(method: string, useMainThread: boolean = false): HandleFunction {
|
|
216
|
+
if (!this.options.useLocalWorker) {
|
|
217
|
+
useMainThread = false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return async (...args) => {
|
|
221
|
+
// Child process workers are slow to start (~600ms).
|
|
222
|
+
// While we're waiting, just run on the main thread.
|
|
223
|
+
// This significantly speeds up startup time.
|
|
224
|
+
if (this.shouldUseRemoteWorkers() && !useMainThread) {
|
|
225
|
+
return this.addCall(method, [...args, false]);
|
|
226
|
+
} else {
|
|
227
|
+
if (this.options.warmWorkers && this.shouldStartRemoteWorkers()) {
|
|
228
|
+
this.warmupWorker(method, args);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let processedArgs;
|
|
232
|
+
if (!useMainThread) {
|
|
233
|
+
processedArgs = restoreDeserializedObject(
|
|
234
|
+
prepareForSerialization([...args, false]),
|
|
235
|
+
);
|
|
236
|
+
} else {
|
|
237
|
+
processedArgs = args;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (this.localWorkerInit != null) {
|
|
241
|
+
await this.localWorkerInit;
|
|
242
|
+
this.localWorkerInit = null;
|
|
243
|
+
}
|
|
244
|
+
return this.localWorker[method](this.workerApi, ...processedArgs);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
onError(error: ErrorWithCode, worker: Worker): void | Promise<void> {
|
|
250
|
+
// Handle ipc errors
|
|
251
|
+
if (error.code === 'ERR_IPC_CHANNEL_CLOSED') {
|
|
252
|
+
return this.stopWorker(worker);
|
|
253
|
+
} else {
|
|
254
|
+
logger.error(error, '@atlaspack/workers');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
startChild() {
|
|
259
|
+
let worker = new Worker({
|
|
260
|
+
forcedKillTime: this.options.forcedKillTime,
|
|
261
|
+
backend: this.options.backend,
|
|
262
|
+
shouldPatchConsole: this.options.shouldPatchConsole,
|
|
263
|
+
shouldTrace: this.options.shouldTrace,
|
|
264
|
+
sharedReferences: this.sharedReferences,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
worker.fork(nullthrows(this.options.workerPath));
|
|
268
|
+
|
|
269
|
+
worker.on('request', data => this.processRequest(data, worker));
|
|
270
|
+
|
|
271
|
+
worker.on('ready', () => {
|
|
272
|
+
this.readyWorkers++;
|
|
273
|
+
if (this.readyWorkers === this.options.maxConcurrentWorkers) {
|
|
274
|
+
this.emit('ready');
|
|
275
|
+
}
|
|
276
|
+
this.processQueue();
|
|
277
|
+
});
|
|
278
|
+
worker.on('response', () => this.processQueue());
|
|
279
|
+
|
|
280
|
+
worker.on('error', err => this.onError(err, worker));
|
|
281
|
+
worker.once('exit', () => this.stopWorker(worker));
|
|
282
|
+
|
|
283
|
+
this.workers.set(worker.id, worker);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async stopWorker(worker: Worker): Promise<void> {
|
|
287
|
+
if (!worker.stopped) {
|
|
288
|
+
this.workers.delete(worker.id);
|
|
289
|
+
|
|
290
|
+
worker.isStopping = true;
|
|
291
|
+
|
|
292
|
+
if (worker.calls.size) {
|
|
293
|
+
for (let call of worker.calls.values()) {
|
|
294
|
+
call.retries++;
|
|
295
|
+
this.callQueue.unshift(call);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
worker.calls.clear();
|
|
300
|
+
|
|
301
|
+
await worker.stop();
|
|
302
|
+
|
|
303
|
+
// Process any requests that failed and start a new worker
|
|
304
|
+
this.processQueue();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
processQueue(): void {
|
|
309
|
+
if (this.ending || !this.callQueue.length) return;
|
|
310
|
+
|
|
311
|
+
if (this.workers.size < this.options.maxConcurrentWorkers) {
|
|
312
|
+
this.startChild();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let workers = [...this.workers.values()].sort(
|
|
316
|
+
(a, b) => a.calls.size - b.calls.size,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
for (let worker of workers) {
|
|
320
|
+
if (!this.callQueue.length) {
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!worker.ready || worker.stopped || worker.isStopping) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (worker.calls.size < this.options.maxConcurrentCallsPerWorker) {
|
|
329
|
+
this.callWorker(worker, this.callQueue.shift());
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async callWorker(worker: Worker, call: WorkerCall): Promise<void> {
|
|
335
|
+
for (let ref of this.sharedReferences.keys()) {
|
|
336
|
+
if (!worker.sentSharedReferences.has(ref)) {
|
|
337
|
+
await worker.sendSharedReference(
|
|
338
|
+
ref,
|
|
339
|
+
this.getSerializedSharedReference(ref),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
worker.call(call);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async processRequest(
|
|
348
|
+
data: {|
|
|
349
|
+
location: FilePath,
|
|
350
|
+
|} & $Shape<WorkerRequest>,
|
|
351
|
+
worker?: Worker,
|
|
352
|
+
): Promise<?string> {
|
|
353
|
+
let {method, args, location, awaitResponse, idx, handle: handleId} = data;
|
|
354
|
+
let mod;
|
|
355
|
+
if (handleId != null) {
|
|
356
|
+
mod = nullthrows(this.handles.get(handleId)?.fn);
|
|
357
|
+
} else if (location) {
|
|
358
|
+
// $FlowFixMe
|
|
359
|
+
if (process.browser) {
|
|
360
|
+
if (location === '@atlaspack/workers/src/bus.js') {
|
|
361
|
+
mod = (bus: any);
|
|
362
|
+
} else {
|
|
363
|
+
throw new Error('No dynamic require possible: ' + location);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
// $FlowFixMe this must be dynamic
|
|
367
|
+
mod = require(location);
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
throw new Error('Unknown request');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const responseFromContent = (content: any): WorkerDataResponse => ({
|
|
374
|
+
idx,
|
|
375
|
+
type: 'response',
|
|
376
|
+
contentType: 'data',
|
|
377
|
+
content,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const errorResponseFromError = (e: Error): WorkerErrorResponse => ({
|
|
381
|
+
idx,
|
|
382
|
+
type: 'response',
|
|
383
|
+
contentType: 'error',
|
|
384
|
+
content: anyToDiagnostic(e),
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
let result;
|
|
388
|
+
if (method == null) {
|
|
389
|
+
try {
|
|
390
|
+
result = responseFromContent(await mod(...args));
|
|
391
|
+
} catch (e) {
|
|
392
|
+
result = errorResponseFromError(e);
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
// ESModule default interop
|
|
396
|
+
if (mod.__esModule && !mod[method] && mod.default) {
|
|
397
|
+
mod = mod.default;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
// $FlowFixMe
|
|
402
|
+
result = responseFromContent(await mod[method](...args));
|
|
403
|
+
} catch (e) {
|
|
404
|
+
result = errorResponseFromError(e);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (awaitResponse) {
|
|
409
|
+
if (worker) {
|
|
410
|
+
worker.send(result);
|
|
411
|
+
} else {
|
|
412
|
+
if (result.contentType === 'error') {
|
|
413
|
+
throw new ThrowableDiagnostic({diagnostic: result.content});
|
|
414
|
+
}
|
|
415
|
+
return result.content;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
addCall(method: string, args: Array<any>): Promise<any> {
|
|
421
|
+
if (this.ending) {
|
|
422
|
+
throw new Error('Cannot add a worker call if workerfarm is ending.');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
this.callQueue.push({
|
|
427
|
+
method,
|
|
428
|
+
args: args,
|
|
429
|
+
retries: 0,
|
|
430
|
+
resolve,
|
|
431
|
+
reject,
|
|
432
|
+
});
|
|
433
|
+
this.processQueue();
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async end(): Promise<void> {
|
|
438
|
+
this.ending = true;
|
|
439
|
+
|
|
440
|
+
await Promise.all(
|
|
441
|
+
Array.from(this.workers.values()).map(worker => this.stopWorker(worker)),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
for (let handle of this.handles.values()) {
|
|
445
|
+
handle.dispose();
|
|
446
|
+
}
|
|
447
|
+
this.handles = new Map();
|
|
448
|
+
this.sharedReferences = new Map();
|
|
449
|
+
this.sharedReferencesByValue = new Map();
|
|
450
|
+
|
|
451
|
+
this.ending = false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
startMaxWorkers(): void {
|
|
455
|
+
// Starts workers until the maximum is reached
|
|
456
|
+
if (this.workers.size < this.options.maxConcurrentWorkers) {
|
|
457
|
+
let toStart = this.options.maxConcurrentWorkers - this.workers.size;
|
|
458
|
+
while (toStart--) {
|
|
459
|
+
this.startChild();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
shouldUseRemoteWorkers(): boolean {
|
|
465
|
+
return (
|
|
466
|
+
!this.options.useLocalWorker ||
|
|
467
|
+
((this.warmWorkers >= this.workers.size || !this.options.warmWorkers) &&
|
|
468
|
+
this.options.maxConcurrentWorkers > 0)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
createReverseHandle(fn: HandleFunction): Handle {
|
|
473
|
+
let handle = new Handle({fn});
|
|
474
|
+
this.handles.set(handle.id, handle);
|
|
475
|
+
return handle;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
createSharedReference(
|
|
479
|
+
value: mixed,
|
|
480
|
+
isCacheable: boolean = true,
|
|
481
|
+
): {|ref: SharedReference, dispose(): Promise<mixed>|} {
|
|
482
|
+
let ref = referenceId++;
|
|
483
|
+
this.sharedReferences.set(ref, value);
|
|
484
|
+
this.sharedReferencesByValue.set(value, ref);
|
|
485
|
+
if (!isCacheable) {
|
|
486
|
+
this.serializedSharedReferences.set(ref, null);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
ref,
|
|
491
|
+
dispose: () => {
|
|
492
|
+
this.sharedReferences.delete(ref);
|
|
493
|
+
this.sharedReferencesByValue.delete(value);
|
|
494
|
+
this.serializedSharedReferences.delete(ref);
|
|
495
|
+
|
|
496
|
+
let promises = [];
|
|
497
|
+
for (let worker of this.workers.values()) {
|
|
498
|
+
if (!worker.sentSharedReferences.has(ref)) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
worker.sentSharedReferences.delete(ref);
|
|
503
|
+
promises.push(
|
|
504
|
+
new Promise((resolve, reject) => {
|
|
505
|
+
worker.call({
|
|
506
|
+
method: 'deleteSharedReference',
|
|
507
|
+
args: [ref],
|
|
508
|
+
resolve,
|
|
509
|
+
reject,
|
|
510
|
+
skipReadyCheck: true,
|
|
511
|
+
retries: 0,
|
|
512
|
+
});
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return Promise.all(promises);
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
getSerializedSharedReference(ref: SharedReference): ArrayBuffer {
|
|
522
|
+
let cached = this.serializedSharedReferences.get(ref);
|
|
523
|
+
if (cached) {
|
|
524
|
+
return cached;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let value = this.sharedReferences.get(ref);
|
|
528
|
+
let buf = serialize(value).buffer;
|
|
529
|
+
|
|
530
|
+
// If the reference was created with the isCacheable option set to false,
|
|
531
|
+
// serializedSharedReferences will contain `null` as the value.
|
|
532
|
+
if (cached !== null) {
|
|
533
|
+
this.serializedSharedReferences.set(ref, buf);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return buf;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async startProfile() {
|
|
540
|
+
let promises = [];
|
|
541
|
+
for (let worker of this.workers.values()) {
|
|
542
|
+
promises.push(
|
|
543
|
+
new Promise((resolve, reject) => {
|
|
544
|
+
worker.call({
|
|
545
|
+
method: 'startProfile',
|
|
546
|
+
args: [],
|
|
547
|
+
resolve,
|
|
548
|
+
reject,
|
|
549
|
+
retries: 0,
|
|
550
|
+
skipReadyCheck: true,
|
|
551
|
+
});
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
this.profiler = new SamplingProfiler();
|
|
557
|
+
|
|
558
|
+
promises.push(this.profiler.startProfiling());
|
|
559
|
+
await Promise.all(promises);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async endProfile() {
|
|
563
|
+
if (!this.profiler) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let promises = [this.profiler.stopProfiling()];
|
|
568
|
+
let names = ['Master'];
|
|
569
|
+
|
|
570
|
+
for (let worker of this.workers.values()) {
|
|
571
|
+
names.push('Worker ' + worker.id);
|
|
572
|
+
promises.push(
|
|
573
|
+
new Promise((resolve, reject) => {
|
|
574
|
+
worker.call({
|
|
575
|
+
method: 'endProfile',
|
|
576
|
+
args: [],
|
|
577
|
+
resolve,
|
|
578
|
+
reject,
|
|
579
|
+
retries: 0,
|
|
580
|
+
skipReadyCheck: true,
|
|
581
|
+
});
|
|
582
|
+
}),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
var profiles = await Promise.all(promises);
|
|
587
|
+
let trace = new Trace();
|
|
588
|
+
let filename = `profile-${getTimeId()}.trace`;
|
|
589
|
+
let stream = trace.pipe(fs.createWriteStream(filename));
|
|
590
|
+
|
|
591
|
+
for (let profile of profiles) {
|
|
592
|
+
trace.addCPUProfile(names.shift(), profile);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
trace.flush();
|
|
596
|
+
await new Promise(resolve => {
|
|
597
|
+
stream.once('finish', resolve);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
logger.info({
|
|
601
|
+
origin: '@atlaspack/workers',
|
|
602
|
+
message: md`Wrote profile to ${filename}`,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async callAllWorkers(method: string, args: Array<any>) {
|
|
607
|
+
let promises = [];
|
|
608
|
+
for (let worker of this.workers.values()) {
|
|
609
|
+
promises.push(
|
|
610
|
+
new Promise((resolve, reject) => {
|
|
611
|
+
worker.call({
|
|
612
|
+
method,
|
|
613
|
+
args,
|
|
614
|
+
resolve,
|
|
615
|
+
reject,
|
|
616
|
+
retries: 0,
|
|
617
|
+
});
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
promises.push(this.localWorker[method](this.workerApi, ...args));
|
|
623
|
+
await Promise.all(promises);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async takeHeapSnapshot() {
|
|
627
|
+
let snapshotId = getTimeId();
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
let snapshotPaths = await Promise.all(
|
|
631
|
+
[...this.workers.values()].map(
|
|
632
|
+
worker =>
|
|
633
|
+
new Promise((resolve, reject) => {
|
|
634
|
+
worker.call({
|
|
635
|
+
method: 'takeHeapSnapshot',
|
|
636
|
+
args: [snapshotId],
|
|
637
|
+
resolve,
|
|
638
|
+
reject,
|
|
639
|
+
retries: 0,
|
|
640
|
+
skipReadyCheck: true,
|
|
641
|
+
});
|
|
642
|
+
}),
|
|
643
|
+
),
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
logger.info({
|
|
647
|
+
origin: '@atlaspack/workers',
|
|
648
|
+
message: md`Wrote heap snapshots to the following paths:\n${snapshotPaths.join(
|
|
649
|
+
'\n',
|
|
650
|
+
)}`,
|
|
651
|
+
});
|
|
652
|
+
} catch {
|
|
653
|
+
logger.error({
|
|
654
|
+
origin: '@atlaspack/workers',
|
|
655
|
+
message: 'Unable to take heap snapshots. Note: requires Node 11.13.0+',
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
static getNumWorkers(): number {
|
|
661
|
+
return process.env.ATLASPACK_WORKERS
|
|
662
|
+
? parseInt(process.env.ATLASPACK_WORKERS, 10)
|
|
663
|
+
: Math.min(4, Math.ceil(cpuCount() / 2));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
static isWorker(): boolean {
|
|
667
|
+
return !!child;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
static getWorkerApi(): {|
|
|
671
|
+
callMaster: (
|
|
672
|
+
request: CallRequest,
|
|
673
|
+
awaitResponse?: ?boolean,
|
|
674
|
+
) => Promise<mixed>,
|
|
675
|
+
createReverseHandle: (fn: (...args: Array<any>) => mixed) => Handle,
|
|
676
|
+
getSharedReference: (ref: SharedReference) => mixed,
|
|
677
|
+
resolveSharedReference: (value: mixed) => void | SharedReference,
|
|
678
|
+
runHandle: (handle: Handle, args: Array<any>) => Promise<mixed>,
|
|
679
|
+
|} {
|
|
680
|
+
invariant(
|
|
681
|
+
child != null,
|
|
682
|
+
'WorkerFarm.getWorkerApi can only be called within workers',
|
|
683
|
+
);
|
|
684
|
+
return child.workerApi;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
static getConcurrentCallsPerWorker(
|
|
688
|
+
defaultValue?: number = DEFAULT_MAX_CONCURRENT_CALLS,
|
|
689
|
+
): number {
|
|
690
|
+
return (
|
|
691
|
+
parseInt(process.env.ATLASPACK_MAX_CONCURRENT_CALLS, 10) || defaultValue
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function getTimeId() {
|
|
697
|
+
let now = new Date();
|
|
698
|
+
return (
|
|
699
|
+
String(now.getFullYear()) +
|
|
700
|
+
String(now.getMonth() + 1).padStart(2, '0') +
|
|
701
|
+
String(now.getDate()).padStart(2, '0') +
|
|
702
|
+
'-' +
|
|
703
|
+
String(now.getHours()).padStart(2, '0') +
|
|
704
|
+
String(now.getMinutes()).padStart(2, '0') +
|
|
705
|
+
String(now.getSeconds()).padStart(2, '0')
|
|
706
|
+
);
|
|
707
|
+
}
|