Serenity Operating System
at master 410 lines 19 kB view raw
1/* 2 * Copyright (c) 2021, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <LibJS/Runtime/VM.h> 9#include <LibWeb/Bindings/MainThreadVM.h> 10#include <LibWeb/DOM/Document.h> 11#include <LibWeb/HTML/BrowsingContext.h> 12#include <LibWeb/HTML/EventLoop/EventLoop.h> 13#include <LibWeb/HTML/Scripting/Environments.h> 14#include <LibWeb/HTML/Window.h> 15#include <LibWeb/HighResolutionTime/Performance.h> 16#include <LibWeb/HighResolutionTime/TimeOrigin.h> 17#include <LibWeb/Platform/EventLoopPlugin.h> 18#include <LibWeb/Platform/Timer.h> 19 20namespace Web::HTML { 21 22EventLoop::EventLoop() 23 : m_task_queue(*this) 24 , m_microtask_queue(*this) 25{ 26} 27 28EventLoop::~EventLoop() = default; 29 30void EventLoop::schedule() 31{ 32 if (!m_system_event_loop_timer) { 33 m_system_event_loop_timer = Platform::Timer::create_single_shot(0, [this] { 34 process(); 35 }); 36 } 37 38 if (!m_system_event_loop_timer->is_active()) 39 m_system_event_loop_timer->restart(); 40} 41 42void EventLoop::set_vm(JS::VM& vm) 43{ 44 VERIFY(!m_vm); 45 m_vm = &vm; 46} 47 48EventLoop& main_thread_event_loop() 49{ 50 return static_cast<Bindings::WebEngineCustomData*>(Bindings::main_thread_vm().custom_data())->event_loop; 51} 52 53// https://html.spec.whatwg.org/multipage/webappapis.html#spin-the-event-loop 54void EventLoop::spin_until(Function<bool()> goal_condition) 55{ 56 // FIXME: 1. Let task be the event loop's currently running task. 57 58 // FIXME: 2. Let task source be task's source. 59 60 // 3. Let old stack be a copy of the JavaScript execution context stack. 61 // 4. Empty the JavaScript execution context stack. 62 auto& vm = Bindings::main_thread_vm(); 63 vm.save_execution_context_stack(); 64 65 // 5. Perform a microtask checkpoint. 66 perform_a_microtask_checkpoint(); 67 68 // 6. In parallel: 69 // NOTE: We do these in reverse order here, but it shouldn't matter. 70 71 // 2. Queue a task on task source to: 72 // 1. Replace the JavaScript execution context stack with old stack. 73 vm.restore_execution_context_stack(); 74 // 2. Perform any steps that appear after this spin the event loop instance in the original algorithm. 75 // NOTE: This is achieved by returning from the function. 76 77 // 1. Wait until the condition goal is met. 78 Platform::EventLoopPlugin::the().spin_until(move(goal_condition)); 79 80 // 7. Stop task, allowing whatever algorithm that invoked it to resume. 81 // NOTE: This is achieved by returning from the function. 82} 83 84// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model 85void EventLoop::process() 86{ 87 // An event loop must continually run through the following steps for as long as it exists: 88 89 // 1. Let oldestTask be null. 90 OwnPtr<Task> oldest_task; 91 92 // 2. Let taskStartTime be the current high resolution time. 93 // FIXME: 'current high resolution time' in hr-time-3 takes a global object, 94 // the HTML spec has not been updated to reflect this, let's use the shared timer. 95 // - https://github.com/whatwg/html/issues/7776 96 double task_start_time = HighResolutionTime::unsafe_shared_current_time(); 97 98 // 3. Let taskQueue be one of the event loop's task queues, chosen in an implementation-defined manner, 99 // with the constraint that the chosen task queue must contain at least one runnable task. 100 // If there is no such task queue, then jump to the microtasks step below. 101 auto& task_queue = m_task_queue; 102 103 // 4. Set oldestTask to the first runnable task in taskQueue, and remove it from taskQueue. 104 oldest_task = task_queue.take_first_runnable(); 105 106 if (oldest_task) { 107 // 5. Set the event loop's currently running task to oldestTask. 108 m_currently_running_task = oldest_task.ptr(); 109 110 // 6. Perform oldestTask's steps. 111 oldest_task->execute(); 112 113 // 7. Set the event loop's currently running task back to null. 114 m_currently_running_task = nullptr; 115 } 116 117 // 8. Microtasks: Perform a microtask checkpoint. 118 perform_a_microtask_checkpoint(); 119 120 // 9. Let hasARenderingOpportunity be false. 121 [[maybe_unused]] bool has_a_rendering_opportunity = false; 122 123 // FIXME: 10. Let now be the current high resolution time. [HRT] 124 125 // FIXME: 11. If oldestTask is not null, then: 126 127 // FIXME: 1. Let top-level browsing contexts be an empty set. 128 129 // 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. 130 131 // FIXME: 3. Report long tasks, passing in taskStartTime, now (the end time of the task), top-level browsing contexts, and oldestTask. 132 133 // FIXME: 12. Update the rendering: if this is a window event loop, then: 134 135 // 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: 136 // - Any Document B whose browsing context's container document is A must be listed after A in the list. 137 // - 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. 138 // FIXME: NOTE: The sort order specified above is missing here! 139 Vector<JS::Handle<DOM::Document>> docs = documents_in_this_event_loop(); 140 141 auto for_each_fully_active_document_in_docs = [&](auto&& callback) { 142 for (auto& document : docs) { 143 if (document->is_fully_active()) 144 callback(*document); 145 } 146 }; 147 148 // 2. Rendering opportunities: Remove from docs all Document objects whose browsing context do not have a rendering opportunity. 149 docs.remove_all_matching([&](auto& document) { 150 return document->browsing_context() && !document->browsing_context()->has_a_rendering_opportunity(); 151 }); 152 153 // 3. If docs is not empty, then set hasARenderingOpportunity to true 154 // and set this event loop's last render opportunity time to taskStartTime. 155 if (!docs.is_empty()) { 156 has_a_rendering_opportunity = true; 157 m_last_render_opportunity_time = task_start_time; 158 } 159 160 // FIXME: 4. Unnecessary rendering: Remove from docs all Document objects which meet both of the following conditions: 161 // - The user agent believes that updating the rendering of the Document's browsing context would have no visible effect, and 162 // - The Document's map of animation frame callbacks is empty. 163 164 // 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. 165 166 // 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. 167 168 // 7. For each fully active Document in docs, run the resize steps for that Document, passing in now as the timestamp. [CSSOMVIEW] 169 for_each_fully_active_document_in_docs([&](DOM::Document& document) { 170 document.run_the_resize_steps(); 171 }); 172 173 // 8. For each fully active Document in docs, run the scroll steps for that Document, passing in now as the timestamp. [CSSOMVIEW] 174 for_each_fully_active_document_in_docs([&](DOM::Document& document) { 175 document.run_the_scroll_steps(); 176 }); 177 178 // 9. For each fully active Document in docs, evaluate media queries and report changes for that Document, passing in now as the timestamp. [CSSOMVIEW] 179 for_each_fully_active_document_in_docs([&](DOM::Document& document) { 180 document.evaluate_media_queries_and_report_changes(); 181 }); 182 183 // FIXME: 10. For each fully active Document in docs, update animations and send events for that Document, passing in now as the timestamp. [WEBANIMATIONS] 184 185 // FIXME: 11. For each fully active Document in docs, run the fullscreen steps for that Document, passing in now as the timestamp. [FULLSCREEN] 186 187 // 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: 188 189 // FIXME: 13. For each fully active Document in docs, run the animation frame callbacks for that Document, passing in now as the timestamp. 190 auto now = HighResolutionTime::unsafe_shared_current_time(); 191 for_each_fully_active_document_in_docs([&](DOM::Document& document) { 192 run_animation_frame_callbacks(document, now); 193 }); 194 195 // 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] 196 197 // FIXME: 15. Invoke the mark paint timing algorithm for each Document object in docs. 198 199 // 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. 200 201 // 13. If all of the following are true 202 // - this is a window event loop 203 // - there is no task in this event loop's task queues whose document is fully active 204 // - this event loop's microtask queue is empty 205 // - hasARenderingOpportunity is false 206 // FIXME: has_a_rendering_opportunity is always true 207 if (m_type == Type::Window && !task_queue.has_runnable_tasks() && m_microtask_queue.is_empty() /*&& !has_a_rendering_opportunity*/) { 208 // 1. Set this event loop's last idle period start time to the current high resolution time. 209 m_last_idle_period_start_time = HighResolutionTime::unsafe_shared_current_time(); 210 211 // 2. Let computeDeadline be the following steps: 212 // NOTE: instead of passing around a function we use this event loop, which has compute_deadline() 213 214 // 3. For each win of the same-loop windows for this event loop, 215 // perform the start an idle period algorithm for win with computeDeadline. [REQUESTIDLECALLBACK] 216 for (auto& win : same_loop_windows()) 217 win->start_an_idle_period(); 218 } 219 220 // FIXME: 14. If this is a worker event loop, then: 221 222 // 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: 223 // FIXME: 1. Let now be the current high resolution time. [HRT] 224 // FIXME: 2. Run the animation frame callbacks for that DedicatedWorkerGlobalScope, passing in now as the timestamp. 225 // FIXME: 3. Update the rendering of that dedicated worker to reflect the current state. 226 227 // 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. 228 229 // If there are tasks in the queue, schedule a new round of processing. :^) 230 if (m_task_queue.has_runnable_tasks() || !m_microtask_queue.is_empty()) 231 schedule(); 232} 233 234// 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. 235// Use queue_global_task instead. 236void old_queue_global_task_with_document(HTML::Task::Source source, DOM::Document& document, JS::SafeFunction<void()> steps) 237{ 238 main_thread_event_loop().task_queue().add(HTML::Task::create(source, &document, move(steps))); 239} 240 241// https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-global-task 242void queue_global_task(HTML::Task::Source source, JS::Object& global_object, JS::SafeFunction<void()> steps) 243{ 244 // 1. Let event loop be global's relevant agent's event loop. 245 auto& global_custom_data = verify_cast<Bindings::WebEngineCustomData>(*global_object.vm().custom_data()); 246 auto& event_loop = global_custom_data.event_loop; 247 248 // 2. Let document be global's associated Document, if global is a Window object; otherwise null. 249 DOM::Document* document { nullptr }; 250 if (is<HTML::Window>(global_object)) { 251 auto& window_object = verify_cast<HTML::Window>(global_object); 252 document = &window_object.associated_document(); 253 } 254 255 // 3. Queue a task given source, event loop, document, and steps. 256 event_loop.task_queue().add(HTML::Task::create(source, document, move(steps))); 257} 258 259// https://html.spec.whatwg.org/#queue-a-microtask 260void queue_a_microtask(DOM::Document const* document, JS::SafeFunction<void()> steps) 261{ 262 // 1. If event loop was not given, set event loop to the implied event loop. 263 auto& event_loop = HTML::main_thread_event_loop(); 264 265 // FIXME: 2. If document was not given, set document to the implied document. 266 267 // 3. Let microtask be a new task. 268 // 4. Set microtask's steps to steps. 269 // 5. Set microtask's source to the microtask task source. 270 // 6. Set microtask's document to document. 271 auto microtask = HTML::Task::create(HTML::Task::Source::Microtask, document, move(steps)); 272 273 // FIXME: 7. Set microtask's script evaluation environment settings object set to an empty set. 274 275 // 8. Enqueue microtask on event loop's microtask queue. 276 event_loop.microtask_queue().enqueue(move(microtask)); 277} 278 279void perform_a_microtask_checkpoint() 280{ 281 main_thread_event_loop().perform_a_microtask_checkpoint(); 282} 283 284// https://html.spec.whatwg.org/#perform-a-microtask-checkpoint 285void EventLoop::perform_a_microtask_checkpoint() 286{ 287 // 1. If the event loop's performing a microtask checkpoint is true, then return. 288 if (m_performing_a_microtask_checkpoint) 289 return; 290 291 // 2. Set the event loop's performing a microtask checkpoint to true. 292 m_performing_a_microtask_checkpoint = true; 293 294 // 3. While the event loop's microtask queue is not empty: 295 while (!m_microtask_queue.is_empty()) { 296 // 1. Let oldestMicrotask be the result of dequeuing from the event loop's microtask queue. 297 auto oldest_microtask = m_microtask_queue.dequeue(); 298 299 // 2. Set the event loop's currently running task to oldestMicrotask. 300 m_currently_running_task = oldest_microtask; 301 302 // 3. Run oldestMicrotask. 303 oldest_microtask->execute(); 304 305 // 4. Set the event loop's currently running task back to null. 306 m_currently_running_task = nullptr; 307 } 308 309 // 4. For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object. 310 for (auto& environment_settings_object : m_related_environment_settings_objects) 311 environment_settings_object.notify_about_rejected_promises({}); 312 313 // FIXME: 5. Cleanup Indexed Database transactions. 314 315 // 6. Perform ClearKeptObjects(). 316 vm().finish_execution_generation(); 317 318 // 7. Set the event loop's performing a microtask checkpoint to false. 319 m_performing_a_microtask_checkpoint = false; 320} 321 322Vector<JS::Handle<DOM::Document>> EventLoop::documents_in_this_event_loop() const 323{ 324 Vector<JS::Handle<DOM::Document>> documents; 325 for (auto& document : m_documents) { 326 VERIFY(document); 327 documents.append(JS::make_handle(*document)); 328 } 329 return documents; 330} 331 332void EventLoop::register_document(Badge<DOM::Document>, DOM::Document& document) 333{ 334 m_documents.append(&document); 335} 336 337void EventLoop::unregister_document(Badge<DOM::Document>, DOM::Document& document) 338{ 339 bool did_remove = m_documents.remove_first_matching([&](auto& entry) { return entry.ptr() == &document; }); 340 VERIFY(did_remove); 341} 342 343void EventLoop::push_onto_backup_incumbent_settings_object_stack(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object) 344{ 345 m_backup_incumbent_settings_object_stack.append(environment_settings_object); 346} 347 348void EventLoop::pop_backup_incumbent_settings_object_stack(Badge<EnvironmentSettingsObject>) 349{ 350 m_backup_incumbent_settings_object_stack.take_last(); 351} 352 353EnvironmentSettingsObject& EventLoop::top_of_backup_incumbent_settings_object_stack() 354{ 355 return m_backup_incumbent_settings_object_stack.last(); 356} 357 358void EventLoop::register_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object) 359{ 360 m_related_environment_settings_objects.append(environment_settings_object); 361} 362 363void EventLoop::unregister_environment_settings_object(Badge<EnvironmentSettingsObject>, EnvironmentSettingsObject& environment_settings_object) 364{ 365 bool did_remove = m_related_environment_settings_objects.remove_first_matching([&](auto& entry) { return &entry == &environment_settings_object; }); 366 VERIFY(did_remove); 367} 368 369// https://html.spec.whatwg.org/multipage/webappapis.html#same-loop-windows 370Vector<JS::Handle<HTML::Window>> EventLoop::same_loop_windows() const 371{ 372 Vector<JS::Handle<HTML::Window>> windows; 373 for (auto& document : documents_in_this_event_loop()) { 374 if (document->is_fully_active()) 375 windows.append(JS::make_handle(document->window())); 376 } 377 return windows; 378} 379 380// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model:last-idle-period-start-time 381double EventLoop::compute_deadline() const 382{ 383 // 1. Let deadline be this event loop's last idle period start time plus 50. 384 auto deadline = m_last_idle_period_start_time + 50; 385 // 2. Let hasPendingRenders be false. 386 auto has_pending_renders = false; 387 // 3. For each windowInSameLoop of the same-loop windows for this event loop: 388 for (auto& window : same_loop_windows()) { 389 // 1. If windowInSameLoop's map of animation frame callbacks is not empty, 390 // or if the user agent believes that the windowInSameLoop might have pending rendering updates, 391 // set hasPendingRenders to true. 392 if (window->has_animation_frame_callbacks()) 393 has_pending_renders = true; 394 // FIXME: 2. Let timerCallbackEstimates be the result of getting the values of windowInSameLoop's map of active timers. 395 // FIXME: 3. For each timeoutDeadline of timerCallbackEstimates, if timeoutDeadline is less than deadline, set deadline to timeoutDeadline. 396 } 397 // 4. If hasPendingRenders is true, then: 398 if (has_pending_renders) { 399 // 1. Let nextRenderDeadline be this event loop's last render opportunity time plus (1000 divided by the current refresh rate). 400 // FIXME: Hardcoded to 60Hz 401 auto next_render_deadline = m_last_render_opportunity_time + (1000.0 / 60.0); 402 // 2. If nextRenderDeadline is less than deadline, then return nextRenderDeadline. 403 if (next_render_deadline < deadline) 404 return next_render_deadline; 405 } 406 // 5. Return deadline. 407 return deadline; 408} 409 410}