summaryrefslogtreecommitdiffstats
path: root/uriloader/prefetch
diff options
context:
space:
mode:
Diffstat (limited to 'uriloader/prefetch')
-rw-r--r--uriloader/prefetch/moz.build26
-rw-r--r--uriloader/prefetch/nsIPrefetchService.idl54
-rw-r--r--uriloader/prefetch/nsPrefetchService.cpp895
-rw-r--r--uriloader/prefetch/nsPrefetchService.h130
4 files changed, 1105 insertions, 0 deletions
diff --git a/uriloader/prefetch/moz.build b/uriloader/prefetch/moz.build
new file mode 100644
index 0000000000..8185124da0
--- /dev/null
+++ b/uriloader/prefetch/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Networking: Cache")
+
+XPIDL_SOURCES += [
+ "nsIPrefetchService.idl",
+]
+
+XPIDL_MODULE = "prefetch"
+
+UNIFIED_SOURCES += [
+ "nsPrefetchService.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+]
diff --git a/uriloader/prefetch/nsIPrefetchService.idl b/uriloader/prefetch/nsIPrefetchService.idl
new file mode 100644
index 0000000000..160d051116
--- /dev/null
+++ b/uriloader/prefetch/nsIPrefetchService.idl
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIContentPolicy.idl"
+
+interface nsIURI;
+interface nsISimpleEnumerator;
+interface nsIReferrerInfo;
+
+webidl Node;
+
+[scriptable, uuid(422a1807-4e7f-463d-b8d7-ca2ceb9b5d53)]
+interface nsIPrefetchService : nsISupports
+{
+ /**
+ * Enqueue a request to prefetch the specified URI.
+ *
+ * @param aURI the URI of the document to prefetch
+ * @param aReferrerInfo the referrerInfo of the request
+ * @param aSource the DOM node (such as a <link> tag) that requested this
+ * fetch, or null if the prefetch was not requested by a DOM node.
+ * @param aExplicit the link element has an explicit prefetch link type
+ */
+ void prefetchURI(in nsIURI aURI,
+ in nsIReferrerInfo aReferrerInfo,
+ in Node aSource,
+ in boolean aExplicit);
+
+ /**
+ * Start a preload of the specified URI.
+ *
+ * @param aURI the URI of the document to preload
+ * @param aReferrerInfo the referrerInfo of the request
+ * @param aSource the DOM node (such as a <link> tag) that requested this
+ * fetch, or null if the prefetch was not requested by a DOM node.
+ * @param aPolicyType content policy to be used for this load.
+ */
+ void preloadURI(in nsIURI aURI,
+ in nsIReferrerInfo aReferrerInfo,
+ in Node aSource,
+ in nsContentPolicyType aPolicyType);
+
+ /**
+ * Find out if there are any prefetches running or queued
+ */
+ boolean hasMoreElements();
+
+ /**
+ * Cancel prefetch or preload for a Node.
+ */
+ void cancelPrefetchPreloadURI(in nsIURI aURI, in Node aSource);
+};
diff --git a/uriloader/prefetch/nsPrefetchService.cpp b/uriloader/prefetch/nsPrefetchService.cpp
new file mode 100644
index 0000000000..635e4e34a9
--- /dev/null
+++ b/uriloader/prefetch/nsPrefetchService.cpp
@@ -0,0 +1,895 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsPrefetchService.h"
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/CORSMode.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/ClientInfo.h"
+#include "mozilla/dom/HTMLLinkElement.h"
+#include "mozilla/dom/ServiceWorkerDescriptor.h"
+#include "mozilla/Preferences.h"
+#include "ReferrerInfo.h"
+
+#include "nsIObserverService.h"
+#include "nsIWebProgress.h"
+#include "nsICacheInfoChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIURL.h"
+#include "nsISupportsPriority.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsStreamUtils.h"
+#include "prtime.h"
+#include "mozilla/Logging.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsINode.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "nsICachingChannel.h"
+#include "nsHttp.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+//
+// To enable logging (see mozilla/Logging.h for full details):
+//
+// set MOZ_LOG=nsPrefetch:5
+// set MOZ_LOG_FILE=prefetch.log
+//
+// this enables LogLevel::Debug level information and places all output in
+// the file prefetch.log
+//
+static LazyLogModule gPrefetchLog("nsPrefetch");
+
+#undef LOG
+#define LOG(args) MOZ_LOG(gPrefetchLog, mozilla::LogLevel::Debug, args)
+
+#undef LOG_ENABLED
+#define LOG_ENABLED() MOZ_LOG_TEST(gPrefetchLog, mozilla::LogLevel::Debug)
+
+#define PREFETCH_PREF "network.prefetch-next"
+#define PARALLELISM_PREF "network.prefetch-next.parallelism"
+#define AGGRESSIVE_PREF "network.prefetch-next.aggressive"
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode <public>
+//-----------------------------------------------------------------------------
+
+nsPrefetchNode::nsPrefetchNode(nsPrefetchService* aService, nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo, nsINode* aSource,
+ nsContentPolicyType aPolicyType, bool aPreload)
+ : mURI(aURI),
+ mReferrerInfo(aReferrerInfo),
+ mPolicyType(aPolicyType),
+ mPreload(aPreload),
+ mService(aService),
+ mChannel(nullptr),
+ mBytesRead(0),
+ mShouldFireLoadEvent(false) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ mSources.AppendElement(source);
+}
+
+nsresult nsPrefetchNode::OpenChannel() {
+ if (mSources.IsEmpty()) {
+ // Don't attempt to prefetch if we don't have a source node
+ // (which should never happen).
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsINode> source;
+ while (!mSources.IsEmpty() &&
+ !(source = do_QueryReferent(mSources.ElementAt(0)))) {
+ // If source is null remove it.
+ // (which should never happen).
+ mSources.RemoveElementAt(0);
+ }
+
+ if (!source) {
+ // Don't attempt to prefetch if we don't have a source node
+ // (which should never happen).
+
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsILoadGroup> loadGroup = source->OwnerDoc()->GetDocumentLoadGroup();
+ CORSMode corsMode = CORS_NONE;
+ if (auto* link = dom::HTMLLinkElement::FromNode(source)) {
+ corsMode = link->GetCORSMode();
+ }
+
+ uint32_t securityFlags;
+ if (corsMode == CORS_NONE) {
+ securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
+ } else {
+ securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
+ if (corsMode == CORS_USE_CREDENTIALS) {
+ securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+ }
+ }
+ nsresult rv = NS_NewChannelInternal(
+ getter_AddRefs(mChannel), mURI, source, source->NodePrincipal(),
+ nullptr, // aTriggeringPrincipal
+ Maybe<ClientInfo>(), Maybe<ServiceWorkerDescriptor>(), securityFlags,
+ mPolicyType, source->OwnerDoc()->CookieJarSettings(),
+ nullptr, // aPerformanceStorage
+ loadGroup, // aLoadGroup
+ this, // aCallbacks
+ nsIRequest::LOAD_BACKGROUND | nsICachingChannel::LOAD_ONLY_IF_MODIFIED);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // configure HTTP specific stuff
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+ if (httpChannel) {
+ DebugOnly<nsresult> success = httpChannel->SetReferrerInfo(mReferrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(success));
+
+ // https://fetch.spec.whatwg.org/#http-sec-purpose
+ success =
+ httpChannel->SetRequestHeader("Sec-Purpose"_ns, "prefetch"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(success));
+ }
+
+ // Reduce the priority of prefetch network requests.
+ nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(mChannel);
+ if (priorityChannel) {
+ priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_LOWEST);
+ }
+
+ rv = mChannel->AsyncOpen(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // Drop the ref to the channel, because we don't want to end up with
+ // cycles through it.
+ mChannel = nullptr;
+ }
+ return rv;
+}
+
+nsresult nsPrefetchNode::CancelChannel(nsresult error) {
+ mChannel->Cancel(error);
+ mChannel = nullptr;
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsPrefetchNode, nsIRequestObserver, nsIStreamListener,
+ nsIInterfaceRequestor, nsIChannelEventSink,
+ nsIRedirectResultListener)
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsIStreamListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchNode::OnStartRequest(nsIRequest* aRequest) {
+ nsresult rv;
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // if the load is cross origin without CORS, or the CORS access is rejected,
+ // always fire load event to avoid leaking site information.
+ nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo();
+ mShouldFireLoadEvent =
+ loadInfo->GetTainting() == LoadTainting::Opaque ||
+ (loadInfo->GetTainting() == LoadTainting::CORS &&
+ (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv)));
+
+ // no need to prefetch http error page
+ bool requestSucceeded;
+ if (NS_FAILED(httpChannel->GetRequestSucceeded(&requestSucceeded)) ||
+ !requestSucceeded) {
+ return NS_BINDING_ABORTED;
+ }
+
+ nsCOMPtr<nsICacheInfoChannel> cacheInfoChannel =
+ do_QueryInterface(aRequest, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // no need to prefetch a document that is already in the cache
+ bool fromCache;
+ if (NS_SUCCEEDED(cacheInfoChannel->IsFromCache(&fromCache)) && fromCache) {
+ LOG(("document is already in the cache; canceling prefetch\n"));
+ // although it's canceled we still want to fire load event
+ mShouldFireLoadEvent = true;
+ return NS_BINDING_ABORTED;
+ }
+
+ //
+ // no need to prefetch a document that must be requested fresh each
+ // and every time.
+ //
+ uint32_t expTime;
+ if (NS_SUCCEEDED(cacheInfoChannel->GetCacheTokenExpirationTime(&expTime))) {
+ if (mozilla::net::NowInSeconds() >= expTime) {
+ LOG(
+ ("document cannot be reused from cache; "
+ "canceling prefetch\n"));
+ return NS_BINDING_ABORTED;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchNode::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ uint32_t bytesRead = 0;
+ aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead);
+ mBytesRead += bytesRead;
+ LOG(("prefetched %u bytes [offset=%" PRIu64 "]\n", bytesRead, aOffset));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchNode::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ LOG(("done prefetching [status=%" PRIx32 "]\n",
+ static_cast<uint32_t>(aStatus)));
+
+ if (mBytesRead == 0 && aStatus == NS_OK && mChannel) {
+ // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was
+ // specified), but the object should report loadedSize as if it
+ // did.
+ mChannel->GetContentLength(&mBytesRead);
+ }
+
+ mService->NotifyLoadCompleted(this);
+ mService->DispatchEvent(this, mShouldFireLoadEvent || NS_SUCCEEDED(aStatus));
+ mService->RemoveNodeAndMaybeStartNextPrefetchURI(this);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsIInterfaceRequestor
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchNode::GetInterface(const nsIID& aIID, void** aResult) {
+ if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
+ NS_ADDREF_THIS();
+ *aResult = static_cast<nsIChannelEventSink*>(this);
+ return NS_OK;
+ }
+
+ if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
+ NS_ADDREF_THIS();
+ *aResult = static_cast<nsIRedirectResultListener*>(this);
+ return NS_OK;
+ }
+
+ return NS_ERROR_NO_INTERFACE;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsIChannelEventSink
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchNode::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* callback) {
+ nsCOMPtr<nsIURI> newURI;
+ nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI));
+ if (NS_FAILED(rv)) return rv;
+
+ if (!newURI->SchemeIs("http") && !newURI->SchemeIs("https")) {
+ LOG(("rejected: URL is not of type http/https\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ // HTTP request headers are not automatically forwarded to the new channel.
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel);
+ NS_ENSURE_STATE(httpChannel);
+
+ // https://fetch.spec.whatwg.org/#http-sec-purpose
+ rv = httpChannel->SetRequestHeader("Sec-Purpose"_ns, "prefetch"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Assign to mChannel after we get notification about success of the
+ // redirect in OnRedirectResult.
+ mRedirectChannel = aNewChannel;
+
+ callback->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsIRedirectResultListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchNode::OnRedirectResult(nsresult status) {
+ if (NS_SUCCEEDED(status) && mRedirectChannel) mChannel = mRedirectChannel;
+
+ mRedirectChannel = nullptr;
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService <public>
+//-----------------------------------------------------------------------------
+
+nsPrefetchService::nsPrefetchService()
+ : mMaxParallelism(6),
+ mStopCount(0),
+ mHaveProcessed(false),
+ mPrefetchDisabled(true),
+ mAggressive(false) {}
+
+nsPrefetchService::~nsPrefetchService() {
+ Preferences::RemoveObserver(this, PREFETCH_PREF);
+ Preferences::RemoveObserver(this, PARALLELISM_PREF);
+ Preferences::RemoveObserver(this, AGGRESSIVE_PREF);
+ // cannot reach destructor if prefetch in progress (listener owns reference
+ // to this service)
+ EmptyPrefetchQueue();
+}
+
+nsresult nsPrefetchService::Init() {
+ nsresult rv;
+
+ // read prefs and hook up pref observer
+ mPrefetchDisabled = !Preferences::GetBool(PREFETCH_PREF, !mPrefetchDisabled);
+ Preferences::AddWeakObserver(this, PREFETCH_PREF);
+
+ mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism);
+ if (mMaxParallelism < 1) {
+ mMaxParallelism = 1;
+ }
+ Preferences::AddWeakObserver(this, PARALLELISM_PREF);
+
+ mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false);
+ Preferences::AddWeakObserver(this, AGGRESSIVE_PREF);
+
+ // Observe xpcom-shutdown event
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) return NS_ERROR_FAILURE;
+
+ rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mPrefetchDisabled) {
+ AddProgressListener();
+ }
+
+ return NS_OK;
+}
+
+void nsPrefetchService::RemoveNodeAndMaybeStartNextPrefetchURI(
+ nsPrefetchNode* aFinished) {
+ if (aFinished) {
+ mCurrentNodes.RemoveElement(aFinished);
+ }
+
+ if ((!mStopCount && mHaveProcessed) || mAggressive) {
+ ProcessNextPrefetchURI();
+ }
+}
+
+void nsPrefetchService::ProcessNextPrefetchURI() {
+ if (mCurrentNodes.Length() >= static_cast<uint32_t>(mMaxParallelism)) {
+ // We already have enough prefetches going on, so hold off
+ // for now.
+ return;
+ }
+
+ nsresult rv;
+
+ do {
+ if (mPrefetchQueue.empty()) {
+ break;
+ }
+ RefPtr<nsPrefetchNode> node = std::move(mPrefetchQueue.front());
+ mPrefetchQueue.pop_front();
+
+ if (LOG_ENABLED()) {
+ LOG(("ProcessNextPrefetchURI [%s]\n",
+ node->mURI->GetSpecOrDefault().get()));
+ }
+
+ //
+ // if opening the channel fails (e.g. security check returns an error),
+ // send an error event and then just skip to the next uri
+ //
+ rv = node->OpenChannel();
+ if (NS_SUCCEEDED(rv)) {
+ mCurrentNodes.AppendElement(node);
+ } else {
+ DispatchEvent(node, false);
+ }
+ } while (NS_FAILED(rv));
+}
+
+void nsPrefetchService::NotifyLoadRequested(nsPrefetchNode* node) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) return;
+
+ observerService->NotifyObservers(
+ static_cast<nsIStreamListener*>(node),
+ (node->mPreload) ? "preload-load-requested" : "prefetch-load-requested",
+ nullptr);
+}
+
+void nsPrefetchService::NotifyLoadCompleted(nsPrefetchNode* node) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) return;
+
+ observerService->NotifyObservers(
+ static_cast<nsIStreamListener*>(node),
+ (node->mPreload) ? "preload-load-completed" : "prefetch-load-completed",
+ nullptr);
+}
+
+void nsPrefetchService::DispatchEvent(nsPrefetchNode* node, bool aSuccess) {
+ for (uint32_t i = 0; i < node->mSources.Length(); i++) {
+ nsCOMPtr<nsINode> domNode = do_QueryReferent(node->mSources.ElementAt(i));
+ if (domNode && domNode->IsInComposedDoc()) {
+ // We don't dispatch synchronously since |node| might be in a DocGroup
+ // that we're not allowed to touch. (Our network request happens in the
+ // DocGroup of one of the mSources nodes--not necessarily this one).
+ RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher(
+ domNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo);
+ dispatcher->RequireNodeInDocument();
+ dispatcher->PostDOMEvent();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService <private>
+//-----------------------------------------------------------------------------
+
+void nsPrefetchService::AddProgressListener() {
+ // Register as an observer for the document loader
+ nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
+ if (progress)
+ progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT);
+}
+
+void nsPrefetchService::RemoveProgressListener() {
+ // Register as an observer for the document loader
+ nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
+ if (progress) progress->RemoveProgressListener(this);
+}
+
+nsresult nsPrefetchService::EnqueueURI(nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource,
+ nsPrefetchNode** aNode) {
+ RefPtr<nsPrefetchNode> node = new nsPrefetchNode(
+ this, aURI, aReferrerInfo, aSource, nsIContentPolicy::TYPE_OTHER, false);
+ mPrefetchQueue.push_back(node);
+ node.forget(aNode);
+ return NS_OK;
+}
+
+void nsPrefetchService::EmptyPrefetchQueue() {
+ while (!mPrefetchQueue.empty()) {
+ mPrefetchQueue.pop_back();
+ }
+}
+
+void nsPrefetchService::StartPrefetching() {
+ //
+ // at initialization time we might miss the first DOCUMENT START
+ // notification, so we have to be careful to avoid letting our
+ // stop count go negative.
+ //
+ if (mStopCount > 0) mStopCount--;
+
+ LOG(("StartPrefetching [stopcount=%d]\n", mStopCount));
+
+ // only start prefetching after we've received enough DOCUMENT
+ // STOP notifications. we do this inorder to defer prefetching
+ // until after all sub-frames have finished loading.
+ if (!mStopCount) {
+ mHaveProcessed = true;
+ while (!mPrefetchQueue.empty() &&
+ mCurrentNodes.Length() < static_cast<uint32_t>(mMaxParallelism)) {
+ ProcessNextPrefetchURI();
+ }
+ }
+}
+
+void nsPrefetchService::StopPrefetching() {
+ mStopCount++;
+
+ LOG(("StopPrefetching [stopcount=%d]\n", mStopCount));
+
+ // When we start a load, we need to stop all prefetches that has been
+ // added by the old load, therefore call StopAll only at the moment we
+ // switch to a new page load (i.e. mStopCount == 1).
+ // TODO: do not stop prefetches that are relevant for the new load.
+ if (mStopCount == 1) {
+ StopAll();
+ }
+}
+
+void nsPrefetchService::StopCurrentPrefetchsPreloads(bool aPreload) {
+ for (int32_t i = mCurrentNodes.Length() - 1; i >= 0; --i) {
+ if (mCurrentNodes[i]->mPreload == aPreload) {
+ mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED);
+ mCurrentNodes.RemoveElementAt(i);
+ }
+ }
+
+ if (!aPreload) {
+ EmptyPrefetchQueue();
+ }
+}
+
+void nsPrefetchService::StopAll() {
+ for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) {
+ mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED);
+ }
+ mCurrentNodes.Clear();
+ EmptyPrefetchQueue();
+}
+
+nsresult nsPrefetchService::CheckURIScheme(nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo) {
+ //
+ // XXX we should really be asking the protocol handler if it supports
+ // caching, so we can determine if there is any value to prefetching.
+ // for now, we'll only prefetch http and https links since we know that's
+ // the most common case.
+ //
+ if (!aURI->SchemeIs("http") && !aURI->SchemeIs("https")) {
+ LOG(("rejected: URL is not of type http/https\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ //
+ // the referrer URI must be http:
+ //
+ nsCOMPtr<nsIURI> referrer = aReferrerInfo->GetOriginalReferrer();
+ if (!referrer) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (!referrer->SchemeIs("http") && !referrer->SchemeIs("https")) {
+ LOG(("rejected: referrer URL is neither http nor https\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsPrefetchService, nsIPrefetchService, nsIWebProgressListener,
+ nsIObserver, nsISupportsWeakReference)
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService::nsIPrefetchService
+//-----------------------------------------------------------------------------
+
+nsresult nsPrefetchService::Preload(nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource,
+ nsContentPolicyType aPolicyType) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(aReferrerInfo);
+ if (LOG_ENABLED()) {
+ LOG(("PreloadURI [%s]\n", aURI->GetSpecOrDefault().get()));
+ }
+
+ LOG(("rejected: preload service is deprecated\n"));
+ return NS_ERROR_ABORT;
+}
+
+nsresult nsPrefetchService::Prefetch(nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, bool aExplicit) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(aReferrerInfo);
+
+ if (LOG_ENABLED()) {
+ LOG(("PrefetchURI [%s]\n", aURI->GetSpecOrDefault().get()));
+ }
+
+ if (mPrefetchDisabled) {
+ LOG(("rejected: prefetch service is disabled\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ nsresult rv = CheckURIScheme(aURI, aReferrerInfo);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // XXX we might want to either leverage nsIProtocolHandler::protocolFlags
+ // or possibly nsIRequest::loadFlags to determine if this URI should be
+ // prefetched.
+ //
+
+ // skip URLs that contain query strings, except URLs for which prefetching
+ // has been explicitly requested.
+ if (!aExplicit) {
+ nsCOMPtr<nsIURL> url(do_QueryInterface(aURI, &rv));
+ if (NS_FAILED(rv)) return rv;
+ nsAutoCString query;
+ rv = url->GetQuery(query);
+ if (NS_FAILED(rv) || !query.IsEmpty()) {
+ LOG(("rejected: URL has a query string\n"));
+ return NS_ERROR_ABORT;
+ }
+ }
+
+ //
+ // Check whether it is being prefetched.
+ //
+ for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) {
+ bool equals;
+ if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ if (mCurrentNodes[i]->mSources.IndexOf(source) ==
+ mCurrentNodes[i]->mSources.NoIndex) {
+ LOG(
+ ("URL is already being prefetched, add a new reference "
+ "document\n"));
+ mCurrentNodes[i]->mSources.AppendElement(source);
+ return NS_OK;
+ } else {
+ LOG(("URL is already being prefetched by this document"));
+ return NS_ERROR_ABORT;
+ }
+ }
+ }
+
+ //
+ // Check whether it is on the prefetch queue.
+ //
+ for (std::deque<RefPtr<nsPrefetchNode>>::iterator nodeIt =
+ mPrefetchQueue.begin();
+ nodeIt != mPrefetchQueue.end(); nodeIt++) {
+ bool equals;
+ RefPtr<nsPrefetchNode> node = nodeIt->get();
+ if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ if (node->mSources.IndexOf(source) == node->mSources.NoIndex) {
+ LOG(
+ ("URL is already being prefetched, add a new reference "
+ "document\n"));
+ node->mSources.AppendElement(do_GetWeakReference(aSource));
+ return NS_OK;
+ } else {
+ LOG(("URL is already being prefetched by this document"));
+ return NS_ERROR_ABORT;
+ }
+ }
+ }
+
+ RefPtr<nsPrefetchNode> enqueuedNode;
+ rv = EnqueueURI(aURI, aReferrerInfo, aSource, getter_AddRefs(enqueuedNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NotifyLoadRequested(enqueuedNode);
+
+ // if there are no pages loading, kick off the request immediately
+ if ((!mStopCount && mHaveProcessed) || mAggressive) {
+ ProcessNextPrefetchURI();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::CancelPrefetchPreloadURI(nsIURI* aURI, nsINode* aSource) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ if (LOG_ENABLED()) {
+ LOG(("CancelPrefetchURI [%s]\n", aURI->GetSpecOrDefault().get()));
+ }
+
+ //
+ // look in current prefetches
+ //
+ for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) {
+ bool equals;
+ if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ if (mCurrentNodes[i]->mSources.IndexOf(source) !=
+ mCurrentNodes[i]->mSources.NoIndex) {
+ mCurrentNodes[i]->mSources.RemoveElement(source);
+ if (mCurrentNodes[i]->mSources.IsEmpty()) {
+ mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED);
+ mCurrentNodes.RemoveElementAt(i);
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ //
+ // look into the prefetch queue
+ //
+ for (std::deque<RefPtr<nsPrefetchNode>>::iterator nodeIt =
+ mPrefetchQueue.begin();
+ nodeIt != mPrefetchQueue.end(); nodeIt++) {
+ bool equals;
+ RefPtr<nsPrefetchNode> node = nodeIt->get();
+ if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ if (node->mSources.IndexOf(source) != node->mSources.NoIndex) {
+#ifdef DEBUG
+ int32_t inx = node->mSources.IndexOf(source);
+ nsCOMPtr<nsINode> domNode =
+ do_QueryReferent(node->mSources.ElementAt(inx));
+ MOZ_ASSERT(domNode);
+#endif
+
+ node->mSources.RemoveElement(source);
+ if (node->mSources.IsEmpty()) {
+ mPrefetchQueue.erase(nodeIt);
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // not found!
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::PreloadURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource,
+ nsContentPolicyType aPolicyType) {
+ return Preload(aURI, aReferrerInfo, aSource, aPolicyType);
+}
+
+NS_IMETHODIMP
+nsPrefetchService::PrefetchURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, bool aExplicit) {
+ return Prefetch(aURI, aReferrerInfo, aSource, aExplicit);
+}
+
+NS_IMETHODIMP
+nsPrefetchService::HasMoreElements(bool* aHasMore) {
+ *aHasMore = (mCurrentNodes.Length() || !mPrefetchQueue.empty());
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService::nsIWebProgressListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchService::OnProgressChange(nsIWebProgress* aProgress,
+ nsIRequest* aRequest,
+ int32_t curSelfProgress,
+ int32_t maxSelfProgress,
+ int32_t curTotalProgress,
+ int32_t maxTotalProgress) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t progressStateFlags,
+ nsresult aStatus) {
+ if (progressStateFlags & STATE_IS_DOCUMENT) {
+ if (progressStateFlags & STATE_STOP)
+ StartPrefetching();
+ else if (progressStateFlags & STATE_START)
+ StopPrefetching();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* location,
+ uint32_t aFlags) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aState) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService::nsIObserver
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ LOG(("nsPrefetchService::Observe [topic=%s]\n", aTopic));
+
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ StopAll();
+ mPrefetchDisabled = true;
+ } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ const nsCString converted = NS_ConvertUTF16toUTF8(aData);
+ const char* pref = converted.get();
+ if (!strcmp(pref, PREFETCH_PREF)) {
+ if (Preferences::GetBool(PREFETCH_PREF, false)) {
+ if (mPrefetchDisabled) {
+ LOG(("enabling prefetching\n"));
+ mPrefetchDisabled = false;
+ AddProgressListener();
+ }
+ } else {
+ if (!mPrefetchDisabled) {
+ LOG(("disabling prefetching\n"));
+ StopCurrentPrefetchsPreloads(false);
+ mPrefetchDisabled = true;
+ RemoveProgressListener();
+ }
+ }
+ } else if (!strcmp(pref, PARALLELISM_PREF)) {
+ mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism);
+ if (mMaxParallelism < 1) {
+ mMaxParallelism = 1;
+ }
+ // If our parallelism has increased, go ahead and kick off enough
+ // prefetches to fill up our allowance. If we're now over our
+ // allowance, we'll just silently let some of them finish to get
+ // back below our limit.
+ while (((!mStopCount && mHaveProcessed) || mAggressive) &&
+ !mPrefetchQueue.empty() &&
+ mCurrentNodes.Length() < static_cast<uint32_t>(mMaxParallelism)) {
+ ProcessNextPrefetchURI();
+ }
+ } else if (!strcmp(pref, AGGRESSIVE_PREF)) {
+ mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false);
+ // in aggressive mode, start prefetching immediately
+ if (mAggressive) {
+ while (mStopCount && !mPrefetchQueue.empty() &&
+ mCurrentNodes.Length() <
+ static_cast<uint32_t>(mMaxParallelism)) {
+ ProcessNextPrefetchURI();
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// vim: ts=4 sw=2 expandtab
diff --git a/uriloader/prefetch/nsPrefetchService.h b/uriloader/prefetch/nsPrefetchService.h
new file mode 100644
index 0000000000..7b931ba52c
--- /dev/null
+++ b/uriloader/prefetch/nsPrefetchService.h
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsPrefetchService_h__
+#define nsPrefetchService_h__
+
+#include "nsIObserver.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIChannelEventSink.h"
+#include "nsIPrefetchService.h"
+#include "nsIRedirectResultListener.h"
+#include "nsIWebProgressListener.h"
+#include "nsIStreamListener.h"
+#include "nsIChannel.h"
+#include "nsIURI.h"
+#include "nsWeakReference.h"
+#include "nsCOMPtr.h"
+#include "mozilla/Attributes.h"
+#include <deque>
+
+class nsPrefetchService;
+class nsPrefetchNode;
+class nsIReferrerInfo;
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService
+//-----------------------------------------------------------------------------
+
+class nsPrefetchService final : public nsIPrefetchService,
+ public nsIWebProgressListener,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPREFETCHSERVICE
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIOBSERVER
+
+ nsPrefetchService();
+
+ nsresult Init();
+ void RemoveNodeAndMaybeStartNextPrefetchURI(nsPrefetchNode* aFinished);
+ void ProcessNextPrefetchURI();
+
+ void NotifyLoadRequested(nsPrefetchNode* node);
+ void NotifyLoadCompleted(nsPrefetchNode* node);
+ void DispatchEvent(nsPrefetchNode* node, bool aSuccess);
+
+ private:
+ ~nsPrefetchService();
+
+ nsresult Prefetch(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, bool aExplicit);
+
+ nsresult Preload(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, nsContentPolicyType aPolicyType);
+
+ void AddProgressListener();
+ void RemoveProgressListener();
+ nsresult EnqueueURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, nsPrefetchNode** node);
+ void EmptyPrefetchQueue();
+
+ void StartPrefetching();
+ void StopPrefetching();
+ void StopCurrentPrefetchsPreloads(bool aPreload);
+ void StopAll();
+ nsresult CheckURIScheme(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo);
+
+ std::deque<RefPtr<nsPrefetchNode>> mPrefetchQueue;
+ nsTArray<RefPtr<nsPrefetchNode>> mCurrentNodes;
+ int32_t mMaxParallelism;
+ int32_t mStopCount;
+ bool mHaveProcessed;
+ bool mPrefetchDisabled;
+
+ // In usual case prefetch does not start until all normal loads are done.
+ // Aggressive mode ignores normal loads and just start prefetch ASAP.
+ // It's mainly for testing purpose and discoraged for normal use;
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=1281415 for details.
+ bool mAggressive;
+};
+
+//-----------------------------------------------------------------------------
+// nsPreFetchingNode
+//-----------------------------------------------------------------------------
+
+class nsPrefetchNode final : public nsIStreamListener,
+ public nsIInterfaceRequestor,
+ public nsIChannelEventSink,
+ public nsIRedirectResultListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIREDIRECTRESULTLISTENER
+
+ nsPrefetchNode(nsPrefetchService* aPrefetchService, nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo, nsINode* aSource,
+ nsContentPolicyType aPolicyType, bool aPreload);
+
+ nsresult OpenChannel();
+ nsresult CancelChannel(nsresult error);
+
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIReferrerInfo> mReferrerInfo;
+ nsTArray<nsWeakPtr> mSources;
+
+ // The policy type to be used for fetching the resource.
+ nsContentPolicyType mPolicyType;
+ // nsPrefetchNode is used for prefetching and preloading resource.
+ // mPreload is true if a resource is preloaded. Preloads and
+ // prefetches are fetched in different phases (during load and
+ // after a page load), therefore we need to distinguish them.
+ bool mPreload;
+
+ private:
+ ~nsPrefetchNode() {}
+
+ RefPtr<nsPrefetchService> mService;
+ nsCOMPtr<nsIChannel> mChannel;
+ nsCOMPtr<nsIChannel> mRedirectChannel;
+ int64_t mBytesRead;
+ bool mShouldFireLoadEvent;
+};
+
+#endif // !nsPrefetchService_h__