Source/WebCore:
Invoke callbacks registered by requestIdleCallback
https://bugs.webkit.org/show_bug.cgi?id=202824

Reviewed by Antti Koivisto.

Invoke callbacks registered by requestIdleCallback unless it's canceled.

To do this, this patch introduces WindowEventLoop class, which represents the HTML5 event loop:
https://html.spec.whatwg.org/multipage/webappapis.html#window-event-loop

Because each and only each agent cluster is meant to have its own window event loop, this class will be shared
across multiple documents of the same registrable domain:
https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-agent-cluster-formalism

Tests: requestidlecallback/requestidlecallback-is-called.html
       requestidlecallback/requestidlecallback-is-not-called-when-canceled.html

* Sources.txt:
* WebCore.xcodeproj/project.pbxproj:
* dom/Document.cpp:
(WebCore::Document::eventLoop): Added.
(WebCore::Document::requestIdleCallback): Associate IdleCallbackController with this document.
* dom/Document.h:
(WebCore::Document::idleCallbackController): Added. Used for release assertions.
* dom/IdleCallbackController.cpp:
(WebCore::IdleCallbackController::IdleCallbackController): Keeps a weak pointer to Document.
(WebCore::IdleCallbackController::queueIdleCallback):
(WebCore::IdleCallbackController::removeIdleCallback):
(WebCore::IdleCallbackController::queueTaskToStartIdlePeriod): Added.
(WebCore::IdleCallbackController::startIdlePeriod): Added. Implements the start an idle period algorithm:
https://w3c.github.io/requestidlecallback/#start-an-idle-period-algorithm
(WebCore::IdleCallbackController::queueTaskToInvokeIdleCallbacks): Added.
(WebCore::IdleCallbackController::invokeIdleCallbacks): Added. The invoke idle callback timeout algorithm:
https://w3c.github.io/requestidlecallback/#invoke-idle-callback-timeout-algorithm
* dom/IdleCallbackController.h:
* dom/IdleDeadline.h:
* dom/WindowEventLoop.cpp: Added.
(WebCore::WindowEventLoop::create): Added.
(WebCore::WindowEventLoop::WindowEventLoop): Added.
(WebCore::WindowEventLoop::queueTask): Added.
(WebCore::WindowEventLoop::run): Added.
* dom/WindowEventLoop.h: Added.
* page/Page.cpp:
(WebCore::Page::updateRendering): Added comments for the missing pieces.

LayoutTests:
Invoke callback registered by requestIdleCallback
https://bugs.webkit.org/show_bug.cgi?id=202824

Reviewed by Antti Koivisto.

Added basic regression tests. The second test (requestidlecallback-is-not-called-when-canceled.html)
found a spec bug (https://github.com/w3c/requestidlecallback/issues/83).

* requestidlecallback/requestidlecallback-is-called-expected.txt: Added.
* requestidlecallback/requestidlecallback-is-called.html: Added.
* requestidlecallback/requestidlecallback-is-not-called-when-canceled-expected.txt: Added.
* requestidlecallback/requestidlecallback-is-not-called-when-canceled.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251050 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 4c63e3b2..22f0f93 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,18 @@
+2019-10-12  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Invoke callback registered by requestIdleCallback
+        https://bugs.webkit.org/show_bug.cgi?id=202824
+
+        Reviewed by Antti Koivisto.
+
+        Added basic regression tests. The second test (requestidlecallback-is-not-called-when-canceled.html)
+        found a spec bug (https://github.com/w3c/requestidlecallback/issues/83).
+
+        * requestidlecallback/requestidlecallback-is-called-expected.txt: Added.
+        * requestidlecallback/requestidlecallback-is-called.html: Added.
+        * requestidlecallback/requestidlecallback-is-not-called-when-canceled-expected.txt: Added.
+        * requestidlecallback/requestidlecallback-is-not-called-when-canceled.html: Added.
+
 2019-10-11  Ryosuke Niwa  <rniwa@webkit.org>
 
         [iOS Debug] Layout Test editing/execCommand/print.html is crashing
diff --git a/LayoutTests/requestidlecallback/requestidlecallback-is-called-expected.txt b/LayoutTests/requestidlecallback/requestidlecallback-is-called-expected.txt
new file mode 100644
index 0000000..056789c
--- /dev/null
+++ b/LayoutTests/requestidlecallback/requestidlecallback-is-called-expected.txt
@@ -0,0 +1,12 @@
+This tests that when requestIdleCallback is enabled, requestIdleCallback is eventaully called in the order.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS requestIdleCallbackIsCalled is true
+PASS logs.length is 4
+PASS logs.join(", ") is "1.A1, 2.B1, 4.A2, 3.B2"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/requestidlecallback/requestidlecallback-is-called.html b/LayoutTests/requestidlecallback/requestidlecallback-is-called.html
new file mode 100644
index 0000000..72c2e04
--- /dev/null
+++ b/LayoutTests/requestidlecallback/requestidlecallback-is-called.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html><!-- webkit-test-runner [ experimental:RequestIdleCallbackEnabled=true ] -->
+<html>
+<body>
+<script src="../resources/js-test.js"></script>
+<script>
+
+description('This tests that when requestIdleCallback is enabled, requestIdleCallback is eventaully called in the order.');
+
+jsTestIsAsync = true;
+
+requestIdleCallbackIsCalled = false;
+const iframe = document.createElement('iframe');
+const logs = [];
+
+iframe.onload = () => {
+    requestIdleCallback(() => {
+        requestIdleCallbackIsCalled = true;
+        logs.push('1.A1');
+    });
+
+    iframe.contentWindow.requestIdleCallback(() => {
+        requestIdleCallbackIsCalled = true;
+        logs.push('2.B1');
+    });
+
+    iframe.contentWindow.requestIdleCallback(() => {
+        requestIdleCallbackIsCalled = true;
+        logs.push('3.B2');
+    });
+
+    requestIdleCallback(() => {
+        requestIdleCallbackIsCalled = true;
+        logs.push('4.A2');
+    });
+}
+document.body.appendChild(iframe);
+
+setTimeout(() => {
+    shouldBeTrue('requestIdleCallbackIsCalled');
+    shouldBe('logs.length', '4');
+    shouldBeEqualToString('logs.join(", ")', '1.A1, 2.B1, 4.A2, 3.B2');
+    finishJSTest();
+}, 200);
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/requestidlecallback/requestidlecallback-is-not-called-when-canceled-expected.txt b/LayoutTests/requestidlecallback/requestidlecallback-is-not-called-when-canceled-expected.txt
new file mode 100644
index 0000000..58d695e
--- /dev/null
+++ b/LayoutTests/requestidlecallback/requestidlecallback-is-not-called-when-canceled-expected.txt
@@ -0,0 +1,12 @@
+This tests that when requestIdleCallback is enabled, requestIdleCallback is eventaully called in the order.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS a3 is a2 + 1
+PASS logs.length is 3
+PASS logs.join(", ") is "1.A1, 3.A2, 4.A3"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/requestidlecallback/requestidlecallback-is-not-called-when-canceled.html b/LayoutTests/requestidlecallback/requestidlecallback-is-not-called-when-canceled.html
new file mode 100644
index 0000000..5eda3fe
--- /dev/null
+++ b/LayoutTests/requestidlecallback/requestidlecallback-is-not-called-when-canceled.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html><!-- webkit-test-runner [ experimental:RequestIdleCallbackEnabled=true ] -->
+<html>
+<body>
+<script src="../resources/js-test.js"></script>
+<script>
+
+description('This tests that when requestIdleCallback is enabled, requestIdleCallback is eventaully called in the order.');
+
+jsTestIsAsync = true;
+
+const iframe = document.createElement('iframe');
+const logs = [];
+
+iframe.onload = () => {
+    requestIdleCallback(() => {
+        logs.push('1.A1');
+        iframe.contentWindow.cancelIdleCallback(b1);
+    });
+
+    const b1 = iframe.contentWindow.requestIdleCallback(() => logs.push('2.B1'));
+
+    window.a2 = requestIdleCallback(() => logs.push('3.A2'));
+    cancelIdleCallback(a2 + 1);
+    window.a3 = requestIdleCallback(() => logs.push('4.A3'));
+    shouldBe('a3', 'a2 + 1');
+}
+document.body.appendChild(iframe);
+
+setTimeout(() => {
+    shouldBe('logs.length', '3');
+    shouldBeEqualToString('logs.join(", ")', '1.A1, 3.A2, 4.A3');
+    finishJSTest();
+}, 200);
+
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 1eb4b3f..0899c04 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,50 @@
+2019-10-12  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Invoke callbacks registered by requestIdleCallback
+        https://bugs.webkit.org/show_bug.cgi?id=202824
+
+        Reviewed by Antti Koivisto.
+
+        Invoke callbacks registered by requestIdleCallback unless it's canceled.
+
+        To do this, this patch introduces WindowEventLoop class, which represents the HTML5 event loop:
+        https://html.spec.whatwg.org/multipage/webappapis.html#window-event-loop
+
+        Because each and only each agent cluster is meant to have its own window event loop, this class will be shared
+        across multiple documents of the same registrable domain:
+        https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-agent-cluster-formalism
+
+        Tests: requestidlecallback/requestidlecallback-is-called.html
+               requestidlecallback/requestidlecallback-is-not-called-when-canceled.html
+
+        * Sources.txt:
+        * WebCore.xcodeproj/project.pbxproj:
+        * dom/Document.cpp:
+        (WebCore::Document::eventLoop): Added.
+        (WebCore::Document::requestIdleCallback): Associate IdleCallbackController with this document.
+        * dom/Document.h:
+        (WebCore::Document::idleCallbackController): Added. Used for release assertions.
+        * dom/IdleCallbackController.cpp:
+        (WebCore::IdleCallbackController::IdleCallbackController): Keeps a weak pointer to Document.
+        (WebCore::IdleCallbackController::queueIdleCallback):
+        (WebCore::IdleCallbackController::removeIdleCallback):
+        (WebCore::IdleCallbackController::queueTaskToStartIdlePeriod): Added.
+        (WebCore::IdleCallbackController::startIdlePeriod): Added. Implements the start an idle period algorithm:
+        https://w3c.github.io/requestidlecallback/#start-an-idle-period-algorithm
+        (WebCore::IdleCallbackController::queueTaskToInvokeIdleCallbacks): Added.
+        (WebCore::IdleCallbackController::invokeIdleCallbacks): Added. The invoke idle callback timeout algorithm:
+        https://w3c.github.io/requestidlecallback/#invoke-idle-callback-timeout-algorithm
+        * dom/IdleCallbackController.h:
+        * dom/IdleDeadline.h:
+        * dom/WindowEventLoop.cpp: Added.
+        (WebCore::WindowEventLoop::create): Added.
+        (WebCore::WindowEventLoop::WindowEventLoop): Added.
+        (WebCore::WindowEventLoop::queueTask): Added.
+        (WebCore::WindowEventLoop::run): Added.
+        * dom/WindowEventLoop.h: Added.
+        * page/Page.cpp:
+        (WebCore::Page::updateRendering): Added comments for the missing pieces.
+
 2019-10-12  Chris Dumez  <cdumez@apple.com>
 
         Back/Forward cache does not work after doing a favorite navigation
diff --git a/Source/WebCore/Sources.txt b/Source/WebCore/Sources.txt
index ae25765..a3a3729 100644
--- a/Source/WebCore/Sources.txt
+++ b/Source/WebCore/Sources.txt
@@ -983,6 +983,7 @@
 dom/WebKitAnimationEvent.cpp
 dom/WebKitTransitionEvent.cpp
 dom/WheelEvent.cpp
+dom/WindowEventLoop.cpp
 dom/XMLDocument.cpp
 
 dom/messageports/MessagePortChannel.cpp
diff --git a/Source/WebCore/WebCore.xcodeproj/project.pbxproj b/Source/WebCore/WebCore.xcodeproj/project.pbxproj
index aea3df3..5c935db 100644
--- a/Source/WebCore/WebCore.xcodeproj/project.pbxproj
+++ b/Source/WebCore/WebCore.xcodeproj/project.pbxproj
@@ -2847,6 +2847,7 @@
 		9A528E8417D7F52F00AA9518 /* FloatingObjects.h in Headers */ = {isa = PBXBuildFile; fileRef = 9A528E8217D7F52F00AA9518 /* FloatingObjects.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		9AB1F38018E2489A00534743 /* CSSToLengthConversionData.h in Headers */ = {isa = PBXBuildFile; fileRef = 9AB1F37E18E2489A00534743 /* CSSToLengthConversionData.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		9B24DE8E15194B9500C59C27 /* HTMLBDIElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B24DE8C15194B9500C59C27 /* HTMLBDIElement.h */; };
+		9B27FC60234D9ADB00394A46 /* WindowEventLoop.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B27FC5E234D9ADA00394A46 /* WindowEventLoop.h */; };
 		9B2D8A7914997CCF00ECEF3E /* UndoStep.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B2D8A7814997CCF00ECEF3E /* UndoStep.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		9B32CDA913DF7FA900F34D13 /* RenderedPosition.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B32CDA713DF7FA900F34D13 /* RenderedPosition.h */; };
 		9B417064125662B3006B28FC /* ApplyBlockElementCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B417062125662B3006B28FC /* ApplyBlockElementCommand.h */; };
@@ -11064,6 +11065,8 @@
 		9B19B67E1B964E5200348745 /* ShadowRoot.idl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ShadowRoot.idl; sourceTree = "<group>"; };
 		9B1AB0791648C69D0051F3F2 /* HTMLFormControlsCollection.idl */ = {isa = PBXFileReference; lastKnownFileType = text; path = HTMLFormControlsCollection.idl; sourceTree = "<group>"; };
 		9B24DE8C15194B9500C59C27 /* HTMLBDIElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLBDIElement.h; sourceTree = "<group>"; };
+		9B27FC5E234D9ADA00394A46 /* WindowEventLoop.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WindowEventLoop.h; sourceTree = "<group>"; };
+		9B27FC5F234D9ADB00394A46 /* WindowEventLoop.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = WindowEventLoop.cpp; sourceTree = "<group>"; };
 		9B2D8A7814997CCF00ECEF3E /* UndoStep.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UndoStep.h; sourceTree = "<group>"; };
 		9B32CDA713DF7FA900F34D13 /* RenderedPosition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RenderedPosition.h; sourceTree = "<group>"; };
 		9B32CDA813DF7FA900F34D13 /* RenderedPosition.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RenderedPosition.cpp; sourceTree = "<group>"; };
@@ -28007,6 +28010,8 @@
 				85031B3A0A44EFC700F992E0 /* WheelEvent.cpp */,
 				85031B3B0A44EFC700F992E0 /* WheelEvent.h */,
 				93EEC1F709C2877700C515D1 /* WheelEvent.idl */,
+				9B27FC5F234D9ADB00394A46 /* WindowEventLoop.cpp */,
+				9B27FC5E234D9ADA00394A46 /* WindowEventLoop.h */,
 				0F26A7AC2055C8D70090A141 /* XMLDocument.cpp */,
 				830784B11C52EE1900104D1D /* XMLDocument.h */,
 				830784B01C52EE1900104D1D /* XMLDocument.idl */,
@@ -32520,6 +32525,7 @@
 				939B02EF0EA2DBC400C54570 /* WidthIterator.h in Headers */,
 				0F15ED5C1B7EC7C500EDDFEB /* WillChangeData.h in Headers */,
 				07D60928214C5BFD00E7396C /* WindowDisplayCaptureSourceMac.h in Headers */,
+				9B27FC60234D9ADB00394A46 /* WindowEventLoop.h in Headers */,
 				BC8243E90D0CFD7500460C8F /* WindowFeatures.h in Headers */,
 				7E99AF530B13846468FB01A5 /* WindowFocusAllowedIndicator.h in Headers */,
 				463521AD2081092A00C28922 /* WindowProxy.h in Headers */,
diff --git a/Source/WebCore/dom/Document.cpp b/Source/WebCore/dom/Document.cpp
index efb4267..190cc70 100644
--- a/Source/WebCore/dom/Document.cpp
+++ b/Source/WebCore/dom/Document.cpp
@@ -224,6 +224,7 @@
 #include "VisitedLinkState.h"
 #include "WebAnimation.h"
 #include "WheelEvent.h"
+#include "WindowEventLoop.h"
 #include "WindowFeatures.h"
 #include "Worklet.h"
 #include "XMLDocument.h"
@@ -6124,6 +6125,18 @@
         task.performTask(*this);
 }
 
+WindowEventLoop& Document::eventLoop()
+{
+    if (!m_eventLoop) {
+        if (m_contextDocument)
+            m_eventLoop = &m_contextDocument->eventLoop();
+        else // FIXME: Documents of similar origin should share the same event loop.
+            m_eventLoop = WindowEventLoop::create();
+    }
+    return *m_eventLoop;
+
+}
+
 void Document::suspendScheduledTasks(ReasonForSuspension reason)
 {
     if (m_scheduledTasksAreSuspended) {
@@ -6365,7 +6378,7 @@
 int Document::requestIdleCallback(Ref<IdleRequestCallback>&& callback, Seconds timeout)
 {
     if (!m_idleCallbackController)
-        m_idleCallbackController = makeUnique<IdleCallbackController>();
+        m_idleCallbackController = makeUnique<IdleCallbackController>(*this);
     return m_idleCallbackController->queueIdleCallback(WTFMove(callback), timeout);
 }
 
diff --git a/Source/WebCore/dom/Document.h b/Source/WebCore/dom/Document.h
index 7232643..4684dda 100644
--- a/Source/WebCore/dom/Document.h
+++ b/Source/WebCore/dom/Document.h
@@ -204,6 +204,7 @@
 class WebGL2RenderingContext;
 class WebGLRenderingContext;
 class GPUCanvasContext;
+class WindowEventLoop;
 class WindowProxy;
 class Worklet;
 class XPathEvaluator;
@@ -1058,6 +1059,8 @@
 
     WEBCORE_EXPORT void postTask(Task&&) final; // Executes the task on context's thread asynchronously.
 
+    WindowEventLoop& eventLoop();
+
     ScriptedAnimationController* scriptedAnimationController() { return m_scriptedAnimationController.get(); }
     void suspendScriptedAnimationControllerCallbacks();
     void resumeScriptedAnimationControllerCallbacks();
@@ -1210,6 +1213,7 @@
 
     int requestIdleCallback(Ref<IdleRequestCallback>&&, Seconds timeout);
     void cancelIdleCallback(int id);
+    IdleCallbackController* idleCallbackController() { return m_idleCallbackController.get(); }
 
     EventTarget* errorEventTarget() final;
     void logExceptionToConsole(const String& errorMessage, const String& sourceURL, int lineNumber, int columnNumber, RefPtr<Inspector::ScriptCallStack>&&) final;
@@ -2031,6 +2035,8 @@
     RefPtr<DocumentTimeline> m_timeline;
     DocumentIdentifier m_identifier;
 
+    RefPtr<WindowEventLoop> m_eventLoop;
+
 #if ENABLE(SERVICE_WORKER)
     RefPtr<SWClientConnection> m_serviceWorkerConnection;
 #endif
diff --git a/Source/WebCore/dom/IdleCallbackController.cpp b/Source/WebCore/dom/IdleCallbackController.cpp
index 789afb7..156680b 100644
--- a/Source/WebCore/dom/IdleCallbackController.cpp
+++ b/Source/WebCore/dom/IdleCallbackController.cpp
@@ -26,16 +26,29 @@
 #include "config.h"
 #include "IdleCallbackController.h"
 
+#include "Document.h"
+#include "IdleDeadline.h"
+#include "WindowEventLoop.h"
+
 namespace WebCore {
 
+IdleCallbackController::IdleCallbackController(Document& document)
+    : m_document(makeWeakPtr(document))
+{
+
+}
 int IdleCallbackController::queueIdleCallback(Ref<IdleRequestCallback>&& callback, Seconds)
 {
+    bool startIdlePeriod = m_idleRequestCallbacks.isEmpty() && m_runnableIdleCallbacks.isEmpty();
+
     ++m_idleCallbackIdentifier;
     auto handle = m_idleCallbackIdentifier;
 
-    m_idleRequests.append({ handle, WTFMove(callback) });
+    m_idleRequestCallbacks.append({ handle, WTFMove(callback) });
 
-    // FIXME: Queue a task if start_idle_period is true.
+    if (startIdlePeriod)
+        queueTaskToStartIdlePeriod();
+
     // FIXME: Queue a task if timeout is positive.
 
     return handle;
@@ -46,9 +59,76 @@
     if (signedIdentifier <= 0)
         return;
     unsigned identifier = signedIdentifier;
-    m_idleRequests.removeFirstMatching([identifier](auto& request) {
+
+    m_idleRequestCallbacks.removeAllMatching([identifier](auto& request) {
         return request.identifier == identifier;
     });
+
+    m_runnableIdleCallbacks.removeAllMatching([identifier](auto& request) {
+        return request.identifier == identifier;
+    });
+}
+
+void IdleCallbackController::queueTaskToStartIdlePeriod()
+{
+    m_document->eventLoop().queueTask(TaskSource::IdleTask, *m_document, [protectedDocument = makeRef(*m_document), this]() {
+        RELEASE_ASSERT(protectedDocument->idleCallbackController() == this);
+        startIdlePeriod();
+    });
+}
+
+// https://w3c.github.io/requestidlecallback/#start-an-idle-period-algorithm
+static const auto deadlineCapToEnsureResponsiveness = 50_ms;
+void IdleCallbackController::startIdlePeriod()
+{
+    auto now = MonotonicTime::now();
+    if (m_lastDeadline > now)
+        return;
+
+    // FIXME: Take other tasks in the WindowEventLoop into account.
+    auto deadline = now + deadlineCapToEnsureResponsiveness;
+
+    for (auto& request : m_idleRequestCallbacks)
+        m_runnableIdleCallbacks.append({ request.identifier, WTFMove(request.callback) });
+    m_idleRequestCallbacks.clear();
+
+    ASSERT(!m_runnableIdleCallbacks.isEmpty());
+    queueTaskToInvokeIdleCallbacks(deadline);
+
+    m_lastDeadline = deadline;
+}
+
+void IdleCallbackController::queueTaskToInvokeIdleCallbacks(MonotonicTime deadline)
+{
+    m_document->eventLoop().queueTask(TaskSource::IdleTask, *m_document, [protectedDocument = makeRef(*m_document), deadline, this]() {
+        RELEASE_ASSERT(protectedDocument->idleCallbackController() == this);
+        invokeIdleCallbacks(deadline);
+    });
+}
+
+// https://w3c.github.io/requestidlecallback/#invoke-idle-callbacks-algorithm
+void IdleCallbackController::invokeIdleCallbacks(MonotonicTime deadline)
+{
+    if (!m_document)
+        return;
+
+    auto now = MonotonicTime::now();
+    if (now < deadline) {
+        // FIXME: Don't do this if there is a higher priority task in the event loop.
+        // https://github.com/w3c/requestidlecallback/issues/83
+        if (m_runnableIdleCallbacks.isEmpty())
+            return;
+
+        auto request = m_runnableIdleCallbacks.takeFirst();
+        auto idleDeadline = IdleDeadline::create(deadline);
+        request.callback->handleEvent(idleDeadline.get());
+        if (!m_runnableIdleCallbacks.isEmpty())
+            queueTaskToInvokeIdleCallbacks(deadline);
+        return;
+    }
+
+    if (!m_idleRequestCallbacks.isEmpty() || !m_runnableIdleCallbacks.isEmpty())
+        queueTaskToStartIdlePeriod();
 }
 
 } // namespace WebCore
diff --git a/Source/WebCore/dom/IdleCallbackController.h b/Source/WebCore/dom/IdleCallbackController.h
index a027d10..95a809c 100644
--- a/Source/WebCore/dom/IdleCallbackController.h
+++ b/Source/WebCore/dom/IdleCallbackController.h
@@ -26,8 +26,10 @@
 #pragma once
 
 #include "IdleRequestCallback.h"
+#include <wtf/Deque.h>
+#include <wtf/MonotonicTime.h>
 #include <wtf/Seconds.h>
-#include <wtf/Vector.h>
+#include <wtf/WeakPtr.h>
 
 namespace WebCore {
 
@@ -35,18 +37,28 @@
     WTF_MAKE_FAST_ALLOCATED;
 
 public:
+    IdleCallbackController(Document&);
+
     int queueIdleCallback(Ref<IdleRequestCallback>&&, Seconds timeout);
     void removeIdleCallback(int);
 
 private:
+    void queueTaskToStartIdlePeriod();
+    void startIdlePeriod();
+    void queueTaskToInvokeIdleCallbacks(MonotonicTime deadline);
+    void invokeIdleCallbacks(MonotonicTime deadline);
+
     unsigned m_idleCallbackIdentifier { 0 };
+    MonotonicTime m_lastDeadline;
 
     struct IdleRequest {
-        unsigned identifier;
+        unsigned identifier { 0 };
         Ref<IdleRequestCallback> callback;
     };
 
-    Vector<IdleRequest> m_idleRequests;
+    Deque<IdleRequest> m_idleRequestCallbacks;
+    Deque<IdleRequest> m_runnableIdleCallbacks;
+    WeakPtr<Document> m_document;
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/dom/IdleDeadline.h b/Source/WebCore/dom/IdleDeadline.h
index 2e4f287..4a7fe6e 100644
--- a/Source/WebCore/dom/IdleDeadline.h
+++ b/Source/WebCore/dom/IdleDeadline.h
@@ -36,7 +36,7 @@
 
 class IdleDeadline final : public RefCounted<IdleDeadline> {
 public:
-    Ref<IdleDeadline> create(MonotonicTime deadline)
+    static Ref<IdleDeadline> create(MonotonicTime deadline)
     {
         return adoptRef(*new IdleDeadline(deadline));
     }
diff --git a/Source/WebCore/dom/WindowEventLoop.cpp b/Source/WebCore/dom/WindowEventLoop.cpp
new file mode 100644
index 0000000..d188ad6
--- /dev/null
+++ b/Source/WebCore/dom/WindowEventLoop.cpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "WindowEventLoop.h"
+
+#include "Document.h"
+
+namespace WebCore {
+
+Ref<WindowEventLoop> WindowEventLoop::create()
+{
+    return adoptRef(*new WindowEventLoop);
+}
+
+WindowEventLoop::WindowEventLoop()
+{
+}
+
+void WindowEventLoop::queueTask(TaskSource source, Document& document, TaskFunction&& task)
+{
+    if (m_tasks.isEmpty()) {
+        callOnMainThread([eventLoop = makeRef(*this)] () {
+            eventLoop->run();
+        });
+    }
+    m_tasks.append(Task { source, WTFMove(task), document.identifier() });
+}
+
+void WindowEventLoop::run()
+{
+    Vector<Task> tasks = WTFMove(m_tasks);
+    ASSERT(m_tasks.isEmpty());
+    for (auto& task : tasks) {
+        auto* document = Document::allDocumentsMap().get(task.documentIdentifier);
+        if (!document || document->activeDOMObjectsAreStopped())
+            continue;
+        // Skip tasks associated with suspended documents.
+        task.task();
+    }
+}
+
+} // namespace WebCore
diff --git a/Source/WebCore/dom/WindowEventLoop.h b/Source/WebCore/dom/WindowEventLoop.h
new file mode 100644
index 0000000..714de66
--- /dev/null
+++ b/Source/WebCore/dom/WindowEventLoop.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "DocumentIdentifier.h"
+#include <memory>
+#include <wtf/HashMap.h>
+
+namespace WebCore {
+
+class Document;
+
+enum class TaskSource : uint8_t {
+    IdleTask,
+};
+
+// https://html.spec.whatwg.org/multipage/webappapis.html#window-event-loop
+class WindowEventLoop : public RefCounted<WindowEventLoop> {
+    WTF_MAKE_FAST_ALLOCATED;
+public:
+    static Ref<WindowEventLoop> create();
+
+    typedef WTF::Function<void ()> TaskFunction;
+
+    void queueTask(TaskSource, Document&, TaskFunction&&);
+
+private:
+    WindowEventLoop();
+
+    void run();
+
+    struct Task {
+        TaskSource source;
+        TaskFunction task;
+        DocumentIdentifier documentIdentifier;
+    };
+
+    // Use a global queue instead of multiple task queues since HTML5 spec allows UA to pick arbitrary queue.
+    Vector<Task> m_tasks;
+};
+
+} // namespace WebCore
diff --git a/Source/WebCore/page/Page.cpp b/Source/WebCore/page/Page.cpp
index bd8c661..63cb5ff 100644
--- a/Source/WebCore/page/Page.cpp
+++ b/Source/WebCore/page/Page.cpp
@@ -1281,6 +1281,7 @@
         view->updateLayoutAndStyleIfNeededRecursive();
 }
 
+// https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering
 void Page::updateRendering()
 {
     // This function is not reentrant, e.g. a rAF callback may force repaint.
@@ -1300,9 +1301,16 @@
         documents.append(&document);
     });
 
+    // FIXME: Run the resize steps
+
+    // FIXME: Run the scroll steps
+
+    // FIXME: Evaluate media queries and report changes.
+
     for (auto& document : documents) {
         DOMHighResTimeStamp timestamp = document->domWindow()->nowTimestamp();
         document->updateAnimationsAndSendEvents(timestamp);
+        // FIXME: Run the fullscreen steps.
         document->serviceRequestAnimationFrameCallbacks(timestamp);
     }
 
@@ -1317,6 +1325,9 @@
         document->updateResizeObservations(*this);
 #endif
 
+    // FIXME: Flush autofocus candidates
+    // https://github.com/whatwg/html/issues/4992
+
     layoutIfNeeded();
 }