Skip to content

Commit bdf2cf0

Browse files
dnlupnovemberborn
andauthored
Make shared workers available in worker threads only
Now that AVA runs test files in worker threads, it makes sense for _shared_ workers to only be available in that execution mode. This makes for more efficient communication and will enable us to transfer byte arrays, message ports and even share memory. Separately this commit fixes how shared worker errors are reported. Co-authored-by: Mark Wubben <[email protected]>
1 parent eef1a55 commit bdf2cf0

File tree

10 files changed

+221
-195
lines changed

10 files changed

+221
-195
lines changed

lib/fork.js

Lines changed: 9 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,6 @@ const {controlFlow} = require('./ipc-flow-control');
66

77
const WORKER_PATH = require.resolve('./worker/base.js');
88

9-
class SharedWorkerChannel extends Emittery {
10-
constructor({channelId, filename, initialData}, sendToFork) {
11-
super();
12-
13-
this.id = channelId;
14-
this.filename = filename;
15-
this.initialData = initialData;
16-
this.sendToFork = sendToFork;
17-
}
18-
19-
signalReady() {
20-
this.sendToFork({
21-
type: 'shared-worker-ready',
22-
channelId: this.id
23-
});
24-
}
25-
26-
signalError() {
27-
this.sendToFork({
28-
type: 'shared-worker-error',
29-
channelId: this.id
30-
});
31-
}
32-
33-
emitMessage({messageId, replyTo, serializedData}) {
34-
this.emit('message', {
35-
messageId,
36-
replyTo,
37-
serializedData
38-
});
39-
}
40-
41-
forwardMessageToFork({messageId, replyTo, serializedData}) {
42-
this.sendToFork({
43-
type: 'shared-worker-message',
44-
channelId: this.id,
45-
messageId,
46-
replyTo,
47-
serializedData
48-
});
49-
}
50-
}
51-
529
let forkCounter = 0;
5310

5411
const createWorker = (options, execArgv) => {
@@ -97,7 +54,6 @@ const createWorker = (options, execArgv) => {
9754
module.exports = (file, options, execArgv = process.execArgv) => {
9855
// TODO: this can be changed to use `threadId` when using worker_threads
9956
const forkId = `fork/${++forkCounter}`;
100-
const sharedWorkerChannels = new Map();
10157

10258
let finished = false;
10359

@@ -146,17 +102,19 @@ module.exports = (file, options, execArgv = process.execArgv) => {
146102
case 'ready-for-options':
147103
send({type: 'options', options});
148104
break;
149-
150105
case 'shared-worker-connect': {
151-
const channel = new SharedWorkerChannel(message.ava, send);
152-
sharedWorkerChannels.set(channel.id, channel);
153-
emitter.emit('connectSharedWorker', channel);
106+
const {channelId, filename, initialData, port} = message.ava;
107+
emitter.emit('connectSharedWorker', {
108+
filename,
109+
initialData,
110+
port,
111+
signalError() {
112+
send({type: 'shared-worker-error', channelId});
113+
}
114+
});
154115
break;
155116
}
156117

157-
case 'shared-worker-message':
158-
sharedWorkerChannels.get(message.ava.channelId).emitMessage(message.ava);
159-
break;
160118
case 'ping':
161119
send({type: 'pong'});
162120
break;

lib/plugin-support/shared-worker-loader.js

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
const {EventEmitter, on} = require('events');
2-
const v8 = require('v8');
32
const {workerData, parentPort} = require('worker_threads');
43
const pkg = require('../../package.json');
54

6-
// Used to forward messages received over the `parentPort`. Every subscription
7-
// adds a listener, so do not enforce any maximums.
5+
// Used to forward messages received over the `parentPort` and any direct ports
6+
// to test workers. Every subscription adds a listener, so do not enforce any
7+
// maximums.
88
const events = new EventEmitter().setMaxListeners(0);
9+
const emitMessage = message => {
10+
// Wait for a turn of the event loop, to allow new subscriptions to be
11+
// set up in response to the previous message.
12+
setImmediate(() => events.emit('message', message));
13+
};
914

1015
// Map of active test workers, used in receiveMessages() to get a reference to
1116
// the TestWorker instance, and relevant release functions.
1217
const activeTestWorkers = new Map();
1318

19+
const internalMessagePort = Symbol('Internal MessagePort');
20+
1421
class TestWorker {
15-
constructor(id, file) {
22+
constructor(id, file, port) {
1623
this.id = id;
1724
this.file = file;
25+
this[internalMessagePort] = port;
1826
}
1927

2028
teardown(fn) {
@@ -47,10 +55,10 @@ class TestWorker {
4755
}
4856

4957
class ReceivedMessage {
50-
constructor(testWorker, id, serializedData) {
58+
constructor(testWorker, id, data) {
5159
this.testWorker = testWorker;
5260
this.id = id;
53-
this.data = v8.deserialize(new Uint8Array(serializedData));
61+
this.data = data;
5462
}
5563

5664
reply(data) {
@@ -98,7 +106,7 @@ async function * receiveMessages(fromTestWorker, replyTo) {
98106

99107
let received = messageCache.get(message);
100108
if (received === undefined) {
101-
received = new ReceivedMessage(active.instance, message.messageId, message.serializedData);
109+
received = new ReceivedMessage(active.instance, message.messageId, message.data);
102110
messageCache.set(message, received);
103111
}
104112

@@ -112,11 +120,10 @@ const nextMessageId = () => `${messageIdPrefix}/${++messageCounter}`;
112120

113121
function publishMessage(testWorker, data, replyTo) {
114122
const id = nextMessageId();
115-
parentPort.postMessage({
123+
testWorker[internalMessagePort].postMessage({
116124
type: 'message',
117125
messageId: id,
118-
testWorkerId: testWorker.id,
119-
serializedData: [...v8.serialize(data)],
126+
data,
120127
replyTo
121128
});
122129

@@ -130,11 +137,13 @@ function publishMessage(testWorker, data, replyTo) {
130137

131138
function broadcastMessage(data) {
132139
const id = nextMessageId();
133-
parentPort.postMessage({
134-
type: 'broadcast',
135-
messageId: id,
136-
serializedData: [...v8.serialize(data)]
137-
});
140+
for (const trackedWorker of activeTestWorkers.values()) {
141+
trackedWorker.instance[internalMessagePort].postMessage({
142+
type: 'message',
143+
messageId: id,
144+
data
145+
});
146+
}
138147

139148
return {
140149
id,
@@ -184,12 +193,13 @@ loadFactory(workerData.filename).then(factory => {
184193

185194
parentPort.on('message', async message => {
186195
if (message.type === 'register-test-worker') {
187-
const {id, file} = message;
188-
const instance = new TestWorker(id, file);
196+
const {id, file, port} = message;
197+
const instance = new TestWorker(id, file, port);
189198

190199
activeTestWorkers.set(id, {instance, teardownFns: new Set()});
191200

192201
produceTestWorker(instance);
202+
port.on('message', message => emitMessage({testWorkerId: id, ...message}));
193203
}
194204

195205
if (message.type === 'deregister-test-worker') {
@@ -207,11 +217,9 @@ loadFactory(workerData.filename).then(factory => {
207217
type: 'deregistered-test-worker',
208218
id
209219
});
210-
}
211220

212-
// Wait for a turn of the event loop, to allow new subscriptions to be
213-
// set up in response to the previous message.
214-
setImmediate(() => events.emit('message', message));
221+
emitMessage(message);
222+
}
215223
});
216224

217225
return {

lib/plugin-support/shared-workers.js

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const events = require('events');
22
const {Worker} = require('worker_threads');
3-
43
const serializeError = require('../serialize-error');
54

65
const LOADER = require.resolve('./shared-worker-loader');
@@ -16,11 +15,12 @@ const waitForAvailable = async worker => {
1615
}
1716
};
1817

19-
function launchWorker({filename, initialData}) {
18+
function launchWorker(filename, initialData) {
2019
if (launchedWorkers.has(filename)) {
2120
return launchedWorkers.get(filename);
2221
}
2322

23+
// TODO: remove the custom id and use the built-in thread id.
2424
const id = `shared-worker/${++sharedWorkerCounter}`;
2525
const worker = new Worker(LOADER, {
2626
// Ensure the worker crashes for unhandled rejections, rather than allowing undefined behavior.
@@ -63,25 +63,10 @@ async function observeWorkerProcess(fork, runStatus) {
6363
}
6464
});
6565

66-
fork.onConnectSharedWorker(async channel => {
67-
const launched = launchWorker(channel);
68-
69-
const handleChannelMessage = ({messageId, replyTo, serializedData}) => {
70-
launched.worker.postMessage({
71-
type: 'message',
72-
testWorkerId: fork.forkId,
73-
messageId,
74-
replyTo,
75-
serializedData
76-
});
77-
};
66+
fork.onConnectSharedWorker(async ({filename, initialData, port, signalError}) => {
67+
const launched = launchWorker(filename, initialData);
7868

7969
const handleWorkerMessage = async message => {
80-
if (message.type === 'broadcast' || (message.type === 'message' && message.testWorkerId === fork.forkId)) {
81-
const {messageId, replyTo, serializedData} = message;
82-
channel.forwardMessageToFork({messageId, replyTo, serializedData});
83-
}
84-
8570
if (message.type === 'deregistered-test-worker' && message.id === fork.forkId) {
8671
launched.worker.off('message', handleWorkerMessage);
8772

@@ -96,31 +81,31 @@ async function observeWorkerProcess(fork, runStatus) {
9681
signalDeregistered();
9782
launched.worker.off('message', handleWorkerMessage);
9883
runStatus.emitStateChange({type: 'shared-worker-error', err: serializeError('Shared worker error', true, error)});
99-
channel.signalError();
84+
signalError();
10085
});
10186

10287
try {
10388
await launched.statePromises.available;
10489

10590
registrationCount++;
91+
92+
port.postMessage({type: 'ready'});
93+
10694
launched.worker.postMessage({
10795
type: 'register-test-worker',
10896
id: fork.forkId,
109-
file: fork.file
110-
});
97+
file: fork.file,
98+
port
99+
}, [port]);
111100

112101
fork.promise.finally(() => {
113102
launched.worker.postMessage({
114103
type: 'deregister-test-worker',
115104
id: fork.forkId
116105
});
117-
118-
channel.off('message', handleChannelMessage);
119106
});
120107

121108
launched.worker.on('message', handleWorkerMessage);
122-
channel.on('message', handleChannelMessage);
123-
channel.signalReady();
124109
} catch {
125110
return;
126111
} finally {

lib/reporters/default.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ class Reporter {
772772
for (const evt of this.sharedWorkerErrors) {
773773
this.lineWriter.writeLine(colors.error(`${figures.cross} Error in shared worker`));
774774
this.lineWriter.writeLine();
775-
this.writeErr(evt.err);
775+
this.writeErr(evt);
776776
if (evt !== last || writeTrailingLines) {
777777
this.lineWriter.writeLine();
778778
this.lineWriter.writeLine();

0 commit comments

Comments
 (0)