Skip to content

Commit 0ab070c

Browse files
author
Micha Reiser
authored
PriorityQueue implementation for jest-worker (#10921)
1 parent e84f682 commit 0ab070c

11 files changed

Lines changed: 583 additions & 65 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- `[jest-runner]` [**BREAKING**] Run transforms over `testRunnner` ([#8823](https://github.com/facebook/jest/pull/8823))
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))
21+
- `[jest-worker]` Add support for custom task queues and adds a `PriorityQueue` implementation. ([#10921](https://github.com/facebook/jest/pull/10921))
2122

2223
### Fixes
2324

packages/jest-worker/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ 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+
### `taskQueue`: TaskQueue` (optional)
95+
96+
The task queue defines in which order tasks (method calls) are processed by the workers. `jest-worker` ships with a `FifoQueue` and `PriorityQueue`:
97+
98+
- `FifoQueue` (default): Processes the method calls (tasks) in the call order.
99+
- `PriorityQueue`: Processes the method calls by a computed priority in natural ordering (lower priorities first). Tasks with the same priority are processed in any order (FIFO not guaranteed). The constructor accepts a single argument, the function that is passed the name of the called function and the arguments and returns a numerical value for the priority: `new require('jest-worker').PriorityQueue((method, filename) => filename.length)`.
100+
94101
## JestWorker
95102

96103
### Methods

packages/jest-worker/src/Farm.ts

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
/* eslint-disable local/ban-types-eventually */
99

10+
import FifoQueue from './FifoQueue';
1011
import {
1112
CHILD_MESSAGE_CALL,
1213
ChildMessage,
@@ -16,36 +17,34 @@ import {
1617
OnStart,
1718
PromiseWithCustomMessage,
1819
QueueChildMessage,
19-
QueueItem,
20+
TaskQueue,
2021
WorkerInterface,
2122
} from './types';
2223

2324
export default class Farm {
2425
private _computeWorkerKey: FarmOptions['computeWorkerKey'];
2526
private _cacheKeys: Record<string, WorkerInterface>;
2627
private _callback: Function;
27-
private _last: Array<QueueItem>;
2828
private _locks: Array<boolean>;
2929
private _numOfWorkers: number;
3030
private _offset: number;
31-
private _queue: Array<QueueItem | null>;
31+
private _taskQueue: TaskQueue;
3232

3333
constructor(
3434
numOfWorkers: number,
3535
callback: Function,
36-
computeWorkerKey?: FarmOptions['computeWorkerKey'],
36+
options: {
37+
computeWorkerKey?: FarmOptions['computeWorkerKey'];
38+
taskQueue?: TaskQueue;
39+
} = {},
3740
) {
3841
this._cacheKeys = Object.create(null);
3942
this._callback = callback;
40-
this._last = [];
4143
this._locks = [];
4244
this._numOfWorkers = numOfWorkers;
4345
this._offset = 0;
44-
this._queue = [];
45-
46-
if (computeWorkerKey) {
47-
this._computeWorkerKey = computeWorkerKey;
48-
}
46+
this._computeWorkerKey = options.computeWorkerKey;
47+
this._taskQueue = options.taskQueue ?? new FifoQueue();
4948
}
5049

5150
doWork(
@@ -96,7 +95,8 @@ export default class Farm {
9695
const task = {onCustomMessage, onEnd, onStart, request};
9796

9897
if (worker) {
99-
this._enqueue(task, worker.getWorkerId());
98+
this._taskQueue.enqueue(task, worker.getWorkerId());
99+
this._process(worker.getWorkerId());
100100
} else {
101101
this._push(task);
102102
}
@@ -108,29 +108,21 @@ export default class Farm {
108108
return promise;
109109
}
110110

111-
private _getNextTask(workerId: number): QueueChildMessage | null {
112-
let queueHead = this._queue[workerId];
113-
114-
while (queueHead && queueHead.task.request[1]) {
115-
queueHead = queueHead.next || null;
116-
}
117-
118-
this._queue[workerId] = queueHead;
119-
120-
return queueHead && queueHead.task;
121-
}
122-
123111
private _process(workerId: number): Farm {
124112
if (this._isLocked(workerId)) {
125113
return this;
126114
}
127115

128-
const task = this._getNextTask(workerId);
116+
const task = this._taskQueue.dequeue(workerId);
129117

130118
if (!task) {
131119
return this;
132120
}
133121

122+
if (task.request[1]) {
123+
throw new Error('Queue implementation returned processed task');
124+
}
125+
134126
const onEnd = (error: Error | null, result: unknown) => {
135127
task.onEnd(error, result);
136128

@@ -152,28 +144,15 @@ export default class Farm {
152144
return this;
153145
}
154146

155-
private _enqueue(task: QueueChildMessage, workerId: number): Farm {
156-
const item = {next: null, task};
157-
158-
if (task.request[1]) {
159-
return this;
160-
}
161-
162-
if (this._queue[workerId]) {
163-
this._last[workerId].next = item;
164-
} else {
165-
this._queue[workerId] = item;
166-
}
167-
168-
this._last[workerId] = item;
169-
this._process(workerId);
170-
171-
return this;
172-
}
173-
174147
private _push(task: QueueChildMessage): Farm {
148+
this._taskQueue.enqueue(task);
149+
175150
for (let i = 0; i < this._numOfWorkers; i++) {
176-
this._enqueue(task, (this._offset + i) % this._numOfWorkers);
151+
this._process((this._offset + i) % this._numOfWorkers);
152+
153+
if (task.request[1]) {
154+
break;
155+
}
177156
}
178157

179158
this._offset++;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import type {QueueChildMessage, TaskQueue} from './types';
9+
10+
type WorkerQueueValue = {
11+
task: QueueChildMessage;
12+
13+
/**
14+
* The task that was at the top of the shared queue at the time this
15+
* worker specific task was enqueued. Required to maintain FIFO ordering
16+
* across queues. The worker specific task should only be dequeued if the
17+
* previous shared task is null or has been processed.
18+
*/
19+
previousSharedTask: QueueChildMessage | null;
20+
};
21+
22+
/**
23+
* First-in, First-out task queue that manages a dedicated pool
24+
* for each worker as well as a shared queue. The FIFO ordering is guaranteed
25+
* across the worker specific and shared queue.
26+
*/
27+
export default class FifoQueue implements TaskQueue {
28+
private _workerQueues: Array<
29+
InternalQueue<WorkerQueueValue> | undefined
30+
> = [];
31+
private _sharedQueue = new InternalQueue<QueueChildMessage>();
32+
33+
enqueue(task: QueueChildMessage, workerId?: number): void {
34+
if (workerId == null) {
35+
this._sharedQueue.enqueue(task);
36+
return;
37+
}
38+
39+
let workerQueue = this._workerQueues[workerId];
40+
if (workerQueue == null) {
41+
workerQueue = this._workerQueues[
42+
workerId
43+
] = new InternalQueue<WorkerQueueValue>();
44+
}
45+
46+
const sharedTop = this._sharedQueue.peekLast();
47+
const item = {previousSharedTask: sharedTop, task};
48+
49+
workerQueue.enqueue(item);
50+
}
51+
52+
dequeue(workerId: number): QueueChildMessage | null {
53+
const workerTop = this._workerQueues[workerId]?.peek();
54+
const sharedTaskIsProcessed =
55+
workerTop?.previousSharedTask?.request[1] ?? true;
56+
57+
// Process the top task from the shared queue if
58+
// - there's no task in the worker specific queue or
59+
// - if the non-worker-specific task after which this worker specifif task
60+
// hasn been queued wasn't processed yet
61+
if (workerTop != null && sharedTaskIsProcessed) {
62+
return this._workerQueues[workerId]?.dequeue()?.task ?? null;
63+
}
64+
65+
return this._sharedQueue.dequeue();
66+
}
67+
}
68+
69+
type QueueItem<TValue> = {
70+
value: TValue;
71+
next: QueueItem<TValue> | null;
72+
};
73+
74+
/**
75+
* FIFO queue for a single worker / shared queue.
76+
*/
77+
class InternalQueue<TValue> {
78+
private _head: QueueItem<TValue> | null = null;
79+
private _last: QueueItem<TValue> | null = null;
80+
81+
enqueue(value: TValue): void {
82+
const item = {next: null, value};
83+
84+
if (this._last == null) {
85+
this._head = item;
86+
} else {
87+
this._last.next = item;
88+
}
89+
90+
this._last = item;
91+
}
92+
93+
dequeue(): TValue | null {
94+
if (this._head == null) {
95+
return null;
96+
}
97+
98+
const item = this._head;
99+
this._head = item.next;
100+
101+
if (this._head == null) {
102+
this._last = null;
103+
}
104+
105+
return item.value;
106+
}
107+
108+
peek(): TValue | null {
109+
return this._head?.value ?? null;
110+
}
111+
112+
peekLast(): TValue | null {
113+
return this._last?.value ?? null;
114+
}
115+
}

0 commit comments

Comments
 (0)