Skip to content

Commit

Permalink
LibWeb: Make EventLoop, TaskQueue, and Task GC-allocated
Browse files Browse the repository at this point in the history
...and use HeapFunction instead of SafeFunction for task steps.

Since there is only one EventLoop per process, it lives as a global
handle in the VM custom data.

This makes it much easier to reason about lifetimes of tasks, task
steps, and random stuff captured by them.
  • Loading branch information
awesomekling committed Apr 4, 2024
1 parent b17a9ad commit ce78fcc
Show file tree
Hide file tree
Showing 20 changed files with 167 additions and 124 deletions.
17 changes: 9 additions & 8 deletions Userland/Libraries/LibWeb/Animations/Animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,7 @@ void Animation::update_finished_state(DidSeek did_seek, SynchronouslyNotify sync
// steps:
if (current_finished_state && !m_is_finished) {
// 1. Let finish notification steps refer to the following procedure:
JS::SafeFunction<void()> finish_notification_steps = [&]() {
auto finish_notification_steps = JS::create_heap_function(heap(), [this, &realm]() {
// 1. If animation’s play state is not equal to finished, abort these steps.
if (play_state() != Bindings::AnimationPlayState::Finished)
return;
Expand Down Expand Up @@ -1135,13 +1135,14 @@ void Animation::update_finished_state(DidSeek did_seek, SynchronouslyNotify sync
else {
// Manually create a task so its ID can be saved
auto& document = verify_cast<HTML::Window>(realm.global_object()).associated_document();
auto task = HTML::Task::create(HTML::Task::Source::DOMManipulation, &document, [this, finish_event]() {
dispatch_event(finish_event);
});
auto task = HTML::Task::create(vm(), HTML::Task::Source::DOMManipulation, &document,
JS::create_heap_function(heap(), [this, finish_event]() {
dispatch_event(finish_event);
}));
m_pending_finish_microtask_id = task->id();
HTML::main_thread_event_loop().task_queue().add(move(task));
HTML::main_thread_event_loop().task_queue().add(task);
}
};
});

// 2. If synchronously notify is true, cancel any queued microtask to run the finish notification steps for this
// animation, and run the finish notification steps immediately.
Expand All @@ -1151,13 +1152,13 @@ void Animation::update_finished_state(DidSeek did_seek, SynchronouslyNotify sync
return task.id() == id;
});
}
finish_notification_steps();
finish_notification_steps->function()();
}
// Otherwise, if synchronously notify is false, queue a microtask to run finish notification steps for
// animation unless there is already a microtask queued to run those steps for animation.
else if (!m_pending_finish_microtask_id.has_value()) {
auto& document = verify_cast<HTML::Window>(realm.global_object()).associated_document();
auto task = HTML::Task::create(HTML::Task::Source::DOMManipulation, &document, move(finish_notification_steps));
auto task = HTML::Task::create(vm(), HTML::Task::Source::DOMManipulation, &document, move(finish_notification_steps));
m_pending_finish_microtask_id = task->id();
HTML::main_thread_event_loop().task_queue().add(move(task));
}
Expand Down
5 changes: 3 additions & 2 deletions Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ ErrorOr<void> initialize_main_thread_vm()
// This avoids doing an exhaustive garbage collection on process exit.
s_main_thread_vm->ref();

auto& custom_data = verify_cast<WebEngineCustomData>(*s_main_thread_vm->custom_data());
custom_data.event_loop = s_main_thread_vm->heap().allocate_without_realm<HTML::EventLoop>();

// These strings could potentially live on the VM similar to CommonPropertyNames.
DOM::MutationType::initialize_strings();
HTML::AttributeNames::initialize_strings();
Expand All @@ -103,8 +106,6 @@ ErrorOr<void> initialize_main_thread_vm()
XHR::EventNames::initialize_strings();
XLink::AttributeNames::initialize_strings();

static_cast<WebEngineCustomData*>(s_main_thread_vm->custom_data())->event_loop.set_vm(*s_main_thread_vm);

// 8.1.5.1 HostEnsureCanAddPrivateElement(O), https://html.spec.whatwg.org/multipage/webappapis.html#the-hostensurecanaddprivateelement-implementation
s_main_thread_vm->host_ensure_can_add_private_element = [](JS::Object const& object) -> JS::ThrowCompletionOr<void> {
// 1. If O is a WindowProxy object, or implements Location, then return Completion { [[Type]]: throw, [[Value]]: a new TypeError }.
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/Bindings/MainThreadVM.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct WebEngineCustomData final : public JS::VM::CustomData {

virtual void spin_event_loop_until(JS::SafeFunction<bool()> goal_condition) override;

HTML::EventLoop event_loop;
JS::Handle<HTML::EventLoop> event_loop;

// FIXME: These should only be on similar-origin window agents, but we don't currently differentiate agent types.

Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3166,7 +3166,7 @@ void Document::unload(JS::GCPtr<Document>)
auto intend_to_store_in_bfcache = false;

// 6. Let eventLoop be oldDocument's relevant agent's event loop.
auto& event_loop = verify_cast<Bindings::WebEngineCustomData>(*vm.custom_data()).event_loop;
auto& event_loop = *verify_cast<Bindings::WebEngineCustomData>(*vm.custom_data()).event_loop;

// 7. Increase eventLoop's termination nesting level by 1.
event_loop.increment_termination_nesting_level();
Expand Down
4 changes: 2 additions & 2 deletions Userland/Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -785,9 +785,9 @@ void Element::make_html_uppercased_qualified_name()
}

// https://html.spec.whatwg.org/multipage/webappapis.html#queue-an-element-task
int Element::queue_an_element_task(HTML::Task::Source source, JS::SafeFunction<void()> steps)
int Element::queue_an_element_task(HTML::Task::Source source, Function<void()> steps)
{
auto task = HTML::Task::create(source, &document(), move(steps));
auto task = HTML::Task::create(vm(), source, &document(), JS::create_heap_function(heap(), move(steps)));
auto id = task->id();

HTML::main_thread_event_loop().task_queue().add(move(task));
Expand Down
3 changes: 2 additions & 1 deletion Userland/Libraries/LibWeb/DOM/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ class Element
void set_custom_properties(Optional<CSS::Selector::PseudoElement::Type>, HashMap<FlyString, CSS::StyleProperty> custom_properties);
[[nodiscard]] HashMap<FlyString, CSS::StyleProperty> const& custom_properties(Optional<CSS::Selector::PseudoElement::Type>) const;

int queue_an_element_task(HTML::Task::Source, JS::SafeFunction<void()>);
// NOTE: The function is wrapped in a JS::HeapFunction immediately.
int queue_an_element_task(HTML::Task::Source, Function<void()>);

bool is_void_element() const;
bool serializes_as_void() const;
Expand Down
4 changes: 2 additions & 2 deletions Userland/Libraries/LibWeb/Fetch/Infrastructure/Task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
namespace Web::Fetch::Infrastructure {

// https://fetch.spec.whatwg.org/#queue-a-fetch-task
int queue_fetch_task(JS::Object& task_destination, JS::SafeFunction<void()> algorithm)
int queue_fetch_task(JS::Object& task_destination, Function<void()> algorithm)
{
// FIXME: 1. If taskDestination is a parallel queue, then enqueue algorithm to taskDestination.

Expand All @@ -21,7 +21,7 @@ int queue_fetch_task(JS::Object& task_destination, JS::SafeFunction<void()> algo

// AD-HOC: This overload allows tracking the queued task within the fetch controller so that we may cancel queued tasks
// when the spec indicates that we must stop an ongoing fetch.
int queue_fetch_task(JS::NonnullGCPtr<FetchController> fetch_controller, JS::Object& task_destination, JS::SafeFunction<void()> algorithm)
int queue_fetch_task(JS::NonnullGCPtr<FetchController> fetch_controller, JS::Object& task_destination, Function<void()> algorithm)
{
auto fetch_task_id = fetch_controller->next_fetch_task_id();

Expand Down
4 changes: 2 additions & 2 deletions Userland/Libraries/LibWeb/Fetch/Infrastructure/Task.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Web::Fetch::Infrastructure {
// FIXME: 'or a parallel queue'
using TaskDestination = Variant<Empty, JS::NonnullGCPtr<JS::Object>>;

int queue_fetch_task(JS::Object&, JS::SafeFunction<void()>);
int queue_fetch_task(JS::NonnullGCPtr<FetchController>, JS::Object&, JS::SafeFunction<void()>);
int queue_fetch_task(JS::Object&, Function<void()>);
int queue_fetch_task(JS::NonnullGCPtr<FetchController>, JS::Object&, Function<void()>);

}
75 changes: 42 additions & 33 deletions Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,24 @@
namespace Web::HTML {

EventLoop::EventLoop()
: m_task_queue(*this)
, m_microtask_queue(*this)
{
m_task_queue = heap().allocate_without_realm<TaskQueue>(*this);
m_microtask_queue = heap().allocate_without_realm<TaskQueue>(*this);
}

EventLoop::~EventLoop() = default;

void EventLoop::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_task_queue);
visitor.visit(m_microtask_queue);
visitor.visit(m_currently_running_task);

for (auto& settings : m_backup_incumbent_settings_object_stack)
visitor.visit(settings);
}

void EventLoop::schedule()
{
if (!m_system_event_loop_timer) {
Expand All @@ -41,15 +52,9 @@ void EventLoop::schedule()
m_system_event_loop_timer->restart();
}

void EventLoop::set_vm(JS::VM& vm)
{
VERIFY(!m_vm);
m_vm = &vm;
}

EventLoop& main_thread_event_loop()
{
return static_cast<Bindings::WebEngineCustomData*>(Bindings::main_thread_vm().custom_data())->event_loop;
return *static_cast<Bindings::WebEngineCustomData*>(Bindings::main_thread_vm().custom_data())->event_loop;
}

// https://html.spec.whatwg.org/multipage/webappapis.html#spin-the-event-loop
Expand All @@ -62,8 +67,9 @@ void EventLoop::spin_until(JS::SafeFunction<bool()> goal_condition)

// 3. Let old stack be a copy of the JavaScript execution context stack.
// 4. Empty the JavaScript execution context stack.
m_vm->save_execution_context_stack();
m_vm->clear_execution_context_stack();
auto& vm = this->vm();
vm.save_execution_context_stack();
vm.clear_execution_context_stack();

// 5. Perform a microtask checkpoint.
perform_a_microtask_checkpoint();
Expand All @@ -78,24 +84,25 @@ void EventLoop::spin_until(JS::SafeFunction<bool()> goal_condition)
Platform::EventLoopPlugin::the().spin_until([&] {
if (goal_condition())
return true;
if (m_task_queue.has_runnable_tasks()) {
if (m_task_queue->has_runnable_tasks()) {
schedule();
// FIXME: Remove the platform event loop plugin so that this doesn't look out of place
Core::EventLoop::current().wake();
}
return goal_condition();
});

m_vm->restore_execution_context_stack();
vm.restore_execution_context_stack();

// 7. Stop task, allowing whatever algorithm that invoked it to resume.
// NOTE: This is achieved by returning from the function.
}

void EventLoop::spin_processing_tasks_with_source_until(Task::Source source, JS::SafeFunction<bool()> goal_condition)
{
m_vm->save_execution_context_stack();
m_vm->clear_execution_context_stack();
auto& vm = this->vm();
vm.save_execution_context_stack();
vm.clear_execution_context_stack();

perform_a_microtask_checkpoint();

Expand All @@ -105,12 +112,12 @@ void EventLoop::spin_processing_tasks_with_source_until(Task::Source source, JS:
Platform::EventLoopPlugin::the().spin_until([&] {
if (goal_condition())
return true;
if (m_task_queue.has_runnable_tasks()) {
auto tasks = m_task_queue.take_tasks_matching([&](auto& task) {
if (m_task_queue->has_runnable_tasks()) {
auto tasks = m_task_queue->take_tasks_matching([&](auto& task) {
return task.source() == source && task.is_runnable();
});

for (auto& task : tasks.value()) {
for (auto& task : tasks) {
m_currently_running_task = task.ptr();
task->execute();
m_currently_running_task = nullptr;
Expand All @@ -126,7 +133,7 @@ void EventLoop::spin_processing_tasks_with_source_until(Task::Source source, JS:

schedule();

m_vm->restore_execution_context_stack();
vm.restore_execution_context_stack();
}

// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
Expand All @@ -138,7 +145,7 @@ void EventLoop::process()
// An event loop must continually run through the following steps for as long as it exists:

// 1. Let oldestTask be null.
OwnPtr<Task> oldest_task;
JS::GCPtr<Task> oldest_task;

// 2. Let taskStartTime be the current high resolution time.
// FIXME: 'current high resolution time' in hr-time-3 takes a global object,
Expand All @@ -149,7 +156,7 @@ void EventLoop::process()
// 3. Let taskQueue be one of the event loop's task queues, chosen in an implementation-defined manner,
// with the constraint that the chosen task queue must contain at least one runnable task.
// If there is no such task queue, then jump to the microtasks step below.
auto& task_queue = m_task_queue;
auto& task_queue = *m_task_queue;

// 4. Set oldestTask to the first runnable task in taskQueue, and remove it from taskQueue.
oldest_task = task_queue.take_first_runnable();
Expand Down Expand Up @@ -333,7 +340,7 @@ void EventLoop::process()
// - this event loop's microtask queue is empty
// - hasARenderingOpportunity is false
// FIXME: has_a_rendering_opportunity is always true
if (m_type == Type::Window && !task_queue.has_runnable_tasks() && m_microtask_queue.is_empty() /*&& !has_a_rendering_opportunity*/) {
if (m_type == Type::Window && !task_queue.has_runnable_tasks() && m_microtask_queue->is_empty() /*&& !has_a_rendering_opportunity*/) {
// 1. Set this event loop's last idle period start time to the current high resolution time.
m_last_idle_period_start_time = HighResolutionTime::unsafe_shared_current_time();

Expand All @@ -356,7 +363,7 @@ void EventLoop::process()
// FIXME: 2. If there are no tasks in the event loop's task queues and the WorkerGlobalScope object's closing flag is true, then destroy the event loop, aborting these steps, resuming the run a worker steps described in the Web workers section below.

// If there are eligible tasks in the queue, schedule a new round of processing. :^)
if (m_task_queue.has_runnable_tasks() || (!m_microtask_queue.is_empty() && !m_performing_a_microtask_checkpoint))
if (m_task_queue->has_runnable_tasks() || (!m_microtask_queue->is_empty() && !m_performing_a_microtask_checkpoint))
schedule();

// For each doc of docs, process top layer removals given doc.
Expand All @@ -366,7 +373,7 @@ void EventLoop::process()
}

// https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-global-task
int queue_global_task(HTML::Task::Source source, JS::Object& global_object, JS::SafeFunction<void()> steps)
int queue_global_task(HTML::Task::Source source, JS::Object& global_object, Function<void()> steps)
{
// 1. Let event loop be global's relevant agent's event loop.
auto& global_custom_data = verify_cast<Bindings::WebEngineCustomData>(*global_object.vm().custom_data());
Expand All @@ -380,13 +387,14 @@ int queue_global_task(HTML::Task::Source source, JS::Object& global_object, JS::
}

// 3. Queue a task given source, event loop, document, and steps.
event_loop.task_queue().add(HTML::Task::create(source, document, move(steps)));
auto& vm = global_object.vm();
event_loop->task_queue().add(HTML::Task::create(vm, source, document, JS::create_heap_function(vm.heap(), move(steps))));

return event_loop.task_queue().last_added_task()->id();
return event_loop->task_queue().last_added_task()->id();
}

// https://html.spec.whatwg.org/#queue-a-microtask
void queue_a_microtask(DOM::Document const* document, JS::SafeFunction<void()> steps)
void queue_a_microtask(DOM::Document const* document, Function<void()> steps)
{
// 1. If event loop was not given, set event loop to the implied event loop.
auto& event_loop = HTML::main_thread_event_loop();
Expand All @@ -397,12 +405,13 @@ void queue_a_microtask(DOM::Document const* document, JS::SafeFunction<void()> s
// 4. Set microtask's steps to steps.
// 5. Set microtask's source to the microtask task source.
// 6. Set microtask's document to document.
auto microtask = HTML::Task::create(HTML::Task::Source::Microtask, document, move(steps));
auto& vm = event_loop.vm();
auto microtask = HTML::Task::create(vm, HTML::Task::Source::Microtask, document, JS::create_heap_function(vm.heap(), move(steps)));

// FIXME: 7. Set microtask's script evaluation environment settings object set to an empty set.

// 8. Enqueue microtask on event loop's microtask queue.
event_loop.microtask_queue().enqueue(move(microtask));
event_loop.microtask_queue().enqueue(microtask);
}

void perform_a_microtask_checkpoint()
Expand All @@ -421,9 +430,9 @@ void EventLoop::perform_a_microtask_checkpoint()
m_performing_a_microtask_checkpoint = true;

// 3. While the event loop's microtask queue is not empty:
while (!m_microtask_queue.is_empty()) {
while (!m_microtask_queue->is_empty()) {
// 1. Let oldestMicrotask be the result of dequeuing from the event loop's microtask queue.
auto oldest_microtask = m_microtask_queue.dequeue();
auto oldest_microtask = m_microtask_queue->dequeue();

// 2. Set the event loop's currently running task to oldestMicrotask.
m_currently_running_task = oldest_microtask;
Expand Down Expand Up @@ -486,12 +495,12 @@ EnvironmentSettingsObject& EventLoop::top_of_backup_incumbent_settings_object_st

void EventLoop::register_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object)
{
m_related_environment_settings_objects.append(environment_settings_object);
m_related_environment_settings_objects.append(&environment_settings_object);
}

void EventLoop::unregister_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object)
{
bool did_remove = m_related_environment_settings_objects.remove_first_matching([&](auto& entry) { return entry.ptr() == &environment_settings_object; });
bool did_remove = m_related_environment_settings_objects.remove_first_matching([&](auto& entry) { return entry == &environment_settings_object; });
VERIFY(did_remove);
}

Expand Down
Loading

0 comments on commit ce78fcc

Please sign in to comment.