FileReader should not prevent entering the back/forward cache
https://bugs.webkit.org/show_bug.cgi?id=203106

Reviewed by Geoffrey Garen.

Source/WebCore:

FileReader should not prevent entering the back/forward cache. To support this,
its implementation now uses a SuspendableTaskQueue to dispatch events.

Test: fast/files/file-reader-back-forward-cache.html

* dom/ActiveDOMObject.cpp:
(WebCore::ActiveDOMObject::isAllowedToRunScript const):
* dom/ActiveDOMObject.h:
* dom/ScriptExecutionContext.cpp:
(WebCore::ScriptExecutionContext::resumeActiveDOMObjects):
* fileapi/FileReader.cpp:
(WebCore::FileReader::FileReader):
(WebCore::FileReader::stop):
(WebCore::FileReader::hasPendingActivity const):
(WebCore::FileReader::readInternal):
(WebCore::FileReader::abort):
(WebCore::FileReader::didStartLoading):
(WebCore::FileReader::didReceiveData):
(WebCore::FileReader::didFinishLoading):
(WebCore::FileReader::didFail):
(WebCore::FileReader::fireEvent):
* fileapi/FileReader.h:

LayoutTests:

Add layout test coverage.

* TestExpectations:
* fast/files/file-reader-back-forward-cache-expected.txt: Added.
* fast/files/file-reader-back-forward-cache.html: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251327 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 9f3365b..0107579 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,16 @@
+2019-10-19  Chris Dumez  <cdumez@apple.com>
+
+        FileReader should not prevent entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=203106
+
+        Reviewed by Geoffrey Garen.
+
+        Add layout test coverage.
+
+        * TestExpectations:
+        * fast/files/file-reader-back-forward-cache-expected.txt: Added.
+        * fast/files/file-reader-back-forward-cache.html: Added.
+
 2019-10-19  Ryosuke Niwa  <rniwa@webkit.org>
 
         Integrate media query evaluation into HTML5 event loop
diff --git a/LayoutTests/TestExpectations b/LayoutTests/TestExpectations
index d453a9a..33bba44 100644
--- a/LayoutTests/TestExpectations
+++ b/LayoutTests/TestExpectations
@@ -266,6 +266,7 @@
 imported/w3c/web-platform-tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken.html [ DumpJSConsoleLogInStdErr Failure Pass ]
 imported/w3c/web-platform-tests/html/browsers/history/the-location-interface/location-protocol-setter-non-broken-weird.html [ DumpJSConsoleLogInStdErr ]
 imported/w3c/web-platform-tests/service-workers/service-worker/fetch-cors-xhr.https.html [ DumpJSConsoleLogInStdErr ]
+fast/files/file-reader-back-forward-cache.html [ DumpJSConsoleLogInStdErr ]
 fast/history/page-cache-createImageBitmap.html [ DumpJSConsoleLogInStdErr ]
 
 webkit.org/b/202495 imported/w3c/web-platform-tests/shadow-dom/directionality-002.tentative.html [ ImageOnlyFailure ]
diff --git a/LayoutTests/fast/files/file-reader-back-forward-cache-expected.txt b/LayoutTests/fast/files/file-reader-back-forward-cache-expected.txt
new file mode 100644
index 0000000..006b31f
--- /dev/null
+++ b/LayoutTests/fast/files/file-reader-back-forward-cache-expected.txt
@@ -0,0 +1,15 @@
+Tests that an active FileReader that not prevent entering the back/forward cache.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+pageshow - not from cache
+pagehide - entering cache
+pageshow - from cache
+PASS Page did enter and was restored from the page cache
+PASS Received load event after restoring from the cache
+PASS Received loadend event
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/files/file-reader-back-forward-cache.html b/LayoutTests/fast/files/file-reader-back-forward-cache.html
new file mode 100644
index 0000000..66682ab
--- /dev/null
+++ b/LayoutTests/fast/files/file-reader-back-forward-cache.html
@@ -0,0 +1,72 @@
+<!-- webkit-test-runner [ enableBackForwardCache=true ] -->
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test.js"></script>
+
+<script>
+let fileReader;
+const testString = 'x'.repeat(10 * 1024);
+let didRestoreFromBackForwardCache = false;
+let successfulyLoaded = false;
+
+window.addEventListener("pageshow", function(event) {
+    debug("pageshow - " + (event.persisted ? "" : "not ") + "from cache");
+
+    if (event.persisted) {
+        testPassed("Page did enter and was restored from the page cache");
+        didRestoreFromBackForwardCache = true;
+    }
+}, false);
+
+window.addEventListener("pagehide", function(event) {
+    debug("pagehide - " + (event.persisted ? "" : "not ") + "entering cache");
+    if (!event.persisted) {
+        testFailed("Page did not enter the page cache.");
+        finishJSTest();
+    }
+}, false);
+
+function onLoad()
+{
+    if (!didRestoreFromBackForwardCache)
+        return;
+
+    testPassed("Received load event after restoring from the cache");
+    successfulyLoaded = true;
+}
+
+function onLoadEnd()
+{
+    if (didRestoreFromBackForwardCache && successfulyLoaded) {
+        testPassed("Received loadend event");
+        finishJSTest();
+        return;
+    }
+
+    setTimeout(() => {
+        fileReader.readAsText(new Blob([testString]));
+    }, 0);
+}
+
+function runTest() {
+    description("Tests that an active FileReader that not prevent entering the back/forward cache.");
+    window.jsTestIsAsync = true;
+
+    setTimeout(() => {
+        fileReader = new FileReader();
+        fileReader.addEventListener("load", onLoad, false);
+        fileReader.addEventListener("loadend", onLoadEnd, false);
+
+        fileReader.readAsText(new Blob([testString]));
+
+        // Force a back navigation back to this page.
+        window.location.href = "../history/resources/page-cache-helper.html";
+    }, 0);
+}
+
+onload = runTest;
+
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 8730ea3..0ad2152 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,33 @@
+2019-10-19  Chris Dumez  <cdumez@apple.com>
+
+        FileReader should not prevent entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=203106
+
+        Reviewed by Geoffrey Garen.
+
+        FileReader should not prevent entering the back/forward cache. To support this,
+        its implementation now uses a SuspendableTaskQueue to dispatch events.
+
+        Test: fast/files/file-reader-back-forward-cache.html
+
+        * dom/ActiveDOMObject.cpp:
+        (WebCore::ActiveDOMObject::isAllowedToRunScript const):
+        * dom/ActiveDOMObject.h:
+        * dom/ScriptExecutionContext.cpp:
+        (WebCore::ScriptExecutionContext::resumeActiveDOMObjects):
+        * fileapi/FileReader.cpp:
+        (WebCore::FileReader::FileReader):
+        (WebCore::FileReader::stop):
+        (WebCore::FileReader::hasPendingActivity const):
+        (WebCore::FileReader::readInternal):
+        (WebCore::FileReader::abort):
+        (WebCore::FileReader::didStartLoading):
+        (WebCore::FileReader::didReceiveData):
+        (WebCore::FileReader::didFinishLoading):
+        (WebCore::FileReader::didFail):
+        (WebCore::FileReader::fireEvent):
+        * fileapi/FileReader.h:
+
 2019-10-19  Adrian Perez de Castro  <aperez@igalia.com>
 
         [GTK][WPE] Fix non-unified builds after r250857
diff --git a/Source/WebCore/dom/ActiveDOMObject.cpp b/Source/WebCore/dom/ActiveDOMObject.cpp
index 249cdbf..6347ea2 100644
--- a/Source/WebCore/dom/ActiveDOMObject.cpp
+++ b/Source/WebCore/dom/ActiveDOMObject.cpp
@@ -125,4 +125,9 @@
     return !scriptExecutionContext() || scriptExecutionContext()->activeDOMObjectsAreStopped();
 }
 
+bool ActiveDOMObject::isAllowedToRunScript() const
+{
+    return scriptExecutionContext() && !scriptExecutionContext()->activeDOMObjectsAreStopped() && !scriptExecutionContext()->activeDOMObjectsAreSuspended();
+}
+
 } // namespace WebCore
diff --git a/Source/WebCore/dom/ActiveDOMObject.h b/Source/WebCore/dom/ActiveDOMObject.h
index 5aa5954..76b5de6 100644
--- a/Source/WebCore/dom/ActiveDOMObject.h
+++ b/Source/WebCore/dom/ActiveDOMObject.h
@@ -109,6 +109,7 @@
     }
 
     bool isContextStopped() const;
+    bool isAllowedToRunScript() const;
 
 protected:
     explicit ActiveDOMObject(ScriptExecutionContext*);
diff --git a/Source/WebCore/dom/ScriptExecutionContext.cpp b/Source/WebCore/dom/ScriptExecutionContext.cpp
index ca5c5a2..b6c5de1 100644
--- a/Source/WebCore/dom/ScriptExecutionContext.cpp
+++ b/Source/WebCore/dom/ScriptExecutionContext.cpp
@@ -301,13 +301,14 @@
 
     if (m_reasonForSuspendingActiveDOMObjects != why)
         return;
-    m_activeDOMObjectsAreSuspended = false;
 
     forEachActiveDOMObject([](auto& activeDOMObject) {
         activeDOMObject.resume();
         return ShouldContinue::Yes;
     });
 
+    m_activeDOMObjectsAreSuspended = false;
+
     // In case there were pending messages at the time the script execution context entered the BackForwardCache,
     // make sure those get dispatched shortly after restoring from the BackForwardCache.
     processMessageWithMessagePortsSoon();
diff --git a/Source/WebCore/fileapi/FileReader.cpp b/Source/WebCore/fileapi/FileReader.cpp
index 62438cd..8355949 100644
--- a/Source/WebCore/fileapi/FileReader.cpp
+++ b/Source/WebCore/fileapi/FileReader.cpp
@@ -36,6 +36,7 @@
 #include "Logging.h"
 #include "ProgressEvent.h"
 #include "ScriptExecutionContext.h"
+#include "SuspendableTaskQueue.h"
 #include <JavaScriptCore/ArrayBuffer.h>
 #include <wtf/IsoMallocInlines.h>
 #include <wtf/text/CString.h>
@@ -56,6 +57,7 @@
 
 FileReader::FileReader(ScriptExecutionContext& context)
     : ActiveDOMObject(&context)
+    , m_taskQueue(SuspendableTaskQueue::create(&context))
 {
 }
 
@@ -65,12 +67,6 @@
         m_loader->cancel();
 }
 
-// FIXME: This should never prevent entering the back/forward cache.
-bool FileReader::shouldPreventEnteringBackForwardCache_DEPRECATED() const
-{
-    return hasPendingActivity();
-}
-
 const char* FileReader::activeDOMObjectName() const
 {
     return "FileReader";
@@ -83,7 +79,11 @@
         m_loader = nullptr;
     }
     m_state = DONE;
-    m_loadingActivity = nullptr;
+}
+
+bool FileReader::hasPendingActivity() const
+{
+    return m_taskQueue->hasPendingTasks() || m_state == LOADING || ActiveDOMObject::hasPendingActivity();
 }
 
 ExceptionOr<void> FileReader::readAsArrayBuffer(Blob* blob)
@@ -133,8 +133,6 @@
     if (m_state == LOADING)
         return Exception { InvalidStateError };
 
-    m_loadingActivity = makePendingActivity(*this);
-
     m_blob = &blob;
     m_readType = type;
     m_state = LOADING;
@@ -157,10 +155,8 @@
     m_aborting = true;
 
     // Schedule to have the abort done later since abort() might be called from the event handler and we do not want the resource loading code to be in the stack.
-    scriptExecutionContext()->postTask([this, protectedThis = makeRef(*this)] (ScriptExecutionContext&) {
-        if (isContextStopped())
-            return;
-
+    m_taskQueue->cancelAllTasks();
+    m_taskQueue->enqueueTask([this] {
         ASSERT(m_state != DONE);
 
         stop();
@@ -176,20 +172,24 @@
 
 void FileReader::didStartLoading()
 {
-    fireEvent(eventNames().loadstartEvent);
+    m_taskQueue->enqueueTask([this] {
+        fireEvent(eventNames().loadstartEvent);
+    });
 }
 
 void FileReader::didReceiveData()
 {
-    auto now = MonotonicTime::now();
-    if (std::isnan(m_lastProgressNotificationTime)) {
-        m_lastProgressNotificationTime = now;
-        return;
-    }
-    if (now - m_lastProgressNotificationTime > progressNotificationInterval) {
-        fireEvent(eventNames().progressEvent);
-        m_lastProgressNotificationTime = now;
-    }
+    m_taskQueue->enqueueTask([this] {
+        auto now = MonotonicTime::now();
+        if (std::isnan(m_lastProgressNotificationTime)) {
+            m_lastProgressNotificationTime = now;
+            return;
+        }
+        if (now - m_lastProgressNotificationTime > progressNotificationInterval) {
+            fireEvent(eventNames().progressEvent);
+            m_lastProgressNotificationTime = now;
+        }
+    });
 }
 
 void FileReader::didFinishLoading()
@@ -197,14 +197,14 @@
     if (m_aborting)
         return;
 
-    ASSERT(m_state != DONE);
-    m_state = DONE;
+    m_taskQueue->enqueueTask([this] {
+        ASSERT(m_state != DONE);
+        m_state = DONE;
 
-    fireEvent(eventNames().progressEvent);
-    fireEvent(eventNames().loadEvent);
-    fireEvent(eventNames().loadendEvent);
-    
-    m_loadingActivity = nullptr;
+        fireEvent(eventNames().progressEvent);
+        fireEvent(eventNames().loadEvent);
+        fireEvent(eventNames().loadendEvent);
+    });
 }
 
 void FileReader::didFail(int errorCode)
@@ -213,18 +213,19 @@
     if (m_aborting)
         return;
 
-    ASSERT(m_state != DONE);
-    m_state = DONE;
+    m_taskQueue->enqueueTask([this, errorCode] {
+        ASSERT(m_state != DONE);
+        m_state = DONE;
 
-    m_error = FileError::create(static_cast<FileError::ErrorCode>(errorCode));
-    fireEvent(eventNames().errorEvent);
-    fireEvent(eventNames().loadendEvent);
-    
-    m_loadingActivity = nullptr;
+        m_error = FileError::create(static_cast<FileError::ErrorCode>(errorCode));
+        fireEvent(eventNames().errorEvent);
+        fireEvent(eventNames().loadendEvent);
+    });
 }
 
 void FileReader::fireEvent(const AtomString& type)
 {
+    RELEASE_ASSERT(isAllowedToRunScript());
     dispatchEvent(ProgressEvent::create(type, true, m_loader ? m_loader->bytesLoaded() : 0, m_loader ? m_loader->totalBytes() : 0));
 }
 
diff --git a/Source/WebCore/fileapi/FileReader.h b/Source/WebCore/fileapi/FileReader.h
index c0b7df7a..ebb8f5b 100644
--- a/Source/WebCore/fileapi/FileReader.h
+++ b/Source/WebCore/fileapi/FileReader.h
@@ -36,6 +36,7 @@
 #include "FileError.h"
 #include "FileReaderLoader.h"
 #include "FileReaderLoaderClient.h"
+#include <wtf/UniqueRef.h>
 
 namespace JSC {
 class ArrayBuffer;
@@ -44,6 +45,7 @@
 namespace WebCore {
 
 class Blob;
+class SuspendableTaskQueue;
 
 class FileReader final : public RefCounted<FileReader>, public ActiveDOMObject, public EventTargetWithInlineData, private FileReaderLoaderClient {
     WTF_MAKE_ISO_ALLOCATED(FileReader);
@@ -74,11 +76,14 @@
     using RefCounted::ref;
     using RefCounted::deref;
 
+    // ActiveDOMObject.
+    bool hasPendingActivity() const final;
+
 private:
     explicit FileReader(ScriptExecutionContext&);
 
+    // ActiveDOMObject.
     const char* activeDOMObjectName() const final;
-    bool shouldPreventEnteringBackForwardCache_DEPRECATED() const final;
     void stop() final;
 
     EventTargetInterface eventTargetInterface() const final { return FileReaderEventTargetInterfaceType; }
@@ -103,7 +108,7 @@
     std::unique_ptr<FileReaderLoader> m_loader;
     RefPtr<FileError> m_error;
     MonotonicTime m_lastProgressNotificationTime { MonotonicTime::nan() };
-    RefPtr<PendingActivity<FileReader>> m_loadingActivity;
+    UniqueRef<SuspendableTaskQueue> m_taskQueue;
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/history/BackForwardCache.cpp b/Source/WebCore/history/BackForwardCache.cpp
index 02acd95..310668a 100644
--- a/Source/WebCore/history/BackForwardCache.cpp
+++ b/Source/WebCore/history/BackForwardCache.cpp
@@ -57,7 +57,7 @@
 
 namespace WebCore {
 
-#define PCLOG(...) LOG(BackForwardCache, "%*s%s", indentLevel*4, "", makeString(__VA_ARGS__).utf8().data())
+#define PCLOG(...) WTFLogAlways("PageCache: %*s%s", indentLevel*4, "", makeString(__VA_ARGS__).utf8().data())
 
 static inline void logBackForwardCacheFailureDiagnosticMessage(DiagnosticLoggingClient& client, const String& reason)
 {