Title: [282252] trunk/Source/WebKit
Revision
282252
Author
[email protected]
Date
2021-09-09 17:28:37 -0700 (Thu, 09 Sep 2021)

Log Message

Preconnected socket is sometimes not used for initial request
https://bugs.webkit.org/show_bug.cgi?id=229686

Reviewed by Alex Christensen.

In the previous release, we preconnected to the main resource origin early in page load from
UIProcess to save ~10-15 ms per page load (https://bugs.webkit.org/show_bug.cgi?id=204992).
The savings comes from the fact that there's some latency in initiating the request in
WebProcess and waiting for the nav delegate reply in UIProcess. We hide that latency by
having the eventual main resource request reuse the early preconnected socket.

However, this optimization doesn't work for connections to known HTTP/1.1 origins due to
CFNetwork's in-memory cache of known HTTP/1.1 origins. If an origin is in this cache, and
a preconnect is still in flight, then the network stack will go wide immediately and issue
the main resource request on a new socket rather than trying to wait for the preconnect to
finish and reuse the preconnected socket. Thus the preconneted socket just goes to waste.
This can also cause issues with single-threaded web servers that serve only one request at
a time, as the main resource request can get head-of-line blocked by the preconnect that
is wasted.

To work around this, this patch tracks a small number of known HTTP/1.1 origins in memory.
If there is a pending preconnect to an HTTP/1.1 origin and a main resource request to that
origin comes in, then we block the request from going out until the preconnect finishes.
This allows the main resource request to utilize the preconnected socket.

This is worth ~1.25%-2.5% on PLT5 depending on device.

* NetworkProcess/NetworkLoad.cpp:
(WebKit::NetworkLoad::didCompleteWithError):
* NetworkProcess/NetworkLoadScheduler.cpp:
(WebKit::NetworkLoadScheduler::schedule):
(WebKit::NetworkLoadScheduler::unschedule):
(WebKit::NetworkLoadScheduler::scheduleLoad):
(WebKit::NetworkLoadScheduler::unscheduleLoad):
(WebKit::NetworkLoadScheduler::scheduleMainResourceLoad):
(WebKit::NetworkLoadScheduler::unscheduleMainResourceLoad):
(WebKit::NetworkLoadScheduler::startedPreconnectForMainResource):
(WebKit::NetworkLoadScheduler::finishedPreconnectForMainResource):
(WebKit::NetworkLoadScheduler::maybePrunePreconnectInfo):
(WebKit::NetworkLoadScheduler::isOriginHTTP1X):
(WebKit::NetworkLoadScheduler::updateOriginProtocolInfo):
* NetworkProcess/NetworkLoadScheduler.h:
* NetworkProcess/NetworkProcess.cpp:
(WebKit::NetworkProcess::preconnectTo):
* NetworkProcess/PreconnectTask.cpp:
(WebKit::PreconnectTask::PreconnectTask):
(WebKit::PreconnectTask::setTimeout):
(WebKit::PreconnectTask::start):
* NetworkProcess/PreconnectTask.h:

Modified Paths

Diff

Modified: trunk/Source/WebKit/ChangeLog (282251 => 282252)


--- trunk/Source/WebKit/ChangeLog	2021-09-10 00:25:41 UTC (rev 282251)
+++ trunk/Source/WebKit/ChangeLog	2021-09-10 00:28:37 UTC (rev 282252)
@@ -1,3 +1,55 @@
+2021-09-09  Ben Nham  <[email protected]>
+
+        Preconnected socket is sometimes not used for initial request
+        https://bugs.webkit.org/show_bug.cgi?id=229686
+
+        Reviewed by Alex Christensen.
+
+        In the previous release, we preconnected to the main resource origin early in page load from
+        UIProcess to save ~10-15 ms per page load (https://bugs.webkit.org/show_bug.cgi?id=204992).
+        The savings comes from the fact that there's some latency in initiating the request in
+        WebProcess and waiting for the nav delegate reply in UIProcess. We hide that latency by
+        having the eventual main resource request reuse the early preconnected socket.
+
+        However, this optimization doesn't work for connections to known HTTP/1.1 origins due to
+        CFNetwork's in-memory cache of known HTTP/1.1 origins. If an origin is in this cache, and
+        a preconnect is still in flight, then the network stack will go wide immediately and issue
+        the main resource request on a new socket rather than trying to wait for the preconnect to
+        finish and reuse the preconnected socket. Thus the preconneted socket just goes to waste.
+        This can also cause issues with single-threaded web servers that serve only one request at
+        a time, as the main resource request can get head-of-line blocked by the preconnect that
+        is wasted.
+
+        To work around this, this patch tracks a small number of known HTTP/1.1 origins in memory.
+        If there is a pending preconnect to an HTTP/1.1 origin and a main resource request to that
+        origin comes in, then we block the request from going out until the preconnect finishes.
+        This allows the main resource request to utilize the preconnected socket.
+
+        This is worth ~1.25%-2.5% on PLT5 depending on device.
+
+        * NetworkProcess/NetworkLoad.cpp:
+        (WebKit::NetworkLoad::didCompleteWithError):
+        * NetworkProcess/NetworkLoadScheduler.cpp:
+        (WebKit::NetworkLoadScheduler::schedule):
+        (WebKit::NetworkLoadScheduler::unschedule):
+        (WebKit::NetworkLoadScheduler::scheduleLoad):
+        (WebKit::NetworkLoadScheduler::unscheduleLoad):
+        (WebKit::NetworkLoadScheduler::scheduleMainResourceLoad):
+        (WebKit::NetworkLoadScheduler::unscheduleMainResourceLoad):
+        (WebKit::NetworkLoadScheduler::startedPreconnectForMainResource):
+        (WebKit::NetworkLoadScheduler::finishedPreconnectForMainResource):
+        (WebKit::NetworkLoadScheduler::maybePrunePreconnectInfo):
+        (WebKit::NetworkLoadScheduler::isOriginHTTP1X):
+        (WebKit::NetworkLoadScheduler::updateOriginProtocolInfo):
+        * NetworkProcess/NetworkLoadScheduler.h:
+        * NetworkProcess/NetworkProcess.cpp:
+        (WebKit::NetworkProcess::preconnectTo):
+        * NetworkProcess/PreconnectTask.cpp:
+        (WebKit::PreconnectTask::PreconnectTask):
+        (WebKit::PreconnectTask::setTimeout):
+        (WebKit::PreconnectTask::start):
+        * NetworkProcess/PreconnectTask.h:
+
 2021-09-09  Wenson Hsieh  <[email protected]>
 
         [Mac Catalyst] Korean characters do not combine correctly in editable fields in WKWebView

Modified: trunk/Source/WebKit/NetworkProcess/NetworkLoad.cpp (282251 => 282252)


--- trunk/Source/WebKit/NetworkProcess/NetworkLoad.cpp	2021-09-10 00:25:41 UTC (rev 282251)
+++ trunk/Source/WebKit/NetworkProcess/NetworkLoad.cpp	2021-09-10 00:28:37 UTC (rev 282252)
@@ -245,7 +245,7 @@
 void NetworkLoad::didCompleteWithError(const ResourceError& error, const WebCore::NetworkLoadMetrics& networkLoadMetrics)
 {
     if (m_scheduler) {
-        m_scheduler->unschedule(*this);
+        m_scheduler->unschedule(*this, &networkLoadMetrics);
         m_scheduler = nullptr;
     }
 

Modified: trunk/Source/WebKit/NetworkProcess/NetworkLoadScheduler.cpp (282251 => 282252)


--- trunk/Source/WebKit/NetworkProcess/NetworkLoadScheduler.cpp	2021-09-10 00:25:41 UTC (rev 282251)
+++ trunk/Source/WebKit/NetworkProcess/NetworkLoadScheduler.cpp	2021-09-10 00:28:37 UTC (rev 282252)
@@ -26,12 +26,14 @@
 #include "config.h"
 #include "NetworkLoadScheduler.h"
 
+#include "Logging.h"
 #include "NetworkLoad.h"
-#include <wtf/ListHashSet.h>
+#include <WebCore/ResourceError.h>
 
 namespace WebKit {
 
 static constexpr size_t maximumActiveCountForLowPriority = 2;
+static constexpr size_t maximumTrackedHTTP1XOrigins = 128;
 
 class NetworkLoadScheduler::HostContext {
     WTF_MAKE_FAST_ALLOCATED;
@@ -117,6 +119,24 @@
 
 void NetworkLoadScheduler::schedule(NetworkLoad& load)
 {
+    bool isMainFrameMainResource = load.currentRequest().isTopSite();
+    if (isMainFrameMainResource)
+        scheduleMainResourceLoad(load);
+    else
+        scheduleLoad(load);
+}
+
+void NetworkLoadScheduler::unschedule(NetworkLoad& load, const WebCore::NetworkLoadMetrics* metrics)
+{
+    bool isMainFrameMainResource = load.currentRequest().isTopSite();
+    if (isMainFrameMainResource)
+        unscheduleMainResourceLoad(load, metrics);
+    else
+        unscheduleLoad(load);
+}
+
+void NetworkLoadScheduler::scheduleLoad(NetworkLoad& load)
+{
     auto* context = contextForLoad(load);
 
     if (!context) {
@@ -127,12 +147,109 @@
     context->schedule(load);
 }
 
-void NetworkLoadScheduler::unschedule(NetworkLoad& load)
+void NetworkLoadScheduler::unscheduleLoad(NetworkLoad& load)
 {
     if (auto* context = contextForLoad(load))
         context->unschedule(load);
 }
 
+void NetworkLoadScheduler::scheduleMainResourceLoad(NetworkLoad& load)
+{
+    String protocolHostAndPort = load.url().protocolHostAndPort();
+    if (!isOriginHTTP1X(protocolHostAndPort)) {
+        load.start();
+        return;
+    }
+
+    auto iter = m_pendingMainResourcePreconnects.find(protocolHostAndPort);
+    if (iter == m_pendingMainResourcePreconnects.end()) {
+        load.start();
+        return;
+    }
+
+    PendingMainResourcePreconnectInfo& info = iter->value;
+    if (!info.pendingPreconnects) {
+        load.start();
+        return;
+    }
+
+    --info.pendingPreconnects;
+    info.pendingLoads.add(&load);
+    RELEASE_LOG(Network, "%p - NetworkLoadScheduler::scheduleMainResourceLoad deferring load %p; %u pending preconnects; %u pending loads", this, &load, info.pendingPreconnects, info.pendingLoads.size());
+}
+
+void NetworkLoadScheduler::unscheduleMainResourceLoad(NetworkLoad& load, const WebCore::NetworkLoadMetrics* metrics)
+{
+    String protocolHostAndPort = load.url().protocolHostAndPort();
+
+    if (metrics)
+        updateOriginProtocolInfo(protocolHostAndPort, metrics->protocol);
+
+    auto iter = m_pendingMainResourcePreconnects.find(protocolHostAndPort);
+    if (iter == m_pendingMainResourcePreconnects.end())
+        return;
+
+    PendingMainResourcePreconnectInfo& info = iter->value;
+    if (info.pendingLoads.remove(&load))
+        maybePrunePreconnectInfo(iter);
+}
+
+void NetworkLoadScheduler::startedPreconnectForMainResource(const URL& url)
+{
+    auto iter = m_pendingMainResourcePreconnects.find(url.protocolHostAndPort());
+    if (iter != m_pendingMainResourcePreconnects.end()) {
+        PendingMainResourcePreconnectInfo& info = iter->value;
+        info.pendingPreconnects++;
+        return;
+    }
+
+    PendingMainResourcePreconnectInfo info;
+    m_pendingMainResourcePreconnects.add(url.protocolHostAndPort(), WTFMove(info));
+}
+
+void NetworkLoadScheduler::finishedPreconnectForMainResource(const URL& url, const WebCore::ResourceError& error)
+{
+    auto iter = m_pendingMainResourcePreconnects.find(url.protocolHostAndPort());
+    if (iter == m_pendingMainResourcePreconnects.end())
+        return;
+
+    PendingMainResourcePreconnectInfo& info = iter->value;
+    if (!info.pendingLoads.isEmpty()) {
+        NetworkLoad* load = info.pendingLoads.takeFirst();
+        RELEASE_LOG(Network, "%p - NetworkLoadScheduler::finishedPreconnectForMainResource (error: %d) starting delayed main resource load %p; %u pending preconnects; %u total pending loads", this, static_cast<int>(error.type()), load, info.pendingPreconnects, info.pendingLoads.size());
+        load->start();
+    } else
+        --info.pendingPreconnects;
+
+    maybePrunePreconnectInfo(iter);
+}
+
+void NetworkLoadScheduler::maybePrunePreconnectInfo(PendingPreconnectMap::iterator& iter)
+{
+    PendingMainResourcePreconnectInfo& info = iter->value;
+    if (!info.pendingPreconnects && info.pendingLoads.isEmpty())
+        m_pendingMainResourcePreconnects.remove(iter);
+}
+
+
+bool NetworkLoadScheduler::isOriginHTTP1X(const String& protocolHostAndPort)
+{
+    return m_http1XOrigins.contains(protocolHostAndPort);
+}
+
+void NetworkLoadScheduler::updateOriginProtocolInfo(const String& protocolHostAndPort, const String& alpnProtocolID)
+{
+    if (alpnProtocolID != "http/1.1"_s) {
+        m_http1XOrigins.remove(protocolHostAndPort);
+        return;
+    }
+
+    if (m_http1XOrigins.size() >= maximumTrackedHTTP1XOrigins)
+        m_http1XOrigins.remove(m_http1XOrigins.random());
+
+    m_http1XOrigins.add(protocolHostAndPort);
+}
+
 void NetworkLoadScheduler::setResourceLoadSchedulingMode(WebCore::PageIdentifier pageIdentifier, WebCore::LoadSchedulingMode mode)
 {
     switch (mode) {

Modified: trunk/Source/WebKit/NetworkProcess/NetworkLoadScheduler.h (282251 => 282252)


--- trunk/Source/WebKit/NetworkProcess/NetworkLoadScheduler.h	2021-09-10 00:25:41 UTC (rev 282251)
+++ trunk/Source/WebKit/NetworkProcess/NetworkLoadScheduler.h	2021-09-10 00:28:37 UTC (rev 282252)
@@ -26,10 +26,17 @@
 #pragma once
 
 #include <WebCore/LoadSchedulingMode.h>
+#include <WebCore/NetworkLoadMetrics.h>
 #include <WebCore/PageIdentifier.h>
 #include <wtf/HashMap.h>
+#include <wtf/HashSet.h>
+#include <wtf/ListHashSet.h>
 #include <wtf/WeakPtr.h>
 
+namespace WebCore {
+class ResourceError;
+}
+
 namespace WebKit {
 
 class NetworkLoad;
@@ -41,18 +48,41 @@
     ~NetworkLoadScheduler();
 
     void schedule(NetworkLoad&);
-    void unschedule(NetworkLoad&);
+    void unschedule(NetworkLoad&, const WebCore::NetworkLoadMetrics* = nullptr);
 
+    void startedPreconnectForMainResource(const URL&);
+    void finishedPreconnectForMainResource(const URL&, const WebCore::ResourceError&);
+
     void setResourceLoadSchedulingMode(WebCore::PageIdentifier, WebCore::LoadSchedulingMode);
     void prioritizeLoads(const Vector<NetworkLoad*>&);
     void clearPageData(WebCore::PageIdentifier);
 
 private:
+    void scheduleLoad(NetworkLoad&);
+    void unscheduleLoad(NetworkLoad&);
+
+    void scheduleMainResourceLoad(NetworkLoad&);
+    void unscheduleMainResourceLoad(NetworkLoad&, const WebCore::NetworkLoadMetrics*);
+
+    bool isOriginHTTP1X(const String&);
+    void updateOriginProtocolInfo(const String&, const String&);
+
     class HostContext;
     HostContext* contextForLoad(const NetworkLoad&);
 
     using PageContext = HashMap<String, std::unique_ptr<HostContext>>;
     HashMap<WebCore::PageIdentifier, std::unique_ptr<PageContext>> m_pageContexts;
+
+    struct PendingMainResourcePreconnectInfo {
+        unsigned pendingPreconnects {1};
+        ListHashSet<NetworkLoad *> pendingLoads;
+    };
+    using PendingPreconnectMap = HashMap<String, PendingMainResourcePreconnectInfo>;
+    PendingPreconnectMap m_pendingMainResourcePreconnects;
+
+    void maybePrunePreconnectInfo(PendingPreconnectMap::iterator&);
+
+    HashSet<String> m_http1XOrigins;
 };
 
 }

Modified: trunk/Source/WebKit/NetworkProcess/NetworkProcess.cpp (282251 => 282252)


--- trunk/Source/WebKit/NetworkProcess/NetworkProcess.cpp	2021-09-10 00:25:41 UTC (rev 282251)
+++ trunk/Source/WebKit/NetworkProcess/NetworkProcess.cpp	2021-09-10 00:28:37 UTC (rev 282252)
@@ -1387,6 +1387,7 @@
     parameters.request = ResourceRequest { url };
     parameters.request.setIsAppInitiated(lastNavigationWasAppInitiated == LastNavigationWasAppInitiated::Yes);
     parameters.request.setFirstPartyForCookies(url);
+    parameters.request.setPriority(WebCore::ResourceLoadPriority::VeryHigh);
     parameters.webPageProxyID = webPageProxyID;
     parameters.webPageID = webPageID;
     parameters.isNavigatingToAppBoundDomain = isNavigatingToAppBoundDomain;
@@ -1398,7 +1399,12 @@
     parameters.storedCredentialsPolicy = storedCredentialsPolicy;
     parameters.shouldPreconnectOnly = PreconnectOnly::Yes;
 
-    (new PreconnectTask(*networkSession, WTFMove(parameters), [](const WebCore::ResourceError&) { }))->start();
+    networkSession->networkLoadScheduler().startedPreconnectForMainResource(url);
+    auto task = new PreconnectTask(*networkSession, WTFMove(parameters), [networkSession, url](const WebCore::ResourceError& error) {
+        networkSession->networkLoadScheduler().finishedPreconnectForMainResource(url, error);
+    });
+    task->setTimeout(10_s);
+    task->start();
 #else
     UNUSED_PARAM(url);
     UNUSED_PARAM(userAgent);

Modified: trunk/Source/WebKit/NetworkProcess/PreconnectTask.cpp (282251 => 282252)


--- trunk/Source/WebKit/NetworkProcess/PreconnectTask.cpp	2021-09-10 00:25:41 UTC (rev 282251)
+++ trunk/Source/WebKit/NetworkProcess/PreconnectTask.cpp	2021-09-10 00:28:37 UTC (rev 282252)
@@ -41,6 +41,7 @@
 
 PreconnectTask::PreconnectTask(NetworkSession& networkSession, NetworkLoadParameters&& parameters, CompletionHandler<void(const ResourceError&)>&& completionHandler)
     : m_completionHandler(WTFMove(completionHandler))
+    , m_timeout(60_s)
     , m_timeoutTimer([this] { didFinish(ResourceError { String(), 0, m_networkLoad->parameters().request.url(), "Preconnection timed out"_s, ResourceError::Type::Timeout }); })
 {
     RELEASE_LOG(Network, "%p - PreconnectTask::PreconnectTask()", this);
@@ -47,7 +48,6 @@
 
     ASSERT(parameters.shouldPreconnectOnly == PreconnectOnly::Yes);
     m_networkLoad = makeUnique<NetworkLoad>(*this, nullptr, WTFMove(parameters), networkSession);
-    m_timeoutTimer.startOneShot(60000_s);
 }
 
 void PreconnectTask::setH2PingCallback(const URL& url, CompletionHandler<void(Expected<WTF::Seconds, WebCore::ResourceError>&&)>&& completionHandler)
@@ -55,8 +55,14 @@
     m_networkLoad->setH2PingCallback(url, WTFMove(completionHandler));
 }
 
+void PreconnectTask::setTimeout(Seconds timeout)
+{
+    m_timeout = timeout;
+}
+
 void PreconnectTask::start()
 {
+    m_timeoutTimer.startOneShot(m_timeout);
     m_networkLoad->start();
 }
 

Modified: trunk/Source/WebKit/NetworkProcess/PreconnectTask.h (282251 => 282252)


--- trunk/Source/WebKit/NetworkProcess/PreconnectTask.h	2021-09-10 00:25:41 UTC (rev 282251)
+++ trunk/Source/WebKit/NetworkProcess/PreconnectTask.h	2021-09-10 00:28:37 UTC (rev 282252)
@@ -45,6 +45,7 @@
     ~PreconnectTask();
 
     void setH2PingCallback(const URL&, CompletionHandler<void(Expected<WTF::Seconds, WebCore::ResourceError>&&)>&&);
+    void setTimeout(Seconds);
     void start();
 
 private:
@@ -62,6 +63,7 @@
 
     std::unique_ptr<NetworkLoad> m_networkLoad;
     CompletionHandler<void(const WebCore::ResourceError&)> m_completionHandler;
+    Seconds m_timeout;
     WebCore::Timer m_timeoutTimer;
 };
 
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to