Title: [250761] trunk
Revision
250761
Author
cdu...@apple.com
Date
2019-10-04 18:38:21 -0700 (Fri, 04 Oct 2019)

Log Message

Allow pages using EventSource to enter the back/forward cache
https://bugs.webkit.org/show_bug.cgi?id=202370
<rdar://problem/55853142>

Reviewed by Geoffrey Garen.

Source/WebCore:

Allow pages using EventSource to enter the back/forward cache. If the EventSource is
connecting at the time it enters PageCache, it will resume connecting after restoring
from PageCache (by making m_connectTimer a SuspendableTimer, which is PageCache-aware).
If the EventSource is was open upon navigating, it gets closed upon navigating (since
all pending loads get cancelled on navigation). To help the page recover, we fire an
error event and attempt to reconnect automatically when restoring the page from page
cache. It is allowed by the specification to attempt reconnection in case of non-fatal
network errors.

Tests: http/tests/eventsource/eventsource-page-cache-connected.html
       http/tests/eventsource/eventsource-page-cache-connecting.html

* page/EventSource.cpp:
(WebCore::EventSource::EventSource):
(WebCore::EventSource::scheduleReconnect):
(WebCore::EventSource::close):
(WebCore::EventSource::didReceiveResponse):
(WebCore::EventSource::dispatchErrorEvent):
(WebCore::EventSource::didReceiveData):
(WebCore::EventSource::didFinishLoading):
(WebCore::EventSource::didFail):
(WebCore::EventSource::abortConnectionAttempt):
(WebCore::EventSource::doExplicitLoadCancellation):
(WebCore::EventSource::canSuspendForDocumentSuspension const):
(WebCore::EventSource::suspend):
(WebCore::EventSource::resume):
(WebCore::EventSource::dispatchMessageEvent):
* page/EventSource.h:

LayoutTests:

Add layout test coverage.

* http/tests/eventsource/eventsource-page-cache-connected-expected.txt: Added.
* http/tests/eventsource/eventsource-page-cache-connected.html: Added.
* http/tests/eventsource/eventsource-page-cache-connecting-expected.txt: Added.
* http/tests/eventsource/eventsource-page-cache-connecting.html: Added.
* http/tests/eventsource/resources/infinite-event-stream.php: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (250760 => 250761)


--- trunk/LayoutTests/ChangeLog	2019-10-05 01:16:58 UTC (rev 250760)
+++ trunk/LayoutTests/ChangeLog	2019-10-05 01:38:21 UTC (rev 250761)
@@ -1,5 +1,21 @@
 2019-10-04  Chris Dumez  <cdu...@apple.com>
 
+        Allow pages using EventSource to enter the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=202370
+        <rdar://problem/55853142>
+
+        Reviewed by Geoffrey Garen.
+
+        Add layout test coverage.
+
+        * http/tests/eventsource/eventsource-page-cache-connected-expected.txt: Added.
+        * http/tests/eventsource/eventsource-page-cache-connected.html: Added.
+        * http/tests/eventsource/eventsource-page-cache-connecting-expected.txt: Added.
+        * http/tests/eventsource/eventsource-page-cache-connecting.html: Added.
+        * http/tests/eventsource/resources/infinite-event-stream.php: Added.
+
+2019-10-04  Chris Dumez  <cdu...@apple.com>
+
         ServiceWorkerContainer should never prevent a page from entering the back/forward cache
         https://bugs.webkit.org/show_bug.cgi?id=202603
 

Added: trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected-expected.txt (0 => 250761)


--- trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected-expected.txt	2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,22 @@
+Tests that a page with an EventSource that is connected can enter the PageCache.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+pageshow - not from cache
+PASS Received open event
+PASS Received message before entering page cache
+pagehide - entering cache
+pageshow - from cache
+PASS Page did enter and was restored from the page cache
+PASS Received error event
+PASS restoredFromPageCache is true
+PASS openEventCount is 1
+PASS Received open event
+PASS Received message after restoring from page cache
+PASS openEventCount is 2
+PASS errorEventCount is 1
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected.html (0 => 250761)


--- trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected.html	                        (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected.html	2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,72 @@
+<!-- webkit-test-runner [ enablePageCache=true ] -->
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+<script>
+description("Tests that a page with an EventSource that is connected can enter the PageCache.");
+jsTestIsAsync = true;
+
+let restoredFromPageCache = false;
+let openEventCount = 0;
+let errorEventCount = 0;
+let shouldNavigateOnMessage = true;
+
+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");
+        restoredFromPageCache = 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);
+
+_onload_ = () => {
+    setTimeout(() => {
+       eventSource = new EventSource("resources/infinite-event-stream.php");
+
+        eventSource._onerror_ = () => {
+            testPassed("Received error event");
+            errorEventCount++;
+            shouldBeTrue("restoredFromPageCache");
+            shouldBe("openEventCount", "1");
+        }
+
+        eventSource._onopen_ = () => {
+            testPassed("Received open event");
+            openEventCount++;
+        }
+
+        eventSource._onmessage_ = () => {
+            if (shouldNavigateOnMessage) {
+                shouldNavigateOnMessage = false;
+                testPassed("Received message before entering page cache");
+                setTimeout(() => {
+                    window.location = "/navigation/resources/page-cache-helper.html";
+                }, 0);
+                return;
+            }
+            if (!restoredFromPageCache)
+                return;
+
+            testPassed("Received message after restoring from page cache");
+            shouldBe("openEventCount", "2");
+            shouldBe("errorEventCount", "1");
+            finishJSTest();
+        }
+    }, 0);
+}
+
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting-expected.txt (0 => 250761)


--- trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting-expected.txt	2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,18 @@
+Tests that a page with an EventSource that is connecting can enter the PageCache.
+
+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 open event
+PASS restoredFromPageCache is true
+PASS Received message
+PASS restoredFromPageCache is true
+PASS receivedOpenEvent is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting.html (0 => 250761)


--- trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting.html	                        (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting.html	2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,59 @@
+<!-- webkit-test-runner [ enablePageCache=true ] -->
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+<script>
+description("Tests that a page with an EventSource that is connecting can enter the PageCache.");
+jsTestIsAsync = true;
+let restoredFromPageCache = false;
+let receivedOpenEvent = 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");
+        restoredFromPageCache = 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();
+    }
+
+    eventSource = new EventSource("resources/infinite-event-stream.php");
+
+    eventSource._onerror_ = () => {
+        testFailed("Received error event");
+        finishJSTest();
+    }
+
+    eventSource._onopen_ = () => {
+        testPassed("Received open event");
+        shouldBeTrue("restoredFromPageCache");
+        receivedOpenEvent  = true;
+    }
+
+    eventSource._onmessage_ = () => {
+        testPassed("Received message");
+        shouldBeTrue("restoredFromPageCache");
+        shouldBeTrue("receivedOpenEvent");
+        finishJSTest();
+    }
+}, false);
+
+_onload_ = () => {
+    setTimeout(() => {
+       window.location = "/navigation/resources/page-cache-helper.html"; 
+    }, 0);
+}
+
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/http/tests/eventsource/resources/infinite-event-stream.php (0 => 250761)


--- trunk/LayoutTests/http/tests/eventsource/resources/infinite-event-stream.php	                        (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/resources/infinite-event-stream.php	2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,27 @@
+<?php
+date_default_timezone_set("America/New_York");
+header("Content-Type: text/event-stream");
+$counter = rand(1, 10); // a random counter
+while (1) {
+// 1 is always true, so repeat the while loop forever (aka event-loop)
+  $curDate = date(DATE_ISO8601);
+  echo "event: ping\n",
+       'data: {"time": "' . $curDate . '"}', "\n\n";
+  // Send a simple message at random intervals.
+  $counter--;
+  if (!$counter) {
+    echo 'data: This is a message at time ' . $curDate, "\n\n";
+    $counter = rand(1, 10); // reset random counter
+  }
+  // flush the output buffer and send echoed messages to the browser
+  while (ob_get_level() > 0) {
+    ob_end_flush();
+  }
+  flush();
+  // break the loop if the client aborted the connection (closed the page)
+  
+  if ( connection_aborted() ) break;
+  // sleep for 1 second before running the loop again
+  
+  sleep(1);
+}

Modified: trunk/Source/WebCore/ChangeLog (250760 => 250761)


--- trunk/Source/WebCore/ChangeLog	2019-10-05 01:16:58 UTC (rev 250760)
+++ trunk/Source/WebCore/ChangeLog	2019-10-05 01:38:21 UTC (rev 250761)
@@ -1,5 +1,42 @@
 2019-10-04  Chris Dumez  <cdu...@apple.com>
 
+        Allow pages using EventSource to enter the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=202370
+        <rdar://problem/55853142>
+
+        Reviewed by Geoffrey Garen.
+
+        Allow pages using EventSource to enter the back/forward cache. If the EventSource is
+        connecting at the time it enters PageCache, it will resume connecting after restoring
+        from PageCache (by making m_connectTimer a SuspendableTimer, which is PageCache-aware).
+        If the EventSource is was open upon navigating, it gets closed upon navigating (since
+        all pending loads get cancelled on navigation). To help the page recover, we fire an
+        error event and attempt to reconnect automatically when restoring the page from page
+        cache. It is allowed by the specification to attempt reconnection in case of non-fatal
+        network errors.
+
+        Tests: http/tests/eventsource/eventsource-page-cache-connected.html
+               http/tests/eventsource/eventsource-page-cache-connecting.html
+
+        * page/EventSource.cpp:
+        (WebCore::EventSource::EventSource):
+        (WebCore::EventSource::scheduleReconnect):
+        (WebCore::EventSource::close):
+        (WebCore::EventSource::didReceiveResponse):
+        (WebCore::EventSource::dispatchErrorEvent):
+        (WebCore::EventSource::didReceiveData):
+        (WebCore::EventSource::didFinishLoading):
+        (WebCore::EventSource::didFail):
+        (WebCore::EventSource::abortConnectionAttempt):
+        (WebCore::EventSource::doExplicitLoadCancellation):
+        (WebCore::EventSource::canSuspendForDocumentSuspension const):
+        (WebCore::EventSource::suspend):
+        (WebCore::EventSource::resume):
+        (WebCore::EventSource::dispatchMessageEvent):
+        * page/EventSource.h:
+
+2019-10-04  Chris Dumez  <cdu...@apple.com>
+
         ServiceWorkerContainer should never prevent a page from entering the back/forward cache
         https://bugs.webkit.org/show_bug.cgi?id=202603
 

Modified: trunk/Source/WebCore/page/EventSource.cpp (250760 => 250761)


--- trunk/Source/WebCore/page/EventSource.cpp	2019-10-05 01:16:58 UTC (rev 250760)
+++ trunk/Source/WebCore/page/EventSource.cpp	2019-10-05 01:38:21 UTC (rev 250761)
@@ -45,6 +45,7 @@
 #include "TextResourceDecoder.h"
 #include "ThreadableLoader.h"
 #include <wtf/IsoMallocInlines.h>
+#include <wtf/SetForScope.h>
 
 namespace WebCore {
 
@@ -57,8 +58,9 @@
     , m_url(url)
     , m_withCredentials(eventSourceInit.withCredentials)
     , m_decoder(TextResourceDecoder::create("text/plain"_s, "UTF-8"))
-    , m_connectTimer(*this, &EventSource::connect)
+    , m_connectTimer(&context, *this, &EventSource::connect)
 {
+    m_connectTimer.suspendIfNeeded();
 }
 
 ExceptionOr<Ref<EventSource>> EventSource::create(ScriptExecutionContext& context, const String& url, const Init& eventSourceInit)
@@ -141,9 +143,10 @@
 
 void EventSource::scheduleReconnect()
 {
+    RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
     m_state = CONNECTING;
     m_connectTimer.startOneShot(1_ms * m_reconnectDelay);
-    dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
+    dispatchErrorEvent();
 }
 
 void EventSource::close()
@@ -155,10 +158,10 @@
 
     // Stop trying to connect/reconnect if EventSource was explicitly closed or if ActiveDOMObject::stop() was called.
     if (m_connectTimer.isActive())
-        m_connectTimer.stop();
+        m_connectTimer.cancel();
 
     if (m_requestInFlight)
-        m_loader->cancel();
+        doExplicitLoadCancellation();
     else {
         m_state = CLOSED;
         unsetPendingActivity(*this);
@@ -196,10 +199,11 @@
 {
     ASSERT(m_state == CONNECTING);
     ASSERT(m_requestInFlight);
+    RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
 
     if (!responseIsValid(response)) {
-        m_loader->cancel();
-        dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
+        doExplicitLoadCancellation();
+        dispatchErrorEvent();
         return;
     }
 
@@ -208,10 +212,16 @@
     dispatchEvent(Event::create(eventNames().openEvent, Event::CanBubble::No, Event::IsCancelable::No));
 }
 
+void EventSource::dispatchErrorEvent()
+{
+    dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
+}
+
 void EventSource::didReceiveData(const char* data, int length)
 {
     ASSERT(m_state == OPEN);
     ASSERT(m_requestInFlight);
+    RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
 
     append(m_receiveBuffer, m_decoder->decode(data, length));
     parseEventStream();
@@ -221,6 +231,7 @@
 {
     ASSERT(m_state == OPEN);
     ASSERT(m_requestInFlight);
+    RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
 
     append(m_receiveBuffer, m_decoder->flush());
     parseEventStream();
@@ -247,6 +258,14 @@
 
     ASSERT(m_requestInFlight);
 
+    // This is the case where the load gets cancelled on navigating away. We only fire an error event and attempt to reconnect
+    // if we end up getting resumed from page cache.
+    if (error.isCancellation() && !m_isDoingExplicitCancellation) {
+        m_shouldReconnectOnResume = true;
+        m_requestInFlight = false;
+        return;
+    }
+
     if (error.isCancellation())
         m_state = CLOSED;
 
@@ -258,9 +277,10 @@
 void EventSource::abortConnectionAttempt()
 {
     ASSERT(m_state == CONNECTING);
+    RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
 
     if (m_requestInFlight)
-        m_loader->cancel();
+        doExplicitLoadCancellation();
     else {
         m_state = CLOSED;
         unsetPendingActivity(*this);
@@ -270,6 +290,13 @@
     dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
 }
 
+void EventSource::doExplicitLoadCancellation()
+{
+    ASSERT(m_requestInFlight);
+    SetForScope<bool> explicitLoadCancellation(m_isDoingExplicitCancellation, true);
+    m_loader->cancel();
+}
+
 void EventSource::parseEventStream()
 {
     unsigned position = 0;
@@ -378,12 +405,36 @@
 
 bool EventSource::canSuspendForDocumentSuspension() const
 {
-    // FIXME: We should return true here when we can because this object is not actually currently active.
-    return false;
+    return true;
 }
 
+void EventSource::suspend(ReasonForSuspension reason)
+{
+    if (reason != ReasonForSuspension::PageCache)
+        return;
+
+    m_isSuspendedForPageCache = true;
+    RELEASE_ASSERT_WITH_MESSAGE(!m_requestInFlight, "Loads get cancelled before entering PageCache.");
+}
+
+void EventSource::resume()
+{
+    if (!m_isSuspendedForPageCache)
+        return;
+
+    m_isSuspendedForPageCache = false;
+    if (std::exchange(m_shouldReconnectOnResume, false)) {
+        scriptExecutionContext()->postTask([this, pendingActivity = makePendingActivity(*this)](ScriptExecutionContext&) {
+            if (!isContextStopped())
+                scheduleReconnect();
+        });
+    }
+}
+
 void EventSource::dispatchMessageEvent()
 {
+    RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
+
     if (!m_currentlyParsedEventId.isNull())
         m_lastEventId = WTFMove(m_currentlyParsedEventId);
 

Modified: trunk/Source/WebCore/page/EventSource.h (250760 => 250761)


--- trunk/Source/WebCore/page/EventSource.h	2019-10-05 01:16:58 UTC (rev 250760)
+++ trunk/Source/WebCore/page/EventSource.h	2019-10-05 01:38:21 UTC (rev 250761)
@@ -34,9 +34,10 @@
 #include "ActiveDOMObject.h"
 #include "EventTarget.h"
 #include "ExceptionOr.h"
-#include <wtf/URL.h>
+#include "SuspendableTimer.h"
 #include "ThreadableLoaderClient.h"
 #include "Timer.h"
+#include <wtf/URL.h>
 #include <wtf/Vector.h>
 
 namespace WebCore {
@@ -78,6 +79,9 @@
     void refEventTarget() final { ref(); }
     void derefEventTarget() final { deref(); }
 
+    void dispatchErrorEvent();
+    void doExplicitLoadCancellation();
+
     // ThreadableLoaderClient
     void didReceiveResponse(unsigned long, const ResourceResponse&) final;
     void didReceiveData(const char*, int) final;
@@ -84,9 +88,12 @@
     void didFinishLoading(unsigned long) final;
     void didFail(const ResourceError&) final;
 
+    // ActiveDOMObject
     void stop() final;
     const char* activeDOMObjectName() const final;
     bool canSuspendForDocumentSuspension() const final;
+    void suspend(ReasonForSuspension) final;
+    void resume() final;
 
     void connect();
     void networkRequestEnded();
@@ -107,10 +114,13 @@
 
     Ref<TextResourceDecoder> m_decoder;
     RefPtr<ThreadableLoader> m_loader;
-    Timer m_connectTimer;
+    SuspendableTimer m_connectTimer;
     Vector<UChar> m_receiveBuffer;
     bool m_discardTrailingNewline { false };
     bool m_requestInFlight { false };
+    bool m_isSuspendedForPageCache { false };
+    bool m_isDoingExplicitCancellation { false };
+    bool m_shouldReconnectOnResume { false };
 
     AtomString m_eventName;
     Vector<UChar> m_data;
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to