/* vim: set ts=2 sts=2 et sw=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 #include "Predictor.h" #include "nsAppDirectoryServiceDefs.h" #include "nsICacheStorage.h" #include "nsICacheStorageService.h" #include "nsICachingChannel.h" #include "nsICancelable.h" #include "nsIChannel.h" #include "nsContentUtils.h" #include "nsIDNSService.h" #include "mozilla/dom/Document.h" #include "nsIFile.h" #include "nsIHttpChannel.h" #include "nsIInputStream.h" #include "nsIIOService.h" #include "nsILoadContext.h" #include "nsILoadContextInfo.h" #include "nsILoadGroup.h" #include "nsINetworkPredictorVerifier.h" #include "nsIObserverService.h" #include "nsIPrefBranch.h" #include "nsIPrefService.h" #include "nsISpeculativeConnect.h" #include "nsITimer.h" #include "nsIURI.h" #include "nsNetUtil.h" #include "nsServiceManagerUtils.h" #include "nsStreamUtils.h" #include "nsString.h" #include "nsThreadUtils.h" #include "mozilla/Logging.h" #include "mozilla/Preferences.h" #include "mozilla/StaticPrefs.h" #include "mozilla/Telemetry.h" #include "mozilla/net/NeckoCommon.h" #include "mozilla/net/NeckoParent.h" #include "LoadContextInfo.h" #include "mozilla/ipc/URIUtils.h" #include "SerializedLoadContext.h" #include "mozilla/net/NeckoChild.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/ClearOnShutdown.h" #include "CacheControlParser.h" #include "ReferrerInfo.h" using namespace mozilla; namespace mozilla { namespace net { Predictor* Predictor::sSelf = nullptr; static LazyLogModule gPredictorLog("NetworkPredictor"); #define PREDICTOR_LOG(args) \ MOZ_LOG(gPredictorLog, mozilla::LogLevel::Debug, args) #define RETURN_IF_FAILED(_rv) \ do { \ if (NS_FAILED(_rv)) { \ return; \ } \ } while (0) #define NOW_IN_SECONDS() static_cast(PR_Now() / PR_USEC_PER_SEC) static const char PREDICTOR_CLEANED_UP_PREF[] = "network.predictor.cleaned-up"; // All these time values are in sec static const uint32_t ONE_DAY = 86400U; static const uint32_t ONE_WEEK = 7U * ONE_DAY; static const uint32_t ONE_MONTH = 30U * ONE_DAY; static const uint32_t ONE_YEAR = 365U * ONE_DAY; static const uint32_t STARTUP_WINDOW = 5U * 60U; // 5min // Version of metadata entries we expect static const uint32_t METADATA_VERSION = 1; // Flags available in entries // FLAG_PREFETCHABLE - we have determined that this item is eligible for // prefetch static const uint32_t FLAG_PREFETCHABLE = 1 << 0; // We save 12 bits in the "flags" section of our metadata for actual flags, the // rest are to keep track of a rolling count of which loads a resource has been // used on to determine if we can prefetch that resource or not; static const uint8_t kRollingLoadOffset = 12; static const int32_t kMaxPrefetchRollingLoadCount = 20; static const uint32_t kFlagsMask = ((1 << kRollingLoadOffset) - 1); static bool sEsniEnabled = false; // ID Extensions for cache entries #define PREDICTOR_ORIGIN_EXTENSION "predictor-origin" // Get the full origin (scheme, host, port) out of a URI (maybe should be part // of nsIURI instead?) static nsresult ExtractOrigin(nsIURI* uri, nsIURI** originUri) { nsAutoCString s; nsresult rv = nsContentUtils::GetASCIIOrigin(uri, s); NS_ENSURE_SUCCESS(rv, rv); return NS_NewURI(originUri, s); } // All URIs we get passed *must* be http or https if they're not null. This // helps ensure that. static bool IsNullOrHttp(nsIURI* uri) { if (!uri) { return true; } bool isHTTP = false; uri->SchemeIs("http", &isHTTP); if (!isHTTP) { uri->SchemeIs("https", &isHTTP); } return isHTTP; } // Listener for the speculative DNS requests we'll fire off, which just ignores // the result (since we're just trying to warm the cache). This also exists to // reduce round-trips to the main thread, by being something threadsafe the // Predictor can use. NS_IMPL_ISUPPORTS(Predictor::DNSListener, nsIDNSListener); NS_IMETHODIMP Predictor::DNSListener::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec, nsresult status) { return NS_OK; } NS_IMETHODIMP Predictor::DNSListener::OnLookupByTypeComplete(nsICancelable* request, nsIDNSByTypeRecord* res, nsresult status) { return NS_OK; } // Class to proxy important information from the initial predictor call through // the cache API and back into the internals of the predictor. We can't use the // predictor itself, as it may have multiple actions in-flight, and each action // has different parameters. NS_IMPL_ISUPPORTS(Predictor::Action, nsICacheEntryOpenCallback); Predictor::Action::Action(bool fullUri, bool predict, Predictor::Reason reason, nsIURI* targetURI, nsIURI* sourceURI, nsINetworkPredictorVerifier* verifier, Predictor* predictor) : mFullUri(fullUri), mPredict(predict), mTargetURI(targetURI), mSourceURI(sourceURI), mVerifier(verifier), mStackCount(0), mPredictor(predictor) { mStartTime = TimeStamp::Now(); if (mPredict) { mPredictReason = reason.mPredict; } else { mLearnReason = reason.mLearn; } } Predictor::Action::Action(bool fullUri, bool predict, Predictor::Reason reason, nsIURI* targetURI, nsIURI* sourceURI, nsINetworkPredictorVerifier* verifier, Predictor* predictor, uint8_t stackCount) : mFullUri(fullUri), mPredict(predict), mTargetURI(targetURI), mSourceURI(sourceURI), mVerifier(verifier), mStackCount(stackCount), mPredictor(predictor) { mStartTime = TimeStamp::Now(); if (mPredict) { mPredictReason = reason.mPredict; } else { mLearnReason = reason.mLearn; } } NS_IMETHODIMP Predictor::Action::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appCache, uint32_t* result) { *result = nsICacheEntryOpenCallback::ENTRY_WANTED; return NS_OK; } NS_IMETHODIMP Predictor::Action::OnCacheEntryAvailable(nsICacheEntry* entry, bool isNew, nsIApplicationCache* appCache, nsresult result) { MOZ_ASSERT(NS_IsMainThread(), "Got cache entry off main thread!"); nsAutoCString targetURI, sourceURI; mTargetURI->GetAsciiSpec(targetURI); if (mSourceURI) { mSourceURI->GetAsciiSpec(sourceURI); } PREDICTOR_LOG( ("OnCacheEntryAvailable %p called. entry=%p mFullUri=%d mPredict=%d " "mPredictReason=%d mLearnReason=%d mTargetURI=%s " "mSourceURI=%s mStackCount=%d isNew=%d result=0x%08" PRIx32, this, entry, mFullUri, mPredict, mPredictReason, mLearnReason, targetURI.get(), sourceURI.get(), mStackCount, isNew, static_cast(result))); if (NS_FAILED(result)) { PREDICTOR_LOG( ("OnCacheEntryAvailable %p FAILED to get cache entry (0x%08" PRIX32 "). Aborting.", this, static_cast(result))); return NS_OK; } Telemetry::AccumulateTimeDelta(Telemetry::PREDICTOR_WAIT_TIME, mStartTime); if (mPredict) { bool predicted = mPredictor->PredictInternal(mPredictReason, entry, isNew, mFullUri, mTargetURI, mVerifier, mStackCount); Telemetry::AccumulateTimeDelta(Telemetry::PREDICTOR_PREDICT_WORK_TIME, mStartTime); if (predicted) { Telemetry::AccumulateTimeDelta( Telemetry::PREDICTOR_PREDICT_TIME_TO_ACTION, mStartTime); } else { Telemetry::AccumulateTimeDelta( Telemetry::PREDICTOR_PREDICT_TIME_TO_INACTION, mStartTime); } } else { mPredictor->LearnInternal(mLearnReason, entry, isNew, mFullUri, mTargetURI, mSourceURI); Telemetry::AccumulateTimeDelta(Telemetry::PREDICTOR_LEARN_WORK_TIME, mStartTime); } return NS_OK; } NS_IMPL_ISUPPORTS(Predictor, nsINetworkPredictor, nsIObserver, nsISpeculativeConnectionOverrider, nsIInterfaceRequestor, nsICacheEntryMetaDataVisitor, nsINetworkPredictorVerifier) Predictor::Predictor() : mInitialized(false), mCleanedUp(false), mStartupTime(0), mLastStartupTime(0), mStartupCount(1) { MOZ_ASSERT(!sSelf, "multiple Predictor instances!"); sSelf = this; } Predictor::~Predictor() { if (mInitialized) Shutdown(); sSelf = nullptr; } // Predictor::nsIObserver nsresult Predictor::InstallObserver() { MOZ_ASSERT(NS_IsMainThread(), "Installing observer off main thread"); nsresult rv = NS_OK; nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) { return NS_ERROR_NOT_AVAILABLE; } rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); NS_ENSURE_SUCCESS(rv, rv); mCleanedUp = Preferences::GetBool(PREDICTOR_CLEANED_UP_PREF, false); if (!mCleanedUp) { NS_NewTimerWithObserver(getter_AddRefs(mCleanupTimer), this, 60 * 1000, nsITimer::TYPE_ONE_SHOT); } return rv; } void Predictor::RemoveObserver() { MOZ_ASSERT(NS_IsMainThread(), "Removing observer off main thread"); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); } if (mCleanupTimer) { mCleanupTimer->Cancel(); mCleanupTimer = nullptr; } } NS_IMETHODIMP Predictor::Observe(nsISupports* subject, const char* topic, const char16_t* data_unicode) { nsresult rv = NS_OK; MOZ_ASSERT(NS_IsMainThread(), "Predictor observing something off main thread!"); if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, topic)) { Shutdown(); } else if (!strcmp("timer-callback", topic)) { MaybeCleanupOldDBFiles(); mCleanupTimer = nullptr; } return rv; } // Predictor::nsISpeculativeConnectionOverrider NS_IMETHODIMP Predictor::GetIgnoreIdle(bool* ignoreIdle) { *ignoreIdle = true; return NS_OK; } NS_IMETHODIMP Predictor::GetParallelSpeculativeConnectLimit( uint32_t* parallelSpeculativeConnectLimit) { *parallelSpeculativeConnectLimit = 6; return NS_OK; } NS_IMETHODIMP Predictor::GetIsFromPredictor(bool* isFromPredictor) { *isFromPredictor = true; return NS_OK; } NS_IMETHODIMP Predictor::GetAllow1918(bool* allow1918) { *allow1918 = false; return NS_OK; } // Predictor::nsIInterfaceRequestor NS_IMETHODIMP Predictor::GetInterface(const nsIID& iid, void** result) { return QueryInterface(iid, result); } // Predictor::nsICacheEntryMetaDataVisitor #define SEEN_META_DATA "predictor::seen" #define RESOURCE_META_DATA "predictor::resource-count" #define META_DATA_PREFIX "predictor::" static bool IsURIMetadataElement(const char* key) { return StringBeginsWith(nsDependentCString(key), NS_LITERAL_CSTRING(META_DATA_PREFIX)) && !NS_LITERAL_CSTRING(SEEN_META_DATA).Equals(key) && !NS_LITERAL_CSTRING(RESOURCE_META_DATA).Equals(key); } nsresult Predictor::OnMetaDataElement(const char* asciiKey, const char* asciiValue) { MOZ_ASSERT(NS_IsMainThread()); if (!IsURIMetadataElement(asciiKey)) { // This isn't a bit of metadata we care about return NS_OK; } nsCString key, value; key.AssignASCII(asciiKey); value.AssignASCII(asciiValue); mKeysToOperateOn.AppendElement(key); mValuesToOperateOn.AppendElement(value); return NS_OK; } // Predictor::nsINetworkPredictor nsresult Predictor::Init() { MOZ_DIAGNOSTIC_ASSERT(!IsNeckoChild()); if (!NS_IsMainThread()) { MOZ_ASSERT(false, "Predictor::Init called off the main thread!"); return NS_ERROR_UNEXPECTED; } nsresult rv = NS_OK; rv = InstallObserver(); NS_ENSURE_SUCCESS(rv, rv); mLastStartupTime = mStartupTime = NOW_IN_SECONDS(); if (!mDNSListener) { mDNSListener = new DNSListener(); } mCacheStorageService = do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); mSpeculativeService = do_GetService("@mozilla.org/network/io-service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); rv = NS_NewURI(getter_AddRefs(mStartupURI), "predictor://startup"); NS_ENSURE_SUCCESS(rv, rv); mDnsService = do_GetService("@mozilla.org/network/dns-service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); Preferences::AddBoolVarCache(&sEsniEnabled, "network.security.esni.enabled"); mInitialized = true; return rv; } namespace { class PredictorThreadShutdownRunner : public Runnable { public: PredictorThreadShutdownRunner(nsIThread* ioThread, bool success) : Runnable("net::PredictorThreadShutdownRunner"), mIOThread(ioThread), mSuccess(success) {} ~PredictorThreadShutdownRunner() = default; NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread(), "Shutting down io thread off main thread!"); if (mSuccess) { // This means the cleanup happened. Mark so we don't try in the // future. Preferences::SetBool(PREDICTOR_CLEANED_UP_PREF, true); } return mIOThread->AsyncShutdown(); } private: nsCOMPtr mIOThread; bool mSuccess; }; class PredictorOldCleanupRunner : public Runnable { public: PredictorOldCleanupRunner(nsIThread* ioThread, nsIFile* dbFile) : Runnable("net::PredictorOldCleanupRunner"), mIOThread(ioThread), mDBFile(dbFile) {} ~PredictorOldCleanupRunner() = default; NS_IMETHOD Run() override { MOZ_ASSERT(!NS_IsMainThread(), "Cleaning up old files on main thread!"); nsresult rv = CheckForAndDeleteOldDBFiles(); RefPtr runner = new PredictorThreadShutdownRunner(mIOThread, NS_SUCCEEDED(rv)); NS_DispatchToMainThread(runner); return NS_OK; } private: nsresult CheckForAndDeleteOldDBFiles() { nsCOMPtr oldDBFile; nsresult rv = mDBFile->GetParent(getter_AddRefs(oldDBFile)); NS_ENSURE_SUCCESS(rv, rv); rv = oldDBFile->AppendNative(NS_LITERAL_CSTRING("seer.sqlite")); NS_ENSURE_SUCCESS(rv, rv); bool fileExists = false; rv = oldDBFile->Exists(&fileExists); NS_ENSURE_SUCCESS(rv, rv); if (fileExists) { rv = oldDBFile->Remove(false); NS_ENSURE_SUCCESS(rv, rv); } fileExists = false; rv = mDBFile->Exists(&fileExists); NS_ENSURE_SUCCESS(rv, rv); if (fileExists) { rv = mDBFile->Remove(false); } return rv; } nsCOMPtr mIOThread; nsCOMPtr mDBFile; }; class PredictorLearnRunnable final : public Runnable { public: PredictorLearnRunnable(nsIURI* targetURI, nsIURI* sourceURI, PredictorLearnReason reason, const OriginAttributes& oa) : Runnable("PredictorLearnRunnable"), mTargetURI(targetURI), mSourceURI(sourceURI), mReason(reason), mOA(oa) {} ~PredictorLearnRunnable() = default; NS_IMETHOD Run() override { if (!gNeckoChild) { // This may have gone away between when this runnable was dispatched and // when it actually runs, so let's be safe here, even though we asserted // earlier. PREDICTOR_LOG(("predictor::learn (async) gNeckoChild went away")); return NS_OK; } ipc::URIParams serTargetURI; SerializeURI(mTargetURI, serTargetURI); Maybe serSourceURI; SerializeURI(mSourceURI, serSourceURI); PREDICTOR_LOG(("predictor::learn (async) forwarding to parent")); gNeckoChild->SendPredLearn(serTargetURI, serSourceURI, mReason, mOA); return NS_OK; } private: nsCOMPtr mTargetURI; nsCOMPtr mSourceURI; PredictorLearnReason mReason; const OriginAttributes mOA; }; } // namespace void Predictor::MaybeCleanupOldDBFiles() { MOZ_ASSERT(NS_IsMainThread()); if (!StaticPrefs::network_predictor_enabled() || mCleanedUp) { return; } mCleanedUp = true; // This is used for cleaning up junk left over from the old backend // built on top of sqlite, if necessary. nsCOMPtr dbFile; nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(dbFile)); RETURN_IF_FAILED(rv); rv = dbFile->AppendNative(NS_LITERAL_CSTRING("netpredictions.sqlite")); RETURN_IF_FAILED(rv); nsCOMPtr ioThread; rv = NS_NewNamedThread("NetPredictClean", getter_AddRefs(ioThread)); RETURN_IF_FAILED(rv); RefPtr runner = new PredictorOldCleanupRunner(ioThread, dbFile); ioThread->Dispatch(runner, NS_DISPATCH_NORMAL); } void Predictor::Shutdown() { if (!NS_IsMainThread()) { MOZ_ASSERT(false, "Predictor::Shutdown called off the main thread!"); return; } RemoveObserver(); mInitialized = false; } nsresult Predictor::Create(nsISupports* aOuter, const nsIID& aIID, void** aResult) { MOZ_ASSERT(NS_IsMainThread()); nsresult rv; if (aOuter != nullptr) { return NS_ERROR_NO_AGGREGATION; } RefPtr svc = new Predictor(); if (IsNeckoChild()) { NeckoChild::InitNeckoChild(); // Child threads only need to be call into the public interface methods // so we don't bother with initialization return svc->QueryInterface(aIID, aResult); } rv = svc->Init(); if (NS_FAILED(rv)) { PREDICTOR_LOG(("Failed to initialize predictor, predictor will be a noop")); } // We treat init failure the same as the service being disabled, since this // is all an optimization anyway. No need to freak people out. That's why we // gladly continue on QI'ing here. rv = svc->QueryInterface(aIID, aResult); return rv; } NS_IMETHODIMP Predictor::Predict(nsIURI* targetURI, nsIURI* sourceURI, PredictorPredictReason reason, JS::HandleValue originAttributes, nsINetworkPredictorVerifier* verifier, JSContext* aCx) { OriginAttributes attrs; if (!originAttributes.isObject() || !attrs.Init(aCx, originAttributes)) { return NS_ERROR_INVALID_ARG; } return PredictNative(targetURI, sourceURI, reason, attrs, verifier); } // Called from the main thread to initiate predictive actions NS_IMETHODIMP Predictor::PredictNative(nsIURI* targetURI, nsIURI* sourceURI, PredictorPredictReason reason, const OriginAttributes& originAttributes, nsINetworkPredictorVerifier* verifier) { MOZ_ASSERT(NS_IsMainThread(), "Predictor interface methods must be called on the main thread"); PREDICTOR_LOG(("Predictor::Predict")); if (IsNeckoChild()) { MOZ_DIAGNOSTIC_ASSERT(gNeckoChild); PREDICTOR_LOG((" called on child process")); Maybe serTargetURI, serSourceURI; SerializeURI(targetURI, serTargetURI); SerializeURI(sourceURI, serSourceURI); // If two different threads are predicting concurently, this will be // overwritten. Thankfully, we only use this in tests, which will // overwrite mVerifier perhaps multiple times for each individual test; // however, within each test, the multiple predict calls should have the // same verifier. if (verifier) { PREDICTOR_LOG((" was given a verifier")); mChildVerifier = verifier; } PREDICTOR_LOG((" forwarding to parent process")); gNeckoChild->SendPredPredict(serTargetURI, serSourceURI, reason, originAttributes, verifier); return NS_OK; } PREDICTOR_LOG((" called on parent process")); if (!mInitialized) { PREDICTOR_LOG((" not initialized")); return NS_OK; } if (!StaticPrefs::network_predictor_enabled()) { PREDICTOR_LOG((" not enabled")); return NS_OK; } if (originAttributes.mPrivateBrowsingId > 0) { // Don't want to do anything in PB mode PREDICTOR_LOG((" in PB mode")); return NS_OK; } if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) { // Nothing we can do for non-HTTP[S] schemes PREDICTOR_LOG((" got non-http[s] URI")); return NS_OK; } // Ensure we've been given the appropriate arguments for the kind of // prediction we're being asked to do nsCOMPtr uriKey = targetURI; nsCOMPtr originKey; switch (reason) { case nsINetworkPredictor::PREDICT_LINK: if (!targetURI || !sourceURI) { PREDICTOR_LOG((" link invalid URI state")); return NS_ERROR_INVALID_ARG; } // Link hover is a special case where we can predict without hitting the // db, so let's go ahead and fire off that prediction here. PredictForLink(targetURI, sourceURI, originAttributes, verifier); return NS_OK; case nsINetworkPredictor::PREDICT_LOAD: if (!targetURI || sourceURI) { PREDICTOR_LOG((" load invalid URI state")); return NS_ERROR_INVALID_ARG; } break; case nsINetworkPredictor::PREDICT_STARTUP: if (targetURI || sourceURI) { PREDICTOR_LOG((" startup invalid URI state")); return NS_ERROR_INVALID_ARG; } uriKey = mStartupURI; originKey = mStartupURI; break; default: PREDICTOR_LOG((" invalid reason")); return NS_ERROR_INVALID_ARG; } Predictor::Reason argReason; argReason.mPredict = reason; // First we open the regular cache entry, to ensure we don't gum up the works // waiting on the less-important predictor-only cache entry RefPtr uriAction = new Predictor::Action( Predictor::Action::IS_FULL_URI, Predictor::Action::DO_PREDICT, argReason, targetURI, nullptr, verifier, this); nsAutoCString uriKeyStr; uriKey->GetAsciiSpec(uriKeyStr); PREDICTOR_LOG((" Predict uri=%s reason=%d action=%p", uriKeyStr.get(), reason, uriAction.get())); nsCOMPtr cacheDiskStorage; RefPtr lci = new LoadContextInfo(false, originAttributes); nsresult rv = mCacheStorageService->DiskCacheStorage( lci, false, getter_AddRefs(cacheDiskStorage)); NS_ENSURE_SUCCESS(rv, rv); uint32_t openFlags = nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY | nsICacheStorage::OPEN_PRIORITY | nsICacheStorage::CHECK_MULTITHREADED; cacheDiskStorage->AsyncOpenURI(uriKey, EmptyCString(), openFlags, uriAction); // Now we do the origin-only (and therefore predictor-only) entry nsCOMPtr targetOrigin; rv = ExtractOrigin(uriKey, getter_AddRefs(targetOrigin)); NS_ENSURE_SUCCESS(rv, rv); if (!originKey) { originKey = targetOrigin; } RefPtr originAction = new Predictor::Action( Predictor::Action::IS_ORIGIN, Predictor::Action::DO_PREDICT, argReason, targetOrigin, nullptr, verifier, this); nsAutoCString originKeyStr; originKey->GetAsciiSpec(originKeyStr); PREDICTOR_LOG((" Predict origin=%s reason=%d action=%p", originKeyStr.get(), reason, originAction.get())); openFlags = nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY | nsICacheStorage::CHECK_MULTITHREADED; cacheDiskStorage->AsyncOpenURI(originKey, NS_LITERAL_CSTRING(PREDICTOR_ORIGIN_EXTENSION), openFlags, originAction); PREDICTOR_LOG((" predict returning")); return NS_OK; } bool Predictor::PredictInternal(PredictorPredictReason reason, nsICacheEntry* entry, bool isNew, bool fullUri, nsIURI* targetURI, nsINetworkPredictorVerifier* verifier, uint8_t stackCount) { MOZ_ASSERT(NS_IsMainThread()); PREDICTOR_LOG(("Predictor::PredictInternal")); bool rv = false; nsCOMPtr lci; entry->GetLoadContextInfo(getter_AddRefs(lci)); if (!lci) { return rv; } if (reason == nsINetworkPredictor::PREDICT_LOAD) { MaybeLearnForStartup(targetURI, fullUri, *lci->OriginAttributesPtr()); } if (isNew) { // nothing else we can do here PREDICTOR_LOG((" new entry")); return rv; } switch (reason) { case nsINetworkPredictor::PREDICT_LOAD: rv = PredictForPageload(entry, targetURI, stackCount, fullUri, verifier); break; case nsINetworkPredictor::PREDICT_STARTUP: rv = PredictForStartup(entry, fullUri, verifier); break; default: PREDICTOR_LOG((" invalid reason")); MOZ_ASSERT(false, "Got unexpected value for prediction reason"); } return rv; } void Predictor::PredictForLink(nsIURI* targetURI, nsIURI* sourceURI, const OriginAttributes& originAttributes, nsINetworkPredictorVerifier* verifier) { MOZ_ASSERT(NS_IsMainThread()); PREDICTOR_LOG(("Predictor::PredictForLink")); if (!mSpeculativeService) { PREDICTOR_LOG((" missing speculative service")); return; } if (!StaticPrefs::network_predictor_enable_hover_on_ssl()) { bool isSSL = false; sourceURI->SchemeIs("https", &isSSL); if (isSSL) { // We don't want to predict from an HTTPS page, to avoid info leakage PREDICTOR_LOG((" Not predicting for link hover - on an SSL page")); return; } } nsCOMPtr principal = BasePrincipal::CreateContentPrincipal(targetURI, originAttributes); mSpeculativeService->SpeculativeConnect(targetURI, principal, nullptr); if (verifier) { PREDICTOR_LOG((" sending verification")); verifier->OnPredictPreconnect(targetURI); } } // This is the driver for prediction based on a new pageload. static const uint8_t MAX_PAGELOAD_DEPTH = 10; bool Predictor::PredictForPageload(nsICacheEntry* entry, nsIURI* targetURI, uint8_t stackCount, bool fullUri, nsINetworkPredictorVerifier* verifier) { MOZ_ASSERT(NS_IsMainThread()); PREDICTOR_LOG(("Predictor::PredictForPageload")); if (stackCount > MAX_PAGELOAD_DEPTH) { PREDICTOR_LOG((" exceeded recursion depth!")); return false; } uint32_t lastLoad; nsresult rv = entry->GetLastFetched(&lastLoad); NS_ENSURE_SUCCESS(rv, false); int32_t globalDegradation = CalculateGlobalDegradation(lastLoad); PREDICTOR_LOG((" globalDegradation = %d", globalDegradation)); int32_t loadCount; rv = entry->GetFetchCount(&loadCount); NS_ENSURE_SUCCESS(rv, false); nsCOMPtr lci; rv = entry->GetLoadContextInfo(getter_AddRefs(lci)); NS_ENSURE_SUCCESS(rv, false); nsCOMPtr redirectURI; if (WouldRedirect(entry, loadCount, lastLoad, globalDegradation, getter_AddRefs(redirectURI))) { mPreconnects.AppendElement(redirectURI); Predictor::Reason reason; reason.mPredict = nsINetworkPredictor::PREDICT_LOAD; RefPtr redirectAction = new Predictor::Action( Predictor::Action::IS_FULL_URI, Predictor::Action::DO_PREDICT, reason, redirectURI, nullptr, verifier, this, stackCount + 1); nsAutoCString redirectUriString; redirectURI->GetAsciiSpec(redirectUriString); nsCOMPtr cacheDiskStorage; rv = mCacheStorageService->DiskCacheStorage( lci, false, getter_AddRefs(cacheDiskStorage)); NS_ENSURE_SUCCESS(rv, false); PREDICTOR_LOG((" Predict redirect uri=%s action=%p", redirectUriString.get(), redirectAction.get())); uint32_t openFlags = nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY | nsICacheStorage::OPEN_PRIORITY | nsICacheStorage::CHECK_MULTITHREADED; cacheDiskStorage->AsyncOpenURI(redirectURI, EmptyCString(), openFlags, redirectAction); return RunPredictions(nullptr, *lci->OriginAttributesPtr(), verifier); } CalculatePredictions(entry, targetURI, lastLoad, loadCount, globalDegradation, fullUri); return RunPredictions(targetURI, *lci->OriginAttributesPtr(), verifier); } // This is the driver for predicting at browser startup time based on pages that // have previously been loaded close to startup. bool Predictor::PredictForStartup(nsICacheEntry* entry, bool fullUri, nsINetworkPredictorVerifier* verifier) { MOZ_ASSERT(NS_IsMainThread()); PREDICTOR_LOG(("Predictor::PredictForStartup")); nsCOMPtr lci; nsresult rv = entry->GetLoadContextInfo(getter_AddRefs(lci)); NS_ENSURE_SUCCESS(rv, false); int32_t globalDegradation = CalculateGlobalDegradation(mLastStartupTime); CalculatePredictions(entry, nullptr, mLastStartupTime, mStartupCount, globalDegradation, fullUri); return RunPredictions(nullptr, *lci->OriginAttributesPtr(), verifier); } // This calculates how much to degrade our confidence in our data based on // the last time this top-level resource was loaded. This "global degradation" // applies to *all* subresources we have associated with the top-level // resource. This will be in addition to any reduction in confidence we have // associated with a particular subresource. int32_t Predictor::CalculateGlobalDegradation(uint32_t lastLoad) { MOZ_ASSERT(NS_IsMainThread()); int32_t globalDegradation; uint32_t delta = NOW_IN_SECONDS() - lastLoad; if (delta < ONE_DAY) { globalDegradation = StaticPrefs::network_predictor_page_degradation_day(); } else if (delta < ONE_WEEK) { globalDegradation = StaticPrefs::network_predictor_page_degradation_week(); } else if (delta < ONE_MONTH) { globalDegradation = StaticPrefs::network_predictor_page_degradation_month(); } else if (delta < ONE_YEAR) { globalDegradation = StaticPrefs::network_predictor_page_degradation_year(); } else { globalDegradation = StaticPrefs::network_predictor_page_degradation_max(); } Telemetry::Accumulate(Telemetry::PREDICTOR_GLOBAL_DEGRADATION, globalDegradation); return globalDegradation; } // This calculates our overall confidence that a particular subresource will be // loaded as part of a top-level load. // @param hitCount - the number of times we have loaded this subresource as part // of this top-level load // @param hitsPossible - the number of times we have performed this top-level // load // @param lastHit - the timestamp of the last time we loaded this subresource as // part of this top-level load // @param lastPossible - the timestamp of the last time we performed this // top-level load // @param globalDegradation - the degradation for this top-level load as // determined by CalculateGlobalDegradation int32_t Predictor::CalculateConfidence(uint32_t hitCount, uint32_t hitsPossible, uint32_t lastHit, uint32_t lastPossible, int32_t globalDegradation) { MOZ_ASSERT(NS_IsMainThread()); Telemetry::AutoCounter predictionsCalculated; ++predictionsCalculated; if (!hitsPossible) { return 0; } int32_t baseConfidence = (hitCount * 100) / hitsPossible; int32_t maxConfidence = 100; int32_t confidenceDegradation = 0; if (lastHit < lastPossible) { // We didn't load this subresource the last time this top-level load was // performed, so let's not bother preconnecting (at the very least). maxConfidence = StaticPrefs::network_predictor_preconnect_min_confidence() - 1; // Now calculate how much we want to degrade our confidence based on how // long it's been between the last time we did this top-level load and the // last time this top-level load included this subresource. PRTime delta = lastPossible - lastHit; if (delta == 0) { confidenceDegradation = 0; } else if (delta < ONE_DAY) { confidenceDegradation = StaticPrefs::network_predictor_subresource_degradation_day(); } else if (delta < ONE_WEEK) { confidenceDegradation = StaticPrefs::network_predictor_subresource_degradation_week(); } else if (delta < ONE_MONTH) { confidenceDegradation = StaticPrefs::network_predictor_subresource_degradation_month(); } else if (delta < ONE_YEAR) { confidenceDegradation = StaticPrefs::network_predictor_subresource_degradation_year(); } else { confidenceDegradation = StaticPrefs::network_predictor_subresource_degradation_max(); maxConfidence = 0; } } // Calculate our confidence and clamp it to between 0 and maxConfidence // (<= 100) int32_t confidence = baseConfidence - confidenceDegradation - globalDegradation; confidence = std::max(confidence, 0); confidence = std::min(confidence, maxConfidence); Telemetry::Accumulate(Telemetry::PREDICTOR_BASE_CONFIDENCE, baseConfidence); Telemetry::Accumulate(Telemetry::PREDICTOR_SUBRESOURCE_DEGRADATION, confidenceDegradation); Telemetry::Accumulate(Telemetry::PREDICTOR_CONFIDENCE, confidence); return confidence; } static void MakeMetadataEntry(const uint32_t hitCount, const uint32_t lastHit, const uint32_t flags, nsCString& newValue) { newValue.Truncate(); newValue.AppendInt(METADATA_VERSION); newValue.Append(','); newValue.AppendInt(hitCount); newValue.Append(','); newValue.AppendInt(lastHit); newValue.Append(','); newValue.AppendInt(flags); } // On every page load, the rolling window gets shifted by one bit, leaving the // lowest bit at 0, to indicate that the subresource in question has not been // seen on the most recent page load. If, at some point later during the page // load, the subresource is seen again, we will then set the lowest bit to 1. // This is how we keep track of how many of the last n pageloads (for n <= 20) a // particular subresource has been seen. The rolling window is kept in the upper // 20 bits of the flags element of the metadata. This saves 12 bits for regular // old flags. void Predictor::UpdateRollingLoadCount(nsICacheEntry* entry, const uint32_t flags, const char* key, const uint32_t hitCount, const uint32_t lastHit) { // Extract just the rolling load count from the flags, shift it to clear the // lowest bit, and put the new value with the existing flags. uint32_t rollingLoadCount = flags & ~kFlagsMask; rollingLoadCount <<= 1; uint32_t newFlags = (flags & kFlagsMask) | rollingLoadCount; // Finally, update the metadata on the cache entry. nsAutoCString newValue; MakeMetadataEntry(hitCount, lastHit, newFlags, newValue); entry->SetMetaDataElement(key, newValue.BeginReading()); } uint32_t Predictor::ClampedPrefetchRollingLoadCount() { int32_t n = StaticPrefs::network_predictor_prefetch_rolling_load_count(); if (n < 0) { return 0; } if (n > kMaxPrefetchRollingLoadCount) { return kMaxPrefetchRollingLoadCount; } return n; } void Predictor::CalculatePredictions(nsICacheEntry* entry, nsIURI* referrer, uint32_t lastLoad, uint32_t loadCount, int32_t globalDegradation, bool fullUri) { MOZ_ASSERT(NS_IsMainThread()); // Since the visitor gets called under a cache lock, all we do there is get // copies of the keys/values we care about, and then do the real work here entry->VisitMetaData(this); nsTArray keysToOperateOn, valuesToOperateOn; keysToOperateOn.SwapElements(mKeysToOperateOn); valuesToOperateOn.SwapElements(mValuesToOperateOn); MOZ_ASSERT(keysToOperateOn.Length() == valuesToOperateOn.Length()); for (size_t i = 0; i < keysToOperateOn.Length(); ++i) { const char* key = keysToOperateOn[i].BeginReading(); const char* value = valuesToOperateOn[i].BeginReading(); nsCString uri; uint32_t hitCount, lastHit, flags; if (!ParseMetaDataEntry(key, value, uri, hitCount, lastHit, flags)) { // This failed, get rid of it so we don't waste space entry->SetMetaDataElement(key, nullptr); continue; } int32_t confidence = CalculateConfidence(hitCount, loadCount, lastHit, lastLoad, globalDegradation); if (fullUri) { UpdateRollingLoadCount(entry, flags, key, hitCount, lastHit); } PREDICTOR_LOG(("CalculatePredictions key=%s value=%s confidence=%d", key, value, confidence)); PrefetchIgnoreReason reason = PREFETCH_OK; if (!fullUri) { // Not full URI - don't prefetch! No sense in it! PREDICTOR_LOG((" forcing non-cacheability - not full URI")); if (flags & FLAG_PREFETCHABLE) { // This only applies if we had somehow otherwise marked this // prefetchable. reason = NOT_FULL_URI; } flags &= ~FLAG_PREFETCHABLE; } else if (!referrer) { // No referrer means we can't prefetch, so pretend it's non-cacheable, // no matter what. PREDICTOR_LOG((" forcing non-cacheability - no referrer")); if (flags & FLAG_PREFETCHABLE) { // This only applies if we had somehow otherwise marked this // prefetchable. reason = NO_REFERRER; } flags &= ~FLAG_PREFETCHABLE; } else { uint32_t expectedRollingLoadCount = (1 << ClampedPrefetchRollingLoadCount()) - 1; expectedRollingLoadCount <<= kRollingLoadOffset; if ((flags & expectedRollingLoadCount) != expectedRollingLoadCount) { PREDICTOR_LOG((" forcing non-cacheability - missed a load")); if (flags & FLAG_PREFETCHABLE) { // This only applies if we had somehow otherwise marked this // prefetchable. reason = MISSED_A_LOAD; } flags &= ~FLAG_PREFETCHABLE; } } PREDICTOR_LOG((" setting up prediction")); SetupPrediction(confidence, flags, uri, reason); } } // (Maybe) adds a predictive action to the prediction runner, based on our // calculated confidence for the subresource in question. void Predictor::SetupPrediction(int32_t confidence, uint32_t flags, const nsCString& uri, PrefetchIgnoreReason earlyReason) { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = NS_OK; PREDICTOR_LOG( ("SetupPrediction enable-prefetch=%d prefetch-min-confidence=%d " "preconnect-min-confidence=%d preresolve-min-confidence=%d " "flags=%d confidence=%d uri=%s", StaticPrefs::network_predictor_enable_prefetch(), StaticPrefs::network_predictor_prefetch_min_confidence(), StaticPrefs::network_predictor_preconnect_min_confidence(), StaticPrefs::network_predictor_preresolve_min_confidence(), flags, confidence, uri.get())); bool prefetchOk = !!(flags & FLAG_PREFETCHABLE); PrefetchIgnoreReason reason = earlyReason; if (prefetchOk && !StaticPrefs::network_predictor_enable_prefetch()) { prefetchOk = false; reason = PREFETCH_DISABLED; } else if (prefetchOk && !ClampedPrefetchRollingLoadCount() && confidence < StaticPrefs::network_predictor_prefetch_min_confidence()) { prefetchOk = false; if (!ClampedPrefetchRollingLoadCount()) { reason = PREFETCH_DISABLED_VIA_COUNT; } else { reason = CONFIDENCE_TOO_LOW; } } // prefetchOk == false and reason == PREFETCH_OK indicates that the reason // we aren't prefetching this item is because it was marked un-prefetchable in // our metadata. We already have separate telemetry on that decision, so we // aren't going to accumulate more here. Right now we only care about why // something we had marked prefetchable isn't being prefetched. if (!prefetchOk && reason != PREFETCH_OK) { Telemetry::Accumulate(Telemetry::PREDICTOR_PREFETCH_IGNORE_REASON, reason); } if (prefetchOk) { nsCOMPtr prefetchURI; rv = NS_NewURI(getter_AddRefs(prefetchURI), uri); if (NS_SUCCEEDED(rv)) { mPrefetches.AppendElement(prefetchURI); } } else if (confidence >= StaticPrefs::network_predictor_preconnect_min_confidence()) { nsCOMPtr preconnectURI; rv = NS_NewURI(getter_AddRefs(preconnectURI), uri); if (NS_SUCCEEDED(rv)) { mPreconnects.AppendElement(preconnectURI); } } else if (confidence >= StaticPrefs::network_predictor_preresolve_min_confidence()) { nsCOMPtr preresolveURI; rv = NS_NewURI(getter_AddRefs(preresolveURI), uri); if (NS_SUCCEEDED(rv)) { mPreresolves.AppendElement(preresolveURI); } } if (NS_FAILED(rv)) { PREDICTOR_LOG( (" NS_NewURI returned 0x%" PRIx32, static_cast(rv))); } } nsresult Predictor::Prefetch(nsIURI* uri, nsIURI* referrer, const OriginAttributes& originAttributes, nsINetworkPredictorVerifier* verifier) { nsAutoCString strUri, strReferrer; uri->GetAsciiSpec(strUri); referrer->GetAsciiSpec(strReferrer); PREDICTOR_LOG(("Predictor::Prefetch uri=%s referrer=%s verifier=%p", strUri.get(), strReferrer.get(), verifier)); nsCOMPtr channel; nsresult rv = NS_NewChannel( getter_AddRefs(channel), uri, nsContentUtils::GetSystemPrincipal(), nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, nsIContentPolicy::TYPE_OTHER, nullptr, /* nsICookieSettings */ nullptr, /* aPerformanceStorage */ nullptr, /* aLoadGroup */ nullptr, /* aCallbacks */ nsIRequest::LOAD_BACKGROUND); if (NS_FAILED(rv)) { PREDICTOR_LOG( (" NS_NewChannel failed rv=0x%" PRIX32, static_cast(rv))); return rv; } nsCOMPtr loadInfo = channel->LoadInfo(); rv = loadInfo->SetOriginAttributes(originAttributes); if (NS_FAILED(rv)) { PREDICTOR_LOG( (" Set originAttributes into loadInfo failed rv=0x%" PRIX32, static_cast(rv))); return rv; } nsCOMPtr httpChannel; httpChannel = do_QueryInterface(channel); if (!httpChannel) { PREDICTOR_LOG((" Could not get HTTP Channel from new channel!")); return NS_ERROR_UNEXPECTED; } nsCOMPtr referrerInfo = new ReferrerInfo(referrer); rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo); NS_ENSURE_SUCCESS(rv, rv); // XXX - set a header here to indicate this is a prefetch? nsCOMPtr listener = new PrefetchListener(verifier, uri, this); PREDICTOR_LOG((" calling AsyncOpen listener=%p channel=%p", listener.get(), channel.get())); rv = channel->AsyncOpen(listener); if (NS_FAILED(rv)) { PREDICTOR_LOG( (" AsyncOpen failed rv=0x%" PRIX32, static_cast(rv))); } return rv; } // Runs predictions that have been set up. bool Predictor::RunPredictions(nsIURI* referrer, const OriginAttributes& originAttributes, nsINetworkPredictorVerifier* verifier) { MOZ_ASSERT(NS_IsMainThread(), "Running prediction off main thread"); PREDICTOR_LOG(("Predictor::RunPredictions")); bool predicted = false; uint32_t len, i; nsTArray> prefetches, preconnects, preresolves; prefetches.SwapElements(mPrefetches); preconnects.SwapElements(mPreconnects); preresolves.SwapElements(mPreresolves); Telemetry::AutoCounter totalPredictions; Telemetry::AutoCounter totalPrefetches; Telemetry::AutoCounter totalPreconnects; Telemetry::AutoCounter totalPreresolves; len = prefetches.Length(); for (i = 0; i < len; ++i) { PREDICTOR_LOG((" doing prefetch")); nsCOMPtr uri = prefetches[i]; if (NS_SUCCEEDED(Prefetch(uri, referrer, originAttributes, verifier))) { ++totalPredictions; ++totalPrefetches; predicted = true; } } len = preconnects.Length(); for (i = 0; i < len; ++i) { PREDICTOR_LOG((" doing preconnect")); nsCOMPtr uri = preconnects[i]; ++totalPredictions; ++totalPreconnects; nsCOMPtr principal = BasePrincipal::CreateContentPrincipal(uri, originAttributes); mSpeculativeService->SpeculativeConnect(uri, principal, this); predicted = true; if (verifier) { PREDICTOR_LOG((" sending preconnect verification")); verifier->OnPredictPreconnect(uri); } } len = preresolves.Length(); for (i = 0; i < len; ++i) { nsCOMPtr uri = preresolves[i]; ++totalPredictions; ++totalPreresolves; nsAutoCString hostname; uri->GetAsciiHost(hostname); PREDICTOR_LOG((" doing preresolve %s", hostname.get())); nsCOMPtr tmpCancelable; mDnsService->AsyncResolveNative(hostname, (nsIDNSService::RESOLVE_PRIORITY_MEDIUM | nsIDNSService::RESOLVE_SPECULATE), mDNSListener, nullptr, originAttributes, getter_AddRefs(tmpCancelable)); bool isHttps; uri->SchemeIs("https", &isHttps); // Fetch esni keys if needed. if (sEsniEnabled && isHttps) { nsAutoCString esniHost; esniHost.Append("_esni."); esniHost.Append(hostname); mDnsService->AsyncResolveByTypeNative( esniHost, nsIDNSService::RESOLVE_TYPE_TXT, (nsIDNSService::RESOLVE_PRIORITY_MEDIUM | nsIDNSService::RESOLVE_SPECULATE), mDNSListener, nullptr, originAttributes, getter_AddRefs(tmpCancelable)); } predicted = true; if (verifier) { PREDICTOR_LOG((" sending preresolve verification")); verifier->OnPredictDNS(uri); } } return predicted; } // Find out if a top-level page is likely to redirect. bool Predictor::WouldRedirect(nsICacheEntry* entry, uint32_t loadCount, uint32_t lastLoad, int32_t globalDegradation, nsIURI** redirectURI) { // TODO - not doing redirects for first go around MOZ_ASSERT(NS_IsMainThread()); return false; } NS_IMETHODIMP Predictor::Learn(nsIURI* targetURI, nsIURI* sourceURI, PredictorLearnReason reason, JS::HandleValue originAttributes, JSContext* aCx) { OriginAttributes attrs; if (!originAttributes.isObject() || !attrs.Init(aCx, originAttributes)) { return NS_ERROR_INVALID_ARG; } return LearnNative(targetURI, sourceURI, reason, attrs); } // Called from the main thread to update the database NS_IMETHODIMP Predictor::LearnNative(nsIURI* targetURI, nsIURI* sourceURI, PredictorLearnReason reason, const OriginAttributes& originAttributes) { MOZ_ASSERT(NS_IsMainThread(), "Predictor interface methods must be called on the main thread"); PREDICTOR_LOG(("Predictor::Learn")); if (IsNeckoChild()) { MOZ_DIAGNOSTIC_ASSERT(gNeckoChild); PREDICTOR_LOG((" called on child process")); RefPtr runnable = new PredictorLearnRunnable( targetURI, sourceURI, reason, originAttributes); SystemGroup::Dispatch(TaskCategory::Other, runnable.forget()); return NS_OK; } PREDICTOR_LOG((" called on parent process")); if (!mInitialized) { PREDICTOR_LOG((" not initialized")); return NS_OK; } if (!StaticPrefs::network_predictor_enabled()) { PREDICTOR_LOG((" not enabled")); return NS_OK; } if (originAttributes.mPrivateBrowsingId > 0) { // Don't want to do anything in PB mode PREDICTOR_LOG((" in PB mode")); return NS_OK; } if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) { PREDICTOR_LOG((" got non-HTTP[S] URI")); return NS_ERROR_INVALID_ARG; } nsCOMPtr targetOrigin; nsCOMPtr sourceOrigin; nsCOMPtr uriKey; nsCOMPtr originKey; nsresult rv; switch (reason) { case nsINetworkPredictor::LEARN_LOAD_TOPLEVEL: if (!targetURI || sourceURI) { PREDICTOR_LOG((" load toplevel invalid URI state")); return NS_ERROR_INVALID_ARG; } rv = ExtractOrigin(targetURI, getter_AddRefs(targetOrigin)); NS_ENSURE_SUCCESS(rv, rv); uriKey = targetURI; originKey = targetOrigin; break; case nsINetworkPredictor::LEARN_STARTUP: if (!targetURI || sourceURI) { PREDICTOR_LOG((" startup invalid URI state")); return NS_ERROR_INVALID_ARG; } rv = ExtractOrigin(targetURI, getter_AddRefs(targetOrigin)); NS_ENSURE_SUCCESS(rv, rv); uriKey = mStartupURI; originKey = mStartupURI; break; case nsINetworkPredictor::LEARN_LOAD_REDIRECT: case nsINetworkPredictor::LEARN_LOAD_SUBRESOURCE: if (!targetURI || !sourceURI) { PREDICTOR_LOG((" redirect/subresource invalid URI state")); return NS_ERROR_INVALID_ARG; } rv = ExtractOrigin(targetURI, getter_AddRefs(targetOrigin)); NS_ENSURE_SUCCESS(rv, rv); rv = ExtractOrigin(sourceURI, getter_AddRefs(sourceOrigin)); NS_ENSURE_SUCCESS(rv, rv); uriKey = sourceURI; originKey = sourceOrigin; break; default: PREDICTOR_LOG((" invalid reason")); return NS_ERROR_INVALID_ARG; } Telemetry::AutoCounter learnAttempts; ++learnAttempts; Predictor::Reason argReason; argReason.mLearn = reason; // We always open the full uri (general cache) entry first, so we don't gum up // the works waiting on predictor-only entries to open RefPtr uriAction = new Predictor::Action( Predictor::Action::IS_FULL_URI, Predictor::Action::DO_LEARN, argReason, targetURI, sourceURI, nullptr, this); nsAutoCString uriKeyStr, targetUriStr, sourceUriStr; uriKey->GetAsciiSpec(uriKeyStr); targetURI->GetAsciiSpec(targetUriStr); if (sourceURI) { sourceURI->GetAsciiSpec(sourceUriStr); } PREDICTOR_LOG( (" Learn uriKey=%s targetURI=%s sourceURI=%s reason=%d " "action=%p", uriKeyStr.get(), targetUriStr.get(), sourceUriStr.get(), reason, uriAction.get())); nsCOMPtr cacheDiskStorage; RefPtr lci = new LoadContextInfo(false, originAttributes); rv = mCacheStorageService->DiskCacheStorage(lci, false, getter_AddRefs(cacheDiskStorage)); NS_ENSURE_SUCCESS(rv, rv); // For learning full URI things, we *always* open readonly and secretly, as we // rely on actual pageloads to update the entry's metadata for us. uint32_t uriOpenFlags = nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY | nsICacheStorage::CHECK_MULTITHREADED; if (reason == nsINetworkPredictor::LEARN_LOAD_TOPLEVEL) { // Learning for toplevel we want to open the full uri entry priority, since // it's likely this entry will be used soon anyway, and we want this to be // opened ASAP. uriOpenFlags |= nsICacheStorage::OPEN_PRIORITY; } cacheDiskStorage->AsyncOpenURI(uriKey, EmptyCString(), uriOpenFlags, uriAction); // Now we open the origin-only (and therefore predictor-only) entry RefPtr originAction = new Predictor::Action( Predictor::Action::IS_ORIGIN, Predictor::Action::DO_LEARN, argReason, targetOrigin, sourceOrigin, nullptr, this); nsAutoCString originKeyStr, targetOriginStr, sourceOriginStr; originKey->GetAsciiSpec(originKeyStr); targetOrigin->GetAsciiSpec(targetOriginStr); if (sourceOrigin) { sourceOrigin->GetAsciiSpec(sourceOriginStr); } PREDICTOR_LOG( (" Learn originKey=%s targetOrigin=%s sourceOrigin=%s reason=%d " "action=%p", originKeyStr.get(), targetOriginStr.get(), sourceOriginStr.get(), reason, originAction.get())); uint32_t originOpenFlags; if (reason == nsINetworkPredictor::LEARN_LOAD_TOPLEVEL) { // This is the only case when we want to update the 'last used' metadata on // the cache entry we're getting. This only applies to predictor-specific // entries. originOpenFlags = nsICacheStorage::OPEN_NORMALLY | nsICacheStorage::CHECK_MULTITHREADED; } else { originOpenFlags = nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY | nsICacheStorage::CHECK_MULTITHREADED; } cacheDiskStorage->AsyncOpenURI(originKey, NS_LITERAL_CSTRING(PREDICTOR_ORIGIN_EXTENSION), originOpenFlags, originAction); PREDICTOR_LOG(("Predictor::Learn returning")); return NS_OK; } void Predictor::LearnInternal(PredictorLearnReason reason, nsICacheEntry* entry, bool isNew, bool fullUri, nsIURI* targetURI, nsIURI* sourceURI) { MOZ_ASSERT(NS_IsMainThread()); PREDICTOR_LOG(("Predictor::LearnInternal")); nsCString junk; if (!fullUri && reason == nsINetworkPredictor::LEARN_LOAD_TOPLEVEL && NS_FAILED( entry->GetMetaDataElement(SEEN_META_DATA, getter_Copies(junk)))) { // This is an origin-only entry that we haven't seen before. Let's mark it // as seen. PREDICTOR_LOG((" marking new origin entry as seen")); nsresult rv = entry->SetMetaDataElement(SEEN_META_DATA, "1"); if (NS_FAILED(rv)) { PREDICTOR_LOG((" failed to mark origin entry seen")); return; } // Need to ensure someone else can get to the entry if necessary entry->MetaDataReady(); return; } switch (reason) { case nsINetworkPredictor::LEARN_LOAD_TOPLEVEL: // This case only exists to be used during tests - code outside the // predictor tests should NEVER call Learn with LEARN_LOAD_TOPLEVEL. // The predictor xpcshell test needs this branch, however, because we // have no real page loads in xpcshell, and this is how we fake it up // so that all the work that normally happens behind the scenes in a // page load can be done for testing purposes. if (fullUri && StaticPrefs::network_predictor_doing_tests()) { PREDICTOR_LOG( (" WARNING - updating rolling load count. " "If you see this outside tests, you did it wrong")); // Since the visitor gets called under a cache lock, all we do there is // get copies of the keys/values we care about, and then do the real // work here entry->VisitMetaData(this); nsTArray keysToOperateOn, valuesToOperateOn; keysToOperateOn.SwapElements(mKeysToOperateOn); valuesToOperateOn.SwapElements(mValuesToOperateOn); MOZ_ASSERT(keysToOperateOn.Length() == valuesToOperateOn.Length()); for (size_t i = 0; i < keysToOperateOn.Length(); ++i) { const char* key = keysToOperateOn[i].BeginReading(); const char* value = valuesToOperateOn[i].BeginReading(); nsCString uri; uint32_t hitCount, lastHit, flags; if (!ParseMetaDataEntry(key, value, uri, hitCount, lastHit, flags)) { // This failed, get rid of it so we don't waste space entry->SetMetaDataElement(key, nullptr); continue; } UpdateRollingLoadCount(entry, flags, key, hitCount, lastHit); } } else { PREDICTOR_LOG((" nothing to do for toplevel")); } break; case nsINetworkPredictor::LEARN_LOAD_REDIRECT: if (fullUri) { LearnForRedirect(entry, targetURI); } break; case nsINetworkPredictor::LEARN_LOAD_SUBRESOURCE: LearnForSubresource(entry, targetURI); break; case nsINetworkPredictor::LEARN_STARTUP: LearnForStartup(entry, targetURI); break; default: PREDICTOR_LOG((" unexpected reason value")); MOZ_ASSERT(false, "Got unexpected value for learn reason!"); } } NS_IMPL_ISUPPORTS(Predictor::SpaceCleaner, nsICacheEntryMetaDataVisitor) NS_IMETHODIMP Predictor::SpaceCleaner::OnMetaDataElement(const char* key, const char* value) { MOZ_ASSERT(NS_IsMainThread()); if (!IsURIMetadataElement(key)) { // This isn't a bit of metadata we care about return NS_OK; } nsCString uri; uint32_t hitCount, lastHit, flags; bool ok = mPredictor->ParseMetaDataEntry(key, value, uri, hitCount, lastHit, flags); if (!ok) { // Couldn't parse this one, just get rid of it nsCString nsKey; nsKey.AssignASCII(key); mLongKeysToDelete.AppendElement(nsKey); return NS_OK; } uint32_t uriLength = uri.Length(); if (uriLength > StaticPrefs::network_predictor_max_uri_length()) { // Default to getting rid of URIs that are too long and were put in before // we had our limit on URI length, in order to free up some space. nsCString nsKey; nsKey.AssignASCII(key); mLongKeysToDelete.AppendElement(nsKey); return NS_OK; } if (!mLRUKeyToDelete || lastHit < mLRUStamp) { mLRUKeyToDelete = key; mLRUStamp = lastHit; } return NS_OK; } void Predictor::SpaceCleaner::Finalize(nsICacheEntry* entry) { MOZ_ASSERT(NS_IsMainThread()); if (mLRUKeyToDelete) { entry->SetMetaDataElement(mLRUKeyToDelete, nullptr); } for (size_t i = 0; i < mLongKeysToDelete.Length(); ++i) { entry->SetMetaDataElement(mLongKeysToDelete[i].BeginReading(), nullptr); } } // Called when a subresource has been hit from a top-level load. Uses the two // helper functions above to update the database appropriately. void Predictor::LearnForSubresource(nsICacheEntry* entry, nsIURI* targetURI) { MOZ_ASSERT(NS_IsMainThread()); PREDICTOR_LOG(("Predictor::LearnForSubresource")); uint32_t lastLoad; nsresult rv = entry->GetLastFetched(&lastLoad); RETURN_IF_FAILED(rv); int32_t loadCount; rv = entry->GetFetchCount(&loadCount); RETURN_IF_FAILED(rv); nsCString key; key.AssignLiteral(META_DATA_PREFIX); nsCString uri; targetURI->GetAsciiSpec(uri); key.Append(uri); if (uri.Length() > StaticPrefs::network_predictor_max_uri_length()) { // We do this to conserve space/prevent OOMs PREDICTOR_LOG((" uri too long!")); entry->SetMetaDataElement(key.BeginReading(), nullptr); return; } nsCString value; rv = entry->GetMetaDataElement(key.BeginReading(), getter_Copies(value)); uint32_t hitCount, lastHit, flags; bool isNewResource = (NS_FAILED(rv) || !ParseMetaDataEntry(key.BeginReading(), value.BeginReading(), uri, hitCount, lastHit, flags)); int32_t resourceCount = 0; if (isNewResource) { // This is a new addition PREDICTOR_LOG((" new resource")); nsCString s; rv = entry->GetMetaDataElement(RESOURCE_META_DATA, getter_Copies(s)); if (NS_SUCCEEDED(rv)) { resourceCount = atoi(s.BeginReading()); } if (resourceCount >= StaticPrefs::network_predictor_max_resources_per_entry()) { RefPtr cleaner = new Predictor::SpaceCleaner(this); entry->VisitMetaData(cleaner); cleaner->Finalize(entry); } else { ++resourceCount; } nsAutoCString count; count.AppendInt(resourceCount); rv = entry->SetMetaDataElement(RESOURCE_META_DATA, count.BeginReading()); if (NS_FAILED(rv)) { PREDICTOR_LOG((" failed to update resource count")); return; } hitCount = 1; flags = 0; } else { PREDICTOR_LOG((" existing resource")); hitCount = std::min(hitCount + 1, static_cast(loadCount)); } // Update the rolling load count to mark this sub-resource as seen on the // most-recent pageload so it can be eligible for prefetch (assuming all // the other stars align). flags |= (1 << kRollingLoadOffset); nsCString newValue; MakeMetadataEntry(hitCount, lastLoad, flags, newValue); rv = entry->SetMetaDataElement(key.BeginReading(), newValue.BeginReading()); PREDICTOR_LOG( (" SetMetaDataElement -> 0x%08" PRIX32, static_cast(rv))); if (NS_FAILED(rv) && isNewResource) { // Roll back the increment to the resource count we made above. PREDICTOR_LOG((" rolling back resource count update")); --resourceCount; if (resourceCount == 0) { entry->SetMetaDataElement(RESOURCE_META_DATA, nullptr); } else { nsAutoCString count; count.AppendInt(resourceCount); entry->SetMetaDataElement(RESOURCE_META_DATA, count.BeginReading()); } } } // This is called when a top-level loaded ended up redirecting to a different // URI so we can keep track of that fact. void Predictor::LearnForRedirect(nsICacheEntry* entry, nsIURI* targetURI) { MOZ_ASSERT(NS_IsMainThread()); // TODO - not doing redirects for first go around PREDICTOR_LOG(("Predictor::LearnForRedirect")); } // This will add a page to our list of startup pages if it's being loaded // before our startup window has expired. void Predictor::MaybeLearnForStartup(nsIURI* uri, bool fullUri, const OriginAttributes& originAttributes) { MOZ_ASSERT(NS_IsMainThread()); // TODO - not doing startup for first go around PREDICTOR_LOG(("Predictor::MaybeLearnForStartup")); } // Add information about a top-level load to our list of startup pages void Predictor::LearnForStartup(nsICacheEntry* entry, nsIURI* targetURI) { MOZ_ASSERT(NS_IsMainThread()); // These actually do the same set of work, just on different entries, so we // can pass through to get the real work done here PREDICTOR_LOG(("Predictor::LearnForStartup")); LearnForSubresource(entry, targetURI); } bool Predictor::ParseMetaDataEntry(const char* key, const char* value, nsCString& uri, uint32_t& hitCount, uint32_t& lastHit, uint32_t& flags) { MOZ_ASSERT(NS_IsMainThread()); PREDICTOR_LOG( ("Predictor::ParseMetaDataEntry key=%s value=%s", key ? key : "", value)); const char* comma = strchr(value, ','); if (!comma) { PREDICTOR_LOG((" could not find first comma")); return false; } uint32_t version = static_cast(atoi(value)); PREDICTOR_LOG((" version -> %u", version)); if (version != METADATA_VERSION) { PREDICTOR_LOG( (" metadata version mismatch %u != %u", version, METADATA_VERSION)); return false; } value = comma + 1; comma = strchr(value, ','); if (!comma) { PREDICTOR_LOG((" could not find second comma")); return false; } hitCount = static_cast(atoi(value)); PREDICTOR_LOG((" hitCount -> %u", hitCount)); value = comma + 1; comma = strchr(value, ','); if (!comma) { PREDICTOR_LOG((" could not find third comma")); return false; } lastHit = static_cast(atoi(value)); PREDICTOR_LOG((" lastHit -> %u", lastHit)); value = comma + 1; flags = static_cast(atoi(value)); PREDICTOR_LOG((" flags -> %u", flags)); if (key) { const char* uriStart = key + (sizeof(META_DATA_PREFIX) - 1); uri.AssignASCII(uriStart); PREDICTOR_LOG((" uri -> %s", uriStart)); } else { uri.Truncate(); } return true; } NS_IMETHODIMP Predictor::Reset() { MOZ_ASSERT(NS_IsMainThread(), "Predictor interface methods must be called on the main thread"); PREDICTOR_LOG(("Predictor::Reset")); if (IsNeckoChild()) { MOZ_DIAGNOSTIC_ASSERT(gNeckoChild); PREDICTOR_LOG((" forwarding to parent process")); gNeckoChild->SendPredReset(); return NS_OK; } PREDICTOR_LOG((" called on parent process")); if (!mInitialized) { PREDICTOR_LOG((" not initialized")); return NS_OK; } if (!StaticPrefs::network_predictor_enabled()) { PREDICTOR_LOG((" not enabled")); return NS_OK; } RefPtr reset = new Predictor::Resetter(this); PREDICTOR_LOG((" created a resetter")); mCacheStorageService->AsyncVisitAllStorages(reset, true); PREDICTOR_LOG((" Cache async launched, returning now")); return NS_OK; } NS_IMPL_ISUPPORTS(Predictor::Resetter, nsICacheEntryOpenCallback, nsICacheEntryMetaDataVisitor, nsICacheStorageVisitor); Predictor::Resetter::Resetter(Predictor* predictor) : mEntriesToVisit(0), mPredictor(predictor) {} NS_IMETHODIMP Predictor::Resetter::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appCache, uint32_t* result) { *result = nsICacheEntryOpenCallback::ENTRY_WANTED; return NS_OK; } NS_IMETHODIMP Predictor::Resetter::OnCacheEntryAvailable(nsICacheEntry* entry, bool isNew, nsIApplicationCache* appCache, nsresult result) { MOZ_ASSERT(NS_IsMainThread()); if (NS_FAILED(result)) { // This can happen when we've tried to open an entry that doesn't exist for // some non-reset operation, and then get reset shortly thereafter (as // happens in some of our tests). --mEntriesToVisit; if (!mEntriesToVisit) { Complete(); } return NS_OK; } entry->VisitMetaData(this); nsTArray keysToDelete; keysToDelete.SwapElements(mKeysToDelete); for (size_t i = 0; i < keysToDelete.Length(); ++i) { const char* key = keysToDelete[i].BeginReading(); entry->SetMetaDataElement(key, nullptr); } --mEntriesToVisit; if (!mEntriesToVisit) { Complete(); } return NS_OK; } NS_IMETHODIMP Predictor::Resetter::OnMetaDataElement(const char* asciiKey, const char* asciiValue) { MOZ_ASSERT(NS_IsMainThread()); if (!StringBeginsWith(nsDependentCString(asciiKey), NS_LITERAL_CSTRING(META_DATA_PREFIX))) { // Not a metadata entry we care about, carry on return NS_OK; } nsCString key; key.AssignASCII(asciiKey); mKeysToDelete.AppendElement(key); return NS_OK; } NS_IMETHODIMP Predictor::Resetter::OnCacheStorageInfo(uint32_t entryCount, uint64_t consumption, uint64_t capacity, nsIFile* diskDirectory) { MOZ_ASSERT(NS_IsMainThread()); return NS_OK; } NS_IMETHODIMP Predictor::Resetter::OnCacheEntryInfo(nsIURI* uri, const nsACString& idEnhance, int64_t dataSize, int32_t fetchCount, uint32_t lastModifiedTime, uint32_t expirationTime, bool aPinned, nsILoadContextInfo* aInfo) { MOZ_ASSERT(NS_IsMainThread()); nsresult rv; // The predictor will only ever touch entries with no idEnhance ("") or an // idEnhance of PREDICTOR_ORIGIN_EXTENSION, so we filter out any entries that // don't match that to avoid doing extra work. if (idEnhance.EqualsLiteral(PREDICTOR_ORIGIN_EXTENSION)) { // This is an entry we own, so we can just doom it entirely nsCOMPtr cacheDiskStorage; rv = mPredictor->mCacheStorageService->DiskCacheStorage( aInfo, false, getter_AddRefs(cacheDiskStorage)); NS_ENSURE_SUCCESS(rv, rv); cacheDiskStorage->AsyncDoomURI(uri, idEnhance, nullptr); } else if (idEnhance.IsEmpty()) { // This is an entry we don't own, so we have to be a little more careful and // just get rid of our own metadata entries. Append it to an array of things // to operate on and then do the operations later so we don't end up calling // Complete() multiple times/too soon. ++mEntriesToVisit; mURIsToVisit.AppendElement(uri); mInfosToVisit.AppendElement(aInfo); } return NS_OK; } NS_IMETHODIMP Predictor::Resetter::OnCacheEntryVisitCompleted() { MOZ_ASSERT(NS_IsMainThread()); nsresult rv; nsTArray> urisToVisit; urisToVisit.SwapElements(mURIsToVisit); MOZ_ASSERT(mEntriesToVisit == urisToVisit.Length()); nsTArray> infosToVisit; infosToVisit.SwapElements(mInfosToVisit); MOZ_ASSERT(mEntriesToVisit == infosToVisit.Length()); if (!mEntriesToVisit) { Complete(); return NS_OK; } uint32_t entriesToVisit = urisToVisit.Length(); for (uint32_t i = 0; i < entriesToVisit; ++i) { nsCString u; nsCOMPtr cacheDiskStorage; rv = mPredictor->mCacheStorageService->DiskCacheStorage( infosToVisit[i], false, getter_AddRefs(cacheDiskStorage)); NS_ENSURE_SUCCESS(rv, rv); urisToVisit[i]->GetAsciiSpec(u); cacheDiskStorage->AsyncOpenURI(urisToVisit[i], EmptyCString(), nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY | nsICacheStorage::CHECK_MULTITHREADED, this); } return NS_OK; } void Predictor::Resetter::Complete() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) { PREDICTOR_LOG(("COULD NOT GET OBSERVER SERVICE!")); return; } obs->NotifyObservers(nullptr, "predictor-reset-complete", nullptr); } // Helper functions to make using the predictor easier from native code static StaticRefPtr sPredictor; static nsresult EnsureGlobalPredictor(nsINetworkPredictor** aPredictor) { MOZ_ASSERT(NS_IsMainThread()); if (!sPredictor) { nsresult rv; nsCOMPtr predictor = do_GetService("@mozilla.org/network/predictor;1", &rv); NS_ENSURE_SUCCESS(rv, rv); sPredictor = predictor; ClearOnShutdown(&sPredictor); } nsCOMPtr predictor = sPredictor.get(); predictor.forget(aPredictor); return NS_OK; } nsresult PredictorPredict(nsIURI* targetURI, nsIURI* sourceURI, PredictorPredictReason reason, const OriginAttributes& originAttributes, nsINetworkPredictorVerifier* verifier) { MOZ_ASSERT(NS_IsMainThread()); if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) { return NS_OK; } nsCOMPtr predictor; nsresult rv = EnsureGlobalPredictor(getter_AddRefs(predictor)); NS_ENSURE_SUCCESS(rv, rv); return predictor->PredictNative(targetURI, sourceURI, reason, originAttributes, verifier); } nsresult PredictorLearn(nsIURI* targetURI, nsIURI* sourceURI, PredictorLearnReason reason, const OriginAttributes& originAttributes) { MOZ_ASSERT(NS_IsMainThread()); if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) { return NS_OK; } nsCOMPtr predictor; nsresult rv = EnsureGlobalPredictor(getter_AddRefs(predictor)); NS_ENSURE_SUCCESS(rv, rv); return predictor->LearnNative(targetURI, sourceURI, reason, originAttributes); } nsresult PredictorLearn(nsIURI* targetURI, nsIURI* sourceURI, PredictorLearnReason reason, nsILoadGroup* loadGroup) { MOZ_ASSERT(NS_IsMainThread()); if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) { return NS_OK; } nsCOMPtr predictor; nsresult rv = EnsureGlobalPredictor(getter_AddRefs(predictor)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr loadContext; OriginAttributes originAttributes; if (loadGroup) { nsCOMPtr callbacks; loadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks)); if (callbacks) { loadContext = do_GetInterface(callbacks); if (loadContext) { loadContext->GetOriginAttributes(originAttributes); } } } return predictor->LearnNative(targetURI, sourceURI, reason, originAttributes); } nsresult PredictorLearn(nsIURI* targetURI, nsIURI* sourceURI, PredictorLearnReason reason, dom::Document* document) { MOZ_ASSERT(NS_IsMainThread()); if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) { return NS_OK; } nsCOMPtr predictor; nsresult rv = EnsureGlobalPredictor(getter_AddRefs(predictor)); NS_ENSURE_SUCCESS(rv, rv); OriginAttributes originAttributes; if (document) { nsCOMPtr docPrincipal = document->NodePrincipal(); if (docPrincipal) { originAttributes = docPrincipal->OriginAttributesRef(); } } return predictor->LearnNative(targetURI, sourceURI, reason, originAttributes); } nsresult PredictorLearnRedirect(nsIURI* targetURI, nsIChannel* channel, const OriginAttributes& originAttributes) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr sourceURI; nsresult rv = channel->GetOriginalURI(getter_AddRefs(sourceURI)); NS_ENSURE_SUCCESS(rv, rv); bool sameUri; rv = targetURI->Equals(sourceURI, &sameUri); NS_ENSURE_SUCCESS(rv, rv); if (sameUri) { return NS_OK; } if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) { return NS_OK; } nsCOMPtr predictor; rv = EnsureGlobalPredictor(getter_AddRefs(predictor)); NS_ENSURE_SUCCESS(rv, rv); return predictor->LearnNative(targetURI, sourceURI, nsINetworkPredictor::LEARN_LOAD_REDIRECT, originAttributes); } // nsINetworkPredictorVerifier /** * Call through to the child's verifier (only during tests) */ NS_IMETHODIMP Predictor::OnPredictPrefetch(nsIURI* aURI, uint32_t httpStatus) { if (IsNeckoChild()) { if (mChildVerifier) { // Ideally, we'd assert here. But since we're slowly moving towards a // world where we have multiple child processes, and only one child // process will be likely to have a verifier, we have to play it safer. return mChildVerifier->OnPredictPrefetch(aURI, httpStatus); } return NS_OK; } ipc::URIParams serURI; SerializeURI(aURI, serURI); for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { PNeckoParent* neckoParent = SingleManagedOrNull(cp->ManagedPNeckoParent()); if (!neckoParent) { continue; } if (!neckoParent->SendPredOnPredictPrefetch(serURI, httpStatus)) { return NS_ERROR_NOT_AVAILABLE; } } return NS_OK; } NS_IMETHODIMP Predictor::OnPredictPreconnect(nsIURI* aURI) { if (IsNeckoChild()) { if (mChildVerifier) { // Ideally, we'd assert here. But since we're slowly moving towards a // world where we have multiple child processes, and only one child // process will be likely to have a verifier, we have to play it safer. return mChildVerifier->OnPredictPreconnect(aURI); } return NS_OK; } ipc::URIParams serURI; SerializeURI(aURI, serURI); for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { PNeckoParent* neckoParent = SingleManagedOrNull(cp->ManagedPNeckoParent()); if (!neckoParent) { continue; } if (!neckoParent->SendPredOnPredictPreconnect(serURI)) { return NS_ERROR_NOT_AVAILABLE; } } return NS_OK; } NS_IMETHODIMP Predictor::OnPredictDNS(nsIURI* aURI) { if (IsNeckoChild()) { if (mChildVerifier) { // Ideally, we'd assert here. But since we're slowly moving towards a // world where we have multiple child processes, and only one child // process will be likely to have a verifier, we have to play it safer. return mChildVerifier->OnPredictDNS(aURI); } return NS_OK; } ipc::URIParams serURI; SerializeURI(aURI, serURI); for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { PNeckoParent* neckoParent = SingleManagedOrNull(cp->ManagedPNeckoParent()); if (!neckoParent) { continue; } if (!neckoParent->SendPredOnPredictDNS(serURI)) { return NS_ERROR_NOT_AVAILABLE; } } return NS_OK; } // Predictor::PrefetchListener // nsISupports NS_IMPL_ISUPPORTS(Predictor::PrefetchListener, nsIStreamListener, nsIRequestObserver) // nsIRequestObserver NS_IMETHODIMP Predictor::PrefetchListener::OnStartRequest(nsIRequest* aRequest) { mStartTime = TimeStamp::Now(); return NS_OK; } NS_IMETHODIMP Predictor::PrefetchListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { PREDICTOR_LOG(("OnStopRequest this=%p aStatusCode=0x%" PRIX32, this, static_cast(aStatusCode))); NS_ENSURE_ARG(aRequest); if (NS_FAILED(aStatusCode)) { return aStatusCode; } Telemetry::AccumulateTimeDelta(Telemetry::PREDICTOR_PREFETCH_TIME, mStartTime); nsCOMPtr httpChannel = do_QueryInterface(aRequest); if (!httpChannel) { PREDICTOR_LOG((" Could not get HTTP Channel!")); return NS_ERROR_UNEXPECTED; } nsCOMPtr cachingChannel = do_QueryInterface(httpChannel); if (!cachingChannel) { PREDICTOR_LOG((" Could not get caching channel!")); return NS_ERROR_UNEXPECTED; } nsresult rv = NS_OK; uint32_t httpStatus; rv = httpChannel->GetResponseStatus(&httpStatus); if (NS_SUCCEEDED(rv) && httpStatus == 200) { rv = cachingChannel->ForceCacheEntryValidFor( StaticPrefs::network_predictor_prefetch_force_valid_for()); PREDICTOR_LOG((" forcing entry valid for %d seconds rv=%" PRIX32, StaticPrefs::network_predictor_prefetch_force_valid_for(), static_cast(rv))); } else { rv = cachingChannel->ForceCacheEntryValidFor(0); PREDICTOR_LOG((" removing any forced validity rv=%" PRIX32, static_cast(rv))); } nsAutoCString reqName; rv = aRequest->GetName(reqName); if (NS_FAILED(rv)) { reqName.AssignLiteral(""); } PREDICTOR_LOG((" request %s status %u", reqName.get(), httpStatus)); if (mVerifier) { mVerifier->OnPredictPrefetch(mURI, httpStatus); } return rv; } // nsIStreamListener NS_IMETHODIMP Predictor::PrefetchListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream, uint64_t aOffset, const uint32_t aCount) { uint32_t result; return aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &result); } // Miscellaneous Predictor void Predictor::UpdateCacheability(nsIURI* sourceURI, nsIURI* targetURI, uint32_t httpStatus, nsHttpRequestHead& requestHead, nsHttpResponseHead* responseHead, nsILoadContextInfo* lci, bool isTracking) { MOZ_ASSERT(NS_IsMainThread()); if (lci && lci->IsPrivate()) { PREDICTOR_LOG(("Predictor::UpdateCacheability in PB mode - ignoring")); return; } if (!sourceURI || !targetURI) { PREDICTOR_LOG( ("Predictor::UpdateCacheability missing source or target uri")); return; } if (!IsNullOrHttp(sourceURI) || !IsNullOrHttp(targetURI)) { PREDICTOR_LOG(("Predictor::UpdateCacheability non-http(s) uri")); return; } RefPtr self = sSelf; if (self) { nsAutoCString method; requestHead.Method(method); nsAutoCString vary; Unused << responseHead->GetHeader(nsHttp::Vary, vary); nsAutoCString cacheControlHeader; Unused << responseHead->GetHeader(nsHttp::Cache_Control, cacheControlHeader); CacheControlParser cacheControl(cacheControlHeader); self->UpdateCacheabilityInternal(sourceURI, targetURI, httpStatus, method, *lci->OriginAttributesPtr(), isTracking, !vary.IsEmpty(), cacheControl.NoStore()); } } void Predictor::UpdateCacheabilityInternal( nsIURI* sourceURI, nsIURI* targetURI, uint32_t httpStatus, const nsCString& method, const OriginAttributes& originAttributes, bool isTracking, bool couldVary, bool isNoStore) { PREDICTOR_LOG(("Predictor::UpdateCacheability httpStatus=%u", httpStatus)); nsresult rv; if (!mInitialized) { PREDICTOR_LOG((" not initialized")); return; } if (!StaticPrefs::network_predictor_enabled()) { PREDICTOR_LOG((" not enabled")); return; } nsCOMPtr cacheDiskStorage; RefPtr lci = new LoadContextInfo(false, originAttributes); rv = mCacheStorageService->DiskCacheStorage(lci, false, getter_AddRefs(cacheDiskStorage)); if (NS_FAILED(rv)) { PREDICTOR_LOG((" cannot get disk cache storage")); return; } uint32_t openFlags = nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY | nsICacheStorage::CHECK_MULTITHREADED; RefPtr action = new Predictor::CacheabilityAction(targetURI, httpStatus, method, isTracking, couldVary, isNoStore, this); nsAutoCString uri; targetURI->GetAsciiSpec(uri); PREDICTOR_LOG((" uri=%s action=%p", uri.get(), action.get())); cacheDiskStorage->AsyncOpenURI(sourceURI, EmptyCString(), openFlags, action); } NS_IMPL_ISUPPORTS(Predictor::CacheabilityAction, nsICacheEntryOpenCallback, nsICacheEntryMetaDataVisitor); NS_IMETHODIMP Predictor::CacheabilityAction::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appCache, uint32_t* result) { *result = nsICacheEntryOpenCallback::ENTRY_WANTED; return NS_OK; } namespace { enum PrefetchDecisionReason { PREFETCHABLE, STATUS_NOT_200, METHOD_NOT_GET, URL_HAS_QUERY_STRING, RESOURCE_IS_TRACKING, RESOURCE_COULD_VARY, RESOURCE_IS_NO_STORE }; } NS_IMETHODIMP Predictor::CacheabilityAction::OnCacheEntryAvailable( nsICacheEntry* entry, bool isNew, nsIApplicationCache* appCache, nsresult result) { MOZ_ASSERT(NS_IsMainThread()); // This is being opened read-only, so isNew should always be false MOZ_ASSERT(!isNew); PREDICTOR_LOG(("CacheabilityAction::OnCacheEntryAvailable this=%p", this)); if (NS_FAILED(result)) { // Nothing to do PREDICTOR_LOG((" nothing to do result=%" PRIX32 " isNew=%d", static_cast(result), isNew)); return NS_OK; } nsCString strTargetURI; nsresult rv = mTargetURI->GetAsciiSpec(strTargetURI); if (NS_FAILED(rv)) { PREDICTOR_LOG( (" GetAsciiSpec returned %" PRIx32, static_cast(rv))); return NS_OK; } rv = entry->VisitMetaData(this); if (NS_FAILED(rv)) { PREDICTOR_LOG( (" VisitMetaData returned %" PRIx32, static_cast(rv))); return NS_OK; } nsTArray keysToCheck, valuesToCheck; keysToCheck.SwapElements(mKeysToCheck); valuesToCheck.SwapElements(mValuesToCheck); bool hasQueryString = false; nsAutoCString query; if (NS_SUCCEEDED(mTargetURI->GetQuery(query)) && !query.IsEmpty()) { hasQueryString = true; } MOZ_ASSERT(keysToCheck.Length() == valuesToCheck.Length()); for (size_t i = 0; i < keysToCheck.Length(); ++i) { const char* key = keysToCheck[i].BeginReading(); const char* value = valuesToCheck[i].BeginReading(); nsCString uri; uint32_t hitCount, lastHit, flags; if (!mPredictor->ParseMetaDataEntry(key, value, uri, hitCount, lastHit, flags)) { PREDICTOR_LOG((" failed to parse key=%s value=%s", key, value)); continue; } if (strTargetURI.Equals(uri)) { bool prefetchable = true; PrefetchDecisionReason reason = PREFETCHABLE; if (mHttpStatus != 200) { prefetchable = false; reason = STATUS_NOT_200; } else if (!mMethod.EqualsLiteral("GET")) { prefetchable = false; reason = METHOD_NOT_GET; } else if (hasQueryString) { prefetchable = false; reason = URL_HAS_QUERY_STRING; } else if (mIsTracking) { prefetchable = false; reason = RESOURCE_IS_TRACKING; } else if (mCouldVary) { prefetchable = false; reason = RESOURCE_COULD_VARY; } else if (mIsNoStore) { // We don't set prefetchable = false yet, because we just want to know // what kind of effect this would have on prefetching. reason = RESOURCE_IS_NO_STORE; } Telemetry::Accumulate(Telemetry::PREDICTOR_PREFETCH_DECISION_REASON, reason); if (prefetchable) { PREDICTOR_LOG((" marking %s cacheable", key)); flags |= FLAG_PREFETCHABLE; } else { PREDICTOR_LOG((" marking %s uncacheable", key)); flags &= ~FLAG_PREFETCHABLE; } nsCString newValue; MakeMetadataEntry(hitCount, lastHit, flags, newValue); entry->SetMetaDataElement(key, newValue.BeginReading()); break; } } return NS_OK; } NS_IMETHODIMP Predictor::CacheabilityAction::OnMetaDataElement(const char* asciiKey, const char* asciiValue) { MOZ_ASSERT(NS_IsMainThread()); if (!IsURIMetadataElement(asciiKey)) { return NS_OK; } nsCString key, value; key.AssignASCII(asciiKey); value.AssignASCII(asciiValue); mKeysToCheck.AppendElement(key); mValuesToCheck.AppendElement(value); return NS_OK; } } // namespace net } // namespace mozilla