Bug 1016628 - Add prefetch abilities to the predictor. r=mayhemer
authorNicholas Hurley <hurley@todesschaf.org>
Thu, 14 Apr 2016 19:26:58 -0700
changeset 294375 b3b5be1d2027f915c3c54b966fb41de8444923ee
parent 294374 a603640aa82d54b224b2325d8a445fd14dd00bc6
child 294376 bba81310ba7bdcadbbfee29c09bfb1fca9ac274e
push id30203
push usercbook@mozilla.com
push dateFri, 22 Apr 2016 13:56:37 +0000
treeherdermozilla-central@fc15477ce628 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer
bugs1016628
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1016628 - Add prefetch abilities to the predictor. r=mayhemer
modules/libpref/init/all.js
netwerk/base/Predictor.cpp
netwerk/base/Predictor.h
netwerk/base/moz.build
netwerk/base/nsICachingChannel.idl
netwerk/base/nsINetworkPredictorVerifier.idl
netwerk/cache2/CacheStorageService.cpp
netwerk/cache2/nsICacheEntry.idl
netwerk/ipc/NeckoChild.cpp
netwerk/ipc/NeckoChild.h
netwerk/ipc/PNecko.ipdl
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/HttpBaseChannel.h
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsIHttpChannel.idl
netwerk/test/unit/test_predictor.js
netwerk/test/unit_ipc/test_predictor_wrap.js
toolkit/components/telemetry/Histograms.json
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1769,29 +1769,33 @@ pref("network.ftp.idleConnectionTimeout"
 pref("network.dir.format", 2);
 
 // enables the prefetch service (i.e., prefetching of <link rel="next"> URLs).
 pref("network.prefetch-next", true);
 
 // enables the predictive service
 pref("network.predictor.enabled", true);
 pref("network.predictor.enable-hover-on-ssl", false);
+pref("network.predictor.enable-prefetch", true);
 pref("network.predictor.page-degradation.day", 0);
 pref("network.predictor.page-degradation.week", 5);
 pref("network.predictor.page-degradation.month", 10);
 pref("network.predictor.page-degradation.year", 25);
 pref("network.predictor.page-degradation.max", 50);
 pref("network.predictor.subresource-degradation.day", 1);
 pref("network.predictor.subresource-degradation.week", 10);
 pref("network.predictor.subresource-degradation.month", 25);
 pref("network.predictor.subresource-degradation.year", 50);
 pref("network.predictor.subresource-degradation.max", 100);
+pref("network.predictor.prefetch-rolling-load-count", 10);
+pref("network.predictor.prefetch-min-confidence", 100);
 pref("network.predictor.preconnect-min-confidence", 90);
 pref("network.predictor.preresolve-min-confidence", 60);
 pref("network.predictor.redirect-likely-confidence", 75);
+pref("network.predictor.prefetch-force-valid-for", 10);
 pref("network.predictor.max-resources-per-entry", 100);
 pref("network.predictor.max-uri-length", 500);
 pref("network.predictor.cleaned-up", false);
 
 // The following prefs pertain to the negotiate-auth extension (see bug 17578),
 // which provides transparent Kerberos or NTLM authentication using the SPNEGO
 // protocol.  Each pref is a comma-separated list of keys, where each key has
 // the format:
--- a/netwerk/base/Predictor.cpp
+++ b/netwerk/base/Predictor.cpp
@@ -5,34 +5,40 @@
 
 #include <algorithm>
 
 #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 "nsIDocument.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"
 #ifdef MOZ_NUWA_PROCESS
 #include "ipc/Nuwa.h"
 #endif
 #include "mozilla/Logging.h"
 
 #include "mozilla/Preferences.h"
 #include "mozilla/Telemetry.h"
@@ -66,82 +72,104 @@ static LazyLogModule gPredictorLog("Netw
     if (NS_FAILED(_rv)) { \
       return; \
     } \
   } while (0)
 
 #define NOW_IN_SECONDS() static_cast<uint32_t>(PR_Now() / PR_USEC_PER_SEC)
 
 
-const char PREDICTOR_ENABLED_PREF[] = "network.predictor.enabled";
-const char PREDICTOR_SSL_HOVER_PREF[] = "network.predictor.enable-hover-on-ssl";
+static const char PREDICTOR_ENABLED_PREF[] = "network.predictor.enabled";
+static const char PREDICTOR_SSL_HOVER_PREF[] = "network.predictor.enable-hover-on-ssl";
+static const char PREDICTOR_PREFETCH_PREF[] = "network.predictor.enable-prefetch";
 
-const char PREDICTOR_PAGE_DELTA_DAY_PREF[] =
+static const char PREDICTOR_PAGE_DELTA_DAY_PREF[] =
   "network.predictor.page-degradation.day";
-const int32_t PREDICTOR_PAGE_DELTA_DAY_DEFAULT = 0;
-const char PREDICTOR_PAGE_DELTA_WEEK_PREF[] =
+static const int32_t PREDICTOR_PAGE_DELTA_DAY_DEFAULT = 0;
+static const char PREDICTOR_PAGE_DELTA_WEEK_PREF[] =
   "network.predictor.page-degradation.week";
-const int32_t PREDICTOR_PAGE_DELTA_WEEK_DEFAULT = 5;
-const char PREDICTOR_PAGE_DELTA_MONTH_PREF[] =
+static const int32_t PREDICTOR_PAGE_DELTA_WEEK_DEFAULT = 5;
+static const char PREDICTOR_PAGE_DELTA_MONTH_PREF[] =
   "network.predictor.page-degradation.month";
-const int32_t PREDICTOR_PAGE_DELTA_MONTH_DEFAULT = 10;
-const char PREDICTOR_PAGE_DELTA_YEAR_PREF[] =
+static const int32_t PREDICTOR_PAGE_DELTA_MONTH_DEFAULT = 10;
+static const char PREDICTOR_PAGE_DELTA_YEAR_PREF[] =
   "network.predictor.page-degradation.year";
-const int32_t PREDICTOR_PAGE_DELTA_YEAR_DEFAULT = 25;
-const char PREDICTOR_PAGE_DELTA_MAX_PREF[] =
+static const int32_t PREDICTOR_PAGE_DELTA_YEAR_DEFAULT = 25;
+static const char PREDICTOR_PAGE_DELTA_MAX_PREF[] =
   "network.predictor.page-degradation.max";
-const int32_t PREDICTOR_PAGE_DELTA_MAX_DEFAULT = 50;
-const char PREDICTOR_SUB_DELTA_DAY_PREF[] =
+static const int32_t PREDICTOR_PAGE_DELTA_MAX_DEFAULT = 50;
+static const char PREDICTOR_SUB_DELTA_DAY_PREF[] =
   "network.predictor.subresource-degradation.day";
-const int32_t PREDICTOR_SUB_DELTA_DAY_DEFAULT = 1;
-const char PREDICTOR_SUB_DELTA_WEEK_PREF[] =
+static const int32_t PREDICTOR_SUB_DELTA_DAY_DEFAULT = 1;
+static const char PREDICTOR_SUB_DELTA_WEEK_PREF[] =
   "network.predictor.subresource-degradation.week";
-const int32_t PREDICTOR_SUB_DELTA_WEEK_DEFAULT = 10;
-const char PREDICTOR_SUB_DELTA_MONTH_PREF[] =
+static const int32_t PREDICTOR_SUB_DELTA_WEEK_DEFAULT = 10;
+static const char PREDICTOR_SUB_DELTA_MONTH_PREF[] =
   "network.predictor.subresource-degradation.month";
-const int32_t PREDICTOR_SUB_DELTA_MONTH_DEFAULT = 25;
-const char PREDICTOR_SUB_DELTA_YEAR_PREF[] =
+static const int32_t PREDICTOR_SUB_DELTA_MONTH_DEFAULT = 25;
+static const char PREDICTOR_SUB_DELTA_YEAR_PREF[] =
   "network.predictor.subresource-degradation.year";
-const int32_t PREDICTOR_SUB_DELTA_YEAR_DEFAULT = 50;
-const char PREDICTOR_SUB_DELTA_MAX_PREF[] =
+static const int32_t PREDICTOR_SUB_DELTA_YEAR_DEFAULT = 50;
+static const char PREDICTOR_SUB_DELTA_MAX_PREF[] =
   "network.predictor.subresource-degradation.max";
-const int32_t PREDICTOR_SUB_DELTA_MAX_DEFAULT = 100;
+static const int32_t PREDICTOR_SUB_DELTA_MAX_DEFAULT = 100;
 
-const char PREDICTOR_PRECONNECT_MIN_PREF[] =
+static const char PREDICTOR_PREFETCH_ROLLING_LOAD_PREF[] =
+  "network.predictor.prefetch-rolling-load-count";
+static const int32_t PREFETCH_ROLLING_LOAD_DEFAULT = 10;
+static const char PREDICTOR_PREFETCH_MIN_PREF[] =
+  "network.predictor.prefetch-min-confidence";
+static const int32_t PREFETCH_MIN_DEFAULT = 100;
+static const char PREDICTOR_PRECONNECT_MIN_PREF[] =
   "network.predictor.preconnect-min-confidence";
-const int32_t PRECONNECT_MIN_DEFAULT = 90;
-const char PREDICTOR_PRERESOLVE_MIN_PREF[] =
+static const int32_t PRECONNECT_MIN_DEFAULT = 90;
+static const char PREDICTOR_PRERESOLVE_MIN_PREF[] =
   "network.predictor.preresolve-min-confidence";
-const int32_t PRERESOLVE_MIN_DEFAULT = 60;
-const char PREDICTOR_REDIRECT_LIKELY_PREF[] =
+static const int32_t PRERESOLVE_MIN_DEFAULT = 60;
+static const char PREDICTOR_REDIRECT_LIKELY_PREF[] =
   "network.predictor.redirect-likely-confidence";
-const int32_t REDIRECT_LIKELY_DEFAULT = 75;
+static const int32_t REDIRECT_LIKELY_DEFAULT = 75;
 
-const char PREDICTOR_MAX_RESOURCES_PREF[] =
+static const char PREDICTOR_PREFETCH_FORCE_VALID_PREF[] =
+  "network.predictor.prefetch-force-valid-for";
+static const int32_t PREFETCH_FORCE_VALID_DEFAULT = 10;
+
+static const char PREDICTOR_MAX_RESOURCES_PREF[] =
   "network.predictor.max-resources-per-entry";
-const uint32_t PREDICTOR_MAX_RESOURCES_DEFAULT = 100;
+static const uint32_t PREDICTOR_MAX_RESOURCES_DEFAULT = 100;
 
 // This is selected in concert with max-resources-per-entry to keep memory usage
 // low-ish. The default of the combo of the two is ~50k
-const char PREDICTOR_MAX_URI_LENGTH_PREF[] =
+static const char PREDICTOR_MAX_URI_LENGTH_PREF[] =
   "network.predictor.max-uri-length";
-const uint32_t PREDICTOR_MAX_URI_LENGTH_DEFAULT = 500;
+static const uint32_t PREDICTOR_MAX_URI_LENGTH_DEFAULT = 500;
 
-const char PREDICTOR_CLEANED_UP_PREF[] = "network.predictor.cleaned-up";
+static const char PREDICTOR_CLEANED_UP_PREF[] = "network.predictor.cleaned-up";
 
 // All these time values are in sec
-const uint32_t ONE_DAY = 86400U;
-const uint32_t ONE_WEEK = 7U * ONE_DAY;
-const uint32_t ONE_MONTH = 30U * ONE_DAY;
-const uint32_t ONE_YEAR = 365U * ONE_DAY;
+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;
 
-const uint32_t STARTUP_WINDOW = 5U * 60U; // 5min
+static const uint32_t STARTUP_WINDOW = 5U * 60U; // 5min
 
 // Version of metadata entries we expect
-const uint32_t METADATA_VERSION = 1;
+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);
 
 // 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, nsIIOService *ioService)
@@ -302,29 +330,33 @@ NS_IMPL_ISUPPORTS(Predictor,
                   nsIInterfaceRequestor,
                   nsICacheEntryMetaDataVisitor,
                   nsINetworkPredictorVerifier)
 
 Predictor::Predictor()
   :mInitialized(false)
   ,mEnabled(true)
   ,mEnableHoverOnSSL(false)
+  ,mEnablePrefetch(true)
   ,mPageDegradationDay(PREDICTOR_PAGE_DELTA_DAY_DEFAULT)
   ,mPageDegradationWeek(PREDICTOR_PAGE_DELTA_WEEK_DEFAULT)
   ,mPageDegradationMonth(PREDICTOR_PAGE_DELTA_MONTH_DEFAULT)
   ,mPageDegradationYear(PREDICTOR_PAGE_DELTA_YEAR_DEFAULT)
   ,mPageDegradationMax(PREDICTOR_PAGE_DELTA_MAX_DEFAULT)
   ,mSubresourceDegradationDay(PREDICTOR_SUB_DELTA_DAY_DEFAULT)
   ,mSubresourceDegradationWeek(PREDICTOR_SUB_DELTA_WEEK_DEFAULT)
   ,mSubresourceDegradationMonth(PREDICTOR_SUB_DELTA_MONTH_DEFAULT)
   ,mSubresourceDegradationYear(PREDICTOR_SUB_DELTA_YEAR_DEFAULT)
   ,mSubresourceDegradationMax(PREDICTOR_SUB_DELTA_MAX_DEFAULT)
+  ,mPrefetchRollingLoadCount(PREFETCH_ROLLING_LOAD_DEFAULT)
+  ,mPrefetchMinConfidence(PREFETCH_MIN_DEFAULT)
   ,mPreconnectMinConfidence(PRECONNECT_MIN_DEFAULT)
   ,mPreresolveMinConfidence(PRERESOLVE_MIN_DEFAULT)
   ,mRedirectLikelyConfidence(REDIRECT_LIKELY_DEFAULT)
+  ,mPrefetchForceValidFor(PREFETCH_FORCE_VALID_DEFAULT)
   ,mMaxResourcesPerEntry(PREDICTOR_MAX_RESOURCES_DEFAULT)
   ,mStartupCount(1)
   ,mMaxURILength(PREDICTOR_MAX_URI_LENGTH_DEFAULT)
 {
   MOZ_ASSERT(!sSelf, "multiple Predictor instances!");
   sSelf = this;
 }
 
@@ -356,16 +388,17 @@ Predictor::InstallObserver()
   nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
   if (!prefs) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   Preferences::AddBoolVarCache(&mEnabled, PREDICTOR_ENABLED_PREF, true);
   Preferences::AddBoolVarCache(&mEnableHoverOnSSL,
                                PREDICTOR_SSL_HOVER_PREF, false);
+  Preferences::AddBoolVarCache(&mEnablePrefetch, PREDICTOR_PREFETCH_PREF, true);
   Preferences::AddIntVarCache(&mPageDegradationDay,
                               PREDICTOR_PAGE_DELTA_DAY_PREF,
                               PREDICTOR_PAGE_DELTA_DAY_DEFAULT);
   Preferences::AddIntVarCache(&mPageDegradationWeek,
                               PREDICTOR_PAGE_DELTA_WEEK_PREF,
                               PREDICTOR_PAGE_DELTA_WEEK_DEFAULT);
   Preferences::AddIntVarCache(&mPageDegradationMonth,
                               PREDICTOR_PAGE_DELTA_MONTH_PREF,
@@ -388,26 +421,36 @@ Predictor::InstallObserver()
                               PREDICTOR_SUB_DELTA_MONTH_DEFAULT);
   Preferences::AddIntVarCache(&mSubresourceDegradationYear,
                               PREDICTOR_SUB_DELTA_YEAR_PREF,
                               PREDICTOR_SUB_DELTA_YEAR_DEFAULT);
   Preferences::AddIntVarCache(&mSubresourceDegradationMax,
                               PREDICTOR_SUB_DELTA_MAX_PREF,
                               PREDICTOR_SUB_DELTA_MAX_DEFAULT);
 
+  Preferences::AddIntVarCache(&mPrefetchRollingLoadCount,
+                              PREDICTOR_PREFETCH_ROLLING_LOAD_PREF,
+                              PREFETCH_ROLLING_LOAD_DEFAULT);
+  Preferences::AddIntVarCache(&mPrefetchMinConfidence,
+                              PREDICTOR_PREFETCH_MIN_PREF,
+                              PREFETCH_MIN_DEFAULT);
   Preferences::AddIntVarCache(&mPreconnectMinConfidence,
                               PREDICTOR_PRECONNECT_MIN_PREF,
                               PRECONNECT_MIN_DEFAULT);
   Preferences::AddIntVarCache(&mPreresolveMinConfidence,
                               PREDICTOR_PRERESOLVE_MIN_PREF,
                               PRERESOLVE_MIN_DEFAULT);
   Preferences::AddIntVarCache(&mRedirectLikelyConfidence,
                               PREDICTOR_REDIRECT_LIKELY_PREF,
                               REDIRECT_LIKELY_DEFAULT);
 
+  Preferences::AddIntVarCache(&mPrefetchForceValidFor,
+                              PREDICTOR_PREFETCH_FORCE_VALID_PREF,
+                              PREFETCH_FORCE_VALID_DEFAULT);
+
   Preferences::AddIntVarCache(&mMaxResourcesPerEntry,
                               PREDICTOR_MAX_RESOURCES_PREF,
                               PREDICTOR_MAX_RESOURCES_DEFAULT);
 
   Preferences::AddBoolVarCache(&mCleanedUp, PREDICTOR_CLEANED_UP_PREF, false);
 
   Preferences::AddUintVarCache(&mMaxURILength, PREDICTOR_MAX_URI_LENGTH_PREF,
                                PREDICTOR_MAX_URI_LENGTH_DEFAULT);
@@ -940,17 +983,17 @@ Predictor::PredictInternal(PredictorPred
   if (isNew) {
     // nothing else we can do here
     PREDICTOR_LOG(("    new entry"));
     return rv;
   }
 
   switch (reason) {
     case nsINetworkPredictor::PREDICT_LOAD:
-      rv = PredictForPageload(entry, stackCount, verifier);
+      rv = PredictForPageload(entry, targetURI, stackCount, verifier);
       break;
     case nsINetworkPredictor::PREDICT_STARTUP:
       rv = PredictForStartup(entry, verifier);
       break;
     default:
       PREDICTOR_LOG(("    invalid reason"));
       MOZ_ASSERT(false, "Got unexpected value for prediction reason");
   }
@@ -983,35 +1026,37 @@ Predictor::PredictForLink(nsIURI *target
   mSpeculativeService->SpeculativeConnect(targetURI, nullptr);
   if (verifier) {
     PREDICTOR_LOG(("    sending verification"));
     verifier->OnPredictPreconnect(targetURI);
   }
 }
 
 // This is the driver for prediction based on a new pageload.
-const uint8_t MAX_PAGELOAD_DEPTH = 10;
+static const uint8_t MAX_PAGELOAD_DEPTH = 10;
 bool
-Predictor::PredictForPageload(nsICacheEntry *entry, uint8_t stackCount,
+Predictor::PredictForPageload(nsICacheEntry *entry, nsIURI *targetURI,
+                              uint8_t stackCount,
                               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<nsIURI> redirectURI;
   if (WouldRedirect(entry, loadCount, lastLoad, globalDegradation,
                     getter_AddRefs(redirectURI))) {
@@ -1027,37 +1072,37 @@ Predictor::PredictForPageload(nsICacheEn
     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;
     mCacheDiskStorage->AsyncOpenURI(redirectURI, EmptyCString(), openFlags,
                                     redirectAction);
-    return RunPredictions(verifier);
+    return RunPredictions(nullptr, verifier);
   }
 
-  CalculatePredictions(entry, lastLoad, loadCount, globalDegradation);
+  CalculatePredictions(entry, targetURI, lastLoad, loadCount, globalDegradation);
 
-  return RunPredictions(verifier);
+  return RunPredictions(targetURI, 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,
                              nsINetworkPredictorVerifier *verifier)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   PREDICTOR_LOG(("Predictor::PredictForStartup"));
   int32_t globalDegradation = CalculateGlobalDegradation(mLastStartupTime);
-  CalculatePredictions(entry, mLastStartupTime, mStartupCount,
+  CalculatePredictions(entry, nullptr, mLastStartupTime, mStartupCount,
                        globalDegradation);
-  return RunPredictions(verifier);
+  return RunPredictions(nullptr, 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
@@ -1147,22 +1192,74 @@ Predictor::CalculateConfidence(uint32_t 
 
   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::CalculatePredictions(nsICacheEntry *entry, uint32_t lastLoad,
-                                uint32_t loadCount, int32_t globalDegradation)
+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());
+}
+
+void
+Predictor::SanitizePrefs()
+{
+  if (mPrefetchRollingLoadCount < 0) {
+    mPrefetchRollingLoadCount = 0;
+  } else if (mPrefetchRollingLoadCount > kMaxPrefetchRollingLoadCount) {
+    mPrefetchRollingLoadCount = kMaxPrefetchRollingLoadCount;
+  }
+}
+
+void
+Predictor::CalculatePredictions(nsICacheEntry *entry, nsIURI *referrer,
+                                uint32_t lastLoad, uint32_t loadCount,
+                                int32_t globalDegradation)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  SanitizePrefs();
+
   // 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<nsCString> keysToOperateOn, valuesToOperateOn;
   keysToOperateOn.SwapElements(mKeysToOperateOn);
   valuesToOperateOn.SwapElements(mValuesToOperateOn);
 
   MOZ_ASSERT(keysToOperateOn.Length() == valuesToOperateOn.Length());
@@ -1175,53 +1272,133 @@ Predictor::CalculatePredictions(nsICache
     if (!ParseMetaDataEntry(key, value, getter_AddRefs(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);
-    SetupPrediction(confidence, uri);
+    UpdateRollingLoadCount(entry, flags, key, hitCount, lastHit);
+    PREDICTOR_LOG(("CalculatePredictions key=%s value=%s confidence=%d", key, value, confidence));
+    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"));
+      flags &= ~FLAG_PREFETCHABLE;
+    } else {
+      uint32_t expectedRollingLoadCount = (1 << mPrefetchRollingLoadCount) - 1;
+      expectedRollingLoadCount <<= kRollingLoadOffset;
+      if ((flags & expectedRollingLoadCount) != expectedRollingLoadCount) {
+        PREDICTOR_LOG(("    forcing non-cacheability - missed a load"));
+        flags &= ~FLAG_PREFETCHABLE;
+      }
+    }
+
+    PREDICTOR_LOG(("    setting up prediction"));
+    SetupPrediction(confidence, flags, uri);
   }
 }
 
 // (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, nsIURI *uri)
+Predictor::SetupPrediction(int32_t confidence, uint32_t flags, nsIURI *uri)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (confidence >= mPreconnectMinConfidence) {
+  nsAutoCString uriStr;
+  uri->GetAsciiSpec(uriStr);
+  PREDICTOR_LOG(("SetupPrediction mEnablePrefetch=%d mPrefetchMinConfidence=%d "
+                 "mPreconnectMinConfidence=%d mPreresolveMinConfidence=%d "
+                 "flags=%d confidence=%d uri=%s", mEnablePrefetch,
+                 mPrefetchMinConfidence, mPreconnectMinConfidence,
+                 mPreresolveMinConfidence, flags, confidence, uriStr.get()));
+  if (mEnablePrefetch && (flags & FLAG_PREFETCHABLE) &&
+      (mPrefetchRollingLoadCount || (confidence >= mPrefetchMinConfidence))) {
+    mPrefetches.AppendElement(uri);
+  } else if (confidence >= mPreconnectMinConfidence) {
     mPreconnects.AppendElement(uri);
   } else if (confidence >= mPreresolveMinConfidence) {
     mPreresolves.AppendElement(uri);
   }
 }
 
+nsresult
+Predictor::Prefetch(nsIURI *uri, nsIURI *referrer,
+                    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<nsIChannel> channel;
+  nsresult rv = NS_NewChannelInternal(getter_AddRefs(channel), uri, nullptr,
+                                      nullptr, nullptr,
+                                      nsIRequest::LOAD_BACKGROUND);
+  if (NS_FAILED(rv)) {
+    PREDICTOR_LOG(("    NS_NewChannelInternal failed rv=0x%X", rv));
+    return rv;
+  }
+
+  nsCOMPtr<nsIHttpChannel> httpChannel;
+  httpChannel = do_QueryInterface(channel);
+  if (!httpChannel) {
+    PREDICTOR_LOG(("    Could not get HTTP Channel from new channel!"));
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  httpChannel->SetReferrer(referrer);
+  // XXX - set a header here to indicate this is a prefetch?
+
+  nsCOMPtr<nsIStreamListener> listener = new PrefetchListener(verifier, uri,
+                                                              this);
+  PREDICTOR_LOG(("    calling AsyncOpen listener=%p channel=%p", listener.get(),
+                 channel.get()));
+  rv = channel->AsyncOpen(listener, nullptr);
+  if (NS_FAILED(rv)) {
+    PREDICTOR_LOG(("    AsyncOpen failed rv=0x%X", rv));
+  }
+
+  return rv;
+}
+
 // Runs predictions that have been set up.
 bool
-Predictor::RunPredictions(nsINetworkPredictorVerifier *verifier)
+Predictor::RunPredictions(nsIURI *referrer, nsINetworkPredictorVerifier *verifier)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Running prediction off main thread");
 
   PREDICTOR_LOG(("Predictor::RunPredictions"));
 
   bool predicted = false;
   uint32_t len, i;
 
-  nsTArray<nsCOMPtr<nsIURI>> preconnects, preresolves;
+  nsTArray<nsCOMPtr<nsIURI>> prefetches, preconnects, preresolves;
+  prefetches.SwapElements(mPrefetches);
   preconnects.SwapElements(mPreconnects);
   preresolves.SwapElements(mPreresolves);
 
   Telemetry::AutoCounter<Telemetry::PREDICTOR_TOTAL_PREDICTIONS> totalPredictions;
+  Telemetry::AutoCounter<Telemetry::PREDICTOR_TOTAL_PREFETCHES> totalPrefetches;
   Telemetry::AutoCounter<Telemetry::PREDICTOR_TOTAL_PRECONNECTS> totalPreconnects;
   Telemetry::AutoCounter<Telemetry::PREDICTOR_TOTAL_PRERESOLVES> totalPreresolves;
 
+  len = prefetches.Length();
+  for (i = 0; i < len; ++i) {
+    PREDICTOR_LOG(("    doing prefetch"));
+    nsCOMPtr<nsIURI> uri = prefetches[i];
+    if (NS_SUCCEEDED(Prefetch(uri, referrer, verifier))) {
+      ++totalPredictions;
+      ++totalPrefetches;
+      predicted = true;
+    }
+  }
+
   len = preconnects.Length();
   for (i = 0; i < len; ++i) {
     PREDICTOR_LOG(("    doing preconnect"));
     nsCOMPtr<nsIURI> uri = preconnects[i];
     ++totalPredictions;
     ++totalPreconnects;
     mSpeculativeService->SpeculativeConnect(uri, this);
     predicted = true;
@@ -1459,20 +1636,51 @@ Predictor::LearnInternal(PredictorLearnR
 
     // Need to ensure someone else can get to the entry if necessary
     entry->MetaDataReady();
     return;
   }
 
   switch (reason) {
     case nsINetworkPredictor::LEARN_LOAD_TOPLEVEL:
-      // This actually has no work associated with it, since all we need to do
-      // is update the timestamps and fetch count, and that's done for us by
-      // opening the cache entry.
-      PREDICTOR_LOG(("    nothing to do for 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) {
+        PREDICTOR_LOG(("    WARNING - updating rolling load count. "
+                       "If you see this outside tests, you did it wrong"));
+        SanitizePrefs();
+
+        // 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<nsCString> 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();
+
+          nsCOMPtr<nsIURI> uri;
+          uint32_t hitCount, lastHit, flags;
+          if (!ParseMetaDataEntry(key, value, getter_AddRefs(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);
@@ -1604,31 +1812,29 @@ Predictor::LearnForSubresource(nsICacheE
     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<uint32_t>(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;
-  newValue.AppendInt(METADATA_VERSION);
-  newValue.AppendLiteral(",");
-  newValue.AppendInt(hitCount);
-  newValue.AppendLiteral(",");
-  newValue.AppendInt(lastLoad);
-  // These are for flags, that will be used for prefetch and possibly other
-  // things later on
-  newValue.AppendLiteral(",");
-  newValue.AppendInt(0);
+  MakeMetadataEntry(hitCount, lastLoad, flags, newValue);
   rv = entry->SetMetaDataElement(key.BeginReading(), newValue.BeginReading());
   PREDICTOR_LOG(("    SetMetaDataElement -> 0x%08X", 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);
@@ -2056,19 +2262,39 @@ PredictorLearnRedirect(nsIURI *targetURI
   return predictor->Learn(targetURI, sourceURI,
                           nsINetworkPredictor::LEARN_LOAD_REDIRECT,
                           loadContext);
 }
 
 // nsINetworkPredictorVerifier
 
 /**
- * Call through to the child's verifier (only during tests).
+ * Call through to the child's verifier (only during tests)
  */
 NS_IMETHODIMP
+Predictor::OnPredictPrefetch(nsIURI *aURI, uint32_t httpStatus)
+{
+  if (IsNeckoChild()) {
+    MOZ_DIAGNOSTIC_ASSERT(mChildVerifier);
+    return mChildVerifier->OnPredictPrefetch(aURI, httpStatus);
+  }
+
+  MOZ_DIAGNOSTIC_ASSERT(gNeckoParent);
+
+  ipc::URIParams serURI;
+  SerializeURI(aURI, serURI);
+
+  if (!gNeckoParent->SendPredOnPredictPrefetch(serURI, httpStatus)) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 Predictor::OnPredictPreconnect(nsIURI *aURI) {
   if (IsNeckoChild()) {
     MOZ_DIAGNOSTIC_ASSERT(mChildVerifier);
     return mChildVerifier->OnPredictPreconnect(aURI);
   }
 
   MOZ_DIAGNOSTIC_ASSERT(gNeckoParent);
 
@@ -2077,19 +2303,16 @@ Predictor::OnPredictPreconnect(nsIURI *a
 
   if (!gNeckoParent->SendPredOnPredictPreconnect(serURI)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   return NS_OK;
 }
 
-/**
- * Call through to the child's verifier (only during tests)
- */
 NS_IMETHODIMP
 Predictor::OnPredictDNS(nsIURI *aURI) {
   if (IsNeckoChild()) {
     MOZ_DIAGNOSTIC_ASSERT(mChildVerifier);
     return mChildVerifier->OnPredictDNS(aURI);
   }
 
   MOZ_DIAGNOSTIC_ASSERT(gNeckoParent);
@@ -2099,10 +2322,219 @@ Predictor::OnPredictDNS(nsIURI *aURI) {
 
   if (!gNeckoParent->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,
+                                            nsISupports *aContext)
+{
+  mStartTime = TimeStamp::Now();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+Predictor::PrefetchListener::OnStopRequest(nsIRequest *aRequest,
+                                           nsISupports *aContext,
+                                           nsresult aStatusCode)
+{
+  PREDICTOR_LOG(("OnStopRequest this=%p aStatusCode=0x%X", this, aStatusCode));
+  NS_ENSURE_ARG(aRequest);
+  if (NS_FAILED(aStatusCode)) {
+    return aStatusCode;
+  }
+  Telemetry::AccumulateTimeDelta(Telemetry::PREDICTOR_PREFETCH_TIME, mStartTime);
+
+  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
+  if (!httpChannel) {
+    PREDICTOR_LOG(("    Could not get HTTP Channel!"));
+    return NS_ERROR_UNEXPECTED;
+  }
+  nsCOMPtr<nsICachingChannel> 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(mPredictor->mPrefetchForceValidFor);
+    PREDICTOR_LOG(("    forcing entry valid for %d seconds rv=%X",
+                   mPredictor->mPrefetchForceValidFor, rv));
+  } else {
+    rv = cachingChannel->ForceCacheEntryValidFor(0);
+    PREDICTOR_LOG(("    removing any forced validity rv=%X", rv));
+  }
+
+  nsAutoCString reqName;
+  rv = aRequest->GetName(reqName);
+  if (NS_FAILED(rv)) {
+    reqName.AssignLiteral("<unknown>");
+  }
+
+  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,
+                                             nsISupports *aContext,
+                                             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)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (lci && lci->IsPrivate()) {
+    PREDICTOR_LOG(("Predictor::UpdateCacheability in PB mode - ignoring"));
+    return;
+  }
+
+  RefPtr<Predictor> self = sSelf;
+  if (self) {
+    const nsCString method = requestHead.Method();
+    self->UpdateCacheabilityInternal(sourceURI, targetURI, httpStatus,
+                                     method);
+  }
+}
+
+void
+Predictor::UpdateCacheabilityInternal(nsIURI *sourceURI, nsIURI *targetURI,
+                                      uint32_t httpStatus,
+                                      const nsCString &method)
+{
+  PREDICTOR_LOG(("Predictor::UpdateCacheability httpStatus=%u", httpStatus));
+  uint32_t openFlags = nsICacheStorage::OPEN_READONLY |
+                       nsICacheStorage::OPEN_SECRETLY |
+                       nsICacheStorage::CHECK_MULTITHREADED;
+  RefPtr<Predictor::CacheabilityAction> action =
+    new Predictor::CacheabilityAction(targetURI, httpStatus, method, this);
+  nsAutoCString uri;
+  targetURI->GetAsciiSpec(uri);
+  PREDICTOR_LOG(("    uri=%s action=%p", uri.get(), action.get()));
+  mCacheDiskStorage->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;
+}
+
+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=%X isNew=%d", result, isNew));
+    return NS_OK;
+  }
+
+  nsresult rv = entry->VisitMetaData(this);
+  if (NS_FAILED(rv)) {
+    PREDICTOR_LOG(("    VisitMetaData returned %x", rv));
+    return NS_OK;
+  }
+
+  nsTArray<nsCString> keysToCheck, valuesToCheck;
+  keysToCheck.SwapElements(mKeysToCheck);
+  valuesToCheck.SwapElements(mValuesToCheck);
+
+  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();
+    nsCOMPtr<nsIURI> uri;
+    uint32_t hitCount, lastHit, flags;
+
+    if (!mPredictor->ParseMetaDataEntry(key, value, getter_AddRefs(uri),
+                                        hitCount, lastHit, flags)) {
+      PREDICTOR_LOG(("    failed to parse key=%s value=%s", key, value));
+      continue;
+    }
+
+    bool eq = false;
+    if (NS_SUCCEEDED(uri->Equals(mTargetURI, &eq)) && eq) {
+      if (mHttpStatus == 200 && mMethod.EqualsLiteral("GET")) {
+        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
--- a/netwerk/base/Predictor.h
+++ b/netwerk/base/Predictor.h
@@ -12,30 +12,35 @@
 #include "nsCOMPtr.h"
 #include "nsICacheEntry.h"
 #include "nsICacheEntryOpenCallback.h"
 #include "nsICacheStorageVisitor.h"
 #include "nsIDNSListener.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIObserver.h"
 #include "nsISpeculativeConnect.h"
+#include "nsIStreamListener.h"
 #include "mozilla/RefPtr.h"
 #include "nsString.h"
 #include "nsTArray.h"
 
 #include "mozilla/TimeStamp.h"
 
 class nsICacheStorage;
 class nsIDNSService;
 class nsIIOService;
+class nsILoadContextInfo;
 class nsITimer;
 
 namespace mozilla {
 namespace net {
 
+class nsHttpRequestHead;
+class nsHttpResponseHead;
+
 class Predictor : public nsINetworkPredictor
                 , public nsIObserver
                 , public nsISpeculativeConnectionOverrider
                 , public nsIInterfaceRequestor
                 , public nsICacheEntryMetaDataVisitor
                 , public nsINetworkPredictorVerifier
 {
 public:
@@ -48,16 +53,25 @@ public:
   NS_DECL_NSINETWORKPREDICTORVERIFIER
 
   Predictor();
 
   nsresult Init();
   void Shutdown();
   static nsresult Create(nsISupports *outer, const nsIID& iid, void **result);
 
+  // Used to update whether a particular URI was cacheable or not.
+  // sourceURI and targetURI are the same as the arguments to Learn
+  // and httpStatus is the status code we got while loading targetURI.
+  static void UpdateCacheability(nsIURI *sourceURI, nsIURI *targetURI,
+                                 uint32_t httpStatus,
+                                 nsHttpRequestHead &requestHead,
+                                 nsHttpResponseHead *reqponseHead,
+                                 nsILoadContextInfo *lci);
+
 private:
   virtual ~Predictor();
 
   // Stores callbacks for a child process predictor (for test purposes)
   nsCOMPtr<nsINetworkPredictorVerifier> mChildVerifier;
 
   union Reason {
     PredictorLearnReason mLearn;
@@ -110,16 +124,43 @@ private:
     nsCOMPtr<nsIURI> mTargetURI;
     nsCOMPtr<nsIURI> mSourceURI;
     nsCOMPtr<nsINetworkPredictorVerifier> mVerifier;
     TimeStamp mStartTime;
     uint8_t mStackCount;
     RefPtr<Predictor> mPredictor;
   };
 
+  class CacheabilityAction : public nsICacheEntryOpenCallback
+                           , public nsICacheEntryMetaDataVisitor
+  {
+  public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+    NS_DECL_NSICACHEENTRYOPENCALLBACK
+    NS_DECL_NSICACHEENTRYMETADATAVISITOR
+
+    CacheabilityAction(nsIURI *targetURI, uint32_t httpStatus,
+                       const nsCString &method, Predictor *predictor)
+      :mTargetURI(targetURI)
+      ,mHttpStatus(httpStatus)
+      ,mMethod(method)
+      ,mPredictor(predictor)
+    { }
+
+  private:
+    virtual ~CacheabilityAction() { }
+
+    nsCOMPtr<nsIURI> mTargetURI;
+    uint32_t mHttpStatus;
+    nsCString mMethod;
+    RefPtr<Predictor> mPredictor;
+    nsTArray<nsCString> mKeysToCheck;
+    nsTArray<nsCString> mValuesToCheck;
+  };
+
   class Resetter : public nsICacheEntryOpenCallback,
                    public nsICacheEntryMetaDataVisitor,
                    public nsICacheStorageVisitor
   {
   public:
     NS_DECL_THREADSAFE_ISUPPORTS
     NS_DECL_NSICACHEENTRYOPENCALLBACK
     NS_DECL_NSICACHEENTRYMETADATAVISITOR
@@ -155,16 +196,39 @@ private:
   private:
     virtual ~SpaceCleaner() { }
     uint32_t mLRUStamp;
     const char *mLRUKeyToDelete;
     nsTArray<nsCString> mLongKeysToDelete;
     RefPtr<Predictor> mPredictor;
   };
 
+  class PrefetchListener : public nsIStreamListener
+  {
+  public:
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSIREQUESTOBSERVER
+    NS_DECL_NSISTREAMLISTENER
+
+    PrefetchListener(nsINetworkPredictorVerifier *verifier, nsIURI *uri,
+                     Predictor *predictor)
+      :mVerifier(verifier)
+      ,mURI(uri)
+      ,mPredictor(predictor)
+    { }
+
+  private:
+    virtual ~PrefetchListener() { }
+
+    nsCOMPtr<nsINetworkPredictorVerifier> mVerifier;
+    nsCOMPtr<nsIURI> mURI;
+    RefPtr<Predictor> mPredictor;
+    TimeStamp mStartTime;
+  };
+
   // Observer-related stuff
   nsresult InstallObserver();
   void RemoveObserver();
 
   // Service startup utilities
   void MaybeCleanupOldDBFiles();
 
   // The guts of prediction
@@ -194,27 +258,40 @@ private:
   void PredictForLink(nsIURI *targetURI,
                       nsIURI *sourceURI,
                       nsINetworkPredictorVerifier *verifier);
 
   // Used when predicting because a page is being loaded (which may include
   // being the target of a redirect). All arguments are the same as for
   // PredictInternal. Returns true if any predictions were queued up.
   bool PredictForPageload(nsICacheEntry *entry,
+                          nsIURI *targetURI,
                           uint8_t stackCount,
                           nsINetworkPredictorVerifier *verifier);
 
   // Used when predicting pages that will be used near browser startup. All
   // arguments are the same as for PredictInternal. Returns true if any
   // predictions were queued up.
   bool PredictForStartup(nsICacheEntry *entry,
                          nsINetworkPredictorVerifier *verifier);
 
   // Utilities related to prediction
 
+  // Used to update our rolling load count (how many of the last n loads was a
+  // partular resource loaded on?)
+  //   * entry - cache entry of page we're loading
+  //   * flags - value that contains our rolling count as the top 20 bits (but
+  //             we may use fewer than those 20 bits for calculations)
+  //   * key - metadata key that we will update on entry
+  //   * hitCount - part of the metadata we need to preserve
+  //   * lastHit - part of the metadata we need to preserve
+  void UpdateRollingLoadCount(nsICacheEntry *entry, const uint32_t flags,
+                              const char *key, const uint32_t hitCount,
+                              const uint32_t lastHit);
+
   // Used to calculate how much to degrade our confidence for all resources
   // on a particular page, because of how long ago the most recent load of that
   // page was. Returns a value between 0 (very recent most recent load) and 100
   // (very distant most recent load)
   //   * lastLoad - time stamp of most recent load of a page
   int32_t CalculateGlobalDegradation(uint32_t lastLoad);
 
   // Used to calculate how confident we are that a particular resource will be
@@ -229,32 +306,42 @@ private:
   //                         this page
   int32_t CalculateConfidence(uint32_t hitCount, uint32_t hitsPossible,
                               uint32_t lastHit, uint32_t lastPossible,
                               int32_t globalDegradation);
 
   // Used to calculate all confidence values for all resources associated with a
   // page.
   //   * entry - the cache entry with all necessary information about this page
+  //   * referrer - the URI that we are loading (may be null)
   //   * lastLoad - timestamp of the last time this page was loaded
   //   * loadCount - number of times this page has been loaded
   //   * gloablDegradation - value calculated by CalculateGlobalDegradation for
   //                         this page
-  void CalculatePredictions(nsICacheEntry *entry, uint32_t lastLoad,
-                            uint32_t loadCount, int32_t globalDegradation);
+  void CalculatePredictions(nsICacheEntry *entry, nsIURI *referrer,
+                            uint32_t lastLoad, uint32_t loadCount,
+                            int32_t globalDegradation);
 
   // Used to prepare any necessary prediction for a resource on a page
   //   * confidence - value calculated by CalculateConfidence for this resource
+  //   * flags - the flags taken from the resource
   //   * uri - the URI of the resource
-  void SetupPrediction(int32_t confidence, nsIURI *uri);
+  void SetupPrediction(int32_t confidence, uint32_t flags, nsIURI *uri);
+
+  // Used to kick off a prefetch from RunPredictions if necessary
+  //   * uri - the URI to prefetch
+  //   * referrer - the URI of the referring page
+  //   * verifier - used for testing to ensure the expected prefetch happens
+  nsresult Prefetch(nsIURI *uri, nsIURI *referrer, nsINetworkPredictorVerifier *verifier);
 
   // Used to actually perform any predictions set up via SetupPrediction.
   // Returns true if any predictions were performed.
+  //   * referrer - the URI we are predicting from
   //   * verifier - used for testing to ensure the expected predictions happen
-  bool RunPredictions(nsINetworkPredictorVerifier *verifier);
+  bool RunPredictions(nsIURI *referrer, nsINetworkPredictorVerifier *verifier);
 
   // Used to guess whether a page will redirect to another page or not. Returns
   // true if a redirection is likely.
   //   * entry - cache entry with all necessary information about this page
   //   * loadCount - number of times this page has been loaded
   //   * lastLoad - timestamp of the last time this page was loaded
   //   * globalDegradation - value calculated by CalculateGlobalDegradation for
   //                         this page
@@ -310,38 +397,52 @@ private:
   //   * uri - (out) the URI this metadata entry was about
   //   * hitCount - (out) the number of times this URI has been seen
   //   * lastHit - (out) timestamp of the last time this URI was seen
   //   * flags - (out) flags for this metadata entry
   bool ParseMetaDataEntry(const char *key, const char *value, nsIURI **uri,
                           uint32_t &hitCount, uint32_t &lastHit,
                           uint32_t &flags);
 
+  // Used to update whether a particular URI was cacheable or not.
+  // sourceURI and targetURI are the same as the arguments to Learn
+  // and httpStatus is the status code we got while loading targetURI.
+  void UpdateCacheabilityInternal(nsIURI *sourceURI, nsIURI *targetURI,
+                                  uint32_t httpStatus, const nsCString &method);
+
+  // Make sure our prefs are in their expected range of values
+  void SanitizePrefs();
+
   // Our state
   bool mInitialized;
 
   bool mEnabled;
   bool mEnableHoverOnSSL;
+  bool mEnablePrefetch;
 
   int32_t mPageDegradationDay;
   int32_t mPageDegradationWeek;
   int32_t mPageDegradationMonth;
   int32_t mPageDegradationYear;
   int32_t mPageDegradationMax;
 
   int32_t mSubresourceDegradationDay;
   int32_t mSubresourceDegradationWeek;
   int32_t mSubresourceDegradationMonth;
   int32_t mSubresourceDegradationYear;
   int32_t mSubresourceDegradationMax;
 
+  int32_t mPrefetchRollingLoadCount;
+  int32_t mPrefetchMinConfidence;
   int32_t mPreconnectMinConfidence;
   int32_t mPreresolveMinConfidence;
   int32_t mRedirectLikelyConfidence;
 
+  int32_t mPrefetchForceValidFor;
+
   int32_t mMaxResourcesPerEntry;
 
   bool mCleanedUp;
   nsCOMPtr<nsITimer> mCleanupTimer;
 
   nsTArray<nsCString> mKeysToOperateOn;
   nsTArray<nsCString> mValuesToOperateOn;
 
@@ -356,16 +457,17 @@ private:
   int32_t mStartupCount;
 
   uint32_t mMaxURILength;
 
   nsCOMPtr<nsIDNSService> mDnsService;
 
   RefPtr<DNSListener> mDNSListener;
 
+  nsTArray<nsCOMPtr<nsIURI>> mPrefetches;
   nsTArray<nsCOMPtr<nsIURI>> mPreconnects;
   nsTArray<nsCOMPtr<nsIURI>> mPreresolves;
 
   static Predictor *sSelf;
 };
 
 } // namespace net
 } // namespace mozilla
--- a/netwerk/base/moz.build
+++ b/netwerk/base/moz.build
@@ -176,16 +176,17 @@ EXPORTS.mozilla += [
 EXPORTS.mozilla.net += [
     'CaptivePortalService.h',
     'ChannelDiverterChild.h',
     'ChannelDiverterParent.h',
     'Dashboard.h',
     'DashboardTypes.h',
     'MemoryDownloader.h',
     'OfflineObserver.h',
+    'Predictor.h',
     'ReferrerPolicy.h',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':
     EXPORTS += [
         'NetStatistics.h',
     ]
 
--- a/netwerk/base/nsICachingChannel.idl
+++ b/netwerk/base/nsICachingChannel.idl
@@ -53,16 +53,24 @@ interface nsICachingChannel : nsICacheIn
      */
     attribute boolean cacheOnlyMetadata;
 
     /**
      * Tells the channel to use the pinning storage.
      */
     attribute boolean pin;
 
+    /**
+     * Overrides cache validation for a time specified in seconds.
+     *
+     * @param aSecondsToTheFuture
+     *
+     */
+    void forceCacheEntryValidFor(in unsigned long aSecondsToTheFuture);
+
     /**************************************************************************
      * Caching channel specific load flags:
      */
 
     /**
      * This load flag inhibits fetching from the net.  An error of
      * NS_ERROR_DOCUMENT_NOT_CACHED will be sent to the listener's
      * onStopRequest if network IO is necessary to complete the request.
--- a/netwerk/base/nsINetworkPredictorVerifier.idl
+++ b/netwerk/base/nsINetworkPredictorVerifier.idl
@@ -7,20 +7,29 @@
  * nsINetworkPredictorVerifier - used for testing the network predictor to
  *                               ensure it does what we expect it to do.
  */
 
 #include "nsISupports.idl"
 
 interface nsIURI;
 
-[scriptable, uuid(00360c7d-a046-4f8d-a1fc-8bdc0f0fb444)]
+[scriptable, uuid(2e43bb32-dabf-4494-9f90-2b3195b1c73d)]
 interface nsINetworkPredictorVerifier : nsISupports
 {
     /**
+     * Callback for when we do a predictive prefetch
+     *
+     * @param uri - The URI that was prefetched
+     * @param status - The request status code returned by the
+     *   prefetch attempt e.g. 200 (OK):w
+     */
+    void onPredictPrefetch(in nsIURI uri, in uint32_t status);
+
+    /**
      * Callback for when we do a predictive preconnect
      *
      * @param uri - The URI that was preconnected to
      */
     void onPredictPreconnect(in nsIURI uri);
 
     /**
      * Callback for when we do a predictive DNS lookup
--- a/netwerk/cache2/CacheStorageService.cpp
+++ b/netwerk/cache2/CacheStorageService.cpp
@@ -1151,16 +1151,18 @@ void CacheStorageService::ForceEntryVali
   mForcedValidEntries.Put(aContextKey + aEntryKey, validUntil);
 }
 
 void CacheStorageService::RemoveEntryForceValid(nsACString const &aContextKey,
                                                 nsACString const &aEntryKey)
 {
   mozilla::MutexAutoLock lock(mForcedValidEntriesLock);
 
+  LOG(("CacheStorageService::RemoveEntryForceValid context='%s' entryKey=%s",
+       aContextKey.BeginReading(), aEntryKey.BeginReading()));
   mForcedValidEntries.Remove(aContextKey + aEntryKey);
 }
 
 // Cleans out the old entries in mForcedValidEntries
 void CacheStorageService::ForcedValidEntriesPrune(TimeStamp &now)
 {
   static TimeDuration const oneMinute = TimeDuration::FromSeconds(60);
   static TimeStamp dontPruneUntil = now + oneMinute;
--- a/netwerk/cache2/nsICacheEntry.idl
+++ b/netwerk/cache2/nsICacheEntry.idl
@@ -66,16 +66,20 @@ interface nsICacheEntry : nsISupports
    * This method is intended to override the per-spec cache validation
    * decisions for a duration specified in seconds. The current state can
    * be examined with isForcedValid (see below). This value is not persisted,
    * so it will not survive session restart. Cache entries that are forced valid
    * will not be evicted from the cache for the duration of forced validity.
    * This means that there is a potential problem if the number of forced valid
    * entries grows to take up more space than the cache size allows.
    *
+   * NOTE: entries that have been forced valid will STILL be ignored by HTTP
+   * channels if they have expired AND the resource in question requires
+   * validation after expiring. This is to avoid using known-stale content.
+   *
    * @param aSecondsToTheFuture
    *        the number of seconds the default cache validation behavior will be
    *        overridden before it returns to normal
    */
   void forceValidFor(in unsigned long aSecondsToTheFuture);
 
   /**
    * The state variable for whether this entry is currently forced valid.
--- a/netwerk/ipc/NeckoChild.cpp
+++ b/netwerk/ipc/NeckoChild.cpp
@@ -339,16 +339,35 @@ NeckoChild::RecvAsyncAuthPromptForNested
     return false;
   }
   tabChild->SendAsyncAuthPrompt(aUri, aRealm, aCallbackId);
   return true;
 }
 
 /* Predictor Messages */
 bool
+NeckoChild::RecvPredOnPredictPrefetch(const URIParams& aURI,
+                                      const uint32_t& aHttpStatus)
+{
+  MOZ_ASSERT(NS_IsMainThread(), "PredictorChild::RecvOnPredictPrefetch "
+                                "off main thread.");
+
+  nsCOMPtr<nsIURI> uri = DeserializeURI(aURI);
+
+  // Get the current predictor
+  nsresult rv = NS_OK;
+  nsCOMPtr<nsINetworkPredictorVerifier> predictor =
+    do_GetService("@mozilla.org/network/predictor;1", &rv);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  predictor->OnPredictPrefetch(uri, aHttpStatus);
+  return true;
+}
+
+bool
 NeckoChild::RecvPredOnPredictPreconnect(const URIParams& aURI)
 {
   MOZ_ASSERT(NS_IsMainThread(), "PredictorChild::RecvOnPredictPreconnect "
                                 "off main thread.");
 
   nsCOMPtr<nsIURI> uri = DeserializeURI(aURI);
 
   // Get the current predictor
--- a/netwerk/ipc/NeckoChild.h
+++ b/netwerk/ipc/NeckoChild.h
@@ -80,16 +80,18 @@ protected:
                                                  const nsString& aRealm,
                                                  const uint64_t& aCallbackId) override;
   virtual bool RecvAppOfflineStatus(const uint32_t& aId, const bool& aOffline) override;
   virtual PWebSocketEventListenerChild*
     AllocPWebSocketEventListenerChild(const uint64_t& aInnerWindowID) override;
   virtual bool DeallocPWebSocketEventListenerChild(PWebSocketEventListenerChild*) override;
 
   /* Predictor Messsages */
+  virtual bool RecvPredOnPredictPrefetch(const URIParams& aURI,
+                                         const uint32_t& aHttpStatus) override;
   virtual bool RecvPredOnPredictPreconnect(const URIParams& aURI) override;
   virtual bool RecvPredOnPredictDNS(const URIParams& aURI) override;
 
   virtual bool RecvSpeculativeConnectRequest(const nsCString& aNotificationData) override;
 };
 
 /**
  * Reference to the PNecko Child protocol.
--- a/netwerk/ipc/PNecko.ipdl
+++ b/netwerk/ipc/PNecko.ipdl
@@ -121,16 +121,17 @@ child:
    * that was passed to the PBrowserOrId param in to the PHttpChannel constructor
    */
   async AsyncAuthPromptForNestedFrame(TabId nestedFrameId, nsCString uri,
                                       nsString realm, uint64_t callbackId);
   // Notifies child that a given app is now offline (or online)
   async AppOfflineStatus(uint32_t appId, bool offline);
 
   /* Predictor Methods */
+  async PredOnPredictPrefetch(URIParams uri, uint32_t httpStatus);
   async PredOnPredictPreconnect(URIParams uri);
   async PredOnPredictDNS(URIParams uri);
 
   async SpeculativeConnectRequest(nsCString notificationData);
 
 both:
   // Actually we need PTCPSocket() for parent. But ipdl disallows us having different
   // signatures on parent and child. So when constructing the parent side object, we just 
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -3283,16 +3283,46 @@ IMPL_TIMING_ATTR(RedirectEnd)
 nsPerformance*
 HttpBaseChannel::GetPerformance()
 {
     // If performance timing is disabled, there is no need for the nsPerformance
     // object anymore.
     if (!mTimingEnabled) {
         return nullptr;
     }
+
+    nsCOMPtr<nsPIDOMWindowInner> pDomWindow = GetInnerDOMWindow();
+    if (!pDomWindow) {
+        return nullptr;
+    }
+
+    nsPerformance* docPerformance = pDomWindow->GetPerformance();
+    if (!docPerformance) {
+        return nullptr;
+    }
+    // iframes should be added to the parent's entries list.
+    if (mLoadFlags & LOAD_DOCUMENT_URI) {
+        return docPerformance->GetParentPerformance();
+    }
+    return docPerformance;
+}
+
+nsIURI*
+HttpBaseChannel::GetReferringPage()
+{
+  nsCOMPtr<nsPIDOMWindowInner> pDomWindow = GetInnerDOMWindow();
+  if (!pDomWindow) {
+    return nullptr;
+  }
+  return pDomWindow->GetDocumentURI();
+}
+
+nsPIDOMWindowInner*
+HttpBaseChannel::GetInnerDOMWindow()
+{
     nsCOMPtr<nsILoadContext> loadContext;
     NS_QueryNotificationCallbacks(this, loadContext);
     if (!loadContext) {
         return nullptr;
     }
     nsCOMPtr<mozIDOMWindowProxy> domWindow;
     loadContext->GetAssociatedWindow(getter_AddRefs(domWindow));
     if (!domWindow) {
@@ -3302,25 +3332,17 @@ HttpBaseChannel::GetPerformance()
     if (!pDomWindow) {
         return nullptr;
     }
     nsCOMPtr<nsPIDOMWindowInner> innerWindow = pDomWindow->GetCurrentInnerWindow();
     if (!innerWindow) {
       return nullptr;
     }
 
-    nsPerformance* docPerformance = innerWindow->GetPerformance();
-    if (!docPerformance) {
-      return nullptr;
-    }
-    // iframes should be added to the parent's entries list.
-    if (mLoadFlags & LOAD_DOCUMENT_URI) {
-      return docPerformance->GetParentPerformance();
-    }
-    return docPerformance;
+    return innerWindow;
 }
 
 //------------------------------------------------------------------------------
 
 bool
 HttpBaseChannel::EnsureSchedulingContextID()
 {
     nsID nullID;
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -311,16 +311,18 @@ protected:
   // Handle notifying listener, removing from loadgroup if request failed.
   void     DoNotifyListener();
   virtual void DoNotifyListenerCleanup() = 0;
 
   // drop reference to listener, its callbacks, and the progress sink
   void ReleaseListeners();
 
   nsPerformance* GetPerformance();
+  nsIURI* GetReferringPage();
+  nsPIDOMWindowInner* GetInnerDOMWindow();
 
   void AddCookiesToRequest();
   virtual nsresult SetupReplacementChannel(nsIURI *,
                                            nsIChannel *,
                                            bool preserveMethod,
                                            uint32_t redirectFlags);
 
   // bundle calling OMR observers and marking flag into one function
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -72,32 +72,34 @@
 #include "nsIHttpChannelInternal.h"
 #include "nsIHttpEventSink.h"
 #include "nsIPrompt.h"
 #include "nsInputStreamPump.h"
 #include "nsURLHelper.h"
 #include "nsISocketTransport.h"
 #include "nsIStreamConverterService.h"
 #include "nsISiteSecurityService.h"
+#include "nsString.h"
 #include "nsCRT.h"
 #include "nsPerformance.h"
 #include "CacheObserver.h"
 #include "mozilla/Telemetry.h"
 #include "AlternateServices.h"
 #include "InterceptedChannel.h"
 #include "nsIHttpPushListener.h"
 #include "nsIX509Cert.h"
 #include "ScopedNSSTypes.h"
 #include "nsNullPrincipal.h"
 #include "nsIPackagedAppService.h"
 #include "nsIDeprecationWarner.h"
 #include "nsIDocument.h"
 #include "nsICompressConvStats.h"
 #include "nsCORSListenerProxy.h"
 #include "nsISocketProvider.h"
+#include "mozilla/net/Predictor.h"
 
 namespace mozilla { namespace net {
 
 namespace {
 
 // Monotonically increasing ID for generating unique cache entries per
 // intercepted channel.
 static uint64_t gNumIntercepted = 0;
@@ -1583,16 +1585,19 @@ nsHttpChannel::ProcessAltService()
 }
 
 nsresult
 nsHttpChannel::ProcessResponse()
 {
     nsresult rv;
     uint32_t httpStatus = mResponseHead->Status();
 
+    LOG(("nsHttpChannel::ProcessResponse [this=%p httpStatus=%u]\n",
+        this, httpStatus));
+
     // do some telemetry
     if (gHttpHandler->IsTelemetryEnabled()) {
         // Gather data on whether the transaction and page (if this is
         // the initial page load) is being loaded with SSL.
         Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_IS_SSL,
                               mConnectionInfo->EndToEndSSL());
         if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) {
             Telemetry::Accumulate(Telemetry::HTTP_PAGELOAD_IS_SSL,
@@ -1600,18 +1605,34 @@ nsHttpChannel::ProcessResponse()
         }
 
         // how often do we see something like Alternate-Protocol: "443:quic,p=1"
         const char *alt_protocol = mResponseHead->PeekHeader(nsHttp::Alternate_Protocol);
         bool saw_quic = (alt_protocol && PL_strstr(alt_protocol, "quic")) ? 1 : 0;
         Telemetry::Accumulate(Telemetry::HTTP_SAW_QUIC_ALT_PROTOCOL, saw_quic);
     }
 
-    LOG(("nsHttpChannel::ProcessResponse [this=%p httpStatus=%u]\n",
-        this, httpStatus));
+    // Let the predictor know whether this was a cacheable response or not so
+    // that it knows whether or not to possibly prefetch this resource in the
+    // future.
+    // We use GetReferringPage because mReferrer may not be set at all, or may
+    // not be a full URI (HttpBaseChannel::SetReferrer has the gorey details).
+    // If that's null, though, we'll fall back to mReferrer just in case (this
+    // is especially useful in xpcshell tests, where we don't have an actual
+    // pageload to get a referrer from).
+    nsCOMPtr<nsIURI> referrer = GetReferringPage();
+    if (!referrer) {
+        referrer = mReferrer;
+    }
+    if (referrer) {
+        nsCOMPtr<nsILoadContextInfo> lci = GetLoadContextInfo(this);
+        mozilla::net::Predictor::UpdateCacheability(mReferrer, mURI, httpStatus,
+                                                    mRequestHead, mResponseHead,
+                                                    lci);
+    }
 
     if (mTransaction->ProxyConnectFailed()) {
         // Only allow 407 (authentication required) to continue
         if (httpStatus != 407)
             return ProcessFailedProxyConnect(httpStatus);
         // If proxy CONNECT response needs to complete, wait to process connection
         // for Strict-Transport-Security.
     } else {
@@ -3379,20 +3400,26 @@ nsHttpChannel::OnCacheEntryCheck(nsICach
     entry->GetIsForcedValid(&isForcedValid);
 
     // Cached entry is not the entity we request (see bug #633743)
     if (ResponseWouldVary(entry)) {
         LOG(("Validating based on Vary headers returning TRUE\n"));
         canAddImsHeader = false;
         doValidation = true;
     }
-    // Check isForcedValid to see if it is possible to skip validation
+    // Check isForcedValid to see if it is possible to skip validation.
+    // Don't skip validation if we have serious reason to believe that this
+    // content is invalid (it's expired).
     // See netwerk/cache2/nsICacheEntry.idl for details
-    else if (isForcedValid) {
+    else if (isForcedValid &&
+             (!mCachedResponseHead->ExpiresInPast() ||
+              !mCachedResponseHead->MustValidateIfExpired())) {
         LOG(("NOT validating based on isForcedValid being true.\n"));
+        Telemetry::AutoCounter<Telemetry::PREDICTOR_TOTAL_PREFETCHES_USED> used;
+        ++used;
         doValidation = false;
     }
     // If the LOAD_FROM_CACHE flag is set, any cached data can simply be used
     else if (mLoadFlags & nsIRequest::LOAD_FROM_CACHE || mAllowStaleCacheContent) {
         LOG(("NOT validating based on LOAD_FROM_CACHE load flag\n"));
         doValidation = false;
     }
     // If the VALIDATE_ALWAYS flag is set, any cached data won't be used until
@@ -6662,16 +6689,36 @@ nsHttpChannel::SetPin(bool aPin)
         this, aPin));
 
     ENSURE_CALLED_BEFORE_CONNECT();
 
     mPinCacheContent = aPin;
     return NS_OK;
 }
 
+NS_IMETHODIMP
+nsHttpChannel::ForceCacheEntryValidFor(uint32_t aSecondsToTheFuture)
+{
+    if (!mCacheEntry) {
+        LOG(("nsHttpChannel::ForceCacheEntryValidFor found no cache entry "
+             "for this channel [this=%p].", this));
+    } else {
+        mCacheEntry->ForceValidFor(aSecondsToTheFuture);
+
+        nsAutoCString key;
+        mCacheEntry->GetKey(key);
+
+        LOG(("nsHttpChannel::ForceCacheEntryValidFor successfully forced valid "
+             "entry with key %s for %d seconds. [this=%p]", ToNewCString(key),
+             aSecondsToTheFuture, this));
+    }
+
+    return NS_OK;
+}
+
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsIResumableChannel
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::ResumeAt(uint64_t aStartPos,
                         const nsACString& aEntityID)
 {
--- a/netwerk/protocol/http/nsIHttpChannel.idl
+++ b/netwerk/protocol/http/nsIHttpChannel.idl
@@ -9,17 +9,17 @@ interface nsIHttpHeaderVisitor;
 
 /**
  * nsIHttpChannel
  *
  * This interface allows for the modification of HTTP request parameters and
  * the inspection of the resulting HTTP response status and headers when they
  * become available.
  */
-[builtinclass, scriptable, uuid(b2596105-3d0d-4e6a-824f-0539713bb879)]
+[builtinclass, scriptable, uuid(c5a4a073-4539-49c7-a3f2-cec3f0619c6c)]
 interface nsIHttpChannel : nsIChannel
 {
     /**************************************************************************
      * REQUEST CONFIGURATION
      *
      * Modifying request parameters after asyncOpen has been called is an error.
      */
 
--- a/netwerk/test/unit/test_predictor.js
+++ b/netwerk/test/unit/test_predictor.js
@@ -1,13 +1,14 @@
-var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cr = Components.results;
+var Cc = Components.classes;
 
+Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/LoadContextInfo.jsm");
 
 var running_single_process = false;
 
 var predictor = null;
 
 function is_child_process() {
@@ -41,25 +42,57 @@ LoadContext.prototype = {
     throw Cr.NS_ERROR_NO_INTERFACE;
   },
 
   originAttributes: {}
 };
 
 var load_context = new LoadContext();
 
-var Verifier = function _verifier(testing, expected_preconnects, expected_preresolves) {
+var ValidityChecker = function(verifier, httpStatus) {
+  this.verifier = verifier;
+  this.httpStatus = httpStatus;
+};
+
+ValidityChecker.prototype = {
+  verifier: null,
+  httpStatus: 0,
+
+  QueryInterface: function listener_qi(iid) {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsICacheEntryOpenCallback)) {
+      return this;
+    }
+    throw Components.results.NS_ERROR_NO_INTERFACE;
+  },
+
+  onCacheEntryCheck: function(entry, appCache)
+  {
+    return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+  },
+
+  onCacheEntryAvailable: function(entry, isnew, appCache, status)
+  {
+    // Check if forced valid
+    do_check_eq(entry.isForcedValid, this.httpStatus === 200);
+    this.verifier.maybe_run_next_test();
+  }
+}
+
+var Verifier = function _verifier(testing, expected_prefetches, expected_preconnects, expected_preresolves) {
   this.verifying = testing;
+  this.expected_prefetches = expected_prefetches;
   this.expected_preconnects = expected_preconnects;
   this.expected_preresolves = expected_preresolves;
 };
 
 Verifier.prototype = {
   complete: false,
   verifying: null,
+  expected_prefetches: null,
   expected_preconnects: null,
   expected_preresolves: null,
 
   getInterface: function verifier_getInterface(iid) {
     return this.QueryInterface(iid);
   },
 
   QueryInterface: function verifier_QueryInterface(iid) {
@@ -67,26 +100,42 @@ Verifier.prototype = {
         iid.equals(Ci.nsISupports)) {
       return this;
     }
 
     throw Cr.NS_ERROR_NO_INTERFACE;
   },
 
   maybe_run_next_test: function verifier_maybe_run_next_test() {
-    if (this.expected_preconnects.length === 0 &&
+    if (this.expected_prefetches.length === 0 &&
+        this.expected_preconnects.length === 0 &&
         this.expected_preresolves.length === 0 &&
         !this.complete) {
       this.complete = true;
       do_check_true(true, "Well this is unexpected...");
       // This kicks off the ability to run the next test
       reset_predictor();
     }
   },
 
+  onPredictPrefetch: function verifier_onPredictPrefetch(uri, status) {
+    var index = this.expected_prefetches.indexOf(uri.asciiSpec);
+    if (index == -1 && !this.complete) {
+      do_check_true(false, "Got prefetch for unexpected uri " + uri.asciiSpec);
+    } else {
+      this.expected_prefetches.splice(index, 1);
+    }
+
+    dump("checking validity of entry for " + uri.spec + "\n");
+    var checker = new ValidityChecker(this, status);
+    asyncOpenCacheEntry(uri.spec, "disk",
+        Ci.nsICacheStorage.OPEN_NORMALLY, LoadContextInfo.default,
+        checker);
+  },
+
   onPredictPreconnect: function verifier_onPredictPreconnect(uri) {
     var origin = extract_origin(uri);
     var index = this.expected_preconnects.indexOf(origin);
     if (index == -1 && !this.complete) {
       do_check_true(false, "Got preconnect for unexpected uri " + origin);
     } else {
       this.expected_preconnects.splice(index, 1);
     }
@@ -167,17 +216,17 @@ function test_link_hover() {
     sendCommand("test_link_hover();");
     return;
   }
 
   var uri = newURI("http://localhost:4444/foo/bar");
   var referrer = newURI("http://localhost:4444/foo");
   var preconns = ["http://localhost:4444"];
 
-  var verifier = new Verifier("hover", preconns, []);
+  var verifier = new Verifier("hover", [], preconns, []);
   predictor.predict(uri, referrer, predictor.PREDICT_LINK, load_context, verifier);
 }
 
 const pageload_toplevel = newURI("http://localhost:4444/index.html");
 
 function continue_test_pageload() {
   var subresources = [
     "http://localhost:4444/style.css",
@@ -189,17 +238,17 @@ function continue_test_pageload() {
   predictor.learn(pageload_toplevel, null, predictor.LEARN_LOAD_TOPLEVEL, load_context);
   var preconns = [];
   for (var i = 0; i < subresources.length; i++) {
     var sruri = newURI(subresources[i]);
     predictor.learn(sruri, pageload_toplevel, predictor.LEARN_LOAD_SUBRESOURCE, load_context);
     preconns.push(extract_origin(sruri));
   }
 
-  var verifier = new Verifier("pageload", preconns, []);
+  var verifier = new Verifier("pageload", [], preconns, []);
   predictor.predict(pageload_toplevel, null, predictor.PREDICT_LOAD, load_context, verifier);
 }
 
 function test_pageload() {
   open_and_continue([pageload_toplevel], function () {
     if (running_single_process) {
       continue_test_pageload();
     } else {
@@ -225,17 +274,17 @@ function continue_test_redrect() {
   var preconns = [];
   preconns.push(extract_origin(redirect_targeturi));
   for (var i = 0; i < subresources.length; i++) {
     var sruri = newURI(subresources[i]);
     predictor.learn(sruri, redirect_targeturi, predictor.LEARN_LOAD_SUBRESOURCE, load_context);
     preconns.push(extract_origin(sruri));
   }
 
-  var verifier = new Verifier("redirect", preconns, []);
+  var verifier = new Verifier("redirect", [], preconns, []);
   predictor.predict(redirect_inituri, null, predictor.PREDICT_LOAD, load_context, verifier);
 }
 
 function test_redirect() {
   open_and_continue([redirect_inituri, redirect_targeturi], function () {
     if (running_single_process) {
       continue_test_redirect();
     } else {
@@ -258,31 +307,31 @@ function test_startup() {
   ];
   var preconns = [];
   for (var i = 0; i < uris.length; i++) {
     var uri = newURI(uris[i]);
     predictor.learn(uri, null, predictor.LEARN_STARTUP, load_context);
     preconns.push(extract_origin(uri));
   }
 
-  var verifier = new Verifier("startup", preconns, []);
+  var verifier = new Verifier("startup", [], preconns, []);
   predictor.predict(null, null, predictor.PREDICT_STARTUP, load_context, verifier);
 }
 
 const dns_toplevel = newURI("http://localhost:4444/index.html");
 
 function continue_test_dns() {
   var subresource = "http://localhost:4443/jquery.js";
 
   predictor.learn(dns_toplevel, null, predictor.LEARN_LOAD_TOPLEVEL, load_context);
   var sruri = newURI(subresource);
   predictor.learn(sruri, dns_toplevel, predictor.LEARN_LOAD_SUBRESOURCE, load_context);
 
   var preresolves = [extract_origin(sruri)];
-  var verifier = new Verifier("dns", [], preresolves);
+  var verifier = new Verifier("dns", [], [], preresolves);
   predictor.predict(dns_toplevel, null, predictor.PREDICT_LOAD, load_context, verifier);
 }
 
 function test_dns() {
   open_and_continue([dns_toplevel], function () {
     // Ensure that this will do preresolves
     Services.prefs.setIntPref("network.predictor.preconnect-min-confidence", 101);
     if (running_single_process) {
@@ -308,46 +357,177 @@ function continue_test_origin() {
     predictor.learn(sruri, origin_toplevel, predictor.LEARN_LOAD_SUBRESOURCE, load_context);
     var origin = extract_origin(sruri);
     if (preconns.indexOf(origin) === -1) {
       preconns.push(origin);
     }
   }
 
   var loaduri = newURI("http://localhost:4444/anotherpage.html");
-  var verifier = new Verifier("origin", preconns, []);
+  var verifier = new Verifier("origin", [], preconns, []);
   predictor.predict(loaduri, null, predictor.PREDICT_LOAD, load_context, verifier);
 }
 
 function test_origin() {
   open_and_continue([origin_toplevel], function () {
     if (running_single_process) {
       continue_test_origin();
     } else {
       sendCommand("continue_test_origin();");
     }
   });
 }
 
+var httpserv = null;
+var prefetch_tluri;
+var prefetch_sruri;
+
+function prefetchHandler(metadata, response) {
+  response.setStatusLine(metadata.httpVersion, 200, "OK");
+  var body = "Success (meow meow meow).";
+
+  response.bodyOutputStream.write(body, body.length);
+}
+
+var prefetchListener = {
+  onStartRequest: function(request, ctx) {
+    do_check_eq(request.status, Cr.NS_OK);
+  },
+
+  onDataAvailable: function(request, cx, stream, offset, cnt) {
+    read_stream(stream, cnt);
+  },
+
+  onStopRequest: function(request, ctx, status) {
+    run_next_test();
+  }
+};
+
+function test_prefetch_setup() {
+  // Disable preconnects and preresolves
+  Services.prefs.setIntPref("network.predictor.preconnect-min-confidence", 101);
+  Services.prefs.setIntPref("network.predictor.preresolve-min-confidence", 101);
+
+  Services.prefs.setBoolPref("network.predictor.enable-prefetch", true);
+
+  // Makes it so we only have to call test_prefetch_prime twice to make prefetch
+  // do its thing.
+  Services.prefs.setIntPref("network.predictor.prefetch-rolling-load-count", 2);
+
+  // This test does not run in e10s-mode, so we'll just go ahead and skip it.
+  // We've left the e10s test code in below, just in case someone wants to try
+  // to make it work at some point in the future.
+  if (!running_single_process) {
+    dump("skipping test_prefetch_setup due to e10s\n");
+    run_next_test();
+    return;
+  }
+
+  httpserv = new HttpServer();
+  httpserv.registerPathHandler("/cat.jpg", prefetchHandler);
+  httpserv.start(-1);
+
+  var tluri = "http://127.0.0.1:" + httpserv.identity.primaryPort + "/index.html";
+  var sruri = "http://127.0.0.1:" + httpserv.identity.primaryPort + "/cat.jpg";
+  prefetch_tluri = newURI(tluri);
+  prefetch_sruri = newURI(sruri);
+  if (!running_single_process && !is_child_process()) {
+    // Give the child process access to these values
+    sendCommand("prefetch_tluri = newURI(\"" + tluri + "\");");
+    sendCommand("prefetch_sruri = newURI(\"" + sruri + "\");");
+  }
+
+  run_next_test();
+}
+
+// Used to "prime the pump" for prefetch - it makes sure all our learns go
+// through as expected so that prefetching will happen.
+function test_prefetch_prime() {
+  // This test does not run in e10s-mode, so we'll just go ahead and skip it.
+  // We've left the e10s test code in below, just in case someone wants to try
+  // to make it work at some point in the future.
+  if (!running_single_process) {
+    dump("skipping test_prefetch_prime due to e10s\n");
+    run_next_test();
+    return;
+  }
+
+  open_and_continue([prefetch_tluri], function() {
+    if (running_single_process) {
+      predictor.learn(prefetch_tluri, null, predictor.LEARN_LOAD_TOPLEVEL, load_context);
+      predictor.learn(prefetch_sruri, prefetch_tluri, predictor.LEARN_LOAD_SUBRESOURCE, load_context);
+    } else {
+      sendCommand("predictor.learn(prefetch_tluri, null, predictor.LEARN_LOAD_TOPLEVEL, load_context);");
+      sendCommand("predictor.learn(prefetch_sruri, prefetch_tluri, predictor.LEARN_LOAD_SUBRESOURCE, load_context);");
+    }
+
+    // This runs in the parent or only process
+    var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+    var channel = ios.newChannel2(prefetch_sruri.asciiSpec, null, null, null,
+        Services.scriptSecurityManager.getSystemPrincipal(), null,
+        Ci.nsILoadInfo.SEC_NORMAL,
+        Ci.nsIContentPolicy.TYPE_OTHER).QueryInterface(Ci.nsIHttpChannel);
+    channel.requestMethod = "GET";
+    channel.referrer = prefetch_tluri;
+    channel.asyncOpen(prefetchListener, channel);
+  });
+}
+
+function test_prefetch() {
+  // This test does not run in e10s-mode, so we'll just go ahead and skip it.
+  // We've left the e10s test code in below, just in case someone wants to try
+  // to make it work at some point in the future.
+  if (!running_single_process) {
+    dump("skipping test_prefetch due to e10s\n");
+    run_next_test();
+    return;
+  }
+
+  // Setup for this has all been taken care of by test_prefetch_prime, so we can
+  // continue on without pausing here.
+  if (running_single_process) {
+    continue_test_prefetch();
+  } else {
+    sendCommand("continue_test_prefetch();");
+  }
+}
+
+function continue_test_prefetch() {
+  var prefetches = [prefetch_sruri.asciiSpec];
+  var verifier = new Verifier("prefetch", prefetches, [], []);
+  predictor.predict(prefetch_tluri, null, predictor.PREDICT_LOAD, load_context, verifier);
+}
+
 function cleanup() {
   observer.cleaningUp = true;
+  if (running_single_process) {
+    // The http server is required (and started) by the prefetch test, which
+    // only runs in single-process mode, so don't try to shut it down if we're
+    // in e10s mode.
+    do_test_pending();
+    httpserv.stop(do_test_finished);
+  }
   reset_predictor();
 }
 
 var tests = [
   // This must ALWAYS come first, to ensure a clean slate
   reset_predictor,
   test_link_hover,
   test_pageload,
   // TODO: These are disabled until the features are re-written
   //test_redirect,
   //test_startup,
   // END DISABLED TESTS
   test_origin,
   test_dns,
+  test_prefetch_setup,
+  test_prefetch_prime,
+  test_prefetch_prime,
+  test_prefetch,
   // This must ALWAYS come last, to ensure we clean up after ourselves
   cleanup
 ];
 
 var observer = {
   cleaningUp: false,
 
   QueryInterface: function (iid) {
@@ -394,16 +574,19 @@ function run_test_real() {
   registerObserver();
 
   do_register_cleanup(() => {
     Services.prefs.clearUserPref("network.predictor.preconnect-min-confidence");
     Services.prefs.clearUserPref("network.predictor.enabled");
     Services.prefs.clearUserPref("network.predictor.cleaned-up");
     Services.prefs.clearUserPref("browser.cache.use_new_backend_temp");
     Services.prefs.clearUserPref("browser.cache.use_new_backend");
+    Services.prefs.clearUserPref("network.predictor.preresolve-min-confidence");
+    Services.prefs.clearUserPref("network.predictor.enable-prefetch");
+    Services.prefs.clearUserPref("network.predictor.prefetch-rolling-load-count");
   });
 
   run_next_test();
 }
 
 function run_test() {
   // This indirection is necessary to make e10s tests work as expected
   running_single_process = true;
--- a/netwerk/test/unit_ipc/test_predictor_wrap.js
+++ b/netwerk/test/unit_ipc/test_predictor_wrap.js
@@ -1,8 +1,32 @@
+// This is the bit that runs in the parent process when the test begins. Here's
+// what happens:
+//
+//  - Load the entire single-process test in the parent
+//  - Setup the test harness within the child process
+//  - Send a command to the child to have the single-process test loaded there, as well
+//  - Send a command to the child to make the predictor available
+//  - run_test_real in the parent
+//  - run_test_real does a bunch of pref-setting and other fun things on the parent
+//  - once all that's done, it calls run_next_test IN THE PARENT
+//  - every time run_next_test is called, it is called IN THE PARENT
+//  - the test that gets started then does any parent-side setup that's necessary
+//  - once the parent-side setup is done, it calls continue_<testname> IN THE CHILD
+//  - when the test is done running on the child, the child calls predictor.reset
+//    this causes some code to be run on the parent which, when complete, sends an
+//    obserer service notification IN THE PARENT, which causes run_next_test to be
+//    called again, bumping us up to the top of the loop outlined here
+//  - when the final test is done, cleanup happens IN THE PARENT and we're done
+//
+// This is a little confusing, but it's what we have to do in order to have some
+// things that must run on the parent (the setup - opening cache entries, etc)
+// but with most of the test running on the child (calls to the predictor api,
+// verification, etc).
+//
 function run_test() {
   var test_path = do_get_file("../unit/test_predictor.js").path.replace(/\\/g, "/");
   load(test_path);
   do_load_child_test_harness();
   do_test_pending();
   sendCommand("load(\"" + test_path + "\");", function () {
     sendCommand("predictor = Cc[\"@mozilla.org/network/predictor;1\"].getService(Ci.nsINetworkPredictor);", function() {
       run_test_real();
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2418,16 +2418,43 @@
   },
   "PREDICTOR_TOTAL_PREDICTIONS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 1000000,
     "n_buckets": 50,
     "description": "How many actual predictions (preresolves, preconnects, ...) happen"
   },
+  "PREDICTOR_TOTAL_PREFETCHES": {
+    "expires_in_version": "never",
+    "alert_emails": [],
+    "bug_numbers": [1016628],
+    "kind": "exponential",
+    "high": 1000000,
+    "n_buckets": 50,
+    "description": "How many actual prefetches happen"
+  },
+  "PREDICTOR_TOTAL_PREFETCHES_USED": {
+    "expires_in_version": "never",
+    "alert_emails": [],
+    "bug_numbers": [1016628],
+    "kind": "exponential",
+    "high": 1000000,
+    "n_buckets": 50,
+    "description": "How many prefetches are actually used by a channel"
+  },
+  "PREDICTOR_PREFETCH_TIME": {
+    "expires_in_version": "never",
+    "alert_emails": [],
+    "bug_numbers": [1016628],
+    "kind": "exponential",
+    "high": 3000,
+    "n_buckets": 10,
+    "description": "How long it takes from OnStartRequest to OnStopRequest for a prefetch"
+  },
   "PREDICTOR_TOTAL_PRECONNECTS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 1000000,
     "n_buckets": 50,
     "description": "How many actual preconnects happen"
   },
   "PREDICTOR_TOTAL_PRECONNECTS_CREATED": {