@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
package/src/backend.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {BackendType, WorkerImpl} from './types';
|
|
3
|
+
|
|
4
|
+
export function detectBackend(): BackendType {
|
|
5
|
+
// $FlowFixMe
|
|
6
|
+
if (process.browser) return 'web';
|
|
7
|
+
|
|
8
|
+
switch (process.env.ATLASPACK_WORKER_BACKEND) {
|
|
9
|
+
case 'threads':
|
|
10
|
+
case 'process':
|
|
11
|
+
return process.env.ATLASPACK_WORKER_BACKEND;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
require('worker_threads');
|
|
16
|
+
return 'threads';
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return 'process';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getWorkerBackend(backend: BackendType): Class<WorkerImpl> {
|
|
23
|
+
switch (backend) {
|
|
24
|
+
case 'threads':
|
|
25
|
+
return require('./threads/ThreadsWorker').default;
|
|
26
|
+
case 'process':
|
|
27
|
+
return require('./process/ProcessWorker').default;
|
|
28
|
+
case 'web':
|
|
29
|
+
return require('./web/WebWorker').default;
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Invalid backend: ${backend}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/bus.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import EventEmitter from 'events';
|
|
3
|
+
import {child} from './childState';
|
|
4
|
+
|
|
5
|
+
class Bus extends EventEmitter {
|
|
6
|
+
emit(event: string, ...args: Array<any>): boolean {
|
|
7
|
+
if (child) {
|
|
8
|
+
child.workerApi.callMaster(
|
|
9
|
+
{
|
|
10
|
+
// $FlowFixMe
|
|
11
|
+
location: process.browser
|
|
12
|
+
? '@atlaspack/workers/src/bus.js'
|
|
13
|
+
: __filename,
|
|
14
|
+
method: 'emit',
|
|
15
|
+
args: [event, ...args],
|
|
16
|
+
},
|
|
17
|
+
false,
|
|
18
|
+
);
|
|
19
|
+
return true;
|
|
20
|
+
} else {
|
|
21
|
+
return super.emit(event, ...args);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default (new Bus(): Bus);
|
package/src/child.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CallRequest,
|
|
5
|
+
WorkerDataResponse,
|
|
6
|
+
WorkerErrorResponse,
|
|
7
|
+
WorkerMessage,
|
|
8
|
+
WorkerRequest,
|
|
9
|
+
WorkerResponse,
|
|
10
|
+
ChildImpl,
|
|
11
|
+
} from './types';
|
|
12
|
+
import type {Async, IDisposable} from '@atlaspack/types-internal';
|
|
13
|
+
import type {SharedReference} from './WorkerFarm';
|
|
14
|
+
|
|
15
|
+
import * as coreWorker from './core-worker';
|
|
16
|
+
import invariant from 'assert';
|
|
17
|
+
import nullthrows from 'nullthrows';
|
|
18
|
+
import Logger, {patchConsole, unpatchConsole} from '@atlaspack/logger';
|
|
19
|
+
import ThrowableDiagnostic, {anyToDiagnostic} from '@atlaspack/diagnostic';
|
|
20
|
+
import {deserialize} from '@atlaspack/core';
|
|
21
|
+
import bus from './bus';
|
|
22
|
+
import {SamplingProfiler, tracer} from '@atlaspack/profiler';
|
|
23
|
+
import _Handle from './Handle';
|
|
24
|
+
|
|
25
|
+
// The import of './Handle' should really be imported eagerly (with @babel/plugin-transform-modules-commonjs's lazy mode).
|
|
26
|
+
const Handle = _Handle;
|
|
27
|
+
|
|
28
|
+
type ChildCall = WorkerRequest & {|
|
|
29
|
+
resolve: (result: Promise<any> | any) => void,
|
|
30
|
+
reject: (error: any) => void,
|
|
31
|
+
|};
|
|
32
|
+
|
|
33
|
+
export class Child {
|
|
34
|
+
callQueue: Array<ChildCall> = [];
|
|
35
|
+
childId: ?number;
|
|
36
|
+
maxConcurrentCalls: number = 10;
|
|
37
|
+
module: ?any;
|
|
38
|
+
responseId: number = 0;
|
|
39
|
+
responseQueue: Map<number, ChildCall> = new Map();
|
|
40
|
+
loggerDisposable: IDisposable;
|
|
41
|
+
tracerDisposable: IDisposable;
|
|
42
|
+
child: ChildImpl;
|
|
43
|
+
profiler: ?SamplingProfiler;
|
|
44
|
+
handles: Map<number, Handle> = new Map();
|
|
45
|
+
sharedReferences: Map<SharedReference, mixed> = new Map();
|
|
46
|
+
sharedReferencesByValue: Map<mixed, SharedReference> = new Map();
|
|
47
|
+
|
|
48
|
+
constructor(ChildBackend: Class<ChildImpl>) {
|
|
49
|
+
this.child = new ChildBackend(
|
|
50
|
+
m => {
|
|
51
|
+
this.messageListener(m);
|
|
52
|
+
},
|
|
53
|
+
() => this.handleEnd(),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Monitior all logging events inside this child process and forward to
|
|
57
|
+
// the main process via the bus.
|
|
58
|
+
this.loggerDisposable = Logger.onLog(event => {
|
|
59
|
+
bus.emit('logEvent', event);
|
|
60
|
+
});
|
|
61
|
+
// .. and do the same for trace events
|
|
62
|
+
this.tracerDisposable = tracer.onTrace(event => {
|
|
63
|
+
bus.emit('traceEvent', event);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
workerApi: {|
|
|
68
|
+
callMaster: (
|
|
69
|
+
request: CallRequest,
|
|
70
|
+
awaitResponse?: ?boolean,
|
|
71
|
+
) => Promise<mixed>,
|
|
72
|
+
createReverseHandle: (fn: (...args: Array<any>) => mixed) => Handle,
|
|
73
|
+
getSharedReference: (ref: SharedReference) => mixed,
|
|
74
|
+
resolveSharedReference: (value: mixed) => void | SharedReference,
|
|
75
|
+
runHandle: (handle: Handle, args: Array<any>) => Promise<mixed>,
|
|
76
|
+
|} = {
|
|
77
|
+
callMaster: (
|
|
78
|
+
request: CallRequest,
|
|
79
|
+
awaitResponse: ?boolean = true,
|
|
80
|
+
): Promise<mixed> => this.addCall(request, awaitResponse),
|
|
81
|
+
createReverseHandle: (fn: (...args: Array<any>) => mixed): Handle =>
|
|
82
|
+
this.createReverseHandle(fn),
|
|
83
|
+
runHandle: (handle: Handle, args: Array<any>): Promise<mixed> =>
|
|
84
|
+
this.workerApi.callMaster({handle: handle.id, args}, true),
|
|
85
|
+
getSharedReference: (ref: SharedReference) =>
|
|
86
|
+
this.sharedReferences.get(ref),
|
|
87
|
+
resolveSharedReference: (value: mixed) =>
|
|
88
|
+
this.sharedReferencesByValue.get(value),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
messageListener(message: WorkerMessage): Async<void> {
|
|
92
|
+
if (message.type === 'response') {
|
|
93
|
+
return this.handleResponse(message);
|
|
94
|
+
} else if (message.type === 'request') {
|
|
95
|
+
return this.handleRequest(message);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
send(data: WorkerMessage): void {
|
|
100
|
+
this.child.send(data);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async childInit(module: string, childId: number): Promise<void> {
|
|
104
|
+
// $FlowFixMe
|
|
105
|
+
if (process.browser) {
|
|
106
|
+
if (module === '@atlaspack/core/src/worker.js') {
|
|
107
|
+
this.module = coreWorker;
|
|
108
|
+
} else {
|
|
109
|
+
throw new Error('No dynamic require possible: ' + module);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// $FlowFixMe this must be dynamic
|
|
113
|
+
this.module = require(module);
|
|
114
|
+
}
|
|
115
|
+
this.childId = childId;
|
|
116
|
+
|
|
117
|
+
if (this.module.childInit != null) {
|
|
118
|
+
await this.module.childInit();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async handleRequest(data: WorkerRequest): Promise<void> {
|
|
123
|
+
let {idx, method, args, handle: handleId} = data;
|
|
124
|
+
let child = nullthrows(data.child);
|
|
125
|
+
|
|
126
|
+
const responseFromContent = (content: any): WorkerDataResponse => ({
|
|
127
|
+
idx,
|
|
128
|
+
child,
|
|
129
|
+
type: 'response',
|
|
130
|
+
contentType: 'data',
|
|
131
|
+
content,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const errorResponseFromError = (e: Error): WorkerErrorResponse => ({
|
|
135
|
+
idx,
|
|
136
|
+
child,
|
|
137
|
+
type: 'response',
|
|
138
|
+
contentType: 'error',
|
|
139
|
+
content: anyToDiagnostic(e),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let result;
|
|
143
|
+
if (handleId != null) {
|
|
144
|
+
try {
|
|
145
|
+
let fn = nullthrows(this.handles.get(handleId)?.fn);
|
|
146
|
+
result = responseFromContent(fn(...args));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
result = errorResponseFromError(e);
|
|
149
|
+
}
|
|
150
|
+
} else if (method === 'childInit') {
|
|
151
|
+
try {
|
|
152
|
+
let [moduleName, childOptions] = args;
|
|
153
|
+
if (childOptions.shouldPatchConsole) {
|
|
154
|
+
patchConsole();
|
|
155
|
+
} else {
|
|
156
|
+
unpatchConsole();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (childOptions.shouldTrace) {
|
|
160
|
+
tracer.enable();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
result = responseFromContent(await this.childInit(moduleName, child));
|
|
164
|
+
} catch (e) {
|
|
165
|
+
result = errorResponseFromError(e);
|
|
166
|
+
}
|
|
167
|
+
} else if (method === 'startProfile') {
|
|
168
|
+
this.profiler = new SamplingProfiler();
|
|
169
|
+
try {
|
|
170
|
+
result = responseFromContent(await this.profiler.startProfiling());
|
|
171
|
+
} catch (e) {
|
|
172
|
+
result = errorResponseFromError(e);
|
|
173
|
+
}
|
|
174
|
+
} else if (method === 'endProfile') {
|
|
175
|
+
try {
|
|
176
|
+
let res = this.profiler ? await this.profiler.stopProfiling() : null;
|
|
177
|
+
result = responseFromContent(res);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
result = errorResponseFromError(e);
|
|
180
|
+
}
|
|
181
|
+
} else if (method === 'takeHeapSnapshot') {
|
|
182
|
+
try {
|
|
183
|
+
let v8 = require('v8');
|
|
184
|
+
result = responseFromContent(
|
|
185
|
+
v8.writeHeapSnapshot(
|
|
186
|
+
'heap-' +
|
|
187
|
+
args[0] +
|
|
188
|
+
'-' +
|
|
189
|
+
(this.childId ? 'worker' + this.childId : 'main') +
|
|
190
|
+
'.heapsnapshot',
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
result = errorResponseFromError(e);
|
|
195
|
+
}
|
|
196
|
+
} else if (method === 'createSharedReference') {
|
|
197
|
+
let [ref, _value] = args;
|
|
198
|
+
let value =
|
|
199
|
+
_value instanceof ArrayBuffer
|
|
200
|
+
? // In the case the value is pre-serialized as a buffer,
|
|
201
|
+
// deserialize it.
|
|
202
|
+
deserialize(Buffer.from(_value))
|
|
203
|
+
: _value;
|
|
204
|
+
this.sharedReferences.set(ref, value);
|
|
205
|
+
this.sharedReferencesByValue.set(value, ref);
|
|
206
|
+
result = responseFromContent(null);
|
|
207
|
+
} else if (method === 'deleteSharedReference') {
|
|
208
|
+
let ref = args[0];
|
|
209
|
+
let value = this.sharedReferences.get(ref);
|
|
210
|
+
this.sharedReferencesByValue.delete(value);
|
|
211
|
+
this.sharedReferences.delete(ref);
|
|
212
|
+
result = responseFromContent(null);
|
|
213
|
+
} else {
|
|
214
|
+
try {
|
|
215
|
+
result = responseFromContent(
|
|
216
|
+
// $FlowFixMe
|
|
217
|
+
await this.module[method](this.workerApi, ...args),
|
|
218
|
+
);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
result = errorResponseFromError(e);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
this.send(result);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
result = this.send(errorResponseFromError(e));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
handleResponse(data: WorkerResponse): void {
|
|
232
|
+
let idx = nullthrows(data.idx);
|
|
233
|
+
let contentType = data.contentType;
|
|
234
|
+
let content = data.content;
|
|
235
|
+
let call = nullthrows(this.responseQueue.get(idx));
|
|
236
|
+
|
|
237
|
+
if (contentType === 'error') {
|
|
238
|
+
invariant(typeof content !== 'string');
|
|
239
|
+
call.reject(new ThrowableDiagnostic({diagnostic: content}));
|
|
240
|
+
} else {
|
|
241
|
+
call.resolve(content);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.responseQueue.delete(idx);
|
|
245
|
+
|
|
246
|
+
// Process the next call
|
|
247
|
+
this.processQueue();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Keep in mind to make sure responses to these calls are JSON.Stringify safe
|
|
251
|
+
addCall(
|
|
252
|
+
request: CallRequest,
|
|
253
|
+
awaitResponse: ?boolean = true,
|
|
254
|
+
): Promise<mixed> {
|
|
255
|
+
// $FlowFixMe
|
|
256
|
+
let call: ChildCall = {
|
|
257
|
+
...request,
|
|
258
|
+
type: 'request',
|
|
259
|
+
child: this.childId,
|
|
260
|
+
// $FlowFixMe Added in Flow 0.121.0 upgrade in #4381
|
|
261
|
+
awaitResponse,
|
|
262
|
+
resolve: () => {},
|
|
263
|
+
reject: () => {},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
let promise;
|
|
267
|
+
if (awaitResponse) {
|
|
268
|
+
promise = new Promise((resolve, reject) => {
|
|
269
|
+
call.resolve = resolve;
|
|
270
|
+
call.reject = reject;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.callQueue.push(call);
|
|
275
|
+
this.processQueue();
|
|
276
|
+
|
|
277
|
+
return promise ?? Promise.resolve();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
sendRequest(call: ChildCall): void {
|
|
281
|
+
let idx;
|
|
282
|
+
if (call.awaitResponse) {
|
|
283
|
+
idx = this.responseId++;
|
|
284
|
+
this.responseQueue.set(idx, call);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.send({
|
|
288
|
+
idx,
|
|
289
|
+
child: call.child,
|
|
290
|
+
type: call.type,
|
|
291
|
+
location: call.location,
|
|
292
|
+
handle: call.handle,
|
|
293
|
+
method: call.method,
|
|
294
|
+
args: call.args,
|
|
295
|
+
awaitResponse: call.awaitResponse,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
processQueue(): void {
|
|
300
|
+
if (!this.callQueue.length) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (this.responseQueue.size < this.maxConcurrentCalls) {
|
|
305
|
+
this.sendRequest(this.callQueue.shift());
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
handleEnd(): void {
|
|
310
|
+
this.loggerDisposable.dispose();
|
|
311
|
+
this.tracerDisposable.dispose();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
createReverseHandle(fn: (...args: Array<any>) => mixed): Handle {
|
|
315
|
+
let handle = new Handle({
|
|
316
|
+
fn,
|
|
317
|
+
childId: this.childId,
|
|
318
|
+
});
|
|
319
|
+
this.handles.set(handle.id, handle);
|
|
320
|
+
return handle;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {Child} from './child';
|
|
3
|
+
|
|
4
|
+
// This file is imported by both the WorkerFarm and child implementation.
|
|
5
|
+
// When a worker is inited, it sets the state in this file.
|
|
6
|
+
// This way, WorkerFarm can access the state without directly importing the child code.
|
|
7
|
+
export let child: ?Child = null;
|
|
8
|
+
export function setChild(c: Child) {
|
|
9
|
+
child = c;
|
|
10
|
+
}
|
package/src/cpuCount.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import {execSync} from 'child_process';
|
|
4
|
+
|
|
5
|
+
const exec = (command: string): string => {
|
|
6
|
+
try {
|
|
7
|
+
let stdout = execSync(command, {
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
// This prevents the command from outputting to the console
|
|
10
|
+
stdio: [null, null, null],
|
|
11
|
+
});
|
|
12
|
+
return stdout.trim();
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function detectRealCores(): number {
|
|
19
|
+
let platform = os.platform();
|
|
20
|
+
let amount = 0;
|
|
21
|
+
|
|
22
|
+
if (platform === 'linux') {
|
|
23
|
+
amount = parseInt(
|
|
24
|
+
exec('lscpu -p | egrep -v "^#" | sort -u -t, -k 2,4 | wc -l'),
|
|
25
|
+
10,
|
|
26
|
+
);
|
|
27
|
+
} else if (platform === 'darwin') {
|
|
28
|
+
amount = parseInt(exec('sysctl -n hw.physicalcpu_max'), 10);
|
|
29
|
+
} else if (platform === 'win32') {
|
|
30
|
+
const str = exec('wmic cpu get NumberOfCores').match(/\d+/g);
|
|
31
|
+
if (str !== null) {
|
|
32
|
+
amount = parseInt(str.filter(n => n !== '')[0], 10);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!amount || amount <= 0) {
|
|
37
|
+
throw new Error('Could not detect cpu count!');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return amount;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let cores;
|
|
44
|
+
export default function getCores(bypassCache?: boolean = false): number {
|
|
45
|
+
// Do not re-run commands if we already have the count...
|
|
46
|
+
if (cores && !bypassCache) {
|
|
47
|
+
return cores;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// $FlowFixMe
|
|
51
|
+
if (process.browser) {
|
|
52
|
+
// eslint-disable-next-line no-undef
|
|
53
|
+
cores = navigator.hardwareConcurrency / 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!cores) {
|
|
57
|
+
try {
|
|
58
|
+
cores = detectRealCores();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Guess the amount of real cores
|
|
61
|
+
cores = os
|
|
62
|
+
.cpus()
|
|
63
|
+
.filter(
|
|
64
|
+
(cpu, index) => !cpu.model.includes('Intel') || index % 2 === 1,
|
|
65
|
+
).length;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Another fallback
|
|
70
|
+
if (!cores) {
|
|
71
|
+
cores = 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return cores;
|
|
75
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {TraceEvent, LogEvent} from '@atlaspack/types-internal';
|
|
3
|
+
import invariant from 'assert';
|
|
4
|
+
import WorkerFarm from './WorkerFarm';
|
|
5
|
+
import Logger from '@atlaspack/logger';
|
|
6
|
+
import bus from './bus';
|
|
7
|
+
import {tracer} from '@atlaspack/profiler';
|
|
8
|
+
|
|
9
|
+
if (!WorkerFarm.isWorker()) {
|
|
10
|
+
// Forward all logger events originating from workers into the main process
|
|
11
|
+
bus.on('logEvent', (e: LogEvent) => {
|
|
12
|
+
switch (e.level) {
|
|
13
|
+
case 'info':
|
|
14
|
+
Logger.info(e.diagnostics);
|
|
15
|
+
break;
|
|
16
|
+
case 'progress':
|
|
17
|
+
invariant(typeof e.message === 'string');
|
|
18
|
+
Logger.progress(e.message);
|
|
19
|
+
break;
|
|
20
|
+
case 'verbose':
|
|
21
|
+
Logger.verbose(e.diagnostics);
|
|
22
|
+
break;
|
|
23
|
+
case 'warn':
|
|
24
|
+
Logger.warn(e.diagnostics);
|
|
25
|
+
break;
|
|
26
|
+
case 'error':
|
|
27
|
+
Logger.error(e.diagnostics);
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
throw new Error('Unknown log level');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Forward all trace events originating from workers into the main process
|
|
35
|
+
bus.on('traceEvent', (e: TraceEvent) => {
|
|
36
|
+
tracer.trace(e);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default WorkerFarm;
|
|
41
|
+
export {bus};
|
|
42
|
+
export {Handle} from './WorkerFarm';
|
|
43
|
+
export type {WorkerApi, FarmOptions, SharedReference} from './WorkerFarm';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ChildImpl,
|
|
5
|
+
MessageHandler,
|
|
6
|
+
ExitHandler,
|
|
7
|
+
WorkerMessage,
|
|
8
|
+
} from '../types';
|
|
9
|
+
import nullthrows from 'nullthrows';
|
|
10
|
+
import {setChild} from '../childState';
|
|
11
|
+
import {Child} from '../child';
|
|
12
|
+
import {serialize, deserialize} from '@atlaspack/core';
|
|
13
|
+
|
|
14
|
+
export default class ProcessChild implements ChildImpl {
|
|
15
|
+
onMessage: MessageHandler;
|
|
16
|
+
onExit: ExitHandler;
|
|
17
|
+
|
|
18
|
+
constructor(onMessage: MessageHandler, onExit: ExitHandler) {
|
|
19
|
+
if (!process.send) {
|
|
20
|
+
throw new Error('Only create ProcessChild instances in a worker!');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.onMessage = onMessage;
|
|
24
|
+
this.onExit = onExit;
|
|
25
|
+
process.on('message', data => this.handleMessage(data));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handleMessage(data: string): void {
|
|
29
|
+
if (data === 'die') {
|
|
30
|
+
return this.stop();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.onMessage(deserialize(Buffer.from(data, 'base64')));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
send(data: WorkerMessage) {
|
|
37
|
+
let processSend = nullthrows(process.send).bind(process);
|
|
38
|
+
processSend(serialize(data).toString('base64'), err => {
|
|
39
|
+
if (err && err instanceof Error) {
|
|
40
|
+
// $FlowFixMe[prop-missing]
|
|
41
|
+
if (err.code === 'ERR_IPC_CHANNEL_CLOSED') {
|
|
42
|
+
// IPC connection closed
|
|
43
|
+
// no need to keep the worker running if it can't send or receive data
|
|
44
|
+
return this.stop();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
stop() {
|
|
51
|
+
this.onExit(0);
|
|
52
|
+
process.exit();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setChild(new Child(ProcessChild));
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
WorkerImpl,
|
|
5
|
+
MessageHandler,
|
|
6
|
+
ErrorHandler,
|
|
7
|
+
ExitHandler,
|
|
8
|
+
WorkerMessage,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import childProcess, {type ChildProcess} from 'child_process';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import {serialize, deserialize} from '@atlaspack/core';
|
|
13
|
+
|
|
14
|
+
const WORKER_PATH = path.join(__dirname, 'ProcessChild.js');
|
|
15
|
+
|
|
16
|
+
export default class ProcessWorker implements WorkerImpl {
|
|
17
|
+
execArgv: Object;
|
|
18
|
+
onMessage: MessageHandler;
|
|
19
|
+
onError: ErrorHandler;
|
|
20
|
+
onExit: ExitHandler;
|
|
21
|
+
child: ChildProcess;
|
|
22
|
+
processQueue: boolean = true;
|
|
23
|
+
sendQueue: Array<any> = [];
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
execArgv: Object,
|
|
27
|
+
onMessage: MessageHandler,
|
|
28
|
+
onError: ErrorHandler,
|
|
29
|
+
onExit: ExitHandler,
|
|
30
|
+
) {
|
|
31
|
+
this.execArgv = execArgv;
|
|
32
|
+
this.onMessage = onMessage;
|
|
33
|
+
this.onError = onError;
|
|
34
|
+
this.onExit = onExit;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
start(): Promise<void> {
|
|
38
|
+
this.child = childProcess.fork(WORKER_PATH, process.argv, {
|
|
39
|
+
execArgv: this.execArgv,
|
|
40
|
+
env: process.env,
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.child.on('message', (data: string) => {
|
|
45
|
+
this.onMessage(deserialize(Buffer.from(data, 'base64')));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.child.once('exit', this.onExit);
|
|
49
|
+
this.child.on('error', this.onError);
|
|
50
|
+
|
|
51
|
+
return Promise.resolve();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async stop() {
|
|
55
|
+
this.child.send('die');
|
|
56
|
+
|
|
57
|
+
let forceKill = setTimeout(() => this.child.kill('SIGINT'), 500);
|
|
58
|
+
await new Promise(resolve => {
|
|
59
|
+
this.child.once('exit', resolve);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
clearTimeout(forceKill);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
send(data: WorkerMessage) {
|
|
66
|
+
if (!this.processQueue) {
|
|
67
|
+
this.sendQueue.push(data);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let result = this.child.send(serialize(data).toString('base64'), error => {
|
|
72
|
+
if (error && error instanceof Error) {
|
|
73
|
+
// Ignore this, the workerfarm handles child errors
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.processQueue = true;
|
|
78
|
+
|
|
79
|
+
if (this.sendQueue.length > 0) {
|
|
80
|
+
let queueCopy = this.sendQueue.slice(0);
|
|
81
|
+
this.sendQueue = [];
|
|
82
|
+
queueCopy.forEach(entry => this.send(entry));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!result || /^win/.test(process.platform)) {
|
|
87
|
+
// Queue is handling too much messages throttle it
|
|
88
|
+
this.processQueue = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|