diff options
Diffstat (limited to 'uriloader/prefetch')
-rw-r--r-- | uriloader/prefetch/moz.build | 26 | ||||
-rw-r--r-- | uriloader/prefetch/nsIPrefetchService.idl | 54 | ||||
-rw-r--r-- | uriloader/prefetch/nsPrefetchService.cpp | 895 | ||||
-rw-r--r-- | uriloader/prefetch/nsPrefetchService.h | 130 |
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__ |