summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp
blob: 1047cd8b94da730feea944688d6dfdcacc342a1f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
/*
 * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
 * Copyright (c) 2022, the SerenityOS developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibJS/Runtime/VM.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HighResolutionTime/CoarsenTime.h>
#include <LibWeb/HighResolutionTime/Performance.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Platform/Timer.h>

namespace Web::HTML {

EventLoop::EventLoop()
    : m_task_queue(*this)
    , m_microtask_queue(*this)
{
}

EventLoop::~EventLoop() = default;

void EventLoop::schedule()
{
    if (!m_system_event_loop_timer) {
        m_system_event_loop_timer = Platform::Timer::create_single_shot(0, [this] {
            process();
        });
    }

    if (!m_system_event_loop_timer->is_active())
        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;
}

// https://html.spec.whatwg.org/multipage/webappapis.html#spin-the-event-loop
void EventLoop::spin_until(Function<bool()> goal_condition)
{
    // FIXME: 1. Let task be the event loop's currently running task.

    // FIXME: 2. Let task source be task's source.

    // 3. Let old stack be a copy of the JavaScript execution context stack.
    // 4. Empty the JavaScript execution context stack.
    auto& vm = Bindings::main_thread_vm();
    vm.save_execution_context_stack();

    // 5. Perform a microtask checkpoint.
    perform_a_microtask_checkpoint();

    // 6. In parallel:
    // NOTE: We do these in reverse order here, but it shouldn't matter.

    //    2. Queue a task on task source to:
    //       1. Replace the JavaScript execution context stack with old stack.
    vm.restore_execution_context_stack();
    //       2. Perform any steps that appear after this spin the event loop instance in the original algorithm.
    //       NOTE: This is achieved by returning from the function.

    //    1. Wait until the condition goal is met.
    Platform::EventLoopPlugin::the().spin_until(move(goal_condition));

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

// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
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;

    // 2. Let taskStartTime be the current high resolution time.
    // FIXME: 'current high resolution time' in hr-time-3 takes a global object,
    //        the HTML spec has not been updated to reflect this, let's use the shared timer.
    //        - https://github.com/whatwg/html/issues/7776
    double task_start_time = HighResolutionTime::unsafe_shared_current_time();

    // 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;

    // 4. Set oldestTask to the first runnable task in taskQueue, and remove it from taskQueue.
    oldest_task = task_queue.take_first_runnable();

    if (oldest_task) {
        // 5. Set the event loop's currently running task to oldestTask.
        m_currently_running_task = oldest_task.ptr();

        // 6. Perform oldestTask's steps.
        oldest_task->execute();

        // 7. Set the event loop's currently running task back to null.
        m_currently_running_task = nullptr;
    }

    // 8. Microtasks: Perform a microtask checkpoint.
    perform_a_microtask_checkpoint();

    // 9. Let hasARenderingOpportunity be false.
    [[maybe_unused]] bool has_a_rendering_opportunity = false;

    // FIXME: 10. Let now be the current high resolution time. [HRT]

    // FIXME: 11. If oldestTask is not null, then:

    // FIXME:     1. Let top-level browsing contexts be an empty set.

    // FIXME:     2. For each environment settings object settings of oldestTask's script evaluation environment settings object set, append setting's top-level browsing context to top-level browsing contexts.

    // FIXME:     3. Report long tasks, passing in taskStartTime, now (the end time of the task), top-level browsing contexts, and oldestTask.

    // FIXME: 12. Update the rendering: if this is a window event loop, then:

    // FIXME:     1. Let docs be all Document objects whose relevant agent's event loop is this event loop, sorted arbitrarily except that the following conditions must be met:
    //               - Any Document B whose browsing context's container document is A must be listed after A in the list.
    //               - If there are two documents A and B whose browsing contexts are both child browsing contexts whose container documents are another Document C, then the order of A and B in the list must match the shadow-including tree order of their respective browsing context containers in C's node tree.
    // FIXME: NOTE: The sort order specified above is missing here!
    Vector<JS::Handle<DOM::Document>> docs = documents_in_this_event_loop();

    auto for_each_fully_active_document_in_docs = [&](auto&& callback) {
        for (auto& document : docs) {
            if (document->is_fully_active())
                callback(*document);
        }
    };

    // 2. Rendering opportunities: Remove from docs all Document objects whose browsing context do not have a rendering opportunity.
    docs.remove_all_matching([&](auto& document) {
        return document->browsing_context() && !document->browsing_context()->has_a_rendering_opportunity();
    });

    // 3. If docs is not empty, then set hasARenderingOpportunity to true
    //    and set this event loop's last render opportunity time to taskStartTime.
    if (!docs.is_empty()) {
        has_a_rendering_opportunity = true;
        m_last_render_opportunity_time = task_start_time;
    }

    // FIXME:     4. Unnecessary rendering: Remove from docs all Document objects which meet both of the following conditions:
    //               - The user agent believes that updating the rendering of the Document's browsing context would have no visible effect, and
    //               - The Document's map of animation frame callbacks is empty.

    // FIXME:     5. Remove from docs all Document objects for which the user agent believes that it's preferable to skip updating the rendering for other reasons.

    // FIXME:     6. For each fully active Document in docs, flush autofocus candidates for that Document if its browsing context is a top-level browsing context.

    // 7. For each fully active Document in docs, run the resize steps for that Document, passing in now as the timestamp. [CSSOMVIEW]
    for_each_fully_active_document_in_docs([&](DOM::Document& document) {
        document.run_the_resize_steps();
    });

    // 8. For each fully active Document in docs, run the scroll steps for that Document, passing in now as the timestamp. [CSSOMVIEW]
    for_each_fully_active_document_in_docs([&](DOM::Document& document) {
        document.run_the_scroll_steps();
    });

    // 9. For each fully active Document in docs, evaluate media queries and report changes for that Document, passing in now as the timestamp. [CSSOMVIEW]
    for_each_fully_active_document_in_docs([&](DOM::Document& document) {
        document.evaluate_media_queries_and_report_changes();
    });

    // FIXME:     10. For each fully active Document in docs, update animations and send events for that Document, passing in now as the timestamp. [WEBANIMATIONS]

    // FIXME:     11. For each fully active Document in docs, run the fullscreen steps for that Document, passing in now as the timestamp. [FULLSCREEN]

    // FIXME:     12. For each fully active Document in docs, if the user agent detects that the backing storage associated with a CanvasRenderingContext2D or an OffscreenCanvasRenderingContext2D, context, has been lost, then it must run the context lost steps for each such context:

    // FIXME:     13. For each fully active Document in docs, run the animation frame callbacks for that Document, passing in now as the timestamp.
    for_each_fully_active_document_in_docs([&](DOM::Document& document) {
        run_animation_frame_callbacks(document, document.window().performance().now());
    });

    // FIXME:     14. For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER]

    // FIXME:     15. Invoke the mark paint timing algorithm for each Document object in docs.

    // FIXME:     16. For each fully active Document in docs, update the rendering or user interface of that Document and its browsing context to reflect the current state.

    // 13. If all of the following are true
    // - this is a window event loop
    // - there is no task in this event loop's task queues whose document is fully active
    // - 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*/) {
        // 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();

        // 2. Let computeDeadline be the following steps:
        // NOTE: instead of passing around a function we use this event loop, which has compute_deadline()

        // 3. For each win of the same-loop windows for this event loop,
        //    perform the start an idle period algorithm for win with computeDeadline. [REQUESTIDLECALLBACK]
        for (auto& win : same_loop_windows())
            win->start_an_idle_period();
    }

    // FIXME: 14. If this is a worker event loop, then:

    // FIXME:     1. If this event loop's agent's single realm's global object is a supported DedicatedWorkerGlobalScope and the user agent believes that it would benefit from having its rendering updated at this time, then:
    // FIXME:        1. Let now be the current high resolution time. [HRT]
    // FIXME:        2. Run the animation frame callbacks for that DedicatedWorkerGlobalScope, passing in now as the timestamp.
    // FIXME:        3. Update the rendering of that dedicated worker to reflect the current state.

    // 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 tasks in the queue, schedule a new round of processing. :^)
    if (m_task_queue.has_runnable_tasks() || !m_microtask_queue.is_empty())
        schedule();
}

// FIXME: This is here to paper over an issue in the HTML parser where it'll create new interpreters (and thus ESOs) on temporary documents created for innerHTML if it uses Document::realm() to get the global object.
//        Use queue_global_task instead.
void old_queue_global_task_with_document(HTML::Task::Source source, DOM::Document& document, JS::SafeFunction<void()> steps)
{
    main_thread_event_loop().task_queue().add(HTML::Task::create(source, &document, move(steps)));
}

// https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-global-task
void queue_global_task(HTML::Task::Source source, JS::Object& global_object, JS::SafeFunction<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());
    auto& event_loop = global_custom_data.event_loop;

    // 2. Let document be global's associated Document, if global is a Window object; otherwise null.
    DOM::Document* document { nullptr };
    if (is<HTML::Window>(global_object)) {
        auto& window_object = verify_cast<HTML::Window>(global_object);
        document = &window_object.associated_document();
    }

    // 3. Queue a task given source, event loop, document, and steps.
    event_loop.task_queue().add(HTML::Task::create(source, document, move(steps)));
}

// https://html.spec.whatwg.org/#queue-a-microtask
void queue_a_microtask(DOM::Document* document, JS::SafeFunction<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();

    // FIXME: 2. If document was not given, set document to the implied document.

    // 3. Let microtask be a new task.
    // 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));

    // 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));
}

void perform_a_microtask_checkpoint()
{
    main_thread_event_loop().perform_a_microtask_checkpoint();
}

// https://html.spec.whatwg.org/#perform-a-microtask-checkpoint
void EventLoop::perform_a_microtask_checkpoint()
{
    // 1. If the event loop's performing a microtask checkpoint is true, then return.
    if (m_performing_a_microtask_checkpoint)
        return;

    // 2. Set the event loop's performing a microtask checkpoint to true.
    m_performing_a_microtask_checkpoint = true;

    // 3. While the event loop's microtask queue is not 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();

        // 2. Set the event loop's currently running task to oldestMicrotask.
        m_currently_running_task = oldest_microtask;

        // 3. Run oldestMicrotask.
        oldest_microtask->execute();

        // 4. Set the event loop's currently running task back to null.
        m_currently_running_task = nullptr;
    }

    // 4. For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object.
    for (auto& environment_settings_object : m_related_environment_settings_objects)
        environment_settings_object.notify_about_rejected_promises({});

    // FIXME: 5. Cleanup Indexed Database transactions.

    // 6. Perform ClearKeptObjects().
    vm().finish_execution_generation();

    // 7. Set the event loop's performing a microtask checkpoint to false.
    m_performing_a_microtask_checkpoint = false;
}

Vector<JS::Handle<DOM::Document>> EventLoop::documents_in_this_event_loop() const
{
    Vector<JS::Handle<DOM::Document>> documents;
    for (auto& document : m_documents) {
        VERIFY(document);
        documents.append(JS::make_handle(*document.ptr()));
    }
    return documents;
}

void EventLoop::register_document(Badge<DOM::Document>, DOM::Document& document)
{
    m_documents.append(&document);
}

void EventLoop::unregister_document(Badge<DOM::Document>, DOM::Document& document)
{
    bool did_remove = m_documents.remove_first_matching([&](auto& entry) { return entry.ptr() == &document; });
    VERIFY(did_remove);
}

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

void EventLoop::pop_backup_incumbent_settings_object_stack(Badge<EnvironmentSettingsObject>)
{
    m_backup_incumbent_settings_object_stack.take_last();
}

EnvironmentSettingsObject& EventLoop::top_of_backup_incumbent_settings_object_stack()
{
    return m_backup_incumbent_settings_object_stack.last();
}

void EventLoop::register_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& 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 == &environment_settings_object; });
    VERIFY(did_remove);
}

// https://html.spec.whatwg.org/multipage/webappapis.html#same-loop-windows
Vector<JS::Handle<HTML::Window>> EventLoop::same_loop_windows() const
{
    Vector<JS::Handle<HTML::Window>> windows;
    for (auto& document : documents_in_this_event_loop()) {
        if (document->is_fully_active())
            windows.append(JS::make_handle(document->window()));
    }
    return windows;
}

// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model:last-idle-period-start-time
double EventLoop::compute_deadline() const
{
    // 1. Let deadline be this event loop's last idle period start time plus 50.
    auto deadline = m_last_idle_period_start_time + 50;
    // 2. Let hasPendingRenders be false.
    auto has_pending_renders = false;
    // 3. For each windowInSameLoop of the same-loop windows for this event loop:
    for (auto& window : same_loop_windows()) {
        // 1. If windowInSameLoop's map of animation frame callbacks is not empty,
        //    or if the user agent believes that the windowInSameLoop might have pending rendering updates,
        //    set hasPendingRenders to true.
        if (window->has_animation_frame_callbacks())
            has_pending_renders = true;
        // FIXME: 2. Let timerCallbackEstimates be the result of getting the values of windowInSameLoop's map of active timers.
        // FIXME: 3. For each timeoutDeadline of timerCallbackEstimates, if timeoutDeadline is less than deadline, set deadline to timeoutDeadline.
    }
    // 4. If hasPendingRenders is true, then:
    if (has_pending_renders) {
        // 1. Let nextRenderDeadline be this event loop's last render opportunity time plus (1000 divided by the current refresh rate).
        // FIXME: Hardcoded to 60Hz
        auto next_render_deadline = m_last_render_opportunity_time + (1000.0 / 60.0);
        // 2. If nextRenderDeadline is less than deadline, then return nextRenderDeadline.
        if (next_render_deadline < deadline)
            return next_render_deadline;
    }
    // 5. Return deadline.
    return deadline;
}

}