| /* |
| * Copyright (C) 2021 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "NetworkLoadScheduler.h" |
| |
| #include "Logging.h" |
| #include "NetworkLoad.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; |
| public: |
| HostContext() = default; |
| ~HostContext(); |
| |
| void schedule(NetworkLoad&); |
| void unschedule(NetworkLoad&); |
| void prioritize(NetworkLoad&); |
| |
| private: |
| void start(NetworkLoad&); |
| bool shouldDelayLowPriority() const { return m_activeLoads.size() >= maximumActiveCountForLowPriority; } |
| |
| HashSet<NetworkLoad*> m_activeLoads; |
| ListHashSet<NetworkLoad*> m_pendingLoads; |
| }; |
| |
| void NetworkLoadScheduler::HostContext::schedule(NetworkLoad& load) |
| { |
| auto startImmediately = [&] { |
| auto& request = load.currentRequest(); |
| if (request.priority() > WebCore::ResourceLoadPriority::Low) |
| return true; |
| |
| if (request.isConditional()) |
| return true; |
| |
| if (!shouldDelayLowPriority()) |
| return true; |
| |
| return false; |
| }(); |
| |
| if (startImmediately) { |
| start(load); |
| return; |
| } |
| |
| m_pendingLoads.add(&load); |
| } |
| |
| void NetworkLoadScheduler::HostContext::unschedule(NetworkLoad& load) |
| { |
| m_activeLoads.remove(&load); |
| m_pendingLoads.remove(&load); |
| |
| if (m_pendingLoads.isEmpty()) |
| return; |
| if (shouldDelayLowPriority()) |
| return; |
| |
| start(*m_pendingLoads.takeFirst()); |
| } |
| |
| void NetworkLoadScheduler::HostContext::prioritize(NetworkLoad& load) |
| { |
| auto priority = load.parameters().request.priority(); |
| load.reprioritizeRequest(++priority); |
| |
| if (!m_pendingLoads.remove(&load)) |
| return; |
| |
| start(load); |
| } |
| |
| void NetworkLoadScheduler::HostContext::start(NetworkLoad& load) |
| { |
| m_activeLoads.add(&load); |
| |
| load.start(); |
| } |
| |
| NetworkLoadScheduler::HostContext::~HostContext() |
| { |
| for (auto* load : m_pendingLoads) |
| start(*load); |
| } |
| |
| NetworkLoadScheduler::NetworkLoadScheduler() = default; |
| NetworkLoadScheduler::~NetworkLoadScheduler() = default; |
| |
| 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) { |
| load.start(); |
| return; |
| } |
| |
| context->schedule(load); |
| } |
| |
| void NetworkLoadScheduler::unscheduleLoad(NetworkLoad& load) |
| { |
| if (auto* context = contextForLoad(load)) |
| context->unschedule(load); |
| } |
| |
| // We add User-Agent to the preconnect key since it part of the HTTP connection cache key used for |
| // coalescing sockets in CFNetwork when using an HTTPS proxy (<rdar://problem/59434166>). |
| static std::tuple<String, String> mainResourceLoadKey(const String& protocolHostAndPort, const String& userAgent) |
| { |
| return std::make_tuple(protocolHostAndPort.isNull() ? emptyString() : protocolHostAndPort, userAgent.isNull() ? emptyString() : userAgent); |
| } |
| |
| void NetworkLoadScheduler::scheduleMainResourceLoad(NetworkLoad& load) |
| { |
| String protocolHostAndPort = load.url().protocolHostAndPort(); |
| if (!isOriginHTTP1X(protocolHostAndPort)) { |
| load.start(); |
| return; |
| } |
| |
| auto iter = m_pendingMainResourcePreconnects.find(mainResourceLoadKey(protocolHostAndPort, load.parameters().request.httpUserAgent())); |
| 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(mainResourceLoadKey(protocolHostAndPort, load.parameters().request.httpUserAgent())); |
| if (iter == m_pendingMainResourcePreconnects.end()) |
| return; |
| |
| PendingMainResourcePreconnectInfo& info = iter->value; |
| if (info.pendingLoads.remove(&load)) |
| maybePrunePreconnectInfo(iter); |
| } |
| |
| void NetworkLoadScheduler::startedPreconnectForMainResource(const URL& url, const String& userAgent) |
| { |
| auto key = mainResourceLoadKey(url.protocolHostAndPort(), userAgent); |
| auto iter = m_pendingMainResourcePreconnects.find(key); |
| if (iter != m_pendingMainResourcePreconnects.end()) { |
| PendingMainResourcePreconnectInfo& info = iter->value; |
| info.pendingPreconnects++; |
| return; |
| } |
| |
| PendingMainResourcePreconnectInfo info; |
| m_pendingMainResourcePreconnects.add(key, WTFMove(info)); |
| } |
| |
| void NetworkLoadScheduler::finishedPreconnectForMainResource(const URL& url, const String& userAgent, const WebCore::ResourceError& error) |
| { |
| auto iter = m_pendingMainResourcePreconnects.find(mainResourceLoadKey(url.protocolHostAndPort(), userAgent)); |
| 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) { |
| case WebCore::LoadSchedulingMode::Prioritized: |
| m_pageContexts.ensure(pageIdentifier, [&] { |
| return makeUnique<PageContext>(); |
| }); |
| break; |
| case WebCore::LoadSchedulingMode::Direct: |
| m_pageContexts.remove(pageIdentifier); |
| break; |
| } |
| } |
| |
| void NetworkLoadScheduler::prioritizeLoads(const Vector<NetworkLoad*>& loads) |
| { |
| for (auto* load : loads) { |
| if (auto* context = contextForLoad(*load)) |
| context->prioritize(*load); |
| } |
| } |
| |
| void NetworkLoadScheduler::clearPageData(WebCore::PageIdentifier pageIdentifier) |
| { |
| m_pageContexts.remove(pageIdentifier); |
| } |
| |
| auto NetworkLoadScheduler::contextForLoad(const NetworkLoad& load) -> HostContext* |
| { |
| if (!load.url().protocolIsInHTTPFamily()) |
| return nullptr; |
| |
| auto* pageContext = m_pageContexts.get(load.parameters().webPageID); |
| if (!pageContext) |
| return nullptr; |
| |
| auto host = load.url().host().toString(); |
| return pageContext->ensure(host, [&] { |
| return makeUnique<HostContext>(); |
| }).iterator->value.get(); |
| } |
| |
| } |