Skip to content

Commit bd826ff

Browse files
authoredNov 10, 2023
refactor(worker): document worker code, improve types (#5034)
This documents a bunch of code related to our node.js worker setup (which was previously not super well-documented) and also makes a few improvements to some relevant types. This was motivated by #5010, which entailed modifying some of this code. It wasn't entirely apparent how the code in `src/sys/node/worker.ts`, well, _worked_ and what it was supposed to do. Now we know!
1 parent ce06708 commit bd826ff

11 files changed

+218
-29
lines changed
 

‎src/compiler/prerender/prerender-main.ts

+7
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,15 @@ const runPrerender = async (
7070
let workerCtrl: d.WorkerMainController;
7171

7272
if (config.sys.createWorkerController == null || config.maxConcurrentWorkers < 1) {
73+
// we don't have a `maxConcurrentWorkers` setting which makes it
74+
// necessary to actually use threaded workers, so we create a
75+
// single-threaded worker context here
7376
workerCtx = createWorkerContext(config.sys);
7477
} else {
78+
// we want to stand up the full threaded worker setup, so we first need
79+
// to build a worker controller and then we create an appropriate worker
80+
// context for it (this will mean that when methods are called on
81+
// `workerCtx` they're dispatched to workers in other threads)
7582
workerCtrl = config.sys.createWorkerController(config.maxConcurrentWorkers);
7683
workerCtx = createWorkerMainContext(workerCtrl);
7784
}

‎src/compiler/sys/stencil-sys.ts

+1
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ export const createSystem = (c?: { logger?: Logger }): CompilerSystem => {
622622
writeFile,
623623
writeFileSync,
624624
generateContentHash,
625+
// no threading when we're running in-memory
625626
createWorkerController: null,
626627
details: {
627628
cpuModel: '',

‎src/compiler/sys/worker/sys-worker.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
import type * as d from '@stencil/core/declarations';
12
import { isFunction } from '@utils';
23

3-
import type { ValidatedConfig } from '../../../declarations';
44
import { createWorkerMainContext } from '../../worker/main-thread';
55
import { createWorkerContext } from '../../worker/worker-thread';
66

7-
export const createSysWorker = (config: ValidatedConfig) => {
7+
/**
8+
* Create a worker context given a Stencil config. If
9+
* {@link d.Config['maxConcurrentWorkers']} is set to an appropriate value this
10+
* will be a worker context that dispatches work to other threads, and if not it
11+
* will be a single-threaded worker context.
12+
*
13+
* @param config the current stencil config
14+
* @returns a worker context
15+
*/
16+
export const createSysWorker = (config: d.ValidatedConfig): d.CompilerWorkerContext => {
817
if (
918
isFunction(config.sys.createWorkerController) &&
1019
config.maxConcurrentWorkers > 0 &&

‎src/compiler/worker/main-thread.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { CompilerWorkerContext, WorkerMainController } from '../../declarations';
22

3+
/**
4+
* Instantiate a worker context which is specific to the 'main thread' and
5+
* which dispatches the tasks it receives to a {@link WorkerMainController}
6+
* (and, thereby, to workers in other threads).
7+
*
8+
* @param workerCtrl a worker controller which can handle the methods on the
9+
* context by passing them to worker threads
10+
* @returns a worker context
11+
*/
312
export const createWorkerMainContext = (workerCtrl: WorkerMainController): CompilerWorkerContext => ({
413
optimizeCss: workerCtrl.handler('optimizeCss'),
514
prepareModule: workerCtrl.handler('prepareModule'),

‎src/compiler/worker/worker-thread.ts

+44-7
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,59 @@ import { prepareModule } from '../optimize/optimize-module';
44
import { prerenderWorker } from '../prerender/prerender-worker';
55
import { transformCssToEsm } from '../style/css-to-esm';
66

7+
/**
8+
* Instantiate a worker context which synchronously calls the methods that are
9+
* defined on it (as opposed to dispatching them to a worker in another thread
10+
* via a worker controller). We want to do this in two cases:
11+
*
12+
* 1. we're in the main thread and not using any workers at all (i.e. the value
13+
* for {@link d.Config['maxConcurrentWorkers']} is set to `1`) or
14+
* 2. we're already in a worker thread, so we want to call the method directly
15+
* instead of dispatching.
16+
*
17+
* @param sys a compiler system appropriate for our current environment
18+
* @returns a worker context which directly calls the supported methods
19+
*/
720
export const createWorkerContext = (sys: d.CompilerSystem): d.CompilerWorkerContext => ({
821
transformCssToEsm,
922
prepareModule,
1023
optimizeCss,
1124
prerenderWorker: (prerenderRequest) => prerenderWorker(sys, prerenderRequest),
1225
});
1326

27+
/**
28+
* Create a handler for the IPC messages ({@link d.MsgToWorker}) that a worker
29+
* thread receives from the main thread. For each message that we receive we
30+
* need to call a specific method on {@link d.CompilerWorkerContext} and then
31+
* return the result
32+
*
33+
* @param sys a compiler system which wil be used to create a worker context
34+
* @returns a message handler capable of digesting and executing tasks
35+
* described by {@link d.MsgToWorker} object
36+
*/
1437
export const createWorkerMessageHandler = (sys: d.CompilerSystem): d.WorkerMsgHandler => {
1538
const workerCtx = createWorkerContext(sys);
1639

17-
return (msgToWorker: d.MsgToWorker) => {
18-
const fnName: string = msgToWorker.args[0];
19-
const fnArgs = msgToWorker.args.slice(1);
20-
const fn = (workerCtx as any)[fnName] as Function;
21-
if (typeof fn === 'function') {
22-
return fn(...fnArgs);
23-
}
40+
return <T extends d.WorkerContextMethod>(msgToWorker: d.MsgToWorker<T>): ReturnType<d.CompilerWorkerContext[T]> => {
41+
const fnName = msgToWorker.method;
42+
const fnArgs = msgToWorker.args;
43+
const fn = workerCtx[fnName];
44+
// This is actually fine! However typescript doesn't agree. Both the
45+
// `Parameters` and the `ReturnType` arguments return a union of tuples in
46+
// the case where their parameter is generic over some type (e.g. `T` here)
47+
// but, annoyingly, TypeScript does not think that you can spread a value
48+
// whose type is a union of tuples as a rest param. Even though the type of
49+
// `fn` and `fnArgs` should 'match' given the type `T` that they are
50+
// generic over, TypeScript does not seem able to realize that and can't
51+
// narrow the type of `fn` _or_ `fnArgs` properly, so the type for the
52+
// parameters of `fn` _and_ the type of `fnArgs` remains a union of tuples.
53+
// Gah!
54+
//
55+
// See this issue for some context on this:
56+
// https://github.com/microsoft/TypeScript/issues/49700
57+
//
58+
// Unfortunately there's not a great solution here other than:
59+
// @ts-ignore
60+
return fn(...fnArgs);
2461
};
2562
};

‎src/declarations/stencil-private.ts

+62-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { result } from '@utils';
2+
import type { Serializable as CPSerializable } from 'child_process';
23

34
import type { InMemoryFileSystem } from '../compiler/sys/in-memory-fs';
45
import type {
@@ -2321,6 +2322,13 @@ export interface VNodeProdData {
23212322
[key: string]: any;
23222323
}
23232324

2325+
/**
2326+
* An abstraction to bundle up four methods which _may_ be handled by
2327+
* dispatching work to workers running in other OS threads or may be called
2328+
* synchronously. Environment and `CompilerSystem` related setup code will
2329+
* determine which one, but in either case the call sites for these methods can
2330+
* dispatch to this shared interface.
2331+
*/
23242332
export interface CompilerWorkerContext {
23252333
optimizeCss(inputOpts: OptimizeCssInput): Promise<OptimizeCssOutput>;
23262334
prepareModule(
@@ -2333,17 +2341,53 @@ export interface CompilerWorkerContext {
23332341
transformCssToEsm(input: TransformCssToEsmInput): Promise<TransformCssToEsmOutput>;
23342342
}
23352343

2336-
export interface MsgToWorker {
2344+
/**
2345+
* The methods that are supported on a {@link d.CompilerWorkerContext}
2346+
*/
2347+
export type WorkerContextMethod = keyof CompilerWorkerContext;
2348+
2349+
/**
2350+
* A little type guard which will cause a type error if the parameter `T` does
2351+
* not satisfy {@link CPSerializable} (i.e. if it's not possible to cleanly
2352+
* serialize it for message passing via an IPC channel).
2353+
*/
2354+
type IPCSerializable<T extends CPSerializable> = T;
2355+
2356+
/**
2357+
* A manifest for a job that a worker thread should carry out, as determined by
2358+
* and dispatched from the main thread. This includes the name of the task to do
2359+
* and any arguments necessary to carry it out properly.
2360+
*
2361+
* This message must satisfy {@link CPSerializable} so it can be sent from the
2362+
* main thread to a worker thread via an IPC channel
2363+
*/
2364+
export type MsgToWorker<T extends WorkerContextMethod> = IPCSerializable<{
23372365
stencilId: number;
2338-
args: any[];
2339-
}
2366+
method: T;
2367+
args: Parameters<CompilerWorkerContext[T]>;
2368+
}>;
23402369

2341-
export interface MsgFromWorker {
2370+
/**
2371+
* A manifest for a job that a worker thread should carry out, as determined by
2372+
* and dispatched from the main thread. This includes the name of the task to do
2373+
* and any arguments necessary to carry it out properly.
2374+
*
2375+
* This message must satisfy {@link CPSerializable} so it can be sent from the
2376+
* main thread to a worker thread via an IPC channel
2377+
*/
2378+
export type MsgFromWorker<T extends WorkerContextMethod> = IPCSerializable<{
23422379
stencilId?: number;
2343-
stencilRtnValue: any;
2344-
stencilRtnError: string;
2345-
}
2380+
stencilRtnValue: ReturnType<CompilerWorkerContext[T]>;
2381+
stencilRtnError: string | null;
2382+
}>;
23462383

2384+
/**
2385+
* A description of a task which should be passed to a worker in another
2386+
* thread. This interface differs from {@link MsgToWorker} in that it doesn't
2387+
* have to be serializable for transmission through an IPC channel, so we can
2388+
* hold things like a `resolve` and `reject` callback to use when the task
2389+
* completes.
2390+
*/
23472391
export interface CompilerWorkerTask {
23482392
stencilId?: number;
23492393
inputArgs?: any[];
@@ -2352,7 +2396,17 @@ export interface CompilerWorkerTask {
23522396
retries?: number;
23532397
}
23542398

2355-
export type WorkerMsgHandler = (msgToWorker: MsgToWorker) => Promise<any>;
2399+
/**
2400+
* A handler for IPC messages from the main thread to a worker thread. This
2401+
* involves dispatching an action specified by a {@link MsgToWorker} object to a
2402+
* {@link CompilerWorkerContext}.
2403+
*
2404+
* @param msgToWorker the message to handle
2405+
* @returns the return value of the specified function
2406+
*/
2407+
export type WorkerMsgHandler = <T extends WorkerContextMethod>(
2408+
msgToWorker: MsgToWorker<T>,
2409+
) => ReturnType<CompilerWorkerContext[T]>;
23562410

23572411
export interface TranspileModuleResults {
23582412
sourceFilePath: string;

‎src/declarations/stencil-public-compiler.ts

+25
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,10 @@ export interface CompilerSystem {
10271027
dynamicImport?(p: string): Promise<any>;
10281028
/**
10291029
* Creates the worker controller for the current system.
1030+
*
1031+
* @param maxConcurrentWorkers the max number of concurrent workers to
1032+
* support
1033+
* @returns a worker controller appropriate for the current platform (node.js)
10301034
*/
10311035
createWorkerController?(maxConcurrentWorkers: number): WorkerMainController;
10321036
encodeToBase64(str: string): string;
@@ -1255,10 +1259,31 @@ export interface ResolveModuleIdResults {
12551259
pkgDirPath: string;
12561260
}
12571261

1262+
// TODO(STENCIL-1005): improve the typing for this interface
1263+
/**
1264+
* A controller which provides for communication and coordination between
1265+
* threaded workers.
1266+
*/
12581267
export interface WorkerMainController {
1268+
/**
1269+
* Send a given set of arguments to a worker
1270+
*/
12591271
send(...args: any[]): Promise<any>;
1272+
/**
1273+
* Handle a particular method
1274+
*
1275+
* @param name of the method to be passed to a worker
1276+
* @returns a Promise wrapping the results
1277+
*/
12601278
handler(name: string): (...args: any[]) => Promise<any>;
1279+
/**
1280+
* Destroy the worker represented by this instance, rejecting all outstanding
1281+
* tasks and killing the child process.
1282+
*/
12611283
destroy(): void;
1284+
/**
1285+
* The current setting for the max number of workers
1286+
*/
12621287
maxWorkers: number;
12631288
}
12641289

‎src/sys/node/node-worker-controller.ts

+15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { cpus } from 'os';
55
import type * as d from '../../declarations';
66
import { NodeWorkerMain } from './node-worker-main';
77

8+
/**
9+
* A custom EventEmitter which provides centralizes dispatching and control for
10+
* node.js workers ({@link NodeWorkerMain} instances)
11+
*/
812
export class NodeWorkerController extends EventEmitter implements d.WorkerMainController {
913
workerIds = 0;
1014
stencilId = 0;
@@ -15,6 +19,17 @@ export class NodeWorkerController extends EventEmitter implements d.WorkerMainCo
1519
useForkedWorkers: boolean;
1620
mainThreadRunner: { [fnName: string]: (...args: any[]) => Promise<any> };
1721

22+
/**
23+
* Create a node.js-specific worker controller, which controls and
24+
* coordinates distributing tasks to a series of child processes (tracked by
25+
* {@link NodeWorkerMain} instances). These child processes are node
26+
* processes executing a special worker script (`src/sys/node/worker.ts`)
27+
* which listens for {@link d.MsgToWorker} messages and runs certain tasks in
28+
* response.
29+
*
30+
* @param forkModulePath the path to the module which k
31+
* @param maxConcurrentWorkers the max number of worker threads to spin up
32+
*/
1833
constructor(
1934
public forkModulePath: string,
2035
maxConcurrentWorkers: number,

‎src/sys/node/node-worker-main.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,31 @@ import { EventEmitter } from 'events';
44

55
import type * as d from '../../declarations';
66

7+
/**
8+
* A class that holds a reference to a node worker sub-process within the main
9+
* thread so that messages may be passed to it.
10+
*/
711
export class NodeWorkerMain extends EventEmitter {
12+
/**
13+
* A handle for the OS process that is running our worker code
14+
*/
815
childProcess: cp.ChildProcess;
916
tasks = new Map<number, d.CompilerWorkerTask>();
1017
exitCode: number = null;
1118
processQueue = true;
12-
sendQueue: d.MsgToWorker[] = [];
19+
sendQueue: d.MsgToWorker<any>[] = [];
1320
stopped = false;
1421
successfulMessage = false;
1522
totalTasksAssigned = 0;
1623

24+
/**
25+
* Create an object for holding and interacting with a reference to a worker
26+
* child-process.
27+
*
28+
* @param id a unique ID
29+
* @param forkModulePath the path to the module which should be run by the
30+
* child process
31+
*/
1732
constructor(
1833
public id: number,
1934
forkModulePath: string,
@@ -60,13 +75,16 @@ export class NodeWorkerMain extends EventEmitter {
6075
this.totalTasksAssigned++;
6176
this.tasks.set(task.stencilId, task);
6277

78+
const [method, ...args] = task.inputArgs;
79+
6380
this.sendToWorker({
6481
stencilId: task.stencilId,
65-
args: task.inputArgs,
82+
method,
83+
args,
6684
});
6785
}
6886

69-
sendToWorker(msg: d.MsgToWorker) {
87+
sendToWorker<T extends d.WorkerContextMethod>(msg: d.MsgToWorker<T>) {
7088
if (!this.processQueue) {
7189
this.sendQueue.push(msg);
7290
return;
@@ -91,7 +109,7 @@ export class NodeWorkerMain extends EventEmitter {
91109
}
92110
}
93111

94-
receiveFromWorker(msgFromWorker: d.MsgFromWorker) {
112+
receiveFromWorker<T extends d.WorkerContextMethod>(msgFromWorker: d.MsgFromWorker<T>) {
95113
this.successfulMessage = true;
96114

97115
if (this.stopped) {

0 commit comments

Comments
 (0)
Please sign in to comment.