Skip to content

Commit e38df49

Browse files
committed
Merge branch 'master' into docs/dontThrow
2 parents 2d9c262 + f0dc993 commit e38df49

8 files changed

Lines changed: 124 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- `[jest-runtime, jest-transform]` share `cacheFS` between runtime and transformer ([#10901](https://github.com/facebook/jest/pull/10901))
2020
- `[jest-transform]` Pass config options defined in Jest's config to transformer's `process` and `getCacheKey` functions ([#10926](https://github.com/facebook/jest/pull/10926))
2121
- `[jest-worker]` Add support for custom task queues and adds a `PriorityQueue` implementation. ([#10921](https://github.com/facebook/jest/pull/10921))
22+
- `[jest-worker]` Add in-order scheduling policy to jest worker ([10902](https://github.com/facebook/jest/pull/10902))
2223

2324
### Fixes
2425

@@ -49,9 +50,10 @@
4950
- `[jest-transform]` Show enhanced `SyntaxError` message for all `SyntaxError`s ([#10749](https://github.com/facebook/jest/pull/10749))
5051
- `[jest-transform]` [**BREAKING**] Refactor API to pass an options bag around rather than multiple boolean options ([#10753](https://github.com/facebook/jest/pull/10753))
5152
- `[jest-transform]` [**BREAKING**] Refactor API of transformers to pass an options bag rather than separate `config` and other options ([#10834](https://github.com/facebook/jest/pull/10834))
52-
- `[jest-worker]` [**BREAKING**] Use named exports ([#10623] (https://github.com/facebook/jest/pull/10623))
53-
- `[jest-worker]` Do not swallow errors during serialization ([#10984] (https://github.com/facebook/jest/pull/10984))
53+
- `[jest-worker]` [**BREAKING**] Use named exports ([#10623](https://github.com/facebook/jest/pull/10623))
54+
- `[jest-worker]` Do not swallow errors during serialization ([#10984](https://github.com/facebook/jest/pull/10984))
5455
- `[pretty-format]` [**BREAKING**] Convert to ES Modules ([#10515](https://github.com/facebook/jest/pull/10515))
56+
- `[pretty-format]` Only call `hasAttribute` if it's a function ([#11000](https://github.com/facebook/jest/pull/11000))
5557

5658
### Chore & Maintenance
5759

packages/jest-worker/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ Provide a custom worker pool to be used for spawning child processes. By default
9191

9292
`jest-worker` will automatically detect if `worker_threads` are available, but will not use them unless passed `enableWorkerThreads: true`.
9393

94+
### `workerSchedulingPolicy: 'round-robin' | 'in-order'` (optional)
95+
96+
Specifies the policy how tasks are assigned to workers if multiple workers are _idle_:
97+
98+
- `round-robin` (default): The task will be sequentially distributed onto the workers. The first task is assigned to the worker 1, the second to the worker 2, to ensure that the work is distributed across workers.
99+
- `in-order`: The task will be assigned to the first free worker starting with worker 1 and only assign the work to worker 2 if the worker 1 is busy.
100+
101+
Tasks are always assigned to the first free worker as soon as tasks start to queue up. The scheduling policy does not define the task scheduling which is always first-in, first-out.
102+
94103
### `taskQueue`: TaskQueue` (optional)
95104

96105
The task queue defines in which order tasks (method calls) are processed by the workers. `jest-worker` ships with a `FifoQueue` and `PriorityQueue`:

packages/jest-worker/src/Farm.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,29 @@ import {
2222
} from './types';
2323

2424
export default class Farm {
25-
private _computeWorkerKey: FarmOptions['computeWorkerKey'];
26-
private _cacheKeys: Record<string, WorkerInterface>;
27-
private _callback: Function;
28-
private _locks: Array<boolean>;
29-
private _numOfWorkers: number;
30-
private _offset: number;
31-
private _taskQueue: TaskQueue;
25+
private readonly _computeWorkerKey: FarmOptions['computeWorkerKey'];
26+
private readonly _workerSchedulingPolicy: NonNullable<
27+
FarmOptions['workerSchedulingPolicy']
28+
>;
29+
private readonly _cacheKeys: Record<string, WorkerInterface> = Object.create(
30+
null,
31+
);
32+
private readonly _locks: Array<boolean> = [];
33+
private _offset = 0;
34+
private readonly _taskQueue: TaskQueue;
3235

3336
constructor(
34-
numOfWorkers: number,
35-
callback: Function,
37+
private _numOfWorkers: number,
38+
private _callback: Function,
3639
options: {
3740
computeWorkerKey?: FarmOptions['computeWorkerKey'];
41+
workerSchedulingPolicy?: FarmOptions['workerSchedulingPolicy'];
3842
taskQueue?: TaskQueue;
3943
} = {},
4044
) {
41-
this._cacheKeys = Object.create(null);
42-
this._callback = callback;
43-
this._locks = [];
44-
this._numOfWorkers = numOfWorkers;
45-
this._offset = 0;
4645
this._computeWorkerKey = options.computeWorkerKey;
46+
this._workerSchedulingPolicy =
47+
options.workerSchedulingPolicy ?? 'round-robin';
4748
this._taskQueue = options.taskQueue ?? new FifoQueue();
4849
}
4950

@@ -147,19 +148,30 @@ export default class Farm {
147148
private _push(task: QueueChildMessage): Farm {
148149
this._taskQueue.enqueue(task);
149150

151+
const offset = this._getNextWorkerOffset();
150152
for (let i = 0; i < this._numOfWorkers; i++) {
151-
this._process((this._offset + i) % this._numOfWorkers);
153+
this._process((offset + i) % this._numOfWorkers);
152154

153155
if (task.request[1]) {
154156
break;
155157
}
156158
}
157159

158-
this._offset++;
159-
160160
return this;
161161
}
162162

163+
// Typescript ensures that the switch statement is exhaustive.
164+
// Adding an explicit return at the end would disable the exhaustive check void.
165+
// eslint-disable-next-line consistent-return
166+
private _getNextWorkerOffset(): number {
167+
switch (this._workerSchedulingPolicy) {
168+
case 'in-order':
169+
return 0;
170+
case 'round-robin':
171+
return this._offset++;
172+
}
173+
}
174+
163175
private _lock(workerId: number): void {
164176
this._locks[workerId] = true;
165177
}

packages/jest-worker/src/__tests__/process-integration.test.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,45 @@ describe('Jest Worker Integration', () => {
8383

8484
// The first call will go to the first child process.
8585
const promise0 = farm.foo('param-0');
86-
8786
assertCallsToChild(0, ['foo', 'param-0']);
8887
replySuccess(0, 'worker-0');
8988
expect(await promise0).toBe('worker-0');
9089

9190
// The second call will go to the second child process.
9291
const promise1 = farm.foo(1);
92+
assertCallsToChild(1, ['foo', 1]);
93+
replySuccess(1, 'worker-1');
94+
expect(await promise1).toBe('worker-1');
95+
});
96+
97+
it('schedules the task on the first available child processes if the scheduling policy is in-order', async () => {
98+
const farm = new Farm('/tmp/baz.js', {
99+
exposedMethods: ['foo', 'bar'],
100+
numWorkers: 4,
101+
workerSchedulingPolicy: 'in-order',
102+
});
93103

104+
// The first call will go to the first child process.
105+
const promise0 = farm.foo('param-0');
106+
assertCallsToChild(0, ['foo', 'param-0']);
107+
108+
// The second call will go to the second child process.
109+
const promise1 = farm.foo(1);
110+
111+
// The first task on worker 0 completes
112+
replySuccess(0, 'worker-0');
113+
expect(await promise0).toBe('worker-0');
114+
115+
// The second task on worker 1 completes
94116
assertCallsToChild(1, ['foo', 1]);
95117
replySuccess(1, 'worker-1');
96118
expect(await promise1).toBe('worker-1');
119+
120+
// The third call will go to the first child process
121+
const promise2 = farm.foo('param-2');
122+
assertCallsToChild(0, ['foo', 'param-0'], ['foo', 'param-2']);
123+
replySuccess(0, 'worker-0');
124+
expect(await promise2).toBe('worker-0');
97125
});
98126

99127
it('distributes concurrent calls across child processes', async () => {

packages/jest-worker/src/__tests__/thread-integration.test.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,63 @@ describe('Jest Worker Process Integration', () => {
8686

8787
// The first call will go to the first child process.
8888
const promise0 = farm.foo('param-0');
89-
9089
assertCallsToChild(0, ['foo', 'param-0']);
9190
replySuccess(0, 'worker-0');
9291
expect(await promise0).toBe('worker-0');
9392

93+
// The second call will go to the second child process.
94+
const promise1 = farm.foo(1);
95+
assertCallsToChild(1, ['foo', 1]);
96+
replySuccess(1, 'worker-1');
97+
expect(await promise1).toBe('worker-1');
98+
});
99+
100+
it('schedules the task on the first available child processes if the scheduling policy is in-order', async () => {
101+
const farm = new Farm('/tmp/baz.js', {
102+
enableWorkerThreads: true,
103+
exposedMethods: ['foo', 'bar'],
104+
numWorkers: 4,
105+
workerSchedulingPolicy: 'in-order',
106+
});
107+
108+
// The first call will go to the first child process.
109+
const promise0 = farm.foo('param-0');
110+
assertCallsToChild(0, ['foo', 'param-0']);
111+
94112
// The second call will go to the second child process.
95113
const promise1 = farm.foo(1);
96114

115+
// The first task on worker 0 completes
116+
replySuccess(0, 'worker-0');
117+
expect(await promise0).toBe('worker-0');
118+
119+
// The second task on worker 1 completes
120+
assertCallsToChild(1, ['foo', 1]);
121+
replySuccess(1, 'worker-1');
122+
expect(await promise1).toBe('worker-1');
123+
124+
// The third call will go to the first child process
125+
const promise2 = farm.foo('param-2');
126+
assertCallsToChild(0, ['foo', 'param-0'], ['foo', 'param-2']);
127+
replySuccess(0, 'worker-0');
128+
expect(await promise2).toBe('worker-0');
129+
});
130+
131+
it('schedules the task on the first available child processes', async () => {
132+
const farm = new Farm('/tmp/baz.js', {
133+
enableWorkerThreads: true,
134+
exposedMethods: ['foo', 'bar'],
135+
numWorkers: 4,
136+
});
137+
138+
// The first call will go to the first child process.
139+
const promise0 = farm.foo('param-0');
140+
assertCallsToChild(0, ['foo', 'param-0']);
141+
replySuccess(0, 'worker-0');
142+
expect(await promise0).toBe('worker-0');
143+
144+
// The second call will go to the second child process.
145+
const promise1 = farm.foo(1);
97146
assertCallsToChild(1, ['foo', 1]);
98147
replySuccess(1, 'worker-1');
99148
expect(await promise1).toBe('worker-1');

packages/jest-worker/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export class Worker {
105105
{
106106
computeWorkerKey: this._options.computeWorkerKey,
107107
taskQueue: this._options.taskQueue,
108+
workerSchedulingPolicy: this._options.workerSchedulingPolicy,
108109
},
109110
);
110111

packages/jest-worker/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export type FarmOptions = {
9696
computeWorkerKey?: (method: string, ...args: Array<unknown>) => string | null;
9797
exposedMethods?: ReadonlyArray<string>;
9898
forkOptions?: ForkOptions;
99+
workerSchedulingPolicy?: 'round-robin' | 'in-order';
99100
resourceLimits?: ResourceLimits;
100101
setupArgs?: Array<unknown>;
101102
maxRetries?: number;

packages/pretty-format/src/plugins/DOMElement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const testNode = (val: any) => {
2727
const {nodeType, tagName} = val;
2828
const isCustomElement =
2929
(typeof tagName === 'string' && tagName.includes('-')) ||
30-
val.hasAttribute?.('is');
30+
(typeof val.hasAttribute === 'function' && val.hasAttribute('is'));
3131

3232
return (
3333
(nodeType === ELEMENT_NODE &&

0 commit comments

Comments
 (0)