Make requestIdleCallback suspendable
https://bugs.webkit.org/show_bug.cgi?id=203023

Reviewed by Chris Dumez.

Source/WebCore:

Make requestIdleCallback suspendable by making WindowEventLoop itself suspendable.
Because WindowEventLoop can be shared across documents, we don't want to make it an ActiveDOMObject.

Instead, we would make CachedFrameBase::restore and CachedFrame manually invoke suspend & resume.

Test: requestidlecallback/requestidlecallback-in-page-cache.html

* dom/Document.h:
(WebCore::Document::eventLoopIfExists): Added. This should probably go away once most of the event loop
is implemented since we're almost always going to have this object then.
* dom/WindowEventLoop.cpp:
(WebCore::WindowEventLoop::queueTask): Because m_tasks may contain tasks of suspended documents,
we check m_activeTaskCount, which is only positive when there is a task for non-suspended documents,
to decide whether we schedule a callback or not.
(WebCore::WindowEventLoop::suspend): Added. No-op for now.
(WebCore::WindowEventLoop::resume): Added. Schedule a callback if there is a task associated with
this document.
(WebCore::WindowEventLoop::run): Skip a task for a suspended document, and add it back to m_tasks along
with other tasks that got scheduled by running the current working set of tasks.
* dom/WindowEventLoop.h:
* history/CachedFrame.cpp:
(WebCore::CachedFrameBase::restore):
(WebCore::CachedFrame::CachedFrame):

LayoutTests:

* requestidlecallback/requestidlecallback-in-page-cache-expected.txt: Added.
* requestidlecallback/requestidlecallback-in-page-cache.html: Added.
* requestidlecallback/resources: Added.
* requestidlecallback/resources/page-cache-helper.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251258 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 3241276..fd3edf1 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,15 @@
+2019-10-17  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Make requestIdleCallback suspendable
+        https://bugs.webkit.org/show_bug.cgi?id=203023
+
+        Reviewed by Chris Dumez.
+
+        * requestidlecallback/requestidlecallback-in-page-cache-expected.txt: Added.
+        * requestidlecallback/requestidlecallback-in-page-cache.html: Added.
+        * requestidlecallback/resources: Added.
+        * requestidlecallback/resources/page-cache-helper.html: Added.
+
 2019-10-17  Dirk Schulze  <krit@webkit.org>
 
         transform-box: content-box, stroke-box missing
diff --git a/LayoutTests/requestidlecallback/requestidlecallback-in-page-cache-expected.txt b/LayoutTests/requestidlecallback/requestidlecallback-in-page-cache-expected.txt
new file mode 100644
index 0000000..254eefd
--- /dev/null
+++ b/LayoutTests/requestidlecallback/requestidlecallback-in-page-cache-expected.txt
@@ -0,0 +1,13 @@
+This tests that when requestIdleCallback is not enabled, requestIdleCallback and IdleDeadline are not defined.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS event.persisted is true
+PASS logs.length is 0
+PASS logs.length is 7
+PASS logs.join(", ") is "A1, B1, A2, B2, A3, B3, A4"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/requestidlecallback/requestidlecallback-in-page-cache.html b/LayoutTests/requestidlecallback/requestidlecallback-in-page-cache.html
new file mode 100644
index 0000000..761cf7a
--- /dev/null
+++ b/LayoutTests/requestidlecallback/requestidlecallback-in-page-cache.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html><!-- webkit-test-runner [ experimental:RequestIdleCallbackEnabled=true enableBackForwardCache=true ] -->
+<html>
+<body>
+<script src="../resources/js-test.js"></script>
+<script>
+
+description('This tests that when requestIdleCallback is not enabled, requestIdleCallback and IdleDeadline are not defined.');
+jsTestIsAsync = true;
+
+const iframe = document.createElement('iframe');
+document.body.appendChild(iframe);
+
+let isInitialLoad = true;
+const logs = [];
+if (window.testRunner)
+  setTimeout(() => testRunner.notifyDone(), 3000);
+
+window.addEventListener("pageshow", function(event) {
+    if (isInitialLoad) {
+        isInitialLoad = false;
+        return;
+    }
+  
+    if (window.testRunner)
+      setTimeout(() => testRunner.notifyDone(), 3000);
+
+    shouldBeTrue('event.persisted');
+    shouldBe('logs.length', '0');
+    iframe.contentWindow.requestIdleCallback(() => logs.push('B3'));
+    requestIdleCallback(() => logs.push('A4'));
+    requestIdleCallback(() => {
+        shouldBe('logs.length', '7');
+        shouldBeEqualToString('logs.join(", ")', 'A1, B1, A2, B2, A3, B3, A4');
+        finishJSTest();
+    });
+});
+
+window.addEventListener("pagehide", function(event) {
+    requestIdleCallback(() => logs.push('A1'));
+    iframe.contentWindow.requestIdleCallback(() => logs.push('B1'));
+    requestIdleCallback(() => logs.push('A2'));
+    iframe.contentWindow.requestIdleCallback(() => logs.push('B2'));
+    requestIdleCallback(() => logs.push('A3'));
+});
+
+onload = () => {
+    setTimeout(() => {
+        window.location = 'resources/page-cache-helper.html';
+    }, 0);
+}
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/requestidlecallback/resources/page-cache-helper.html b/LayoutTests/requestidlecallback/resources/page-cache-helper.html
new file mode 100644
index 0000000..4d2c787
--- /dev/null
+++ b/LayoutTests/requestidlecallback/resources/page-cache-helper.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script>
+
+window.onload = () => {
+    requestIdleCallback(() => {
+        setTimeout(() => {
+            history.back();
+        }, 0);
+    });
+}
+
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 186110d..d74dfd8 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,34 @@
+2019-10-17  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Make requestIdleCallback suspendable
+        https://bugs.webkit.org/show_bug.cgi?id=203023
+
+        Reviewed by Chris Dumez.
+
+        Make requestIdleCallback suspendable by making WindowEventLoop itself suspendable.
+        Because WindowEventLoop can be shared across documents, we don't want to make it an ActiveDOMObject.
+
+        Instead, we would make CachedFrameBase::restore and CachedFrame manually invoke suspend & resume.
+
+        Test: requestidlecallback/requestidlecallback-in-page-cache.html
+
+        * dom/Document.h:
+        (WebCore::Document::eventLoopIfExists): Added. This should probably go away once most of the event loop
+        is implemented since we're almost always going to have this object then.
+        * dom/WindowEventLoop.cpp:
+        (WebCore::WindowEventLoop::queueTask): Because m_tasks may contain tasks of suspended documents,
+        we check m_activeTaskCount, which is only positive when there is a task for non-suspended documents,
+        to decide whether we schedule a callback or not.
+        (WebCore::WindowEventLoop::suspend): Added. No-op for now.
+        (WebCore::WindowEventLoop::resume): Added. Schedule a callback if there is a task associated with
+        this document.
+        (WebCore::WindowEventLoop::run): Skip a task for a suspended document, and add it back to m_tasks along
+        with other tasks that got scheduled by running the current working set of tasks.
+        * dom/WindowEventLoop.h:
+        * history/CachedFrame.cpp:
+        (WebCore::CachedFrameBase::restore):
+        (WebCore::CachedFrame::CachedFrame):
+
 2019-10-17  Chris Dumez  <cdumez@apple.com>
 
         Don't put pages that have not reached the non-visually empty layout milestone in the back/forward cache
diff --git a/Source/WebCore/dom/Document.h b/Source/WebCore/dom/Document.h
index 986f9b7..566a4f8 100644
--- a/Source/WebCore/dom/Document.h
+++ b/Source/WebCore/dom/Document.h
@@ -1061,6 +1061,7 @@
     WEBCORE_EXPORT void postTask(Task&&) final; // Executes the task on context's thread asynchronously.
 
     WindowEventLoop& eventLoop();
+    WindowEventLoop* eventLoopIfExists() { return m_eventLoop.get(); }
 
     ScriptedAnimationController* scriptedAnimationController() { return m_scriptedAnimationController.get(); }
     void suspendScriptedAnimationControllerCallbacks();
diff --git a/Source/WebCore/dom/WindowEventLoop.cpp b/Source/WebCore/dom/WindowEventLoop.cpp
index d188ad6..698d463 100644
--- a/Source/WebCore/dom/WindowEventLoop.cpp
+++ b/Source/WebCore/dom/WindowEventLoop.cpp
@@ -41,25 +41,49 @@
 
 void WindowEventLoop::queueTask(TaskSource source, Document& document, TaskFunction&& task)
 {
-    if (m_tasks.isEmpty()) {
+    if (!m_activeTaskCount) {
         callOnMainThread([eventLoop = makeRef(*this)] () {
             eventLoop->run();
         });
     }
+    ++m_activeTaskCount;
     m_tasks.append(Task { source, WTFMove(task), document.identifier() });
 }
 
+void WindowEventLoop::suspend(Document&)
+{
+}
+
+void WindowEventLoop::resume(Document& document)
+{
+    if (!m_documentIdentifiersForSuspendedTasks.contains(document.identifier()))
+        return;
+
+    callOnMainThread([eventLoop = makeRef(*this)] () {
+        eventLoop->run();
+    });
+}
+
 void WindowEventLoop::run()
 {
+    m_activeTaskCount = 0;
     Vector<Task> tasks = WTFMove(m_tasks);
-    ASSERT(m_tasks.isEmpty());
+    m_documentIdentifiersForSuspendedTasks.clear();
+    Vector<Task> remainingTasks;
     for (auto& task : tasks) {
         auto* document = Document::allDocumentsMap().get(task.documentIdentifier);
         if (!document || document->activeDOMObjectsAreStopped())
             continue;
-        // Skip tasks associated with suspended documents.
+        if (document->activeDOMObjectsAreSuspended()) {
+            m_documentIdentifiersForSuspendedTasks.add(task.documentIdentifier);
+            remainingTasks.append(WTFMove(task));
+            continue;
+        }
         task.task();
     }
+    for (auto& task : m_tasks)
+        remainingTasks.append(WTFMove(task));
+    m_tasks = WTFMove(remainingTasks);
 }
 
 } // namespace WebCore
diff --git a/Source/WebCore/dom/WindowEventLoop.h b/Source/WebCore/dom/WindowEventLoop.h
index 714de66..5e38bf5 100644
--- a/Source/WebCore/dom/WindowEventLoop.h
+++ b/Source/WebCore/dom/WindowEventLoop.h
@@ -27,7 +27,8 @@
 
 #include "DocumentIdentifier.h"
 #include <memory>
-#include <wtf/HashMap.h>
+#include <wtf/HashSet.h>
+#include <wtf/Vector.h>
 
 namespace WebCore {
 
@@ -47,6 +48,9 @@
 
     void queueTask(TaskSource, Document&, TaskFunction&&);
 
+    void suspend(Document&);
+    void resume(Document&);
+
 private:
     WindowEventLoop();
 
@@ -60,6 +64,8 @@
 
     // Use a global queue instead of multiple task queues since HTML5 spec allows UA to pick arbitrary queue.
     Vector<Task> m_tasks;
+    size_t m_activeTaskCount { 0 };
+    HashSet<DocumentIdentifier> m_documentIdentifiersForSuspendedTasks;
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/history/CachedFrame.cpp b/Source/WebCore/history/CachedFrame.cpp
index 87ab173..1e8c02e 100644
--- a/Source/WebCore/history/CachedFrame.cpp
+++ b/Source/WebCore/history/CachedFrame.cpp
@@ -47,6 +47,7 @@
 #include "ScriptController.h"
 #include "SerializedScriptValue.h"
 #include "StyleTreeResolver.h"
+#include "WindowEventLoop.h"
 #include <wtf/RefCountedLeakCounter.h>
 #include <wtf/text/CString.h>
 
@@ -106,6 +107,9 @@
         if (m_document->svgExtensions())
             m_document->accessSVGExtensions().unpauseAnimations();
 
+        if (auto* eventLoop = m_document->eventLoopIfExists())
+            eventLoop->resume(*m_document);
+
         m_document->resume(ReasonForSuspension::BackForwardCache);
 
         // It is necessary to update any platform script objects after restoring the
@@ -171,6 +175,9 @@
     // Active DOM objects must be suspended before we cache the frame script data.
     m_document->suspend(ReasonForSuspension::BackForwardCache);
 
+    if (auto* eventLoop = m_document->eventLoopIfExists())
+        eventLoop->suspend(*m_document);
+
     m_cachedFrameScriptData = makeUnique<ScriptCachedFrameData>(frame);
 
     m_document->domWindow()->suspendForBackForwardCache();