Skip to content

Commit b10fee0

Browse files
ADKasterawesomekling
authored andcommitted
LibWeb+WebWorker: Convert Workers to use MessagePorts for postMessage
This aligns Workers and Window and MessagePorts to all use the same mechanism for transferring serialized messages across realms. It also allows transferring more message ports into a worker. Re-enable the Worker-echo test, as none of the MessagePort tests have themselves been flaky, and those are now using the same underlying implementation.
1 parent 37f2d49 commit b10fee0

21 files changed

+159
-222
lines changed

Base/res/html/misc/worker.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
onmessage = evt => {
22
console.log("In Worker - Got message:", JSON.stringify(evt.data));
33

4-
postMessage(evt.data, null);
4+
postMessage(evt.data);
55
};
66

77
console.log("In Worker - Loaded", this);
88
console.log("Keys: ", JSON.stringify(Object.keys(this)));
99

10-
postMessage("loaded", null);
10+
postMessage("loaded");

Tests/LibWeb/TestConfig.ini

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
[Skipped]
2-
Text/input/Worker/Worker-echo.html
3-
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
Got message from worker: "loaded"
2-
Got message from worker: {"msg":"marco"}
2+
Got message from port: "Worker got message port!"
3+
Got message from port: "Extra Port got message: \"Hello from port2\""
34
DONE

Tests/LibWeb/Text/input/Worker/Worker-echo.html

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,31 @@
22
<script>
33
asyncTest((done) => {
44
let work = new Worker("worker.js");
5+
let channel = new MessageChannel();
6+
7+
function finishTest() {
8+
println("DONE");
9+
work.onmessage = null;
10+
work.terminate();
11+
channel.port2.onmessage = null;
12+
done();
13+
}
514
let count = 0;
615
work.onmessage = (evt) => {
716
println("Got message from worker: " + JSON.stringify(evt.data));
817
count++;
9-
work.postMessage({"msg": "marco"});
10-
if (count === 2) {
11-
println("DONE");
12-
work.onmessage = null;
13-
work.terminate();
14-
done();
18+
if (count === 3) {
19+
finishTest();
1520
}
1621
};
22+
channel.port2.onmessage = (evt) => {
23+
println("Got message from port: " + JSON.stringify(evt.data));
24+
channel.port2.postMessage("Hello from port2");
25+
count++;
26+
if (count === 3) {
27+
finishTest();
28+
}
29+
};
30+
work.postMessage({ port: channel.port1 }, { transfer : [channel.port1]});
1731
});
1832
</script>
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1+
let extraPort = null;
2+
13
onmessage = evt => {
2-
postMessage(evt.data, null);
4+
if (evt.ports.length > 0) {
5+
extraPort = evt.ports[0];
6+
extraPort.onmessage = evt => {
7+
extraPort.postMessage("Extra Port got message: " + JSON.stringify(evt.data));
8+
};
9+
extraPort.postMessage("Worker got message port!");
10+
} else {
11+
postMessage(evt.data);
12+
}
313
};
4-
postMessage("loaded", null);
14+
postMessage("loaded");

Userland/Libraries/LibWeb/Forward.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ class Timer;
446446
class TimeRanges;
447447
class ToggleEvent;
448448
class TrackEvent;
449+
struct TransferDataHolder;
449450
class TraversableNavigable;
450451
class VideoTrack;
451452
class VideoTrackList;

Userland/Libraries/LibWeb/HTML/MessagePort.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <LibWeb/HTML/MessageEvent.h>
2020
#include <LibWeb/HTML/MessagePort.h>
2121
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
22+
#include <LibWeb/HTML/WorkerGlobalScope.h>
2223

2324
namespace Web::HTML {
2425

@@ -53,6 +54,11 @@ void MessagePort::visit_edges(Cell::Visitor& visitor)
5354
visitor.visit(m_remote_port);
5455
}
5556

57+
void MessagePort::set_worker_event_target(JS::NonnullGCPtr<DOM::EventTarget> target)
58+
{
59+
m_worker_event_target = target;
60+
}
61+
5662
// https://html.spec.whatwg.org/multipage/web-messaging.html#message-ports:transfer-steps
5763
WebIDL::ExceptionOr<void> MessagePort::transfer_steps(HTML::TransferDataHolder& data_holder)
5864
{
@@ -107,6 +113,10 @@ WebIDL::ExceptionOr<void> MessagePort::transfer_receiving_steps(HTML::TransferDa
107113
VERIFY(fd_tag == IPC_FILE_TAG);
108114
fd = data_holder.fds.take_first();
109115
m_fd_passing_socket = MUST(Core::LocalSocket::adopt_fd(fd.take_fd(), Core::LocalSocket::PreventSIGPIPE::Yes));
116+
117+
m_socket->on_ready_to_read = [strong_this = JS::make_handle(this)]() {
118+
strong_this->read_from_socket();
119+
};
110120
} else if (fd_tag != 0) {
111121
dbgln("Unexpected byte {:x} in MessagePort transfer data", fd_tag);
112122
VERIFY_NOT_REACHED();
@@ -348,6 +358,16 @@ void MessagePort::post_message_task_steps(SerializedTransferRecord& serialize_wi
348358
// NOTE: This can be different from targetPort, if targetPort itself was transferred and thus all its tasks moved along with it.
349359
auto* final_target_port = this;
350360

361+
// IMPLEMENTATION DEFINED:
362+
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-worker-interface
363+
// Worker objects act as if they had an implicit MessagePort associated with them.
364+
// All messages received by that port must immediately be retargeted at the Worker object.
365+
// We therefore set a special event target for those implicit ports on the Worker and the WorkerGlobalScope objects
366+
EventTarget* message_event_target = final_target_port;
367+
if (m_worker_event_target != nullptr) {
368+
message_event_target = m_worker_event_target;
369+
}
370+
351371
// 2. Let targetRealm be finalTargetPort's relevant realm.
352372
auto& target_realm = relevant_realm(*final_target_port);
353373
auto& target_vm = target_realm.vm();
@@ -359,7 +379,7 @@ void MessagePort::post_message_task_steps(SerializedTransferRecord& serialize_wi
359379
// If this throws an exception, catch it, fire an event named messageerror at finalTargetPort, using MessageEvent, and then return.
360380
auto exception = deserialize_record_or_error.release_error();
361381
MessageEventInit event_init {};
362-
final_target_port->dispatch_event(MessageEvent::create(target_realm, HTML::EventNames::messageerror, event_init));
382+
message_event_target->dispatch_event(MessageEvent::create(target_realm, HTML::EventNames::messageerror, event_init));
363383
return;
364384
}
365385
auto deserialize_record = deserialize_record_or_error.release_value();
@@ -380,7 +400,7 @@ void MessagePort::post_message_task_steps(SerializedTransferRecord& serialize_wi
380400
MessageEventInit event_init {};
381401
event_init.data = message_clone;
382402
event_init.ports = move(new_ports);
383-
final_target_port->dispatch_event(MessageEvent::create(target_realm, HTML::EventNames::message, event_init));
403+
message_event_target->dispatch_event(MessageEvent::create(target_realm, HTML::EventNames::message, event_init));
384404
}
385405

386406
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-start

Userland/Libraries/LibWeb/HTML/MessagePort.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ class MessagePort final : public DOM::EventTarget
6161
virtual WebIDL::ExceptionOr<void> transfer_receiving_steps(HTML::TransferDataHolder&) override;
6262
virtual HTML::TransferType primary_interface() const override { return HTML::TransferType::MessagePort; }
6363

64+
void set_worker_event_target(JS::NonnullGCPtr<DOM::EventTarget>);
65+
6466
private:
6567
explicit MessagePort(JS::Realm&);
6668

@@ -91,6 +93,8 @@ class MessagePort final : public DOM::EventTarget
9193
Error,
9294
} m_socket_state { SocketState::Header };
9395
size_t m_socket_incoming_message_size { 0 };
96+
97+
JS::GCPtr<DOM::EventTarget> m_worker_event_target;
9498
};
9599

96100
}

Userland/Libraries/LibWeb/HTML/Worker.cpp

Lines changed: 10 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@
1111
#include <LibWeb/HTML/Scripting/Environments.h>
1212
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
1313
#include <LibWeb/HTML/Worker.h>
14-
#include <LibWeb/HTML/WorkerDebugConsoleClient.h>
1514
#include <LibWeb/WebIDL/ExceptionOr.h>
1615

1716
namespace Web::HTML {
1817

1918
JS_DEFINE_ALLOCATOR(Worker);
2019

2120
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-worker-interface
22-
Worker::Worker(String const& script_url, WorkerOptions const options, DOM::Document& document)
21+
Worker::Worker(String const& script_url, WorkerOptions const& options, DOM::Document& document)
2322
: DOM::EventTarget(document.realm())
2423
, m_script_url(script_url)
2524
, m_options(options)
@@ -42,7 +41,7 @@ void Worker::visit_edges(Cell::Visitor& visitor)
4241
}
4342

4443
// https://html.spec.whatwg.org/multipage/workers.html#dom-worker
45-
WebIDL::ExceptionOr<JS::NonnullGCPtr<Worker>> Worker::create(String const& script_url, WorkerOptions const options, DOM::Document& document)
44+
WebIDL::ExceptionOr<JS::NonnullGCPtr<Worker>> Worker::create(String const& script_url, WorkerOptions const& options, DOM::Document& document)
4645
{
4746
dbgln_if(WEB_WORKER_DEBUG, "WebWorker: Creating worker with script_url = {}", script_url);
4847

@@ -79,6 +78,7 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Worker>> Worker::create(String const& scrip
7978

8079
// 8. Associate the outside port with worker
8180
worker->m_outside_port = outside_port;
81+
worker->m_outside_port->set_worker_event_target(worker);
8282

8383
// 9. Run this step in parallel:
8484
// 1. Run a worker given worker, worker URL, outside settings, outside port, and options.
@@ -89,7 +89,7 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Worker>> Worker::create(String const& scrip
8989
}
9090

9191
// https://html.spec.whatwg.org/multipage/workers.html#run-a-worker
92-
void Worker::run_a_worker(AK::URL& url, EnvironmentSettingsObject& outside_settings, MessagePort&, WorkerOptions const& options)
92+
void Worker::run_a_worker(AK::URL& url, EnvironmentSettingsObject& outside_settings, JS::GCPtr<MessagePort> port, WorkerOptions const& options)
9393
{
9494
// 1. Let is shared be true if worker is a SharedWorker object, and false otherwise.
9595
// FIXME: SharedWorker support
@@ -110,55 +110,7 @@ void Worker::run_a_worker(AK::URL& url, EnvironmentSettingsObject& outside_setti
110110
// and is shared. Run the rest of these steps in that agent.
111111

112112
// Note: This spawns a new process to act as the 'agent' for the worker.
113-
m_agent = heap().allocate_without_realm<WorkerAgent>(url, options);
114-
115-
auto& socket = m_agent->socket();
116-
// FIXME: Hide this logic in MessagePort
117-
socket.set_notifications_enabled(true);
118-
socket.on_ready_to_read = [this] {
119-
auto& socket = this->m_agent->socket();
120-
auto& vm = this->vm();
121-
auto& realm = this->realm();
122-
123-
auto num_bytes_ready = MUST(socket.pending_bytes());
124-
switch (m_outside_port_state) {
125-
case PortState::Header: {
126-
if (num_bytes_ready < 8)
127-
break;
128-
auto const magic = MUST(socket.read_value<u32>());
129-
if (magic != 0xDEADBEEF) {
130-
m_outside_port_state = PortState::Error;
131-
break;
132-
}
133-
m_outside_port_incoming_message_size = MUST(socket.read_value<u32>());
134-
num_bytes_ready -= 8;
135-
m_outside_port_state = PortState::Data;
136-
}
137-
[[fallthrough]];
138-
case PortState::Data: {
139-
if (num_bytes_ready < m_outside_port_incoming_message_size)
140-
break;
141-
SerializationRecord rec; // FIXME: Keep in class scope
142-
rec.resize(m_outside_port_incoming_message_size / sizeof(u32));
143-
144-
MUST(socket.read_until_filled(to_bytes(rec.span())));
145-
146-
TemporaryExecutionContext cxt(relevant_settings_object(*this));
147-
VERIFY(&realm == vm.current_realm());
148-
MessageEventInit event_init {};
149-
event_init.data = MUST(structured_deserialize(vm, rec, realm, {}));
150-
// FIXME: Fill in the rest of the info from MessagePort
151-
152-
this->dispatch_event(MessageEvent::create(realm, EventNames::message, event_init));
153-
154-
m_outside_port_state = PortState::Header;
155-
break;
156-
}
157-
case PortState::Error:
158-
VERIFY_NOT_REACHED();
159-
break;
160-
}
161-
};
113+
m_agent = heap().allocate<WorkerAgent>(outside_settings.realm(), url, options, port);
162114
}
163115

164116
// https://html.spec.whatwg.org/multipage/workers.html#dom-worker-terminate
@@ -170,29 +122,15 @@ WebIDL::ExceptionOr<void> Worker::terminate()
170122
}
171123

172124
// https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
173-
WebIDL::ExceptionOr<void> Worker::post_message(JS::Value message, JS::Value)
125+
WebIDL::ExceptionOr<void> Worker::post_message(JS::Value message, StructuredSerializeOptions const& options)
174126
{
175127
dbgln_if(WEB_WORKER_DEBUG, "WebWorker: Post Message: {}", message.to_string_without_side_effects());
176128

177-
// FIXME: 1. Let targetPort be the port with which this is entangled, if any; otherwise let it be null.
178-
// FIXME: 2. Let options be «[ "transfer" → transfer ]».
179-
// FIXME: 3. Run the message port post message steps providing this, targetPort, message and options.
129+
// The postMessage(message, transfer) and postMessage(message, options) methods on Worker objects act as if,
130+
// when invoked, they immediately invoked the respective postMessage(message, transfer) and
131+
// postMessage(message, options) on the port, with the same arguments, and returned the same return value.
180132

181-
auto& realm = this->realm();
182-
auto& vm = this->vm();
183-
184-
// FIXME: Use the with-transfer variant, which should(?) prepend the magic + size at the front
185-
auto data = TRY(structured_serialize(vm, message));
186-
187-
Array<u32, 2> header = { 0xDEADBEEF, static_cast<u32>(data.size() * sizeof(u32)) };
188-
189-
if (auto const err = m_agent->socket().write_until_depleted(to_readonly_bytes(header.span())); err.is_error())
190-
return WebIDL::DataCloneError::create(realm, TRY_OR_THROW_OOM(vm, String::formatted("{}", err.error())));
191-
192-
if (auto const err = m_agent->socket().write_until_depleted(to_readonly_bytes(data.span())); err.is_error())
193-
return WebIDL::DataCloneError::create(realm, TRY_OR_THROW_OOM(vm, String::formatted("{}", err.error())));
194-
195-
return {};
133+
return m_outside_port->post_message(message, options);
196134
}
197135

198136
#undef __ENUMERATE

Userland/Libraries/LibWeb/HTML/Worker.h

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ class Worker : public DOM::EventTarget {
3232
JS_DECLARE_ALLOCATOR(Worker);
3333

3434
public:
35-
static WebIDL::ExceptionOr<JS::NonnullGCPtr<Worker>> create(String const& script_url, WorkerOptions const options, DOM::Document& document);
36-
static WebIDL::ExceptionOr<JS::NonnullGCPtr<Worker>> construct_impl(JS::Realm& realm, String const& script_url, WorkerOptions const options)
35+
static WebIDL::ExceptionOr<JS::NonnullGCPtr<Worker>> create(String const& script_url, WorkerOptions const& options, DOM::Document& document);
36+
static WebIDL::ExceptionOr<JS::NonnullGCPtr<Worker>> construct_impl(JS::Realm& realm, String const& script_url, WorkerOptions const& options)
3737
{
3838
auto& window = verify_cast<HTML::Window>(realm.global_object());
3939
return Worker::create(script_url, options, window.associated_document());
4040
}
4141

4242
WebIDL::ExceptionOr<void> terminate();
4343

44-
WebIDL::ExceptionOr<void> post_message(JS::Value message, JS::Value transfer);
44+
WebIDL::ExceptionOr<void> post_message(JS::Value message, StructuredSerializeOptions const&);
4545

4646
virtual ~Worker() = default;
4747

@@ -55,7 +55,7 @@ class Worker : public DOM::EventTarget {
5555
#undef __ENUMERATE
5656

5757
protected:
58-
Worker(String const&, const WorkerOptions, DOM::Document&);
58+
Worker(String const&, WorkerOptions const&, DOM::Document&);
5959

6060
private:
6161
virtual void initialize(JS::Realm&) override;
@@ -66,17 +66,10 @@ class Worker : public DOM::EventTarget {
6666

6767
JS::GCPtr<DOM::Document> m_document;
6868
JS::GCPtr<MessagePort> m_outside_port;
69-
// FIXME: Move tihs state into the message port (and actually use it :) )
70-
enum class PortState : u8 {
71-
Header,
72-
Data,
73-
Error,
74-
} m_outside_port_state { PortState::Header };
75-
size_t m_outside_port_incoming_message_size { 0 };
7669

7770
JS::GCPtr<WorkerAgent> m_agent;
7871

79-
void run_a_worker(AK::URL& url, EnvironmentSettingsObject& outside_settings, MessagePort& outside_port, WorkerOptions const& options);
72+
void run_a_worker(AK::URL& url, EnvironmentSettingsObject& outside_settings, JS::GCPtr<MessagePort> outside_port, WorkerOptions const& options);
8073
};
8174

8275
}

0 commit comments

Comments
 (0)