Bug 881804 (part 1) - Add support for predictive network actions. r=mcmanus f=honzab sr=biesi
authorNicholas Hurley <hurley@todesschaf.org>
Fri, 25 Oct 2013 14:56:51 -0700
changeset 166117 4bcbb58917c962d1e4c51ce436d4bc6fe7852754
parent 166116 e61f767c0dcc65d2f7480663c55b006fe9a84937
child 166118 85ebad9a27c9c21c8aa7e9ad78ed1de7276c869f
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcmanus, biesi
bugs881804
milestone27.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 881804 (part 1) - Add support for predictive network actions. r=mcmanus f=honzab sr=biesi
modules/libpref/src/init/all.js
netwerk/base/public/moz.build
netwerk/base/public/nsINetworkSeer.idl
netwerk/base/public/nsINetworkSeerVerifier.idl
netwerk/base/public/nsISpeculativeConnect.idl
netwerk/base/src/Seer.cpp
netwerk/base/src/Seer.h
netwerk/base/src/moz.build
netwerk/build/nsNetCID.h
netwerk/build/nsNetModule.cpp
netwerk/protocol/http/nsHttpConnectionMgr.cpp
netwerk/protocol/http/nsHttpConnectionMgr.h
netwerk/test/unit/test_seer.js
netwerk/test/unit/xpcshell.ini
toolkit/components/telemetry/Histograms.json
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -1369,16 +1369,34 @@ pref("network.ftp.idleConnectionTimeout"
 // 2: HTML
 // 3: XUL directory viewer
 // all other values are treated like 2
 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.seer.enabled", true);
+pref("network.seer.enable-hover-on-ssl", false);
+pref("network.seer.page-degradation.day", 0);
+pref("network.seer.page-degradation.week", 5);
+pref("network.seer.page-degradation.month", 10);
+pref("network.seer.page-degradation.year", 25);
+pref("network.seer.page-degradation.max", 50);
+pref("network.seer.subresource-degradation.day", 1);
+pref("network.seer.subresource-degradation.week", 10);
+pref("network.seer.subresource-degradation.month", 25);
+pref("network.seer.subresource-degradation.year", 50);
+pref("network.seer.subresource-degradation.max", 100);
+pref("network.seer.preconnect-min-confidence", 90);
+pref("network.seer.preresolve-min-confidence", 60);
+pref("network.seer.redirect-likely-confidence", 75);
+pref("network.seer.max-queue-size", 50);
+
 
 // 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:
 //   [scheme "://"] [host [":" port]]
 // For example, "foo.com" would match "http://www.foo.com/bar", etc.
 
--- a/netwerk/base/public/moz.build
+++ b/netwerk/base/public/moz.build
@@ -51,16 +51,18 @@ XPIDL_SOURCES += [
     'nsILoadGroupChild.idl',
     'nsIMIMEInputStream.idl',
     'nsIMultiPartChannel.idl',
     'nsINestedURI.idl',
     'nsINetAddr.idl',
     'nsINetUtil.idl',
     'nsINetworkLinkService.idl',
     'nsINetworkProperties.idl',
+    'nsINetworkSeer.idl',
+    'nsINetworkSeerVerifier.idl',
     'nsINSSErrorsService.idl',
     'nsIParentChannel.idl',
     'nsIParentRedirectingChannel.idl',
     'nsIPermission.idl',
     'nsIPermissionManager.idl',
     'nsIPrivateBrowsingChannel.idl',
     'nsIProgressEventSink.idl',
     'nsIPrompt.idl',
new file mode 100644
--- /dev/null
+++ b/netwerk/base/public/nsINetworkSeer.idl
@@ -0,0 +1,164 @@
+/* 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 "nsISupports.idl"
+
+interface nsIURI;
+interface nsILoadContext;
+interface nsINetworkSeerVerifier;
+
+typedef unsigned long SeerPredictReason;
+typedef unsigned long SeerLearnReason;
+
+/**
+ * nsINetworkSeer - learn about pages users visit, and allow us to take
+ *                  predictive actions upon future visits.
+ *                  NOTE: nsINetworkSeer should only be used on the main thread
+ */
+[scriptable, uuid(884a39a0-a3ed-4855-826a-fabb73ae878d)]
+interface nsINetworkSeer : nsISupports
+{
+  /**
+   * Prediction reasons
+   *
+   * PREDICT_LINK - we are being asked to take predictive action because
+   * the user is hovering over a link.
+   *
+   * PREDICT_LOAD - we are being asked to take predictive action because
+   * the user has initiated a pageload.
+   *
+   * PREDICT_STARTUP - we are being asked to take predictive action
+   * because the browser is starting up.
+   */
+  const SeerPredictReason PREDICT_LINK = 0;
+  const SeerPredictReason PREDICT_LOAD = 1;
+  const SeerPredictReason PREDICT_STARTUP = 2;
+
+  /**
+   * Start taking predictive actions
+   *
+   * Calling this will cause the seer to (possibly) start
+   * taking actions such as DNS prefetch and/or TCP preconnect based on
+   * (1) the host name that we are given, and (2) the reason we are being
+   * asked to take actions.
+   *
+   * @param targetURI - The URI we are being asked to take actions based on.
+   * @param sourceURI - The URI that is currently loaded. This is so we can
+   *   avoid doing predictive actions for link hover on an HTTPS page (for
+   *   example).
+   * @param reason - The reason we are being asked to take actions. Can be
+   *   any of the PREDICT_* values above.
+   *   In the case of PREDICT_LINK, targetURI should be the URI of the link
+   *   that is being hovered over, and sourceURI should be the URI of the page
+   *   on which the link appears.
+   *   In the case of PREDICT_LOAD, targetURI should be the URI of the page that
+   *   is being loaded and sourceURI should be null.
+   *   In the case of PREDICT_STARTUP, both targetURI and sourceURI should be
+   *   null.
+   * @param loadContext - The nsILoadContext of the page load we are predicting
+   *   about.
+   * @param verifier - An nsINetworkSeerVerifier used in testing to ensure we're
+   *   predicting the way we expect to. Not necessary (or desired) for normal
+   *   operation.
+   */
+  void predict(in nsIURI targetURI,
+               in nsIURI sourceURI,
+               in SeerPredictReason reason,
+               in nsILoadContext loadContext,
+               in nsINetworkSeerVerifier verifier);
+
+
+  /*
+   * Reasons we are learning something
+   *
+   * LEARN_LOAD_TOPLEVEL - we are learning about the toplevel resource of a
+   *                       pageload (NOTE: this should ONLY be used by tests)
+   *
+   * LEARN_LOAD_SUBRESOURCE - we are learning a subresource from a pageload
+   *
+   * LEARN_LOAD_REDIRECT - we are learning about the re-direct of a URI
+   *
+   * LEARN_STARTUP - we are learning about a page loaded during startup
+   */
+  const SeerLearnReason LEARN_LOAD_TOPLEVEL = 0;
+  const SeerLearnReason LEARN_LOAD_SUBRESOURCE = 1;
+  const SeerLearnReason LEARN_LOAD_REDIRECT = 2;
+  const SeerLearnReason LEARN_STARTUP = 3;
+
+  /**
+   * Add to our compendium of knowledge
+   *
+   * This adds to our prediction database to make things (hopefully)
+   * smarter next time we predict something.
+   *
+   * @param targetURI - The URI that was loaded that we are keeping track of.
+   * @param sourceURI - The URI that caused targetURI to be loaded (for page
+   *   loads). This means the DOCUMENT URI.
+   * @param reason - The reason we are learning this bit of knowledge.
+   *   Reason can be any of the LEARN_* values.
+   *   In the case of LEARN_LOAD_SUBRESOURCE, targetURI should be the URI of a
+   *   subresource of a page, and sourceURI should be the top-level URI.
+   *   In the case of LEARN_LOAD_REDIRECT, targetURI is the NEW URI of a
+   *   top-level resource that was redirected to, and sourceURI is the
+   *   ORIGINAL URI of said top-level resource.
+   *   In the case of LEARN_STARTUP, targetURI should be the URI of a page
+   *   that was loaded immediately after browser startup, and sourceURI should
+   *   be null.
+   * @param loadContext - The nsILoadContext for the page load that we are
+   *   learning about.
+   */
+  void learn(in nsIURI targetURI,
+             in nsIURI sourceURI,
+             in SeerLearnReason reason,
+             in nsILoadContext loadContext);
+
+  /**
+   * Clear out all our learned knowledge
+   *
+   * This removes everything from our database so that any predictions begun
+   * after this completes will start from a blank slate.
+   */
+  void reset();
+};
+
+%{C++
+// Wrapper functions to make use of the seer easier and less invasive
+class nsIChannel;
+class nsIDocument;
+class nsILoadContext;
+class nsILoadGroup;
+class nsINetworkSeerVerifier;
+
+namespace mozilla {
+namespace net {
+
+nsresult SeerPredict(nsIURI *targetURI,
+                     nsIURI *sourceURI,
+                     SeerPredictReason reason,
+                     nsILoadContext *loadContext,
+                     nsINetworkSeerVerifier *verifier);
+
+nsresult SeerLearn(nsIURI *targetURI,
+                   nsIURI *sourceURI,
+                   SeerLearnReason reason,
+                   nsILoadContext *loadContext);
+
+nsresult SeerLearn(nsIURI *targetURI,
+                   nsIURI *sourceURI,
+                   SeerLearnReason reason,
+                   nsILoadGroup *loadGroup);
+
+nsresult SeerLearn(nsIURI *targetURI,
+                   nsIURI *sourceURI,
+                   SeerLearnReason reason,
+                   nsIDocument *document);
+
+nsresult SeerLearnRedirect(nsIURI *targetURI,
+                           nsIChannel *channel,
+                           nsILoadContext *loadContext);
+
+} // mozilla::net
+} // mozilla
+%}
new file mode 100644
--- /dev/null
+++ b/netwerk/base/public/nsINetworkSeerVerifier.idl
@@ -0,0 +1,31 @@
+/* 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/. */
+
+/**
+ * nsINetworkSeerVerifier - used for testing the network seer to ensure it
+ *                          does what we expect it to do.
+ */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+[scriptable, uuid(ea273653-43a8-4632-8b30-4032e0918e8b)]
+interface nsINetworkSeerVerifier : nsISupports
+{
+    /**
+     * 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
+     *
+     * @param uri - The URI that was looked up
+     */
+    void onPredictDNS(in nsIURI uri);
+};
--- a/netwerk/base/public/nsISpeculativeConnect.idl
+++ b/netwerk/base/public/nsISpeculativeConnect.idl
@@ -25,8 +25,36 @@ interface nsISpeculativeConnect : nsISup
      *        such as nsIBadCertListener. May be null.
      *
      */
     void speculativeConnect(in nsIURI aURI,
                             in nsIInterfaceRequestor aCallbacks);
 
 };
 
+/**
+ * This is used to override the default values for various values (documented
+ * inline) to determine whether or not to actually make a speculative
+ * connection.
+ */
+[builtinclass, uuid(2b6d6fb6-ab28-4f4c-af84-bfdbb7866d72)]
+interface nsISpeculativeConnectionOverrider : nsISupports
+{
+    /**
+     * Used to determine the maximum number of unused speculative connections
+     * we will have open for a host at any one time
+     */
+    [infallible] readonly attribute unsigned long parallelSpeculativeConnectLimit;
+
+    /**
+     * Used to loosen the restrictions nsHttpConnectionMgr::RestrictConnections
+     * to allow more speculative connections when we're unsure if a host will
+     * connect via SPDY or not.
+     */
+    [infallible] readonly attribute boolean ignorePossibleSpdyConnections;
+
+    /**
+     * Used to determine if we will ignore the existence of any currently idle
+     * connections when we decide whether or not to make a speculative
+     * connection.
+     */
+    [infallible] readonly attribute boolean ignoreIdle;
+};
new file mode 100644
--- /dev/null
+++ b/netwerk/base/src/Seer.cpp
@@ -0,0 +1,2189 @@
+/* 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 <algorithm>
+
+#include "Seer.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsICancelable.h"
+#include "nsIChannel.h"
+#include "nsIDNSListener.h"
+#include "nsIDNSService.h"
+#include "nsIDocument.h"
+#include "nsIFile.h"
+#include "nsILoadContext.h"
+#include "nsILoadGroup.h"
+#include "nsINetworkSeerVerifier.h"
+#include "nsIObserverService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsISpeculativeConnect.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+#include "prlog.h"
+
+#include "mozIStorageConnection.h"
+#include "mozIStorageService.h"
+#include "mozIStorageStatement.h"
+#include "mozStorageHelper.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/storage.h"
+#include "mozilla/Telemetry.h"
+
+#if defined(ANDROID) && !defined(MOZ_WIDGET_GONK)
+#include "nsIPropertyBag2.h"
+static const int32_t ANDROID_23_VERSION = 10;
+#endif
+
+using namespace mozilla;
+
+namespace mozilla {
+namespace net {
+
+#define RETURN_IF_FAILED(_rv) \
+  do { \
+    if (NS_FAILED(_rv)) { \
+      return; \
+    } \
+  } while (0)
+
+const char SEER_ENABLED_PREF[] = "network.seer.enabled";
+const char SEER_SSL_HOVER_PREF[] = "network.seer.enable-hover-on-ssl";
+
+const char SEER_PAGE_DELTA_DAY_PREF[] = "network.seer.page-degradation.day";
+const int SEER_PAGE_DELTA_DAY_DEFAULT = 0;
+const char SEER_PAGE_DELTA_WEEK_PREF[] = "network.seer.page-degradation.week";
+const int SEER_PAGE_DELTA_WEEK_DEFAULT = 5;
+const char SEER_PAGE_DELTA_MONTH_PREF[] = "network.seer.page-degradation.month";
+const int SEER_PAGE_DELTA_MONTH_DEFAULT = 10;
+const char SEER_PAGE_DELTA_YEAR_PREF[] = "network.seer.page-degradation.year";
+const int SEER_PAGE_DELTA_YEAR_DEFAULT = 25;
+const char SEER_PAGE_DELTA_MAX_PREF[] = "network.seer.page-degradation.max";
+const int SEER_PAGE_DELTA_MAX_DEFAULT = 50;
+const char SEER_SUB_DELTA_DAY_PREF[] =
+  "network.seer.subresource-degradation.day";
+const int SEER_SUB_DELTA_DAY_DEFAULT = 1;
+const char SEER_SUB_DELTA_WEEK_PREF[] =
+  "network.seer.subresource-degradation.week";
+const int SEER_SUB_DELTA_WEEK_DEFAULT = 10;
+const char SEER_SUB_DELTA_MONTH_PREF[] =
+  "network.seer.subresource-degradation.month";
+const int SEER_SUB_DELTA_MONTH_DEFAULT = 25;
+const char SEER_SUB_DELTA_YEAR_PREF[] =
+  "network.seer.subresource-degradation.year";
+const int SEER_SUB_DELTA_YEAR_DEFAULT = 50;
+const char SEER_SUB_DELTA_MAX_PREF[] =
+  "network.seer.subresource-degradation.max";
+const int SEER_SUB_DELTA_MAX_DEFAULT = 100;
+
+const char SEER_PRECONNECT_MIN_PREF[] =
+  "network.seer.preconnect-min-confidence";
+const int PRECONNECT_MIN_DEFAULT = 90;
+const char SEER_PRERESOLVE_MIN_PREF[] =
+  "network.seer.preresolve-min-confidence";
+const int PRERESOLVE_MIN_DEFAULT = 60;
+const char SEER_REDIRECT_LIKELY_PREF[] =
+  "network.seer.redirect-likely-confidence";
+const int REDIRECT_LIKELY_DEFAULT = 75;
+
+const char SEER_MAX_QUEUE_SIZE_PREF[] = "network.seer.max-queue-size";
+const uint32_t SEER_MAX_QUEUE_SIZE_DEFAULT = 50;
+
+// All these time values are in usec
+const long long ONE_DAY = 86400LL * 1000000LL;
+const long long ONE_WEEK = 7LL * ONE_DAY;
+const long long ONE_MONTH = 30LL * ONE_DAY;
+const long long ONE_YEAR = 365LL * ONE_DAY;
+
+const long STARTUP_WINDOW = 5L * 60L * 1000000L; // 5min
+
+// Version for the database schema
+static const int32_t SEER_SCHEMA_VERSION = 1;
+
+struct SeerTelemetryAccumulators {
+  Telemetry::AutoCounter<Telemetry::SEER_PREDICT_ATTEMPTS> mPredictAttempts;
+  Telemetry::AutoCounter<Telemetry::SEER_LEARN_ATTEMPTS> mLearnAttempts;
+  Telemetry::AutoCounter<Telemetry::SEER_PREDICT_FULL_QUEUE> mPredictFullQueue;
+  Telemetry::AutoCounter<Telemetry::SEER_LEARN_FULL_QUEUE> mLearnFullQueue;
+  Telemetry::AutoCounter<Telemetry::SEER_TOTAL_PREDICTIONS> mTotalPredictions;
+  Telemetry::AutoCounter<Telemetry::SEER_TOTAL_PRECONNECTS> mTotalPreconnects;
+  Telemetry::AutoCounter<Telemetry::SEER_TOTAL_PRERESOLVES> mTotalPreresolves;
+  Telemetry::AutoCounter<Telemetry::SEER_PREDICTIONS_CALCULATED> mPredictionsCalculated;
+};
+
+// 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 Seer
+// can use.
+
+class SeerDNSListener : public nsIDNSListener
+{
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSIDNSLISTENER
+
+  SeerDNSListener()
+  { }
+
+  virtual ~SeerDNSListener()
+  { }
+};
+
+NS_IMPL_ISUPPORTS1(SeerDNSListener, nsIDNSListener);
+
+NS_IMETHODIMP
+SeerDNSListener::OnLookupComplete(nsICancelable *request,
+                                  nsIDNSRecord *rec,
+                                  nsresult status)
+{
+  return NS_OK;
+}
+
+// Are you ready for the fun part? Because here comes the fun part. The seer,
+// which will do awesome stuff as you browse to make your browsing experience
+// faster.
+
+static Seer *gSeer = nullptr;
+
+#if defined(PR_LOGGING)
+static PRLogModuleInfo *gSeerLog = nullptr;
+#define SEER_LOG(args) PR_LOG(gSeerLog, 4, args)
+#else
+#define SEER_LOG(args)
+#endif
+
+NS_IMPL_ISUPPORTS4(Seer,
+                   nsINetworkSeer,
+                   nsIObserver,
+                   nsISpeculativeConnectionOverrider,
+                   nsIInterfaceRequestor)
+
+Seer::Seer()
+  :mInitialized(false)
+  ,mEnabled(true)
+  ,mEnableHoverOnSSL(false)
+  ,mPageDegradationDay(SEER_PAGE_DELTA_DAY_DEFAULT)
+  ,mPageDegradationWeek(SEER_PAGE_DELTA_WEEK_DEFAULT)
+  ,mPageDegradationMonth(SEER_PAGE_DELTA_MONTH_DEFAULT)
+  ,mPageDegradationYear(SEER_PAGE_DELTA_YEAR_DEFAULT)
+  ,mPageDegradationMax(SEER_PAGE_DELTA_MAX_DEFAULT)
+  ,mSubresourceDegradationDay(SEER_SUB_DELTA_DAY_DEFAULT)
+  ,mSubresourceDegradationWeek(SEER_SUB_DELTA_WEEK_DEFAULT)
+  ,mSubresourceDegradationMonth(SEER_SUB_DELTA_MONTH_DEFAULT)
+  ,mSubresourceDegradationYear(SEER_SUB_DELTA_YEAR_DEFAULT)
+  ,mSubresourceDegradationMax(SEER_SUB_DELTA_MAX_DEFAULT)
+  ,mPreconnectMinConfidence(PRECONNECT_MIN_DEFAULT)
+  ,mPreresolveMinConfidence(PRERESOLVE_MIN_DEFAULT)
+  ,mRedirectLikelyConfidence(REDIRECT_LIKELY_DEFAULT)
+  ,mMaxQueueSize(SEER_MAX_QUEUE_SIZE_DEFAULT)
+  ,mStatements(mDB)
+  ,mLastStartupTime(0)
+  ,mStartupCount(0)
+  ,mQueueSize(0)
+  ,mQueueSizeLock("Seer.mQueueSizeLock")
+{
+#if defined(PR_LOGGING)
+  gSeerLog = PR_NewLogModule("NetworkSeer");
+#endif
+
+  MOZ_ASSERT(!gSeer, "multiple Seer instances!");
+  gSeer = this;
+}
+
+Seer::~Seer()
+{
+  if (mInitialized)
+    Shutdown();
+
+  RemoveObserver();
+
+  gSeer = nullptr;
+}
+
+// Seer::nsIObserver
+
+nsresult
+Seer::InstallObserver()
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Installing observer off main thread");
+
+  nsresult rv = NS_OK;
+  nsCOMPtr<nsIObserverService> 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);
+
+  nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+  if (!prefs) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  Preferences::AddBoolVarCache(&mEnabled, SEER_ENABLED_PREF, true);
+  Preferences::AddBoolVarCache(&mEnableHoverOnSSL, SEER_SSL_HOVER_PREF, false);
+  Preferences::AddIntVarCache(&mPageDegradationDay, SEER_PAGE_DELTA_DAY_PREF,
+                              SEER_PAGE_DELTA_DAY_DEFAULT);
+  Preferences::AddIntVarCache(&mPageDegradationWeek, SEER_PAGE_DELTA_WEEK_PREF,
+                              SEER_PAGE_DELTA_WEEK_DEFAULT);
+  Preferences::AddIntVarCache(&mPageDegradationMonth,
+                              SEER_PAGE_DELTA_MONTH_PREF,
+                              SEER_PAGE_DELTA_MONTH_DEFAULT);
+  Preferences::AddIntVarCache(&mPageDegradationYear, SEER_PAGE_DELTA_YEAR_PREF,
+                              SEER_PAGE_DELTA_YEAR_DEFAULT);
+  Preferences::AddIntVarCache(&mPageDegradationMax, SEER_PAGE_DELTA_MAX_PREF,
+                              SEER_PAGE_DELTA_MAX_DEFAULT);
+
+  Preferences::AddIntVarCache(&mSubresourceDegradationDay,
+                              SEER_SUB_DELTA_DAY_PREF,
+                              SEER_SUB_DELTA_DAY_DEFAULT);
+  Preferences::AddIntVarCache(&mSubresourceDegradationWeek,
+                              SEER_SUB_DELTA_WEEK_PREF,
+                              SEER_SUB_DELTA_WEEK_DEFAULT);
+  Preferences::AddIntVarCache(&mSubresourceDegradationMonth,
+                              SEER_SUB_DELTA_MONTH_PREF,
+                              SEER_SUB_DELTA_MONTH_DEFAULT);
+  Preferences::AddIntVarCache(&mSubresourceDegradationYear,
+                              SEER_SUB_DELTA_YEAR_PREF,
+                              SEER_SUB_DELTA_YEAR_DEFAULT);
+  Preferences::AddIntVarCache(&mSubresourceDegradationMax,
+                              SEER_SUB_DELTA_MAX_PREF,
+                              SEER_SUB_DELTA_MAX_DEFAULT);
+
+  Preferences::AddIntVarCache(&mPreconnectMinConfidence,
+                              SEER_PRECONNECT_MIN_PREF,
+                              PRECONNECT_MIN_DEFAULT);
+  Preferences::AddIntVarCache(&mPreresolveMinConfidence,
+                              SEER_PRERESOLVE_MIN_PREF,
+                              PRERESOLVE_MIN_DEFAULT);
+  Preferences::AddIntVarCache(&mRedirectLikelyConfidence,
+                              SEER_REDIRECT_LIKELY_PREF,
+                              REDIRECT_LIKELY_DEFAULT);
+
+  Preferences::AddIntVarCache(&mMaxQueueSize, SEER_MAX_QUEUE_SIZE_PREF,
+                              SEER_MAX_QUEUE_SIZE_DEFAULT);
+
+  return rv;
+}
+
+void
+Seer::RemoveObserver()
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Removing observer off main thread");
+
+  nsCOMPtr<nsIObserverService> obs =
+    mozilla::services::GetObserverService();
+  if (obs) {
+    obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+  }
+}
+
+NS_IMETHODIMP
+Seer::Observe(nsISupports *subject, const char *topic,
+              const PRUnichar *data_unicode)
+{
+  nsresult rv = NS_OK;
+
+  if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, topic)) {
+    gSeer->Shutdown();
+  }
+
+  return rv;
+}
+
+// Seer::nsISpeculativeConnectionOverrider
+
+NS_IMETHODIMP
+Seer::GetIgnoreIdle(bool *ignoreIdle)
+{
+  *ignoreIdle = true;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+Seer::GetIgnorePossibleSpdyConnections(bool *ignorePossibleSpdyConnections)
+{
+  *ignorePossibleSpdyConnections = true;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+Seer::GetParallelSpeculativeConnectLimit(
+    uint32_t *parallelSpeculativeConnectLimit)
+{
+  *parallelSpeculativeConnectLimit = 6;
+  return NS_OK;
+}
+
+// Seer::nsIInterfaceRequestor
+
+NS_IMETHODIMP
+Seer::GetInterface(const nsIID &iid, void **result)
+{
+  return QueryInterface(iid, result);
+}
+
+// Seer::nsINetworkSeer
+
+nsresult
+Seer::Init()
+{
+  if (!NS_IsMainThread()) {
+    MOZ_ASSERT(false, "Seer::Init called off the main thread!");
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  nsresult rv = NS_OK;
+
+#if defined(ANDROID) && !defined(MOZ_WIDGET_GONK)
+  // This is an ugly hack to disable the seer on android < 2.3, as it doesn't
+  // play nicely with those android versions, at least on our infra. Causes
+  // timeouts in reftests. See bug 881804 comment 86.
+  nsCOMPtr<nsIPropertyBag2> infoService =
+    do_GetService("@mozilla.org/system-info;1");
+  if (infoService) {
+    int32_t androidVersion = -1;
+    rv = infoService->GetPropertyAsInt32(NS_LITERAL_STRING("version"),
+                                         &androidVersion);
+    if (NS_SUCCEEDED(rv) && (androidVersion < ANDROID_23_VERSION)) {
+      return NS_ERROR_NOT_AVAILABLE;
+    }
+  }
+#endif
+
+  mStartupTime = PR_Now();
+
+  mAccumulators = new SeerTelemetryAccumulators();
+
+  rv = InstallObserver();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!mDNSListener) {
+    mDNSListener = new SeerDNSListener();
+  }
+
+  rv = NS_NewNamedThread("Network Seer", getter_AddRefs(mIOThread));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mSpeculativeService = do_GetService("@mozilla.org/network/io-service;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mDnsService = do_GetService("@mozilla.org/network/dns-service;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mStorageService = do_GetService("@mozilla.org/storage/service;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+                              getter_AddRefs(mDBFile));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = mDBFile->AppendNative(NS_LITERAL_CSTRING("seer.sqlite"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mInitialized = true;
+
+  return rv;
+}
+
+// Make sure that our sqlite storage is all set up with all the tables we need
+// to do the work. It isn't the end of the world if this fails, since this is
+// all an optimization, anyway.
+
+nsresult
+Seer::EnsureInitStorage()
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "Initializing seer storage on main thread");
+
+  if (mDB) {
+    return NS_OK;
+  }
+
+  nsresult rv;
+
+  rv = mStorageService->OpenDatabase(mDBFile, getter_AddRefs(mDB));
+  if (NS_FAILED(rv)) {
+    // Retry once by trashing the file and trying to open again. If this fails,
+    // we can just bail, and hope for better luck next time.
+    rv = mDBFile->Remove(false);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = mStorageService->OpenDatabase(mDBFile, getter_AddRefs(mDB));
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("PRAGMA synchronous = OFF;"));
+  mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("PRAGMA foreign_keys = ON;"));
+
+  // A table to make sure we're working with the database layout we expect
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_seer_version (\n"
+                         "  version INTEGER NOT NULL\n"
+                         ");\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  rv = mDB->CreateStatement(
+      NS_LITERAL_CSTRING("SELECT version FROM moz_seer_version;\n"),
+      getter_AddRefs(stmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  bool hasRows;
+  rv = stmt->ExecuteStep(&hasRows);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (hasRows) {
+    int32_t currentVersion;
+    rv = stmt->GetInt32(0, &currentVersion);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // This is what we do while we only have one schema version. Later, we'll
+    // have to change this to actually upgrade things as appropriate.
+    MOZ_ASSERT(currentVersion == SEER_SCHEMA_VERSION,
+               "Invalid seer schema version!");
+    if (currentVersion != SEER_SCHEMA_VERSION) {
+      return NS_ERROR_UNEXPECTED;
+    }
+  } else {
+    stmt = nullptr;
+    rv = mDB->CreateStatement(
+        NS_LITERAL_CSTRING("INSERT INTO moz_seer_version (version) VALUES "
+                           "(:seer_version);"),
+        getter_AddRefs(stmt));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("seer_version"),
+                               SEER_SCHEMA_VERSION);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    stmt->Execute();
+  }
+
+  stmt = nullptr;
+
+  // This table keeps track of the hosts we've seen at the top level of a
+  // pageload so we can map them to hosts used for subresources of a pageload.
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_hosts (\n"
+                         "  id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
+                         "  origin TEXT NOT NULL,\n"
+                         "  loads INTEGER DEFAULT 0,\n"
+                         "  last_load INTEGER DEFAULT 0\n"
+                         ");\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // And this is the table that keeps track of the hosts for subresources of a
+  // pageload.
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_subhosts (\n"
+                         "  id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
+                         "  hid INTEGER NOT NULL,\n"
+                         "  origin TEXT NOT NULL,\n"
+                         "  hits INTEGER DEFAULT 0,\n"
+                         "  last_hit INTEGER DEFAULT 0,\n"
+                         "  FOREIGN KEY(hid) REFERENCES moz_hosts(id) ON DELETE CASCADE\n"
+                         ");\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE INDEX IF NOT EXISTS subhost_hid_origin_index "
+                         "ON moz_subhosts (hid, origin);"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Table to keep track of how many times we've started up, and when the last
+  // time was.
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_startups (\n"
+                         "  startups INTEGER,\n"
+                         "  last_startup INTEGER\n"
+                         ");\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mDB->CreateStatement(
+      NS_LITERAL_CSTRING("SELECT startups, last_startup FROM moz_startups;\n"),
+      getter_AddRefs(stmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // We'll go ahead and keep track of our startup count here, since we can
+  // (mostly) equate "the service was created and asked to do stuff" with
+  // "the browser was started up".
+  rv = stmt->ExecuteStep(&hasRows);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (hasRows) {
+    // We've started up before. Update our startup statistics
+    stmt->GetInt32(0, &mStartupCount);
+    stmt->GetInt64(1, &mLastStartupTime);
+
+    // This finalizes the statement
+    stmt = nullptr;
+
+    rv = mDB->CreateStatement(
+        NS_LITERAL_CSTRING("UPDATE moz_startups SET startups = :startup_count "
+                           "last_startup = :startup_time;\n"),
+        getter_AddRefs(stmt));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("startup_count"),
+                               mStartupCount + 1);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startup_time"),
+                               mStartupTime);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    stmt->Execute();
+  } else {
+    // This is our first startup, so let's go ahead and mark it as such
+    mStartupCount = 1;
+
+    rv = mDB->CreateStatement(
+        NS_LITERAL_CSTRING("INSERT INTO moz_startups (startups, last_startup) "
+                           "VALUES (1, :startup_time);\n"),
+        getter_AddRefs(stmt));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startup_time"),
+                               mStartupTime);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    stmt->Execute();
+  }
+
+  // This finalizes the statement
+  stmt = nullptr;
+
+  // This table lists URIs loaded at startup, along with how many startups
+  // they've been loaded during, and when the last time was.
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_startup_pages (\n"
+                         "  id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
+                         "  uri TEXT NOT NULL,\n"
+                         "  hits INTEGER DEFAULT 0,\n"
+                         "  last_hit INTEGER DEFAULT 0\n"
+                         ");\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // This table is similar to moz_hosts above, but uses full URIs instead of
+  // hosts so that we can get more specific predictions for URIs that people
+  // visit often (such as their email or social network home pages).
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_pages (\n"
+                         "  id integer PRIMARY KEY AUTOINCREMENT,\n"
+                         "  uri TEXT NOT NULL,\n"
+                         "  loads INTEGER DEFAULT 0,\n"
+                         "  last_load INTEGER DEFAULT 0\n"
+                         ");\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // This table is similar to moz_subhosts above, but is instead related to
+  // moz_pages for finer granularity.
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_subresources (\n"
+                         "  id integer PRIMARY KEY AUTOINCREMENT,\n"
+                         "  pid INTEGER NOT NULL,\n"
+                         "  uri TEXT NOT NULL,\n"
+                         "  hits INTEGER DEFAULT 0,\n"
+                         "  last_hit INTEGER DEFAULT 0,\n"
+                         "  FOREIGN KEY(pid) REFERENCES moz_pages(id) ON DELETE CASCADE\n"
+                         ");\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE INDEX IF NOT EXISTS subresource_pid_uri_index "
+                         "ON moz_subresources (pid, uri);"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // This table keeps track of URIs and what they end up finally redirecting to
+  // so we can handle redirects in a sane fashion, as well.
+  rv = mDB->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_redirects (\n"
+                         "  id integer PRIMARY KEY AUTOINCREMENT,\n"
+                         "  pid integer NOT NULL,\n"
+                         "  uri TEXT NOT NULL,\n"
+                         "  origin TEXT NOT NULL,\n"
+                         "  hits INTEGER DEFAULT 0,\n"
+                         "  last_hit INTEGER DEFAULT 0,\n"
+                         "  FOREIGN KEY(pid) REFERENCES moz_pages(id)\n"
+                         ");\n"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+class SeerThreadShutdownRunner : public nsRunnable
+{
+public:
+  SeerThreadShutdownRunner(nsIThread *ioThread)
+    :mIOThread(ioThread)
+  { }
+
+  NS_IMETHODIMP Run() MOZ_OVERRIDE
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "Shut down seer io thread off main thread");
+    mIOThread->Shutdown();
+    return NS_OK;
+  }
+
+private:
+  nsCOMPtr<nsIThread> mIOThread;
+};
+
+class SeerDBShutdownRunner : public nsRunnable
+{
+public:
+  SeerDBShutdownRunner(nsIThread *ioThread, nsINetworkSeer *seer)
+    :mIOThread(ioThread)
+  {
+    mSeer = new nsMainThreadPtrHolder<nsINetworkSeer>(seer);
+  }
+
+  NS_IMETHODIMP Run() MOZ_OVERRIDE
+  {
+    MOZ_ASSERT(!NS_IsMainThread(), "Shutting down DB on main thread");
+
+    gSeer->mStatements.FinalizeStatements();
+    gSeer->mDB->Close();
+    gSeer->mDB = nullptr;
+
+    nsRefPtr<SeerThreadShutdownRunner> runner =
+      new SeerThreadShutdownRunner(mIOThread);
+    NS_DispatchToMainThread(runner);
+
+    return NS_OK;
+  }
+
+private:
+  nsCOMPtr<nsIThread> mIOThread;
+
+  // Death grip to keep seer alive while we cleanly close its DB connection
+  nsMainThreadPtrHandle<nsINetworkSeer> mSeer;
+};
+
+void
+Seer::Shutdown()
+{
+  if (!NS_IsMainThread()) {
+    MOZ_ASSERT(false, "Seer::Shutdown called off the main thread!");
+    return;
+  }
+
+  mInitialized = false;
+
+  if (mIOThread) {
+    nsCOMPtr<nsIThread> ioThread;
+    mIOThread.swap(ioThread);
+
+    if (mDB) {
+      nsRefPtr<SeerDBShutdownRunner> runner =
+        new SeerDBShutdownRunner(ioThread, this);
+      ioThread->Dispatch(runner, NS_DISPATCH_NORMAL);
+    } else {
+      nsRefPtr<SeerThreadShutdownRunner> runner =
+        new SeerThreadShutdownRunner(ioThread);
+      NS_DispatchToMainThread(runner);
+    }
+  }
+}
+
+nsresult
+Seer::Create(nsISupports *aOuter, const nsIID& aIID,
+             void **aResult)
+{
+  nsresult rv;
+
+  if (aOuter != nullptr) {
+    return NS_ERROR_NO_AGGREGATION;
+  }
+
+  nsRefPtr<Seer> svc = new Seer();
+
+  rv = svc->Init();
+  if (NS_FAILED(rv)) {
+    SEER_LOG(("Failed to initialize seer, seer 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;
+}
+
+// Get the full origin (scheme, host, port) out of a URI (maybe should be part
+// of nsIURI instead?)
+static void
+ExtractOrigin(nsIURI *uri, nsAutoCString &s)
+{
+  s.Truncate();
+
+  nsAutoCString scheme;
+  nsresult rv = uri->GetScheme(scheme);
+  RETURN_IF_FAILED(rv);
+
+  nsAutoCString host;
+  rv = uri->GetAsciiHost(host);
+  RETURN_IF_FAILED(rv);
+
+  int32_t port;
+  rv = uri->GetPort(&port);
+  RETURN_IF_FAILED(rv);
+
+  s.Assign(scheme);
+  s.AppendLiteral("://");
+  s.Append(host);
+  if (port != -1) {
+    s.AppendLiteral(":");
+    s.AppendInt(port);
+  }
+}
+
+// An event to do the work for a prediction that needs to hit the sqlite
+// database. These events should be created on the main thread, and run on
+// the seer thread.
+class SeerPredictionEvent : public nsRunnable
+{
+public:
+  SeerPredictionEvent(nsIURI *targetURI, nsIURI *sourceURI,
+                      SeerPredictReason reason,
+                      nsINetworkSeerVerifier *verifier)
+    :mReason(reason)
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "Creating prediction event off main thread");
+
+    mEnqueueTime = TimeStamp::Now();
+
+    if (verifier) {
+      mVerifier = new nsMainThreadPtrHolder<nsINetworkSeerVerifier>(verifier);
+    }
+    if (targetURI) {
+      targetURI->GetAsciiSpec(mTargetURI.spec);
+      ExtractOrigin(targetURI, mTargetURI.origin);
+    }
+    if (sourceURI) {
+      sourceURI->GetAsciiSpec(mSourceURI.spec);
+      ExtractOrigin(sourceURI, mSourceURI.origin);
+    }
+  }
+
+  NS_IMETHOD Run() MOZ_OVERRIDE
+  {
+    MOZ_ASSERT(!NS_IsMainThread(), "Running prediction event on main thread");
+
+    Telemetry::AccumulateTimeDelta(Telemetry::SEER_PREDICT_WAIT_TIME,
+                                   mEnqueueTime);
+
+    TimeStamp startTime = TimeStamp::Now();
+
+    nsresult rv = NS_OK;
+
+    switch (mReason) {
+      case nsINetworkSeer::PREDICT_LOAD:
+        gSeer->PredictForPageload(mTargetURI, mVerifier, 0, mEnqueueTime);
+        break;
+      case nsINetworkSeer::PREDICT_STARTUP:
+        gSeer->PredictForStartup(mVerifier, mEnqueueTime);
+        break;
+      default:
+        MOZ_ASSERT(false, "Got unexpected value for predict reason");
+        rv = NS_ERROR_UNEXPECTED;
+    }
+
+    gSeer->FreeSpaceInQueue();
+
+    Telemetry::AccumulateTimeDelta(Telemetry::SEER_PREDICT_WORK_TIME,
+                                   startTime);
+
+    return rv;
+  }
+
+private:
+  Seer::UriInfo mTargetURI;
+  Seer::UriInfo mSourceURI;
+  SeerPredictReason mReason;
+  SeerVerifierHandle mVerifier;
+  TimeStamp mEnqueueTime;
+};
+
+// Predicting for a link is easy, and doesn't require the round-trip to the
+// seer thread and back to the main thread, since we don't have to hit the db
+// for that.
+void
+Seer::PredictForLink(nsIURI *targetURI, nsIURI *sourceURI,
+                     nsINetworkSeerVerifier *verifier)
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Predicting for link off main thread");
+
+  if (!mSpeculativeService) {
+    return;
+  }
+
+  if (!mEnableHoverOnSSL) {
+    bool isSSL = false;
+    sourceURI->SchemeIs("https", &isSSL);
+    if (isSSL) {
+      // We don't want to predict from an HTTPS page, to avoid info leakage
+      SEER_LOG(("Not predicting for link hover - on an SSL page"));
+      return;
+    }
+  }
+
+  mSpeculativeService->SpeculativeConnect(targetURI, this);
+  if (verifier) {
+    verifier->OnPredictPreconnect(targetURI);
+  }
+}
+
+// This runnable runs on the main thread, and is responsible for actually
+// firing off predictive actions (such as TCP/TLS preconnects and DNS lookups)
+class SeerPredictionRunner : public nsRunnable
+{
+public:
+  SeerPredictionRunner(SeerVerifierHandle &verifier, TimeStamp predictStartTime)
+    :mVerifier(verifier)
+    ,mPredictStartTime(predictStartTime)
+  { }
+
+  void AddPreconnect(const nsACString &uri)
+  {
+    mPreconnects.AppendElement(uri);
+  }
+
+  void AddPreresolve(const nsACString &uri)
+  {
+    mPreresolves.AppendElement(uri);
+  }
+
+  bool HasWork() const
+  {
+    return !(mPreconnects.IsEmpty() && mPreresolves.IsEmpty());
+  }
+
+  NS_IMETHOD Run() MOZ_OVERRIDE
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "Running prediction off main thread");
+
+    Telemetry::AccumulateTimeDelta(Telemetry::SEER_PREDICT_TIME_TO_ACTION,
+                                   mPredictStartTime);
+
+    uint32_t len, i;
+
+    len = mPreconnects.Length();
+    for (i = 0; i < len; ++i) {
+      nsCOMPtr<nsIURI> uri;
+      nsresult rv = NS_NewURI(getter_AddRefs(uri), mPreconnects[i]);
+      if (NS_FAILED(rv)) {
+        continue;
+      }
+
+      ++gSeer->mAccumulators->mTotalPredictions;
+      ++gSeer->mAccumulators->mTotalPreconnects;
+      gSeer->mSpeculativeService->SpeculativeConnect(uri, gSeer);
+      if (mVerifier) {
+        mVerifier->OnPredictPreconnect(uri);
+      }
+    }
+
+    len = mPreresolves.Length();
+    nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+    for (i = 0; i < len; ++i) {
+      nsCOMPtr<nsIURI> uri;
+      nsresult rv = NS_NewURI(getter_AddRefs(uri), mPreresolves[i]);
+      if (NS_FAILED(rv)) {
+        continue;
+      }
+
+      ++gSeer->mAccumulators->mTotalPredictions;
+      ++gSeer->mAccumulators->mTotalPreresolves;
+      nsAutoCString hostname;
+      uri->GetAsciiHost(hostname);
+      nsCOMPtr<nsICancelable> tmpCancelable;
+      gSeer->mDnsService->AsyncResolve(hostname,
+                                       (nsIDNSService::RESOLVE_PRIORITY_MEDIUM |
+                                        nsIDNSService::RESOLVE_SPECULATE),
+                                       gSeer->mDNSListener, nullptr,
+                                       getter_AddRefs(tmpCancelable));
+      if (mVerifier) {
+        mVerifier->OnPredictDNS(uri);
+      }
+    }
+
+    mPreconnects.Clear();
+    mPreresolves.Clear();
+
+    return NS_OK;
+  }
+
+private:
+  nsTArray<nsCString> mPreconnects;
+  nsTArray<nsCString> mPreresolves;
+  SeerVerifierHandle mVerifier;
+  TimeStamp mPredictStartTime;
+};
+
+// 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.
+int
+Seer::CalculateGlobalDegradation(PRTime now, PRTime lastLoad)
+{
+  int globalDegradation;
+  PRTime delta = now - lastLoad;
+  if (delta < ONE_DAY) {
+    globalDegradation = mPageDegradationDay;
+  } else if (delta < ONE_WEEK) {
+    globalDegradation = mPageDegradationWeek;
+  } else if (delta < ONE_MONTH) {
+    globalDegradation = mPageDegradationMonth;
+  } else if (delta < ONE_YEAR) {
+    globalDegradation = mPageDegradationYear;
+  } else {
+    globalDegradation = mPageDegradationMax;
+  }
+
+  Telemetry::Accumulate(Telemetry::SEER_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 baseConfidence - the basic confidence we have for this subresource,
+//                         which is the percentage of time this top-level load
+//                         loads the subresource in question
+// @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
+int
+Seer::CalculateConfidence(int baseConfidence, PRTime lastHit,
+                          PRTime lastPossible, int globalDegradation)
+{
+  ++mAccumulators->mPredictionsCalculated;
+
+  int maxConfidence = 100;
+  int 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 = mPreconnectMinConfidence - 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 = mSubresourceDegradationDay;
+    } else if (delta < ONE_WEEK) {
+      confidenceDegradation = mSubresourceDegradationWeek;
+    } else if (delta < ONE_MONTH) {
+      confidenceDegradation = mSubresourceDegradationMonth;
+    } else if (delta < ONE_YEAR) {
+      confidenceDegradation = mSubresourceDegradationYear;
+    } else {
+      confidenceDegradation = mSubresourceDegradationMax;
+      maxConfidence = 0;
+    }
+  }
+
+  // Calculate our confidence and clamp it to between 0 and maxConfidence
+  // (<= 100)
+  int confidence = baseConfidence - confidenceDegradation - globalDegradation;
+  confidence = std::max(confidence, 0);
+  confidence = std::min(confidence, maxConfidence);
+
+  Telemetry::Accumulate(Telemetry::SEER_BASE_CONFIDENCE, baseConfidence);
+  Telemetry::Accumulate(Telemetry::SEER_SUBRESOURCE_DEGRADATION,
+                        confidenceDegradation);
+  Telemetry::Accumulate(Telemetry::SEER_CONFIDENCE, confidence);
+  return confidence;
+}
+
+// (Maybe) adds a predictive action to the prediction runner, based on our
+// calculated confidence for the subresource in question.
+void
+Seer::SetupPrediction(int confidence, const nsACString &uri,
+                      SeerPredictionRunner *runner)
+{
+    if (confidence >= mPreconnectMinConfidence) {
+      runner->AddPreconnect(uri);
+    } else if (confidence >= mPreresolveMinConfidence) {
+      runner->AddPreresolve(uri);
+    }
+}
+
+// This gets the data about the top-level load from our database, either from
+// the pages table (which is specific to a particular URI), or from the hosts
+// table (which is for a particular origin).
+bool
+Seer::LookupTopLevel(QueryType queryType, const nsACString &key,
+                     TopLevelInfo &info)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "LookupTopLevel called on main thread.");
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (queryType == QUERY_PAGE) {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("SELECT id, loads, last_load FROM moz_pages WHERE "
+                           "uri = :key;"));
+  } else {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("SELECT id, loads, last_load FROM moz_hosts WHERE "
+                           "origin = :key;"));
+  }
+  NS_ENSURE_TRUE(stmt, false);
+  mozStorageStatementScoper scope(stmt);
+
+  nsresult rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("key"), key);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  bool hasRows;
+  rv = stmt->ExecuteStep(&hasRows);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  if (!hasRows) {
+    return false;
+  }
+
+  rv = stmt->GetInt32(0, &info.id);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  rv = stmt->GetInt32(1, &info.loadCount);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  rv = stmt->GetInt64(2, &info.lastLoad);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  return true;
+}
+
+// Insert data about either a top-level page or a top-level origin into
+// the database.
+void
+Seer::AddTopLevel(QueryType queryType, const nsACString &key, PRTime now)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "AddTopLevel called on main thread.");
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (queryType == QUERY_PAGE) {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("INSERT INTO moz_pages (uri, loads, last_load) "
+                           "VALUES (:key, 1, :now);"));
+  } else {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("INSERT INTO moz_hosts (origin, loads, last_load) "
+                           "VALUES (:key, 1, :now);"));
+  }
+  if (!stmt) {
+    return;
+  }
+  mozStorageStatementScoper scope(stmt);
+
+  // Loading a page implicitly makes the seer learn about the page,
+  // so since we don't have it already, let's add it.
+  nsresult rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("key"), key);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("now"), now);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->Execute();
+}
+
+// Update data about either a top-level page or a top-level origin in the
+// database.
+void
+Seer::UpdateTopLevel(QueryType queryType, const TopLevelInfo &info, PRTime now)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "UpdateTopLevel called on main thread.");
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (queryType == QUERY_PAGE) {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("UPDATE moz_pages SET loads = :load_count, "
+                           "last_load = :now WHERE id = :id;"));
+  } else {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("UPDATE moz_hosts SET loads = :load_count, "
+                           "last_load = :now WHERE id = :id;"));
+  }
+  if (!stmt) {
+    return;
+  }
+  mozStorageStatementScoper scope(stmt);
+
+  // First, let's update the page in the database, since loading a page
+  // implicitly learns about the page.
+  nsresult rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("load_count"),
+                                      info.loadCount + 1);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("now"), now);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("id"), info.id);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->Execute();
+}
+
+// Tries to predict for a top-level load (either page-based or origin-based).
+// Returns false if it failed to predict at all, true if it did some sort of
+// prediction.
+// @param queryType - whether to predict based on page or origin
+// @param info - the db info about the top-level resource
+bool
+Seer::TryPredict(QueryType queryType, const TopLevelInfo &info, PRTime now,
+                 SeerVerifierHandle &verifier, TimeStamp &predictStartTime)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "TryPredict called on main thread.");
+
+  int globalDegradation = CalculateGlobalDegradation(now, info.lastLoad);
+
+  // Now let's look up the subresources we know about for this page
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (queryType == QUERY_PAGE) {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("SELECT uri, hits, last_hit FROM moz_subresources "
+                           "WHERE pid = :id;"));
+  } else {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("SELECT origin, hits, last_hit FROM moz_subhosts "
+                           "WHERE hid = :id;"));
+  }
+  NS_ENSURE_TRUE(stmt, false);
+  mozStorageStatementScoper scope(stmt);
+
+  nsresult rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("id"), info.id);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  bool hasRows;
+  rv = stmt->ExecuteStep(&hasRows);
+  if (NS_FAILED(rv) || !hasRows) {
+    return false;
+  }
+
+  nsRefPtr<SeerPredictionRunner> runner =
+    new SeerPredictionRunner(verifier, predictStartTime);
+
+  while (hasRows) {
+    int32_t hitCount;
+    PRTime lastHit;
+    nsAutoCString subresource;
+    int baseConfidence, confidence;
+
+    // We use goto nextrow here instead of just failling, because we want
+    // to do some sort of prediction if at all possible. Of course, it's
+    // probably unlikely that subsequent rows will succeed if one fails, but
+    // it's worth a shot.
+
+    rv = stmt->GetUTF8String(0, subresource);
+    if NS_FAILED(rv) {
+      goto nextrow;
+    }
+
+    rv = stmt->GetInt32(1, &hitCount);
+    if (NS_FAILED(rv)) {
+      goto nextrow;
+    }
+
+    rv = stmt->GetInt64(2, &lastHit);
+    if (NS_FAILED(rv)) {
+      goto nextrow;
+    }
+
+    baseConfidence = (hitCount * 100) / info.loadCount;
+    confidence = CalculateConfidence(baseConfidence, lastHit, info.lastLoad,
+                                     globalDegradation);
+    SetupPrediction(confidence, subresource, runner);
+
+nextrow:
+    rv = stmt->ExecuteStep(&hasRows);
+    NS_ENSURE_SUCCESS(rv, false);
+  }
+
+  bool predicted = false;
+
+  if (runner->HasWork()) {
+    NS_DispatchToMainThread(runner);
+    predicted = true;
+  }
+
+  return predicted;
+}
+
+// Find out if a top-level page is likely to redirect.
+bool
+Seer::WouldRedirect(const TopLevelInfo &info, PRTime now, UriInfo &newUri)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "WouldRedirect called on main thread.");
+
+  nsCOMPtr<mozIStorageStatement> stmt = mStatements.GetCachedStatement(
+      NS_LITERAL_CSTRING("SELECT uri, origin, hits, last_hit "
+                         "FROM moz_redirects WHERE pid = :id;"));
+  NS_ENSURE_TRUE(stmt, false);
+  mozStorageStatementScoper scope(stmt);
+
+  nsresult rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("id"), info.id);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  bool hasRows;
+  rv = stmt->ExecuteStep(&hasRows);
+  if (NS_FAILED(rv) || !hasRows) {
+    return false;
+  }
+
+  rv = stmt->GetUTF8String(0, newUri.spec);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  rv = stmt->GetUTF8String(1, newUri.origin);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  int32_t hitCount;
+  rv = stmt->GetInt32(2, &hitCount);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  PRTime lastHit;
+  rv = stmt->GetInt64(3, &lastHit);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  int globalDegradation = CalculateGlobalDegradation(now, info.lastLoad);
+  int baseConfidence = (hitCount * 100) / info.loadCount;
+  int confidence = CalculateConfidence(baseConfidence, lastHit, info.lastLoad,
+                                       globalDegradation);
+
+  if (confidence > mRedirectLikelyConfidence) {
+    return true;
+  }
+
+  return false;
+}
+
+// This will add a page to our list of startup pages if it's being loaded
+// before our startup window has expired.
+void
+Seer::MaybeLearnForStartup(const UriInfo &uri, const PRTime now)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "MaybeLearnForStartup called on main thread.");
+
+  if ((now - mStartupTime) < STARTUP_WINDOW) {
+    LearnForStartup(uri);
+  }
+}
+
+const int MAX_PAGELOAD_DEPTH = 10;
+
+// This is the driver for prediction based on a new pageload.
+void
+Seer::PredictForPageload(const UriInfo &uri, SeerVerifierHandle &verifier,
+                         int stackCount, TimeStamp &predictStartTime)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "PredictForPageload called on main thread.");
+
+  if (stackCount > MAX_PAGELOAD_DEPTH) {
+    SEER_LOG(("Too deep into pageload prediction"));
+    return;
+  }
+
+  if (NS_FAILED(EnsureInitStorage())) {
+    return;
+  }
+
+  PRTime now = PR_Now();
+
+  MaybeLearnForStartup(uri, now);
+
+  TopLevelInfo pageInfo;
+  TopLevelInfo originInfo;
+  bool havePage = LookupTopLevel(QUERY_PAGE, uri.spec, pageInfo);
+  bool haveOrigin = LookupTopLevel(QUERY_ORIGIN, uri.origin, originInfo);
+
+  if (!havePage) {
+    AddTopLevel(QUERY_PAGE, uri.spec, now);
+  } else {
+    UpdateTopLevel(QUERY_PAGE, pageInfo, now);
+  }
+
+  if (!haveOrigin) {
+    AddTopLevel(QUERY_ORIGIN, uri.origin, now);
+  } else {
+    UpdateTopLevel(QUERY_ORIGIN, originInfo, now);
+  }
+
+  UriInfo newUri;
+  if (havePage && WouldRedirect(pageInfo, now, newUri)) {
+    nsRefPtr<SeerPredictionRunner> runner =
+      new SeerPredictionRunner(verifier, predictStartTime);
+    runner->AddPreconnect(newUri.spec);
+    NS_DispatchToMainThread(runner);
+    PredictForPageload(newUri, verifier, stackCount + 1, predictStartTime);
+    return;
+  }
+
+  bool predicted = false;
+
+  // We always try to be as specific as possible in our predictions, so try
+  // to predict based on the full URI before we fall back to the origin.
+  if (havePage) {
+    predicted = TryPredict(QUERY_PAGE, pageInfo, now, verifier,
+                           predictStartTime);
+  }
+
+  if (!predicted && haveOrigin) {
+    predicted = TryPredict(QUERY_ORIGIN, originInfo, now, verifier,
+                           predictStartTime);
+  }
+
+  if (!predicted) {
+    Telemetry::AccumulateTimeDelta(Telemetry::SEER_PREDICT_TIME_TO_INACTION,
+                                   predictStartTime);
+  }
+}
+
+// This is the driver for predicting at browser startup time based on pages that
+// have previously been loaded close to startup.
+void
+Seer::PredictForStartup(SeerVerifierHandle &verifier,
+                        TimeStamp &predictStartTime)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "PredictForStartup called on main thread");
+
+  if (NS_FAILED(EnsureInitStorage())) {
+    return;
+  }
+
+  nsCOMPtr<mozIStorageStatement> stmt = mStatements.GetCachedStatement(
+      NS_LITERAL_CSTRING("SELECT uri, hits, last_hit FROM moz_startup_pages;"));
+  if (!stmt) {
+    return;
+  }
+  mozStorageStatementScoper scope(stmt);
+  nsresult rv;
+  bool hasRows;
+
+  nsRefPtr<SeerPredictionRunner> runner =
+    new SeerPredictionRunner(verifier, predictStartTime);
+
+  rv = stmt->ExecuteStep(&hasRows);
+  RETURN_IF_FAILED(rv);
+
+  while (hasRows) {
+    nsAutoCString uri;
+    int32_t hitCount;
+    PRTime lastHit;
+    int baseConfidence, confidence;
+
+    // We use goto nextrow here instead of just failling, because we want
+    // to do some sort of prediction if at all possible. Of course, it's
+    // probably unlikely that subsequent rows will succeed if one fails, but
+    // it's worth a shot.
+
+    rv = stmt->GetUTF8String(0, uri);
+    if (NS_FAILED(rv)) {
+      goto nextrow;
+    }
+
+    rv = stmt->GetInt32(1, &hitCount);
+    if (NS_FAILED(rv)) {
+      goto nextrow;
+    }
+
+    rv = stmt->GetInt64(2, &lastHit);
+    if (NS_FAILED(rv)) {
+      goto nextrow;
+    }
+
+    baseConfidence = (hitCount * 100) / mStartupCount;
+    confidence = CalculateConfidence(baseConfidence, lastHit,
+                                     mLastStartupTime, 0);
+    SetupPrediction(confidence, uri, runner);
+
+nextrow:
+    rv = stmt->ExecuteStep(&hasRows);
+    RETURN_IF_FAILED(rv);
+  }
+
+  if (runner->HasWork()) {
+    NS_DispatchToMainThread(runner);
+  } else {
+    Telemetry::AccumulateTimeDelta(Telemetry::SEER_PREDICT_TIME_TO_INACTION,
+                                   predictStartTime);
+  }
+}
+
+// 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;
+}
+
+nsresult
+Seer::ReserveSpaceInQueue()
+{
+  MutexAutoLock lock(mQueueSizeLock);
+
+  if (mQueueSize >= mMaxQueueSize) {
+    SEER_LOG(("Not enqueuing event - queue too large"));
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  mQueueSize++;
+  return NS_OK;
+}
+
+void
+Seer::FreeSpaceInQueue()
+{
+  MutexAutoLock lock(mQueueSizeLock);
+  MOZ_ASSERT(mQueueSize > 0, "unexpected mQueueSize");
+  mQueueSize--;
+}
+
+// Called from the main thread to initiate predictive actions
+NS_IMETHODIMP
+Seer::Predict(nsIURI *targetURI, nsIURI *sourceURI, SeerPredictReason reason,
+              nsILoadContext *loadContext, nsINetworkSeerVerifier *verifier)
+{
+  MOZ_ASSERT(NS_IsMainThread(),
+             "Seer interface methods must be called on the main thread");
+
+  if (!mInitialized) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  if (!mEnabled) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  if (loadContext && loadContext->UsePrivateBrowsing()) {
+    // Don't want to do anything in PB mode
+    return NS_OK;
+  }
+
+  if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) {
+    // Nothing we can do for non-HTTP[S] schemes
+    return NS_OK;
+  }
+
+  // Ensure we've been given the appropriate arguments for the kind of
+  // prediction we're being asked to do
+  switch (reason) {
+    case nsINetworkSeer::PREDICT_LINK:
+      if (!targetURI || !sourceURI) {
+        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, verifier);
+      return NS_OK;
+    case nsINetworkSeer::PREDICT_LOAD:
+      if (!targetURI || sourceURI) {
+        return NS_ERROR_INVALID_ARG;
+      }
+      break;
+    case nsINetworkSeer::PREDICT_STARTUP:
+      if (targetURI || sourceURI) {
+        return NS_ERROR_INVALID_ARG;
+      }
+      break;
+    default:
+      return NS_ERROR_INVALID_ARG;
+  }
+
+  ++mAccumulators->mPredictAttempts;
+  nsresult rv = ReserveSpaceInQueue();
+  if (NS_FAILED(rv)) {
+    ++mAccumulators->mPredictFullQueue;
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  nsRefPtr<SeerPredictionEvent> event = new SeerPredictionEvent(targetURI,
+                                                                sourceURI,
+                                                                reason,
+                                                                verifier);
+  return mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+}
+
+// A runnable for updating our information in the database. This must always
+// be dispatched to the seer thread.
+class SeerLearnEvent : public nsRunnable
+{
+public:
+  SeerLearnEvent(nsIURI *targetURI, nsIURI *sourceURI, SeerLearnReason reason)
+    :mReason(reason)
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "Creating learn event off main thread");
+
+    mEnqueueTime = TimeStamp::Now();
+
+    targetURI->GetAsciiSpec(mTargetURI.spec);
+    ExtractOrigin(targetURI, mTargetURI.origin);
+    if (sourceURI) {
+      sourceURI->GetAsciiSpec(mSourceURI.spec);
+      ExtractOrigin(sourceURI, mSourceURI.origin);
+    }
+  }
+
+  NS_IMETHOD Run() MOZ_OVERRIDE
+  {
+    MOZ_ASSERT(!NS_IsMainThread(), "Running learn off main thread");
+
+    nsresult rv = NS_OK;
+
+    Telemetry::AccumulateTimeDelta(Telemetry::SEER_LEARN_WAIT_TIME,
+                                   mEnqueueTime);
+
+    TimeStamp startTime = TimeStamp::Now();
+
+    switch (mReason) {
+    case nsINetworkSeer::LEARN_LOAD_TOPLEVEL:
+      gSeer->LearnForToplevel(mTargetURI);
+      break;
+    case nsINetworkSeer::LEARN_LOAD_REDIRECT:
+      gSeer->LearnForRedirect(mTargetURI, mSourceURI);
+      break;
+    case nsINetworkSeer::LEARN_LOAD_SUBRESOURCE:
+      gSeer->LearnForSubresource(mTargetURI, mSourceURI);
+      break;
+    case nsINetworkSeer::LEARN_STARTUP:
+      gSeer->LearnForStartup(mTargetURI);
+      break;
+    default:
+      MOZ_ASSERT(false, "Got unexpected value for learn reason");
+      rv = NS_ERROR_UNEXPECTED;
+    }
+
+    gSeer->FreeSpaceInQueue();
+
+    Telemetry::AccumulateTimeDelta(Telemetry::SEER_LEARN_WORK_TIME, startTime);
+
+    return rv;
+  }
+private:
+  Seer::UriInfo mTargetURI;
+  Seer::UriInfo mSourceURI;
+  SeerLearnReason mReason;
+  TimeStamp mEnqueueTime;
+};
+
+void
+Seer::LearnForToplevel(const UriInfo &uri)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "LearnForToplevel called on main thread.");
+
+  if (NS_FAILED(EnsureInitStorage())) {
+    return;
+  }
+
+  PRTime now = PR_Now();
+
+  MaybeLearnForStartup(uri, now);
+
+  TopLevelInfo pageInfo;
+  TopLevelInfo originInfo;
+  bool havePage = LookupTopLevel(QUERY_PAGE, uri.spec, pageInfo);
+  bool haveOrigin = LookupTopLevel(QUERY_ORIGIN, uri.origin, originInfo);
+
+  if (!havePage) {
+    AddTopLevel(QUERY_PAGE, uri.spec, now);
+  } else {
+    UpdateTopLevel(QUERY_PAGE, pageInfo, now);
+  }
+
+  if (!haveOrigin) {
+    AddTopLevel(QUERY_ORIGIN, uri.origin, now);
+  } else {
+    UpdateTopLevel(QUERY_ORIGIN, originInfo, now);
+  }
+}
+
+// Queries to look up information about a *specific* subresource associated
+// with a *specific* top-level load.
+bool
+Seer::LookupSubresource(QueryType queryType, const int32_t parentId,
+                        const nsACString &key, SubresourceInfo &info)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "LookupSubresource called on main thread.");
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (queryType == QUERY_PAGE) {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("SELECT id, hits, last_hit FROM moz_subresources "
+                           "WHERE pid = :parent_id AND uri = :key;"));
+  } else {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("SELECT id, hits, last_hit FROM moz_subhosts WHERE "
+                           "hid = :parent_id AND origin = :key;"));
+  }
+  NS_ENSURE_TRUE(stmt, false);
+  mozStorageStatementScoper scope(stmt);
+
+  nsresult rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("parent_id"),
+                                      parentId);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("key"), key);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  bool hasRows;
+  rv = stmt->ExecuteStep(&hasRows);
+  NS_ENSURE_SUCCESS(rv, false);
+  if (!hasRows) {
+    return false;
+  }
+
+  rv = stmt->GetInt32(0, &info.id);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  rv = stmt->GetInt32(1, &info.hitCount);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  rv = stmt->GetInt64(2, &info.lastHit);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  return true;
+}
+
+// Add information about a new subresource associated with a top-level load.
+void
+Seer::AddSubresource(QueryType queryType, const int32_t parentId,
+                     const nsACString &key, const PRTime now)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "AddSubresource called on main thread.");
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (queryType == QUERY_PAGE) {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("INSERT INTO moz_subresources "
+                           "(pid, uri, hits, last_hit) VALUES "
+                           "(:parent_id, :key, 1, :now);"));
+  } else {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("INSERT INTO moz_subhosts "
+                           "(hid, origin, hits, last_hit) VALUES "
+                           "(:parent_id, :key, 1, :now);"));
+  }
+  if (!stmt) {
+    return;
+  }
+  mozStorageStatementScoper scope(stmt);
+
+  nsresult rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("parent_id"),
+                                      parentId);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("key"), key);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("now"), now);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->Execute();
+}
+
+// Update the information about a particular subresource associated with a
+// top-level load
+void
+Seer::UpdateSubresource(QueryType queryType, const SubresourceInfo &info,
+                        const PRTime now)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "UpdateSubresource called on main thread.");
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (queryType == QUERY_PAGE) {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("UPDATE moz_subresources SET hits = :hit_count, "
+                           "last_hit = :now WHERE id = :id;"));
+  } else {
+    stmt = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("UPDATE moz_subhosts SET hits = :hit_count, "
+                           "last_hit = :now WHERE id = :id;"));
+  }
+  if (!stmt) {
+    return;
+  }
+  mozStorageStatementScoper scope(stmt);
+
+  nsresult rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hit_count"),
+                                      info.hitCount + 1);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("now"), now);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("id"), info.id);
+  RETURN_IF_FAILED(rv);
+
+  rv = stmt->Execute();
+}
+
+// Called when a subresource has been hit from a top-level load. Uses the two
+// helper functions above to update the database appropriately.
+void
+Seer::LearnForSubresource(const UriInfo &targetURI, const UriInfo &sourceURI)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "LearnForSubresource called on main thread.");
+
+  if (NS_FAILED(EnsureInitStorage())) {
+    return;
+  }
+
+  TopLevelInfo pageInfo, originInfo;
+  bool havePage = LookupTopLevel(QUERY_PAGE, sourceURI.spec, pageInfo);
+  bool haveOrigin = LookupTopLevel(QUERY_ORIGIN, sourceURI.origin,
+                                   originInfo);
+
+  if (!havePage && !haveOrigin) {
+    // Nothing to do, since we know nothing about the top level resource
+    return;
+  }
+
+  SubresourceInfo resourceInfo;
+  bool haveResource = false;
+  if (havePage) {
+    haveResource = LookupSubresource(QUERY_PAGE, pageInfo.id, targetURI.spec,
+                                     resourceInfo);
+  }
+
+  SubresourceInfo hostInfo;
+  bool haveHost = false;
+  if (haveOrigin) {
+    haveHost = LookupSubresource(QUERY_ORIGIN, originInfo.id, targetURI.origin,
+                                 hostInfo);
+  }
+
+  PRTime now = PR_Now();
+
+  if (haveResource) {
+    UpdateSubresource(QUERY_PAGE, resourceInfo, now);
+  } else if (havePage) {
+    AddSubresource(QUERY_PAGE, pageInfo.id, targetURI.spec, now);
+  }
+  // Can't add a subresource to a page we don't have in our db.
+
+  if (haveHost) {
+    UpdateSubresource(QUERY_ORIGIN, hostInfo, now);
+  } else if (haveOrigin) {
+    AddSubresource(QUERY_ORIGIN, originInfo.id, targetURI.origin, now);
+  }
+  // Can't add a subhost to a host we don't have in our db
+}
+
+// This is called when a top-level loaded ended up redirecting to a different
+// URI so we can keep track of that fact.
+void
+Seer::LearnForRedirect(const UriInfo &targetURI, const UriInfo &sourceURI)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "LearnForRedirect called on main thread.");
+
+  if (NS_FAILED(EnsureInitStorage())) {
+    return;
+  }
+
+  PRTime now = PR_Now();
+  nsresult rv;
+
+  nsCOMPtr<mozIStorageStatement> getPage = mStatements.GetCachedStatement(
+      NS_LITERAL_CSTRING("SELECT id FROM moz_pages WHERE uri = :spec;"));
+  if (!getPage) {
+    return;
+  }
+  mozStorageStatementScoper scopedPage(getPage);
+
+  // look up source in moz_pages
+  rv = getPage->BindUTF8StringByName(NS_LITERAL_CSTRING("spec"),
+                                     sourceURI.spec);
+  RETURN_IF_FAILED(rv);
+
+  bool hasRows;
+  rv = getPage->ExecuteStep(&hasRows);
+  if (NS_FAILED(rv) || !hasRows) {
+    return;
+  }
+
+  int32_t pageId;
+  rv = getPage->GetInt32(0, &pageId);
+  RETURN_IF_FAILED(rv);
+
+  nsCOMPtr<mozIStorageStatement> getRedirect = mStatements.GetCachedStatement(
+      NS_LITERAL_CSTRING("SELECT id, hits FROM moz_redirects WHERE "
+                         "pid = :page_id AND uri = :spec;"));
+  if (!getRedirect) {
+    return;
+  }
+  mozStorageStatementScoper scopedRedirect(getRedirect);
+
+  rv = getRedirect->BindInt32ByName(NS_LITERAL_CSTRING("page_id"), pageId);
+  RETURN_IF_FAILED(rv);
+
+  rv = getRedirect->BindUTF8StringByName(NS_LITERAL_CSTRING("spec"),
+                                         targetURI.spec);
+  RETURN_IF_FAILED(rv);
+
+  rv = getRedirect->ExecuteStep(&hasRows);
+  RETURN_IF_FAILED(rv);
+
+  if (!hasRows) {
+    // This is the first time we've seen this top-level redirect to this URI
+    nsCOMPtr<mozIStorageStatement> addRedirect = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("INSERT INTO moz_redirects "
+                           "(pid, uri, origin, hits, last_hit) VALUES "
+                           "(:page_id, :spec, :origin, 1, :now);"));
+    if (!addRedirect) {
+      return;
+    }
+    mozStorageStatementScoper scopedAdd(addRedirect);
+
+    rv = addRedirect->BindInt32ByName(NS_LITERAL_CSTRING("page_id"), pageId);
+    RETURN_IF_FAILED(rv);
+
+    rv = addRedirect->BindUTF8StringByName(NS_LITERAL_CSTRING("spec"),
+                                           targetURI.spec);
+    RETURN_IF_FAILED(rv);
+
+    rv = addRedirect->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"),
+                                           targetURI.origin);
+    RETURN_IF_FAILED(rv);
+
+    rv = addRedirect->BindInt64ByName(NS_LITERAL_CSTRING("now"), now);
+    RETURN_IF_FAILED(rv);
+
+    rv = addRedirect->Execute();
+  } else {
+    // We've seen this redirect before
+    int32_t redirId, hits;
+    rv = getRedirect->GetInt32(0, &redirId);
+    RETURN_IF_FAILED(rv);
+
+    rv = getRedirect->GetInt32(1, &hits);
+    RETURN_IF_FAILED(rv);
+
+    nsCOMPtr<mozIStorageStatement> updateRedirect =
+      mStatements.GetCachedStatement(
+          NS_LITERAL_CSTRING("UPDATE moz_redirects SET hits = :hits, "
+                             "last_hit = :now WHERE id = :redir;"));
+    if (!updateRedirect) {
+      return;
+    }
+    mozStorageStatementScoper scopedUpdate(updateRedirect);
+
+    rv = updateRedirect->BindInt32ByName(NS_LITERAL_CSTRING("hits"), hits + 1);
+    RETURN_IF_FAILED(rv);
+
+    rv = updateRedirect->BindInt64ByName(NS_LITERAL_CSTRING("now"), now);
+    RETURN_IF_FAILED(rv);
+
+    rv = updateRedirect->BindInt32ByName(NS_LITERAL_CSTRING("redir"), redirId);
+    RETURN_IF_FAILED(rv);
+
+    updateRedirect->Execute();
+  }
+}
+
+// Add information about a top-level load to our list of startup pages
+void
+Seer::LearnForStartup(const UriInfo &uri)
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "LearnForStartup called on main thread.");
+
+  if (NS_FAILED(EnsureInitStorage())) {
+    return;
+  }
+
+  nsCOMPtr<mozIStorageStatement> getPage = mStatements.GetCachedStatement(
+      NS_LITERAL_CSTRING("SELECT id, hits FROM moz_startup_pages WHERE "
+                         "uri = :origin;"));
+  if (!getPage) {
+    return;
+  }
+  mozStorageStatementScoper scopedPage(getPage);
+  nsresult rv;
+
+  rv = getPage->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), uri.origin);
+  RETURN_IF_FAILED(rv);
+
+  bool hasRows;
+  rv = getPage->ExecuteStep(&hasRows);
+  RETURN_IF_FAILED(rv);
+
+  if (hasRows) {
+    // We've loaded this page on startup before
+    int32_t pageId, hitCount;
+
+    rv = getPage->GetInt32(0, &pageId);
+    RETURN_IF_FAILED(rv);
+
+    rv = getPage->GetInt32(1, &hitCount);
+    RETURN_IF_FAILED(rv);
+
+    nsCOMPtr<mozIStorageStatement> updatePage = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("UPDATE moz_startup_pages SET hits = :hit_count, "
+                           "last_hit = :startup_time WHERE id = :page_id;"));
+    if (!updatePage) {
+      return;
+    }
+    mozStorageStatementScoper scopedUpdate(updatePage);
+
+    rv = updatePage->BindInt32ByName(NS_LITERAL_CSTRING("hit_count"),
+                                     hitCount + 1);
+    RETURN_IF_FAILED(rv);
+
+    rv = updatePage->BindInt64ByName(NS_LITERAL_CSTRING("startup_time"),
+                                     mStartupTime);
+    RETURN_IF_FAILED(rv);
+
+    rv = updatePage->BindInt32ByName(NS_LITERAL_CSTRING("page_id"), pageId);
+    RETURN_IF_FAILED(rv);
+
+    updatePage->Execute();
+  } else {
+    // New startup page
+    nsCOMPtr<mozIStorageStatement> addPage = mStatements.GetCachedStatement(
+        NS_LITERAL_CSTRING("INSERT INTO moz_startup_pages (uri, hits, "
+                           "last_hit) VALUES (:origin, 1, :startup_time);"));
+    if (!addPage) {
+      return;
+    }
+    mozStorageStatementScoper scopedAdd(addPage);
+    rv = addPage->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"),
+                                       uri.origin);
+    RETURN_IF_FAILED(rv);
+
+    rv = addPage->BindInt64ByName(NS_LITERAL_CSTRING("startup_time"),
+                                  mStartupTime);
+    RETURN_IF_FAILED(rv);
+
+    addPage->Execute();
+  }
+}
+
+// Called from the main thread to update the database
+NS_IMETHODIMP
+Seer::Learn(nsIURI *targetURI, nsIURI *sourceURI, SeerLearnReason reason,
+            nsILoadContext *loadContext)
+{
+  MOZ_ASSERT(NS_IsMainThread(),
+             "Seer interface methods must be called on the main thread");
+
+  if (!mInitialized) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  if (!mEnabled) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  if (loadContext && loadContext->UsePrivateBrowsing()) {
+    // Don't want to do anything in PB mode
+    return NS_OK;
+  }
+
+  if (!IsNullOrHttp(targetURI) || !IsNullOrHttp(sourceURI)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  switch (reason) {
+  case nsINetworkSeer::LEARN_LOAD_TOPLEVEL:
+  case nsINetworkSeer::LEARN_STARTUP:
+    if (!targetURI || sourceURI) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    break;
+  case nsINetworkSeer::LEARN_LOAD_REDIRECT:
+  case nsINetworkSeer::LEARN_LOAD_SUBRESOURCE:
+    if (!targetURI || !sourceURI) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    break;
+  default:
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  ++mAccumulators->mLearnAttempts;
+  nsresult rv = ReserveSpaceInQueue();
+  if (NS_FAILED(rv)) {
+    ++mAccumulators->mLearnFullQueue;
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  nsRefPtr<SeerLearnEvent> event = new SeerLearnEvent(targetURI, sourceURI,
+                                                      reason);
+  return mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+}
+
+// Runnable to clear out the database. Dispatched from the main thread to the
+// seer thread
+class SeerResetEvent : public nsRunnable
+{
+public:
+  SeerResetEvent()
+  { }
+
+  NS_IMETHOD Run() MOZ_OVERRIDE
+  {
+    MOZ_ASSERT(!NS_IsMainThread(), "Running reset on main thread");
+
+    gSeer->ResetInternal();
+
+    return NS_OK;
+  }
+};
+
+// Helper that actually does the database wipe.
+void
+Seer::ResetInternal()
+{
+  MOZ_ASSERT(!NS_IsMainThread(), "Resetting db on main thread");
+
+  nsresult rv = EnsureInitStorage();
+  RETURN_IF_FAILED(rv);
+
+  mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM moz_redirects"));
+  mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM moz_startup_pages"));
+  mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM moz_startups"));
+
+  // These cascade to moz_subresources and moz_subhosts, respectively.
+  mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM moz_pages"));
+  mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM moz_hosts"));
+}
+
+// Called on the main thread to clear out all our knowledge. Tabula Rasa FTW!
+NS_IMETHODIMP
+Seer::Reset()
+{
+  MOZ_ASSERT(NS_IsMainThread(),
+             "Seer interface methods must be called on the main thread");
+
+  if (!mInitialized) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  nsRefPtr<SeerResetEvent> event = new SeerResetEvent();
+  return mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
+}
+
+// Helper functions to make using the seer easier from native code
+
+static nsresult
+EnsureGlobalSeer(nsINetworkSeer **aSeer)
+{
+  nsresult rv;
+  nsCOMPtr<nsINetworkSeer> seer = do_GetService("@mozilla.org/network/seer;1",
+                                                &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  NS_IF_ADDREF(*aSeer = seer);
+  return NS_OK;
+}
+
+nsresult
+SeerPredict(nsIURI *targetURI, nsIURI *sourceURI, SeerPredictReason reason,
+            nsILoadContext *loadContext, nsINetworkSeerVerifier *verifier)
+{
+  nsCOMPtr<nsINetworkSeer> seer;
+  nsresult rv = EnsureGlobalSeer(getter_AddRefs(seer));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return seer->Predict(targetURI, sourceURI, reason, loadContext, verifier);
+}
+
+nsresult
+SeerLearn(nsIURI *targetURI, nsIURI *sourceURI, SeerLearnReason reason,
+          nsILoadContext *loadContext)
+{
+  nsCOMPtr<nsINetworkSeer> seer;
+  nsresult rv = EnsureGlobalSeer(getter_AddRefs(seer));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return seer->Learn(targetURI, sourceURI, reason, loadContext);
+}
+
+nsresult
+SeerLearn(nsIURI *targetURI, nsIURI *sourceURI, SeerLearnReason reason,
+          nsILoadGroup *loadGroup)
+{
+  nsCOMPtr<nsINetworkSeer> seer;
+  nsresult rv = EnsureGlobalSeer(getter_AddRefs(seer));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsILoadContext> loadContext;
+
+  if (loadGroup) {
+    nsCOMPtr<nsIInterfaceRequestor> callbacks;
+    loadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks));
+    if (callbacks) {
+      loadContext = do_GetInterface(callbacks);
+    }
+  }
+
+  return seer->Learn(targetURI, sourceURI, reason, loadContext);
+}
+
+nsresult
+SeerLearn(nsIURI *targetURI, nsIURI *sourceURI, SeerLearnReason reason,
+          nsIDocument *document)
+{
+  nsCOMPtr<nsINetworkSeer> seer;
+  nsresult rv = EnsureGlobalSeer(getter_AddRefs(seer));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsILoadContext> loadContext;
+
+  if (document) {
+    loadContext = document->GetLoadContext();
+  }
+
+  return seer->Learn(targetURI, sourceURI, reason, loadContext);
+}
+
+nsresult
+SeerLearnRedirect(nsIURI *targetURI, nsIChannel *channel,
+                  nsILoadContext *loadContext)
+{
+  nsCOMPtr<nsINetworkSeer> seer;
+  nsresult rv = EnsureGlobalSeer(getter_AddRefs(seer));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIURI> sourceURI;
+  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;
+  }
+
+  return seer->Learn(targetURI, sourceURI,
+                     nsINetworkSeer::LEARN_LOAD_REDIRECT, loadContext);
+}
+
+} // ::mozilla::net
+} // ::mozilla
new file mode 100644
--- /dev/null
+++ b/netwerk/base/src/Seer.h
@@ -0,0 +1,207 @@
+/* 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/. */
+
+#ifndef mozilla_net_Seer_h
+#define mozilla_net_Seer_h
+
+#include "nsINetworkSeer.h"
+
+#include "nsCOMPtr.h"
+#include "nsIDNSListener.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIObserver.h"
+#include "nsISpeculativeConnect.h"
+#include "nsProxyRelease.h"
+
+#include "mozilla/Mutex.h"
+#include "mozilla/storage/StatementCache.h"
+#include "mozilla/TimeStamp.h"
+
+class nsIDNSService;
+class nsINetworkSeerVerifier;
+class nsIThread;
+
+class mozIStorageConnection;
+class mozIStorageService;
+class mozIStorageStatement;
+
+namespace mozilla {
+namespace net {
+
+typedef nsMainThreadPtrHandle<nsINetworkSeerVerifier> SeerVerifierHandle;
+
+class SeerPredictionRunner;
+struct SeerTelemetryAccumulators;
+class SeerDNSListener;
+
+class Seer : public nsINetworkSeer
+           , public nsIObserver
+           , public nsISpeculativeConnectionOverrider
+           , public nsIInterfaceRequestor
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSINETWORKSEER
+  NS_DECL_NSIOBSERVER
+  NS_DECL_NSISPECULATIVECONNECTIONOVERRIDER
+  NS_DECL_NSIINTERFACEREQUESTOR
+
+  Seer();
+  virtual ~Seer();
+
+  nsresult Init();
+  void Shutdown();
+  static nsresult Create(nsISupports *outer, const nsIID& iid, void **result);
+
+private:
+  friend class SeerPredictionEvent;
+  friend class SeerLearnEvent;
+  friend class SeerResetEvent;
+  friend class SeerPredictionRunner;
+  friend class SeerDBShutdownRunner;
+
+  nsresult EnsureInitStorage();
+
+  // This is a proxy for the information we need from an nsIURI
+  struct UriInfo {
+    nsAutoCString spec;
+    nsAutoCString origin;
+  };
+
+  void PredictForLink(nsIURI *targetURI,
+                      nsIURI *sourceURI,
+                      nsINetworkSeerVerifier *verifier);
+  void PredictForPageload(const UriInfo &dest,
+                          SeerVerifierHandle &verifier,
+                          int stackCount,
+                          TimeStamp &predictStartTime);
+  void PredictForStartup(SeerVerifierHandle &verifier,
+                         TimeStamp &predictStartTime);
+
+  // Whether we're working on a page or an origin
+  enum QueryType {
+    QUERY_PAGE = 0,
+    QUERY_ORIGIN
+  };
+
+  // Holds info from the db about a top-level page or origin
+  struct TopLevelInfo {
+    int32_t id;
+    int32_t loadCount;
+    PRTime lastLoad;
+  };
+
+  // Holds info from the db about a subresource
+  struct SubresourceInfo {
+    int32_t id;
+    int32_t hitCount;
+    PRTime lastHit;
+  };
+
+  nsresult ReserveSpaceInQueue();
+  void FreeSpaceInQueue();
+
+  int CalculateGlobalDegradation(PRTime now,
+                                 PRTime lastLoad);
+  int CalculateConfidence(int baseConfidence,
+                          PRTime lastHit,
+                          PRTime lastPossible,
+                          int globalDegradation);
+  void SetupPrediction(int confidence,
+                       const nsACString &uri,
+                       SeerPredictionRunner *runner);
+
+  bool LookupTopLevel(QueryType queryType,
+                      const nsACString &key,
+                      TopLevelInfo &info);
+  void AddTopLevel(QueryType queryType,
+                   const nsACString &key,
+                   PRTime now);
+  void UpdateTopLevel(QueryType queryType,
+                      const TopLevelInfo &info,
+                      PRTime now);
+  bool TryPredict(QueryType queryType,
+                  const TopLevelInfo &info,
+                  PRTime now,
+                  SeerVerifierHandle &verifier,
+                  TimeStamp &predictStartTime);
+  bool WouldRedirect(const TopLevelInfo &info,
+                     PRTime now,
+                     UriInfo &newUri);
+
+  bool LookupSubresource(QueryType queryType,
+                         const int32_t parentId,
+                         const nsACString &key,
+                         SubresourceInfo &info);
+  void AddSubresource(QueryType queryType,
+                      const int32_t parentId,
+                      const nsACString &key, PRTime now);
+  void UpdateSubresource(QueryType queryType,
+                         const SubresourceInfo &info,
+                         PRTime now);
+
+  void MaybeLearnForStartup(const UriInfo &uri, const PRTime now);
+
+  void LearnForToplevel(const UriInfo &uri);
+  void LearnForSubresource(const UriInfo &targetURI, const UriInfo &sourceURI);
+  void LearnForRedirect(const UriInfo &targetURI, const UriInfo &sourceURI);
+  void LearnForStartup(const UriInfo &uri);
+
+  void ResetInternal();
+
+  // Observer-related stuff
+  nsresult InstallObserver();
+  void RemoveObserver();
+
+  bool mInitialized;
+
+  bool mEnabled;
+  bool mEnableHoverOnSSL;
+
+  int mPageDegradationDay;
+  int mPageDegradationWeek;
+  int mPageDegradationMonth;
+  int mPageDegradationYear;
+  int mPageDegradationMax;
+
+  int mSubresourceDegradationDay;
+  int mSubresourceDegradationWeek;
+  int mSubresourceDegradationMonth;
+  int mSubresourceDegradationYear;
+  int mSubresourceDegradationMax;
+
+  int mPreconnectMinConfidence;
+  int mPreresolveMinConfidence;
+  int mRedirectLikelyConfidence;
+
+  int32_t mMaxQueueSize;
+
+  nsCOMPtr<nsIThread> mIOThread;
+
+  nsCOMPtr<nsISpeculativeConnect> mSpeculativeService;
+
+  nsCOMPtr<nsIFile> mDBFile;
+  nsCOMPtr<mozIStorageService> mStorageService;
+  nsCOMPtr<mozIStorageConnection> mDB;
+  mozilla::storage::StatementCache<mozIStorageStatement> mStatements;
+
+  PRTime mStartupTime;
+  PRTime mLastStartupTime;
+  int32_t mStartupCount;
+
+  nsCOMPtr<nsIDNSService> mDnsService;
+
+  int32_t mQueueSize;
+  mozilla::Mutex mQueueSizeLock;
+
+  nsAutoPtr<SeerTelemetryAccumulators> mAccumulators;
+
+  nsRefPtr<SeerDNSListener> mDNSListener;
+};
+
+} // ::mozilla::net
+} // ::mozilla
+
+#endif // mozilla_net_Seer_h
--- a/netwerk/base/src/moz.build
+++ b/netwerk/base/src/moz.build
@@ -68,16 +68,17 @@ SOURCES += [
     'nsTransportUtils.cpp',
     'nsUDPServerSocket.cpp',
     'nsUnicharStreamLoader.cpp',
     'nsURIChecker.cpp',
     'nsURLHelper.cpp',
     'nsURLParsers.cpp',
     'ProxyAutoConfig.cpp',
     'RedirectChannelRegistrar.cpp',
+    'Seer.cpp',
     'StreamingProtocolService.cpp',
     'Tickler.cpp',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'os2':
     SOURCES += [
         'nsURLHelperOS2.cpp',
     ]
--- a/netwerk/build/nsNetCID.h
+++ b/netwerk/build/nsNetCID.h
@@ -431,16 +431,27 @@
 #define NS_REDIRECTCHANNELREGISTRAR_CID \
 { /* {b69043a6-8929-4d60-8d17-a27e44a8393e} */ \
     0xb69043a6, \
     0x8929, \
     0x4d60, \
     { 0x8d, 0x17, 0xa2, 0x7e, 0x44, 0xa8, 0x39, 0x3e } \
 }
 
+// service implementing nsINetworkSeer
+#define NS_NETWORKSEER_CONTRACTID \
+    "@mozilla.org/network/seer;1"
+#define NS_NETWORKSEER_CID \
+{ /* {1C218009-A531-46AD-8351-1E7F45D5A3C4} */ \
+    0x1C218009, \
+    0xA531, \
+    0x46AD, \
+    { 0x83, 0x51, 0x1E, 0x7F, 0x45, 0xD5, 0xA3, 0xC4 } \
+}
+
 /******************************************************************************
  * netwerk/cache/ classes
  */
 
 // service implementing nsICacheService.
 #define NS_CACHESERVICE_CONTRACTID \
     "@mozilla.org/network/cache-service;1"
 #define NS_CACHESERVICE_CID                          \
--- a/netwerk/build/nsNetModule.cpp
+++ b/netwerk/build/nsNetModule.cpp
@@ -31,16 +31,17 @@
 #include "nsApplicationCacheService.h"
 #include "nsMimeTypes.h"
 #include "nsNetStrings.h"
 #include "nsDNSPrefetch.h"
 #include "nsAboutProtocolHandler.h"
 #include "nsXULAppAPI.h"
 #include "nsCategoryCache.h"
 #include "nsIContentSniffer.h"
+#include "Seer.h"
 #include "nsNetUtil.h"
 #include "nsIThreadPool.h"
 #include "mozilla/net/NeckoChild.h"
 
 #include "nsNetCID.h"
 
 #ifndef XP_MACOSX
 #define BUILD_BINHEX_DECODER 1
@@ -805,16 +806,17 @@ NS_DEFINE_NAMED_CID(NS_NETWORK_LINK_SERV
 #elif defined(MOZ_ENABLE_QTNETWORK)
 NS_DEFINE_NAMED_CID(NS_NETWORK_LINK_SERVICE_CID);
 #elif defined(MOZ_WIDGET_ANDROID)
 NS_DEFINE_NAMED_CID(NS_NETWORK_LINK_SERVICE_CID);
 #endif
 NS_DEFINE_NAMED_CID(NS_SERIALIZATION_HELPER_CID);
 NS_DEFINE_NAMED_CID(NS_REDIRECTCHANNELREGISTRAR_CID);
 NS_DEFINE_NAMED_CID(NS_CACHE_STORAGE_SERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_NETWORKSEER_CID);
 
 static const mozilla::Module::CIDEntry kNeckoCIDs[] = {
     { &kNS_IOSERVICE_CID, false, nullptr, nsIOServiceConstructor },
     { &kNS_STREAMTRANSPORTSERVICE_CID, false, nullptr, nsStreamTransportServiceConstructor },
     { &kNS_SOCKETTRANSPORTSERVICE_CID, false, nullptr, nsSocketTransportServiceConstructor },
     { &kNS_SERVERSOCKET_CID, false, nullptr, nsServerSocketConstructor },
     { &kNS_UDPSERVERSOCKET_CID, false, nullptr, nsUDPServerSocketConstructor },
     { &kNS_SOCKETPROVIDERSERVICE_CID, false, nullptr, nsSocketProviderService::Create },
@@ -945,16 +947,17 @@ static const mozilla::Module::CIDEntry k
 #elif defined(MOZ_ENABLE_QTNETWORK)
     { &kNS_NETWORK_LINK_SERVICE_CID, false, nullptr, nsQtNetworkLinkServiceConstructor },
 #elif defined(MOZ_WIDGET_ANDROID)
     { &kNS_NETWORK_LINK_SERVICE_CID, false, nullptr, nsAndroidNetworkLinkServiceConstructor },
 #endif
     { &kNS_SERIALIZATION_HELPER_CID, false, nullptr, nsSerializationHelperConstructor },
     { &kNS_REDIRECTCHANNELREGISTRAR_CID, false, nullptr, RedirectChannelRegistrarConstructor },
     { &kNS_CACHE_STORAGE_SERVICE_CID, false, nullptr, CacheStorageServiceConstructor },
+    { &kNS_NETWORKSEER_CID, false, NULL, mozilla::net::Seer::Create },
     { nullptr }
 };
 
 static const mozilla::Module::ContractIDEntry kNeckoContracts[] = {
     { NS_IOSERVICE_CONTRACTID, &kNS_IOSERVICE_CID },
     { NS_NETUTIL_CONTRACTID, &kNS_IOSERVICE_CID },
     { NS_STREAMTRANSPORTSERVICE_CONTRACTID, &kNS_STREAMTRANSPORTSERVICE_CID },
     { NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &kNS_SOCKETTRANSPORTSERVICE_CID },
@@ -1088,16 +1091,17 @@ static const mozilla::Module::ContractID
 #elif defined(MOZ_ENABLE_QTNETWORK)
     { NS_NETWORK_LINK_SERVICE_CONTRACTID, &kNS_NETWORK_LINK_SERVICE_CID },
 #elif defined(MOZ_WIDGET_ANDROID)
     { NS_NETWORK_LINK_SERVICE_CONTRACTID, &kNS_NETWORK_LINK_SERVICE_CID },
 #endif
     { NS_SERIALIZATION_HELPER_CONTRACTID, &kNS_SERIALIZATION_HELPER_CID },
     { NS_REDIRECTCHANNELREGISTRAR_CONTRACTID, &kNS_REDIRECTCHANNELREGISTRAR_CID },
     { NS_CACHE_STORAGE_SERVICE_CONTRACTID, &kNS_CACHE_STORAGE_SERVICE_CID },
+    { NS_NETWORKSEER_CONTRACTID, &kNS_NETWORKSEER_CID },
     { nullptr }
 };
 
 static const mozilla::Module kNeckoModule = {
     mozilla::Module::kVersion,
     kNeckoCIDs,
     kNeckoContracts,
     kNeckoCategories,
--- a/netwerk/protocol/http/nsHttpConnectionMgr.cpp
+++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp
@@ -313,37 +313,80 @@ nsHttpConnectionMgr::DoShiftReloadConnec
 
     nsresult rv = PostEvent(&nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup,
                             0, connInfo);
     if (NS_SUCCEEDED(rv))
         connInfo.forget();
     return rv;
 }
 
+class SpeculativeConnectArgs
+{
+public:
+    SpeculativeConnectArgs() { mOverridesOK = false; }
+    virtual ~SpeculativeConnectArgs() {}
+
+    // Added manually so we can use nsRefPtr without inheriting from
+    // nsISupports
+    NS_IMETHOD_(nsrefcnt) AddRef(void);
+    NS_IMETHOD_(nsrefcnt) Release(void);
+
+public: // intentional!
+    nsRefPtr<NullHttpTransaction> mTrans;
+
+    bool mOverridesOK;
+    uint32_t mParallelSpeculativeConnectLimit;
+    bool mIgnoreIdle;
+    bool mIgnorePossibleSpdyConnections;
+
+    // As above, added manually so we can use nsRefPtr without inheriting from
+    // nsISupports
+protected:
+    ::mozilla::ThreadSafeAutoRefCnt mRefCnt;
+    NS_DECL_OWNINGTHREAD
+};
+
+NS_IMPL_ADDREF(SpeculativeConnectArgs)
+NS_IMPL_RELEASE(SpeculativeConnectArgs)
+
 nsresult
 nsHttpConnectionMgr::SpeculativeConnect(nsHttpConnectionInfo *ci,
                                         nsIInterfaceRequestor *callbacks,
                                         uint32_t caps)
 {
+    MOZ_ASSERT(NS_IsMainThread(), "nsHttpConnectionMgr::SpeculativeConnect called off main thread!");
+
     LOG(("nsHttpConnectionMgr::SpeculativeConnect [ci=%s]\n",
          ci->HashKey().get()));
 
+    nsRefPtr<SpeculativeConnectArgs> args = new SpeculativeConnectArgs();
+
     // Wrap up the callbacks and the target to ensure they're released on the target
     // thread properly.
     nsCOMPtr<nsIInterfaceRequestor> wrappedCallbacks;
     NS_NewInterfaceRequestorAggregation(callbacks, nullptr, getter_AddRefs(wrappedCallbacks));
 
     caps |= ci->GetAnonymous() ? NS_HTTP_LOAD_ANONYMOUS : 0;
-    nsRefPtr<NullHttpTransaction> trans =
-        new NullHttpTransaction(ci, wrappedCallbacks, caps);
+    args->mTrans = new NullHttpTransaction(ci, wrappedCallbacks, caps);
+
+    nsCOMPtr<nsISpeculativeConnectionOverrider> overrider =
+        do_GetInterface(callbacks);
+    if (overrider) {
+        args->mOverridesOK = true;
+        overrider->GetParallelSpeculativeConnectLimit(
+            &args->mParallelSpeculativeConnectLimit);
+        overrider->GetIgnoreIdle(&args->mIgnoreIdle);
+        overrider->GetIgnorePossibleSpdyConnections(
+            &args->mIgnorePossibleSpdyConnections);
+    }
 
     nsresult rv =
-        PostEvent(&nsHttpConnectionMgr::OnMsgSpeculativeConnect, 0, trans);
+        PostEvent(&nsHttpConnectionMgr::OnMsgSpeculativeConnect, 0, args);
     if (NS_SUCCEEDED(rv))
-        trans.forget();
+        args.forget();
     return rv;
 }
 
 nsresult
 nsHttpConnectionMgr::GetSocketThreadTarget(nsIEventTarget **target)
 {
     EnsureSocketThreadTarget();
 
@@ -1222,36 +1265,38 @@ nsHttpConnectionMgr::ClosePersistentConn
                                                   void *closure)
 {
     nsHttpConnectionMgr *self = static_cast<nsHttpConnectionMgr *>(closure);
     self->ClosePersistentConnections(ent);
     return PL_DHASH_NEXT;
 }
 
 bool
-nsHttpConnectionMgr::RestrictConnections(nsConnectionEntry *ent)
+nsHttpConnectionMgr::RestrictConnections(nsConnectionEntry *ent,
+                                         bool ignorePossibleSpdyConnections)
 {
     MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
 
     // If this host is trying to negotiate a SPDY session right now,
     // don't create any new ssl connections until the result of the
     // negotiation is known.
 
     bool doRestrict = ent->mConnInfo->UsingSSL() &&
         gHttpHandler->IsSpdyEnabled() &&
-        (!ent->mTestedSpdy || ent->mUsingSpdy) &&
+        ((!ent->mTestedSpdy && !ignorePossibleSpdyConnections) ||
+         ent->mUsingSpdy) &&
         (ent->mHalfOpens.Length() || ent->mActiveConns.Length());
 
     // If there are no restrictions, we are done
     if (!doRestrict)
         return false;
 
     // If the restriction is based on a tcp handshake in progress
     // let that connect and then see if it was SPDY or not
-    if (ent->UnconnectedHalfOpens())
+    if (ent->UnconnectedHalfOpens() && !ignorePossibleSpdyConnections)
         return true;
 
     // There is a concern that a host is using a mix of HTTP/1 and SPDY.
     // In that case we don't want to restrict connections just because
     // there is a single active HTTP/1 session in use.
     if (ent->mUsingSpdy && ent->mActiveConns.Length()) {
         bool confirmedRestrict = false;
         for (uint32_t index = 0; index < ent->mActiveConns.Length(); ++index) {
@@ -2513,36 +2558,48 @@ nsConnectionHandle::TakeTransport(nsISoc
     return mConn->TakeTransport(aTransport, aInputStream, aOutputStream);
 }
 
 void
 nsHttpConnectionMgr::OnMsgSpeculativeConnect(int32_t, void *param)
 {
     MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
 
-    nsRefPtr<NullHttpTransaction> trans =
-        dont_AddRef(static_cast<NullHttpTransaction *>(param));
+    nsRefPtr<SpeculativeConnectArgs> args =
+        dont_AddRef(static_cast<SpeculativeConnectArgs *>(param));
 
     LOG(("nsHttpConnectionMgr::OnMsgSpeculativeConnect [ci=%s]\n",
-         trans->ConnectionInfo()->HashKey().get()));
+         args->mTrans->ConnectionInfo()->HashKey().get()));
 
     nsConnectionEntry *ent =
-        GetOrCreateConnectionEntry(trans->ConnectionInfo());
+        GetOrCreateConnectionEntry(args->mTrans->ConnectionInfo());
 
     // If spdy has previously made a preferred entry for this host via
     // the ip pooling rules. If so, connect to the preferred host instead of
     // the one directly passed in here.
     nsConnectionEntry *preferredEntry = GetSpdyPreferredEnt(ent);
     if (preferredEntry)
         ent = preferredEntry;
 
-    if (mNumHalfOpenConns < gHttpHandler->ParallelSpeculativeConnectLimit() &&
-        !ent->mIdleConns.Length() && !RestrictConnections(ent) &&
-        !AtActiveConnectionLimit(ent, trans->Caps())) {
-        CreateTransport(ent, trans, trans->Caps(), true);
+    uint32_t parallelSpeculativeConnectLimit =
+        gHttpHandler->ParallelSpeculativeConnectLimit();
+    bool ignorePossibleSpdyConnections = false;
+    bool ignoreIdle = false;
+
+    if (args->mOverridesOK) {
+        parallelSpeculativeConnectLimit = args->mParallelSpeculativeConnectLimit;
+        ignorePossibleSpdyConnections = args->mIgnorePossibleSpdyConnections;
+        ignoreIdle = args->mIgnoreIdle;
+    }
+
+    if (mNumHalfOpenConns < parallelSpeculativeConnectLimit &&
+        (ignoreIdle || !ent->mIdleConns.Length()) &&
+        !RestrictConnections(ent, ignorePossibleSpdyConnections) &&
+        !AtActiveConnectionLimit(ent, args->mTrans->Caps())) {
+        CreateTransport(ent, args->mTrans, args->mTrans->Caps(), true);
     }
     else {
         LOG(("  Transport not created due to existing connection count\n"));
     }
 }
 
 bool
 nsHttpConnectionMgr::nsConnectionHandle::IsPersistent()
--- a/netwerk/protocol/http/nsHttpConnectionMgr.h
+++ b/netwerk/protocol/http/nsHttpConnectionMgr.h
@@ -507,17 +507,17 @@ private:
     nsresult DispatchAbstractTransaction(nsConnectionEntry *,
                                          nsAHttpTransaction *,
                                          uint32_t,
                                          nsHttpConnection *,
                                          int32_t);
     nsresult BuildPipeline(nsConnectionEntry *,
                            nsAHttpTransaction *,
                            nsHttpPipeline **);
-    bool     RestrictConnections(nsConnectionEntry *);
+    bool     RestrictConnections(nsConnectionEntry *, bool = false);
     nsresult ProcessNewTransaction(nsHttpTransaction *);
     nsresult EnsureSocketThreadTarget();
     void     ClosePersistentConnections(nsConnectionEntry *ent);
     void     ReportProxyTelemetry(nsConnectionEntry *ent);
     nsresult CreateTransport(nsConnectionEntry *, nsAHttpTransaction *,
                              uint32_t, bool);
     void     AddActiveConn(nsHttpConnection *, nsConnectionEntry *);
     void     DecrementActiveConnCount(nsHttpConnection *);
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_seer.js
@@ -0,0 +1,295 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+var seer = null;
+var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+var profile = null;
+
+function extract_origin(uri) {
+  var o = uri.scheme + "://" + uri.asciiHost;
+  if (uri.port !== -1) {
+    o = o + ":" + uri.port;
+  }
+  return o;
+}
+
+var LoadContext = function _loadContext() {
+};
+
+LoadContext.prototype = {
+  usePrivateBrowsing: false,
+
+  getInterface: function loadContext_getInterface(iid) {
+    return this.QueryInterface(iid);
+  },
+
+  QueryInterface: function loadContext_QueryInterface(iid) {
+    if (iid.equals(Ci.nsINetworkSeerVerifier) ||
+        iid.equals(Ci.nsILoadContext)) {
+      return this;
+    }
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+};
+
+var load_context = new LoadContext();
+
+var Verifier = function _verifier(testing, expected_preconnects, expected_preresolves) {
+  this.verifying = testing;
+  this.expected_preconnects = expected_preconnects;
+  this.expected_preresolves = expected_preresolves;
+};
+
+Verifier.prototype = {
+  verifying: null,
+  expected_preconnects: null,
+  expected_preresolves: null,
+
+  getInterface: function verifier_getInterface(iid) {
+    return this.QueryInterface(iid);
+  },
+
+  QueryInterface: function verifier_QueryInterface(iid) {
+    if (iid.equals(Ci.nsINetworkSeerVerifier) ||
+        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 &&
+        this.expected_preresolves.length === 0) {
+      do_check_true(true, "Well this is unexpected...");
+      run_next_test();
+    }
+  },
+
+  onPredictPreconnect: function verifier_onPredictPreconnect(uri) {
+    var origin = extract_origin(uri);
+    var index = this.expected_preconnects.indexOf(origin);
+    if (index == -1) {
+      do_check_true(false, "Got preconnect for unexpected uri " + origin);
+    } else {
+      this.expected_preconnects.splice(index, 1);
+    }
+    this.maybe_run_next_test();
+  },
+
+  onPredictDNS: function verifier_onPredictDNS(uri) {
+    var origin = extract_origin(uri);
+    var index = this.expected_preresolves.indexOf(origin);
+    if (index == -1) {
+      do_check_true(false, "Got preresolve for unexpected uri " + origin);
+    } else {
+      this.expected_preresolves.splice(index, 1);
+    }
+    this.maybe_run_next_test();
+  }
+};
+
+function reset_seer() {
+  seer.reset();
+}
+
+function newURI(s) {
+  return ios.newURI(s, null, null);
+}
+
+function test_link_hover() {
+  reset_seer();
+  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, []);
+  seer.predict(uri, referrer, seer.PREDICT_LINK, load_context, verifier);
+}
+
+function test_pageload() {
+  reset_seer();
+  var toplevel = "http://localhost:4444/index.html";
+  var subresources = [
+    "http://localhost:4444/style.css",
+    "http://localhost:4443/jquery.js",
+    "http://localhost:4444/image.png"
+  ];
+
+  var tluri = newURI(toplevel);
+  seer.learn(tluri, null, seer.LEARN_LOAD_TOPLEVEL, load_context);
+  var preconns = [];
+  for (var i = 0; i < subresources.length; i++) {
+    var sruri = newURI(subresources[i]);
+    seer.learn(sruri, tluri, seer.LEARN_LOAD_SUBRESOURCE, load_context);
+    preconns.push(extract_origin(sruri));
+  }
+
+  var verifier = new Verifier("pageload", preconns, []);
+  seer.predict(tluri, null, seer.PREDICT_LOAD, load_context, verifier);
+}
+
+function test_redirect() {
+  reset_seer();
+  var initial = "http://localhost:4443/redirect";
+  var target = "http://localhost:4444/index.html";
+  var subresources = [
+    "http://localhost:4444/style.css",
+    "http://localhost:4443/jquery.js",
+    "http://localhost:4444/image.png"
+  ];
+
+  var inituri = newURI(initial);
+  var targeturi = newURI(target);
+  seer.learn(inituri, null, seer.LEARN_LOAD_TOPLEVEL, load_context);
+  seer.learn(targeturi, inituri, seer.LEARN_LOAD_REDIRECT, load_context);
+  seer.learn(targeturi, null, seer.LEARN_LOAD_TOPLEVEL, load_context);
+
+  var preconns = [];
+  preconns.push(extract_origin(targeturi));
+  for (var i = 0; i < subresources.length; i++) {
+    var sruri = newURI(subresources[i]);
+    seer.learn(sruri, targeturi, seer.LEARN_LOAD_SUBRESOURCE, load_context);
+    preconns.push(extract_origin(sruri));
+  }
+
+  var verifier = new Verifier("redirect", preconns, []);
+  seer.predict(inituri, null, seer.PREDICT_LOAD, load_context, verifier);
+}
+
+function test_startup() {
+  reset_seer();
+  var uris = [
+    "http://localhost:4444/startup",
+    "http://localhost:4443/startup"
+  ];
+  var preconns = [];
+  for (var i = 0; i < uris.length; i++) {
+    var uri = newURI(uris[i]);
+    seer.learn(uri, null, seer.LEARN_STARTUP, load_context);
+    preconns.push(extract_origin(uri));
+  }
+
+  var verifier = new Verifier("startup", preconns, []);
+  seer.predict(null, null, seer.PREDICT_STARTUP, load_context, verifier);
+}
+
+// A class used to guarantee serialization of SQL queries so we can properly
+// update last hit times on subresources to ensure the seer tries to do DNS
+// preresolve on them instead of preconnecting
+var DnsContinueVerifier = function _dnsContinueVerifier(subresource, tluri, preresolves) {
+  this.subresource = subresource;
+  this.tluri = tluri;
+  this.preresolves = preresolves;
+};
+
+DnsContinueVerifier.prototype = {
+  subresource: null,
+  tluri: null,
+  preresolves: null,
+
+  getInterface: function _dnsContinueVerifier_getInterface(iid) {
+    return this.QueryInterface(iid);
+  },
+
+  QueryInterface: function _dnsContinueVerifier_QueryInterface(iid) {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsINetworkSeerVerifier)) {
+      return this;
+    }
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  onPredictPreconnect: function _dnsContinueVerifier_onPredictPreconnect() {
+    // This means that the seer has learned and done our "checkpoint" prediction
+    // Now we can get on with the prediction we actually want to test
+
+    // tstamp is 10 days older than now - just over 1 week, which will ensure we
+    // hit our cutoff for dns vs. preconnect. This is all in usec, hence the
+    // x1000 on the Date object value.
+    var tstamp = (new Date().valueOf() * 1000) - (10 * 86400 * 1000000);
+
+    var dbfile = FileUtils.getFile("ProfD", ["seer.sqlite"]);
+    var dbconn = Services.storage.openDatabase(dbfile);
+    // We also need to update hits, since the toplevel has been "loaded" a
+    // second time (from the prediction that kicked off this callback) to ensure
+    // that the seer will try to do anything for this subresource.
+    var stmt = "UPDATE moz_subresources SET last_hit = " + tstamp + ", hits = 2 WHERE uri = '" + this.subresource + "';";
+    dbconn.executeSimpleSQL(stmt);
+    dbconn.close();
+
+    var verifier = new Verifier("dns", [], this.preresolves);
+    seer.predict(this.tluri, null, seer.PREDICT_LOAD, load_context, verifier);
+  },
+
+  onPredictDNS: function _dnsContinueVerifier_onPredictDNS() {
+    do_check_true(false, "Shouldn't have gotten a preresolve prediction here!");
+  }
+};
+
+function test_dns() {
+  reset_seer();
+  var toplevel = "http://localhost:4444/index.html";
+  var subresource = "http://localhost:4443/jquery.js";
+
+  var tluri = newURI(toplevel);
+  seer.learn(tluri, null, seer.LEARN_LOAD_TOPLEVEL, load_context);
+  var sruri = newURI(subresource);
+  seer.learn(sruri, tluri, seer.LEARN_LOAD_SUBRESOURCE, load_context);
+
+  var preresolves = [extract_origin(sruri)];
+  var continue_verifier = new DnsContinueVerifier(subresource, tluri, preresolves);
+  // Fire off a prediction that will do preconnects so we know when the seer
+  // thread has gotten to the point where we can update the database manually
+  seer.predict(tluri, null, seer.PREDICT_LOAD, load_context, continue_verifier);
+}
+
+function test_origin() {
+  reset_seer();
+  var toplevel = "http://localhost:4444/index.html";
+  var subresources = [
+    "http://localhost:4444/style.css",
+    "http://localhost:4443/jquery.js",
+    "http://localhost:4444/image.png"
+  ];
+
+  var tluri = newURI(toplevel);
+  seer.learn(tluri, null, seer.LEARN_LOAD_TOPLEVEL, load_context);
+  var preconns = [];
+  for (var i = 0; i < subresources.length; i++) {
+    var sruri = newURI(subresources[i]);
+    seer.learn(sruri, tluri, seer.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, []);
+  seer.predict(loaduri, null, seer.PREDICT_LOAD, load_context, verifier);
+}
+
+var tests = [
+  test_link_hover,
+  test_pageload,
+  test_redirect,
+  test_startup,
+  test_dns,
+  test_origin
+];
+
+function run_test() {
+  tests.forEach(add_test);
+  profile = do_get_profile();
+  seer = Cc["@mozilla.org/network/seer;1"].getService(Ci.nsINetworkSeer);
+  do_register_cleanup(reset_seer);
+  run_next_test();
+}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -290,8 +290,13 @@ skip-if = os == "android"
 [test_bug856978.js]
 [test_unix_domain.js]
 # The xpcshell temp directory on Android doesn't seem to let us create
 # Unix domain sockets. (Perhaps it's a FAT filesystem?)
 skip-if = os == "android"
 [test_addr_in_use_error.js]
 [test_about_networking.js]
 [test_ping_aboutnetworking.js]
+[test_seer.js]
+# Android version detection w/in gecko does not work right on infra, so we just
+# disable this test on all android versions, even though it's enabled on 2.3+ in
+# the wild.
+skip-if = os == "android"
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -1754,16 +1754,136 @@
   },
   "DNS_FAILED_LOOKUP_TIME": {
     "kind": "exponential",
     "high": "60000",
     "n_buckets": 50,
     "extended_statistics_ok": true,
     "description": "Time for an unsuccessful DNS OS resolution (msec)"
   },
+  "SEER_PREDICT_ATTEMPTS": {
+      "kind": "exponential",
+      "high": "1000 * 1000",
+      "n_buckets": 50,
+      "extended_statistics_ok": true,
+      "description": "Number of times nsINetworkSeer::Predict is called and attempts to predict"
+  },
+  "SEER_LEARN_ATTEMPTS": {
+      "kind": "exponential",
+      "high": "1000 * 1000",
+      "n_buckets": 50,
+      "extended_statistics_ok": true,
+      "description": "Number of times nsINetworkSeer::Learn is called and attempts to learn"
+  },
+  "SEER_PREDICT_FULL_QUEUE": {
+      "kind": "exponential",
+      "high": "60000",
+      "n_buckets": 50,
+      "extended_statistics_ok": true,
+      "description": "Number of times nsINetworkSeer::Predict doesn't continue because the queue is full"
+  },
+  "SEER_LEARN_FULL_QUEUE": {
+      "kind": "exponential",
+      "high": "60000",
+      "n_buckets": 50,
+      "extended_statistics_ok": true,
+      "description": "Number of times nsINetworkSeer::Learn doesn't continue because the queue is full"
+  },
+  "SEER_PREDICT_WAIT_TIME": {
+      "kind": "exponential",
+      "high": "3000",
+      "n_buckets": 10,
+      "extended_statistics_ok": true,
+      "description": "Amount of time a predict event waits in the queue (ms)"
+  },
+  "SEER_PREDICT_WORK_TIME": {
+      "kind": "exponential",
+      "high": "3000",
+      "n_buckets": 10,
+      "extended_statistics_ok": true,
+      "description": "Amount of time spent doing the work for predict (ms)"
+  },
+  "SEER_LEARN_WAIT_TIME": {
+      "kind": "exponential",
+      "high": "3000",
+      "n_buckets": 10,
+      "extended_statistics_ok": true,
+      "description": "Amount of time a learn event waits in the queue (ms)"
+  },
+  "SEER_LEARN_WORK_TIME": {
+      "kind": "exponential",
+      "high": "3000",
+      "n_buckets": 10,
+      "extended_statistics_ok": true,
+      "description": "Amount of time spent doing the work for learn (ms)"
+  },
+  "SEER_TOTAL_PREDICTIONS": {
+      "kind": "exponential",
+      "high": "1000 * 1000",
+      "n_buckets": 50,
+      "extended_statistics_ok": true,
+      "description": "How many actual predictions (preresolves, preconnects, ...) happen"
+  },
+  "SEER_TOTAL_PRECONNECTS": {
+      "kind": "exponential",
+      "high": "1000 * 1000",
+      "n_buckets": 50,
+      "extended_statistics_ok": true,
+      "description": "How many actual preconnects happen"
+  },
+  "SEER_TOTAL_PRERESOLVES": {
+      "kind": "exponential",
+      "high": "1000 * 1000",
+      "n_buckets": 50,
+      "extended_statistics_ok": true,
+      "description": "How many actual preresolves happen"
+  },
+  "SEER_PREDICTIONS_CALCULATED": {
+      "kind": "exponential",
+      "high": "1000 * 1000",
+      "n_buckets": 50,
+      "extended_statistics_ok": true,
+      "description": "How many prediction calculations are performed"
+  },
+  "SEER_GLOBAL_DEGRADATION": {
+      "kind": "linear",
+      "high": "100",
+      "n_buckets": 50,
+      "description": "The global degradation calculated"
+  },
+  "SEER_SUBRESOURCE_DEGRADATION": {
+      "kind": "linear",
+      "high": "100",
+      "n_buckets": 50,
+      "description": "The degradation calculated for a subresource"
+  },
+  "SEER_BASE_CONFIDENCE": {
+      "kind": "linear",
+      "high": "100",
+      "n_buckets": 50,
+      "description": "The base confidence calculated for a subresource"
+  },
+  "SEER_CONFIDENCE": {
+      "kind": "linear",
+      "high": "100",
+      "n_buckets": 50,
+      "description": "The final confidence calculated for a subresource"
+  },
+  "SEER_PREDICT_TIME_TO_ACTION": {
+      "kind": "exponential",
+      "high": "3000",
+      "n_buckets": 10,
+      "description": "How long it takes from the time Predict() is called to the time we take action"
+  },
+  "SEER_PREDICT_TIME_TO_INACTION": {
+      "kind": "exponential",
+      "high": "3000",
+      "n_buckets": 10,
+      "description": "How long it takes from the time Predict() is called to the time we figure out there's nothing to do"
+  },
   "FIND_PLUGINS": {
     "kind": "exponential",
     "high": "3000",
     "n_buckets": 10,
     "extended_statistics_ok": true,
     "description": "Time spent scanning filesystem for plugins (ms)"
   },
   "CHECK_JAVA_ENABLED": {