Suspend dedicated worker threads while in the back/forward cache
https://bugs.webkit.org/show_bug.cgi?id=203186
<rdar://problem/56447493>

Reviewed by Ryosuke Niwa.

Source/WebCore:

When a page with a (dedicated) Worker enters the back/forward cache, we now
pause the worker thread, and resume it only when taking the page out of the
back/forward cache. This avoids having the worker use CPU while the page is
in the cache.

* workers/Worker.cpp:
(WebCore::Worker::suspend):
(WebCore::Worker::resume):
* workers/Worker.h:
* workers/WorkerGlobalScopeProxy.h:
* workers/WorkerMessagingProxy.cpp:
(WebCore::WorkerMessagingProxy::suspend):
(WebCore::WorkerMessagingProxy::resume):
(WebCore::WorkerMessagingProxy::workerThreadCreated):
* workers/WorkerMessagingProxy.h:
* workers/WorkerThread.cpp:
(WebCore::WorkerThread::WorkerThread):
(WebCore::WorkerThread::suspend):
(WebCore::WorkerThread::resume):
(WebCore::WorkerThread::stop):
* workers/WorkerThread.h:

LayoutTests:

Extend layout test coverage.

* fast/workers/resources/worker-setInterval.js: Added.
(onmessage):
(setInterval):
* fast/workers/worker-page-cache.html:


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251416 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 2ec1a7b..232d1b9 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,18 @@
+2019-10-21  Chris Dumez  <cdumez@apple.com>
+
+        Suspend dedicated worker threads while in the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=203186
+        <rdar://problem/56447493>
+
+        Reviewed by Ryosuke Niwa.
+
+        Extend layout test coverage.
+
+        * fast/workers/resources/worker-setInterval.js: Added.
+        (onmessage):
+        (setInterval):
+        * fast/workers/worker-page-cache.html:
+
 2019-10-21  Myles C. Maxfield  <mmaxfield@apple.com>
 
         [Cocoa] Move ui-serif, ui-monospaced, and ui-rounded out from behind SPI
diff --git a/LayoutTests/fast/workers/resources/worker-setInterval.js b/LayoutTests/fast/workers/resources/worker-setInterval.js
new file mode 100644
index 0000000..b1e3d99
--- /dev/null
+++ b/LayoutTests/fast/workers/resources/worker-setInterval.js
@@ -0,0 +1,12 @@
+function onmessage(evt)
+{
+    postMessage("SUCCESS");
+}
+
+let i = 0;
+setInterval(() => {
+    postMessage("" + i);
+    i++;
+}, 0);
+
+addEventListener("message", onmessage, true);
diff --git a/LayoutTests/fast/workers/worker-page-cache.html b/LayoutTests/fast/workers/worker-page-cache.html
index 141631e..d4ac90e 100644
--- a/LayoutTests/fast/workers/worker-page-cache.html
+++ b/LayoutTests/fast/workers/worker-page-cache.html
@@ -26,11 +26,10 @@
 });
 
 let firstMessage = true;
+let messageCountAfterResume = 0;
 onload = () => {
-    worker = new Worker('resources/worker-event-listener.js');
-    setInterval(() => {
-        worker.postMessage("");
-    }, 1);
+    worker = new Worker('resources/worker-setInterval.js');
+    worker.postMessage("");
 
     worker.onmessage = function(evt) {
         if (firstMessage) {
@@ -40,9 +39,12 @@
             return;
         }
         if (restoredFromPageCache) {
-            restoredFromPageCache = false;
-            testPassed("Received message after restoring from page cache.");
-            finishJSTest();
+            messageCountAfterResume++;
+            if (messageCountAfterResume == 10) {
+                restoredFromPageCache = false;
+                testPassed("Received message after restoring from page cache.");
+                finishJSTest();
+            }
         }
     };
 }
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index a444eae..17b598b 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,33 @@
+2019-10-21  Chris Dumez  <cdumez@apple.com>
+
+        Suspend dedicated worker threads while in the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=203186
+        <rdar://problem/56447493>
+
+        Reviewed by Ryosuke Niwa.
+
+        When a page with a (dedicated) Worker enters the back/forward cache, we now
+        pause the worker thread, and resume it only when taking the page out of the
+        back/forward cache. This avoids having the worker use CPU while the page is
+        in the cache.
+
+        * workers/Worker.cpp:
+        (WebCore::Worker::suspend):
+        (WebCore::Worker::resume):
+        * workers/Worker.h:
+        * workers/WorkerGlobalScopeProxy.h:
+        * workers/WorkerMessagingProxy.cpp:
+        (WebCore::WorkerMessagingProxy::suspend):
+        (WebCore::WorkerMessagingProxy::resume):
+        (WebCore::WorkerMessagingProxy::workerThreadCreated):
+        * workers/WorkerMessagingProxy.h:
+        * workers/WorkerThread.cpp:
+        (WebCore::WorkerThread::WorkerThread):
+        (WebCore::WorkerThread::suspend):
+        (WebCore::WorkerThread::resume):
+        (WebCore::WorkerThread::stop):
+        * workers/WorkerThread.h:
+
 2019-10-21  Tim Horton  <timothy_horton@apple.com>
 
         Fix the build
diff --git a/Source/WebCore/workers/Worker.cpp b/Source/WebCore/workers/Worker.cpp
index 0f5078e..b3aa708 100644
--- a/Source/WebCore/workers/Worker.cpp
+++ b/Source/WebCore/workers/Worker.cpp
@@ -160,6 +160,22 @@
     terminate();
 }
 
+void Worker::suspend(ReasonForSuspension reason)
+{
+    if (reason == ReasonForSuspension::BackForwardCache) {
+        m_contextProxy.suspendForBackForwardCache();
+        m_isSuspendedForBackForwardCache = true;
+    }
+}
+
+void Worker::resume()
+{
+    if (m_isSuspendedForBackForwardCache) {
+        m_contextProxy.resumeForBackForwardCache();
+        m_isSuspendedForBackForwardCache = false;
+    }
+}
+
 bool Worker::hasPendingActivity() const
 {
     return m_contextProxy.hasPendingActivity() || ActiveDOMObject::hasPendingActivity() || m_eventQueue->hasPendingEvents();
diff --git a/Source/WebCore/workers/Worker.h b/Source/WebCore/workers/Worker.h
index 4c54139..d52a802 100644
--- a/Source/WebCore/workers/Worker.h
+++ b/Source/WebCore/workers/Worker.h
@@ -84,6 +84,8 @@
     void notifyFinished() final;
 
     void stop() final;
+    void suspend(ReasonForSuspension) final;
+    void resume() final;
     const char* activeDOMObjectName() const final;
 
     static void networkStateChanged(bool isOnLine);
@@ -95,6 +97,7 @@
     Optional<ContentSecurityPolicyResponseHeaders> m_contentSecurityPolicyResponseHeaders;
     MonotonicTime m_workerCreationTime;
     bool m_shouldBypassMainWorldContentSecurityPolicy { false };
+    bool m_isSuspendedForBackForwardCache { false };
     JSC::RuntimeFlags m_runtimeFlags;
     UniqueRef<GenericEventQueue> m_eventQueue;
 };
diff --git a/Source/WebCore/workers/WorkerGlobalScopeProxy.h b/Source/WebCore/workers/WorkerGlobalScopeProxy.h
index 0657d6e..74c648b 100644
--- a/Source/WebCore/workers/WorkerGlobalScopeProxy.h
+++ b/Source/WebCore/workers/WorkerGlobalScopeProxy.h
@@ -52,6 +52,9 @@
     virtual void workerObjectDestroyed() = 0;
     virtual void notifyNetworkStateChange(bool isOnline) = 0;
 
+    virtual void suspendForBackForwardCache() = 0;
+    virtual void resumeForBackForwardCache() = 0;
+
 protected:
     virtual ~WorkerGlobalScopeProxy() = default;
 };
diff --git a/Source/WebCore/workers/WorkerMessagingProxy.cpp b/Source/WebCore/workers/WorkerMessagingProxy.cpp
index a77bb7e..6d89e83 100644
--- a/Source/WebCore/workers/WorkerMessagingProxy.cpp
+++ b/Source/WebCore/workers/WorkerMessagingProxy.cpp
@@ -128,6 +128,22 @@
         m_queuedEarlyTasks.append(makeUnique<ScriptExecutionContext::Task>(WTFMove(task)));
 }
 
+void WorkerMessagingProxy::suspendForBackForwardCache()
+{
+    if (m_workerThread)
+        m_workerThread->suspend();
+    else
+        m_askedToSuspend = true;
+}
+
+void WorkerMessagingProxy::resumeForBackForwardCache()
+{
+    if (m_workerThread)
+        m_workerThread->resume();
+    else
+        m_askedToSuspend = false;
+}
+
 void WorkerMessagingProxy::postTaskToLoader(ScriptExecutionContext::Task&& task)
 {
     // FIXME: In case of nested workers, this should go directly to the root Document context.
@@ -190,6 +206,11 @@
         // Worker.terminate() could be called from JS before the thread was created.
         m_workerThread->stop(nullptr);
     } else {
+        if (m_askedToSuspend) {
+            m_askedToSuspend = false;
+            m_workerThread->suspend();
+        }
+
         ASSERT(!m_unconfirmedMessageCount);
         m_unconfirmedMessageCount = m_queuedEarlyTasks.size();
         m_workerThreadHadPendingActivity = true; // Worker initialization means a pending activity.
diff --git a/Source/WebCore/workers/WorkerMessagingProxy.h b/Source/WebCore/workers/WorkerMessagingProxy.h
index ed17fc3..fd554e1 100644
--- a/Source/WebCore/workers/WorkerMessagingProxy.h
+++ b/Source/WebCore/workers/WorkerMessagingProxy.h
@@ -56,6 +56,8 @@
     bool hasPendingActivity() const final;
     void workerObjectDestroyed() final;
     void notifyNetworkStateChange(bool isOnline) final;
+    void suspendForBackForwardCache() final;
+    void resumeForBackForwardCache() final;
 
     // Implementation of WorkerObjectProxy.
     // (Only use these functions in the worker context thread.)
@@ -96,6 +98,7 @@
     unsigned m_unconfirmedMessageCount { 0 }; // Unconfirmed messages from worker object to worker thread.
     bool m_workerThreadHadPendingActivity { false }; // The latest confirmation from worker thread reported that it was still active.
 
+    bool m_askedToSuspend { false };
     bool m_askedToTerminate { false };
 
     Vector<std::unique_ptr<ScriptExecutionContext::Task>> m_queuedEarlyTasks; // Tasks are queued here until there's a thread object created.
diff --git a/Source/WebCore/workers/WorkerThread.cpp b/Source/WebCore/workers/WorkerThread.cpp
index 19d03a1..275b5ee 100644
--- a/Source/WebCore/workers/WorkerThread.cpp
+++ b/Source/WebCore/workers/WorkerThread.cpp
@@ -265,6 +265,21 @@
     m_runLoop.run(m_workerGlobalScope.get());
 }
 
+void WorkerThread::suspend()
+{
+    m_isSuspended = true;
+    runLoop().postTask([&](ScriptExecutionContext&) {
+        m_suspensionSemaphore.wait();
+    });
+}
+
+void WorkerThread::resume()
+{
+    ASSERT(m_isSuspended);
+    m_isSuspended = false;
+    m_suspensionSemaphore.signal();
+}
+
 void WorkerThread::stop(WTF::Function<void()>&& stoppedCallback)
 {
     // Mutex protection is necessary to ensure that m_workerGlobalScope isn't changed by
@@ -280,6 +295,10 @@
         return;
     }
 
+    // If the thread is suspended, resume it now so that we can dispatch the cleanup tasks below.
+    if (m_isSuspended)
+        resume();
+
     ASSERT(!m_stoppedCallback);
     m_stoppedCallback = WTFMove(stoppedCallback);
 
diff --git a/Source/WebCore/workers/WorkerThread.h b/Source/WebCore/workers/WorkerThread.h
index cd6c78d..14eebf1 100644
--- a/Source/WebCore/workers/WorkerThread.h
+++ b/Source/WebCore/workers/WorkerThread.h
@@ -31,6 +31,7 @@
 #include <wtf/Forward.h>
 #include <wtf/Function.h>
 #include <wtf/RefCounted.h>
+#include <wtf/threads/BinarySemaphore.h>
 
 namespace WebCore {
 
@@ -63,6 +64,9 @@
 
     void stop(WTF::Function<void()>&& terminatedCallback);
 
+    void suspend();
+    void resume();
+
     Thread* thread() const { return m_thread.get(); }
     WorkerRunLoop& runLoop() { return m_runLoop; }
     WorkerLoaderProxy& workerLoaderProxy() const { return m_workerLoaderProxy; }
@@ -133,6 +137,8 @@
     RefPtr<SocketProvider> m_socketProvider;
 
     WTF::Function<void()> m_stoppedCallback;
+    BinarySemaphore m_suspensionSemaphore;
+    bool m_isSuspended { false };
 };
 
 } // namespace WebCore