Bug 1239708: Improve awesomebar autofill. Part 4: Frecency stats. r?mak draft
authorDrew Willcoxon <adw@mozilla.com>
Mon, 14 May 2018 11:27:11 -0700
changeset 794916 bc660adfc85e78d69ab254b9d2d9867657bac8b7
parent 794915 b97b491806a9113d57a2c2bb51e85f87841d01ce
child 794917 521df249727e5fead2a5beea351af95ccd789451
push id109819
push userbmo:adw@mozilla.com
push dateMon, 14 May 2018 20:01:28 +0000
reviewersmak
bugs1239708
milestone62.0a1
Bug 1239708: Improve awesomebar autofill. Part 4: Frecency stats. r?mak MozReview-Commit-ID: GD8rglOocBn
toolkit/components/places/Database.cpp
toolkit/components/places/SQLFunctions.cpp
toolkit/components/places/SQLFunctions.h
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/nsINavHistoryService.idl
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/nsNavHistory.h
toolkit/components/places/nsPlacesTriggers.h
toolkit/components/places/tests/unit/test_frecency_stats.js
toolkit/components/places/tests/unit/xpcshell.ini
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -1548,16 +1548,18 @@ Database::InitFunctions()
   rv = GetPrefixFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = GetHostAndPortFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = StripPrefixAndUserinfoFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = IsFrecencyDecayingFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
+  rv = UpdateFrecencyStatsFunction::create(mMainConn);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 nsresult
 Database::InitTempEntities()
 {
   MOZ_ASSERT(NS_IsMainThread());
--- a/toolkit/components/places/SQLFunctions.cpp
+++ b/toolkit/components/places/SQLFunctions.cpp
@@ -1385,10 +1385,58 @@ namespace places {
 
     RefPtr<nsVariant> result = new nsVariant();
     rv = result->SetAsBool(navHistory->IsFrecencyDecaying());
     NS_ENSURE_SUCCESS(rv, rv);
     result.forget(_result);
     return NS_OK;
   }
 
+////////////////////////////////////////////////////////////////////////////////
+//// Update frecency stats function
+
+  /* static */
+  nsresult
+  UpdateFrecencyStatsFunction::create(mozIStorageConnection *aDBConn)
+  {
+    RefPtr<UpdateFrecencyStatsFunction> function =
+      new UpdateFrecencyStatsFunction();
+    nsresult rv = aDBConn->CreateFunction(
+      NS_LITERAL_CSTRING("update_frecency_stats"), 3, function
+    );
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+  NS_IMPL_ISUPPORTS(
+    UpdateFrecencyStatsFunction,
+    mozIStorageFunction
+  )
+
+  NS_IMETHODIMP
+  UpdateFrecencyStatsFunction::OnFunctionCall(mozIStorageValueArray *aArgs,
+                                              nsIVariant **_result)
+  {
+    MOZ_ASSERT(aArgs);
+
+    uint32_t numArgs;
+    nsresult rv = aArgs->GetNumEntries(&numArgs);
+    NS_ENSURE_SUCCESS(rv, rv);
+    MOZ_ASSERT(numArgs == 3);
+
+    int64_t placeId = aArgs->AsInt64(0);
+    int32_t oldFrecency = aArgs->AsInt32(1);
+    int32_t newFrecency = aArgs->AsInt32(2);
+
+    const nsNavHistory* navHistory = nsNavHistory::GetConstHistoryService();
+    NS_ENSURE_STATE(navHistory);
+    navHistory->DispatchFrecencyStatsUpdate(placeId, oldFrecency, newFrecency);
+
+    RefPtr<nsVariant> result = new nsVariant();
+    rv = result->SetAsVoid();
+    NS_ENSURE_SUCCESS(rv, rv);
+    result.forget(_result);
+    return NS_OK;
+  }
+
 } // namespace places
 } // namespace mozilla
--- a/toolkit/components/places/SQLFunctions.h
+++ b/toolkit/components/places/SQLFunctions.h
@@ -561,12 +561,44 @@ public:
    *        The database connection to register with.
    */
   static nsresult create(mozIStorageConnection *aDBConn);
 private:
   ~IsFrecencyDecayingFunction() {}
 };
 
 
+////////////////////////////////////////////////////////////////////////////////
+//// Update frecency stats function
+
+/**
+ * Calls nsNavHistory::UpdateFrecencyStats with the old and new frecencies of a
+ * particular moz_places row.
+ *
+ * @param placeID
+ *        The moz_places row ID.
+ * @param oldFrecency
+ *        The old frecency of a moz_places row that changed frecencies.
+ * @param newFrecency
+ *        The new frecency of the same moz_places row.
+ */
+class UpdateFrecencyStatsFunction final : public mozIStorageFunction
+{
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_MOZISTORAGEFUNCTION
+
+  /**
+   * Registers the function with the specified database connection.
+   *
+   * @param aDBConn
+   *        The database connection to register with.
+   */
+  static nsresult create(mozIStorageConnection *aDBConn);
+private:
+  ~UpdateFrecencyStatsFunction() {}
+};
+
+
 } // namespace places
 } // namespace mozilla
 
 #endif // mozilla_places_SQLFunctions_h_
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -252,28 +252,28 @@ function originQuery(conditions = "", bo
   return `SELECT :query_type,
                  host || '/',
                  prefix || host || '/',
                  frecency,
                  ${bookmarkedFragment} AS bookmarked,
                  id
           FROM moz_origins
           WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
-                AND frecency <> 0
+                AND frecency >= :frecencyThreshold
                 ${conditions}
           UNION ALL
           SELECT :query_type,
                  fixup_url(host) || '/',
                  prefix || host || '/',
                  frecency,
                  ${bookmarkedFragment} AS bookmarked,
                  id
           FROM moz_origins
           WHERE host BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF'
-                AND frecency <> 0
+                AND frecency >= :frecencyThreshold
                 ${conditions}
           ORDER BY frecency DESC, id DESC
           LIMIT 1 `;
 }
 
 const SQL_ORIGIN_QUERY = originQuery();
 
 const SQL_ORIGIN_PREFIX_QUERY = originQuery(
@@ -303,28 +303,28 @@ function urlQuery(conditions1, condition
           SELECT :query_type,
                  url,
                  :strippedURL,
                  frecency,
                  foreign_count > 0 AS bookmarked,
                  id
           FROM moz_places
           WHERE rev_host = :revHost
-                AND frecency <> 0
+                AND frecency >= :frecencyThreshold
                 ${conditions1}
           UNION ALL
           SELECT :query_type,
                  url,
                  :strippedURL,
                  frecency,
                  foreign_count > 0 AS bookmarked,
                  id
           FROM moz_places
           WHERE rev_host = :revHost || 'www.'
-                AND frecency <> 0
+                AND frecency >= :frecencyThreshold
                 ${conditions2}
           ORDER BY frecency DESC, id DESC
           LIMIT 1 `;
 }
 
 const SQL_URL_QUERY = urlQuery(
   `AND strip_prefix_and_userinfo(url) BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
   `AND strip_prefix_and_userinfo(url) BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
@@ -2043,17 +2043,16 @@ Search.prototype = {
   _addAutofillMatch(autofilledValue, finalCompleteValue, frecency, extraStyles = []) {
     // The match's comment is only for display.  Set it to finalCompleteValue,
     // the actual URL that will be visited when the user chooses the match, so
     // that the user knows exactly where the match will take them.  To make it
     // look a little nicer, remove "http://", and if the user typed a host
     // without a trailing slash, remove any trailing slash, too.
     let comment = stripHttpAndTrim(finalCompleteValue,
                                    !this._searchString.includes("/"));
-
     this._addMatch({
       value: this._strippedPrefix + autofilledValue,
       finalCompleteValue,
       comment,
       frecency,
       style: ["autofill"].concat(extraStyles).join(" "),
       icon: "page-icon:" + finalCompleteValue,
     });
@@ -2279,16 +2278,17 @@ Search.prototype = {
     let searchStr =
       this._searchString.endsWith("/") ?
       this._searchString.slice(0, -1) :
       this._searchString;
 
     let opts = {
       query_type: QUERYTYPE_AUTOFILL_ORIGIN,
       searchString: searchStr.toLowerCase(),
+      frecencyThreshold: this._autofillFrecencyThreshold,
     };
 
     let bookmarked = this.hasBehavior("bookmark") &&
                      !this.hasBehavior("history");
 
     if (this._strippedPrefix) {
       opts.prefix = this._strippedPrefix;
       if (bookmarked) {
@@ -2333,16 +2333,17 @@ Search.prototype = {
       strippedURL = strippedURL.substr(this._strippedPrefix.length);
     }
     strippedURL = host + strippedURL.substr(host.length);
 
     let opts = {
       query_type: QUERYTYPE_AUTOFILL_URL,
       revHost,
       strippedURL,
+      frecencyThreshold: this._autofillFrecencyThreshold,
     };
 
     let bookmarked = this.hasBehavior("bookmark") &&
                      !this.hasBehavior("history");
 
     if (this._strippedPrefix) {
       opts.prefix = this._strippedPrefix;
       if (bookmarked) {
@@ -2351,16 +2352,26 @@ Search.prototype = {
       return [SQL_URL_PREFIX_QUERY, opts];
     }
     if (bookmarked) {
       return [SQL_URL_BOOKMARKED_QUERY, opts];
     }
     return [SQL_URL_QUERY, opts];
   },
 
+  get _autofillFrecencyThreshold() {
+    // Places with 0 frecency (and below) shouldn't be autofilled, so use 1 as a
+    // lower bound.
+    return Math.max(
+      1,
+      PlacesUtils.history.frecencyMean +
+        PlacesUtils.history.frecencyStandardDeviation
+    );
+  },
+
   // The result is notified to the search listener on a timer, to chunk multiple
   // match updates together and avoid rebuilding the popup at every new match.
   _notifyTimer: null,
 
   /**
    * Notifies the current result to the listener.
    *
    * @param searchOngoing
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -1172,17 +1172,17 @@ interface nsINavHistoryQueryOptions : ns
   attribute boolean asyncEnabled;
 
   /**
    * Creates a new options item with the same parameters of this one.
    */
   nsINavHistoryQueryOptions clone();
 };
 
-[scriptable, uuid(8a1f527e-c9d7-4a51-bf0c-d86f0379b701)]
+[scriptable, uuid(20c974ff-ee16-4828-9326-1b7c9e036622)]
 interface nsINavHistoryService : nsISupports
 {
   /**
    * System Notifications:
    *
    * places-init-complete - Sent once the History service is completely
    *                        initialized successfully.
    * places-database-locked - Sent if initialization of the History service
@@ -1385,16 +1385,25 @@ interface nsINavHistoryService : nsISupp
    * @param aSpec
    *        The URI spec to hash.
    * @param aMode
    *        The hash mode: `""` (default), `"prefix_lo"`, or `"prefix_hi"`.
    */
   unsigned long long hashURL(in ACString aSpec, [optional] in ACString aMode);
 
   /**
+   * The mean and standard deviation of all frecencies > 0 in the database.
+   * These are used to determine a frecency threshold for URLs that should be
+   * autofilled in the urlbar.  They are updated whenever a frecency changes,
+   * but not when frecencies decay.
+   */
+  readonly attribute double frecencyMean;
+  readonly attribute double frecencyStandardDeviation;
+
+  /**
    * The database connection used by Places.
    */
   readonly attribute mozIStorageConnection DBConnection;
 
   /**
    * Asynchronously executes the statement created from a query.
    *
    * @see nsINavHistoryService::executeQuery
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -105,16 +105,20 @@ using namespace mozilla::places;
 #define PREF_FREC_UNVISITED_TYPED_BONUS         "places.frecency.unvisitedTypedBonus"
 #define PREF_FREC_UNVISITED_TYPED_BONUS_DEF     200
 #define PREF_FREC_RELOAD_VISIT_BONUS            "places.frecency.reloadVisitBonus"
 #define PREF_FREC_RELOAD_VISIT_BONUS_DEF        0
 
 // This is a 'hidden' pref for the purposes of unit tests.
 #define PREF_FREC_DECAY_RATE     "places.frecency.decayRate"
 
+#define PREF_FREC_STATS_COUNT   "places.frecency.stats.count"
+#define PREF_FREC_STATS_SUM     "places.frecency.stats.sum"
+#define PREF_FREC_STATS_SQUARES "places.frecency.stats.sumOfSquares"
+
 // In order to avoid calling PR_now() too often we use a cached "now" value
 // for repeating stuff.  These are milliseconds between "now" cache refreshes.
 #define RENEW_CACHED_NOW_TIMEOUT ((int32_t)3 * PR_MSEC_PER_SEC)
 
 // character-set annotation
 #define CHARSET_ANNO NS_LITERAL_CSTRING("URIProperties/characterSet")
 
 // These macros are used when splitting history by date.
@@ -260,19 +264,43 @@ const int32_t nsNavHistory::kGetInfoInde
 const int32_t nsNavHistory::kGetInfoIndex_VisitType = 17;
 // These columns are followed by corresponding constants in nsNavBookmarks.cpp,
 // which must be kept in sync:
 // nsNavBookmarks::kGetChildrenIndex_Guid = 18;
 // nsNavBookmarks::kGetChildrenIndex_Position = 19;
 // nsNavBookmarks::kGetChildrenIndex_Type = 20;
 // nsNavBookmarks::kGetChildrenIndex_PlaceID = 21;
 
+static uint64_t
+GetUInt64Pref(const char *prefName)
+{
+  // `Preferences` doesn't support uint64_t, so we store it as a string instead.
+  nsAutoCString strVal;
+  nsresult rv = Preferences::GetCString(prefName, strVal);
+  if (NS_SUCCEEDED(rv)) {
+    int64_t val = strVal.ToInteger64(&rv);
+    if (NS_SUCCEEDED(rv)) {
+      return static_cast<uint64_t>(val);
+    }
+  }
+  return 0U;
+}
+
+static void
+SetUInt64Pref(const char *prefName,
+              uint64_t val)
+{
+  nsAutoCString strVal;
+  strVal.AppendInt(val);
+  Unused << Preferences::SetCString(prefName, strVal);
+}
+
+
 PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavHistory, gHistoryService)
 
-
 nsNavHistory::nsNavHistory()
   : mBatchLevel(0)
   , mBatchDBTransaction(nullptr)
   , mCachedNow(0)
   , mRecentTyped(RECENT_EVENTS_INITIAL_CACHE_LENGTH)
   , mRecentLink(RECENT_EVENTS_INITIAL_CACHE_LENGTH)
   , mRecentBookmark(RECENT_EVENTS_INITIAL_CACHE_LENGTH)
   , mEmbedVisits(EMBED_VISITS_INITIAL_CACHE_LENGTH)
@@ -298,16 +326,18 @@ nsNavHistory::nsNavHistory()
   }
 #endif
   gHistoryService = this;
 }
 
 
 nsNavHistory::~nsNavHistory()
 {
+  MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
+
   // remove the static reference to the service. Check to make sure its us
   // in case somebody creates an extra instance of the service.
   NS_ASSERTION(gHistoryService == this,
                "Deleting a non-singleton instance of the service");
 
   if (gHistoryService == this)
     gHistoryService = nullptr;
 
@@ -322,16 +352,20 @@ nsNavHistory::~nsNavHistory()
 nsresult
 nsNavHistory::Init()
 {
   LoadPrefs();
 
   mDB = Database::GetDatabase();
   NS_ENSURE_STATE(mDB);
 
+  mFrecencyStatsCount = GetUInt64Pref(PREF_FREC_STATS_COUNT);
+  mFrecencyStatsSum = GetUInt64Pref(PREF_FREC_STATS_SUM);
+  mFrecencyStatsSumOfSquares = GetUInt64Pref(PREF_FREC_STATS_SQUARES);
+
   /*****************************************************************************
    *** IMPORTANT NOTICE!
    ***
    *** Nothing after these add observer calls should return anything but NS_OK.
    *** If a failure code is returned, this nsNavHistory object will be held onto
    *** by the observer service and the preference service.
    ****************************************************************************/
 
@@ -543,94 +577,175 @@ nsNavHistory::NotifyTitleChange(nsIURI* 
                                 const nsACString& aGUID)
 {
   MOZ_ASSERT(!aGUID.IsEmpty());
   NOTIFY_OBSERVERS(mCanNotify, mObservers, nsINavHistoryObserver,
                    OnTitleChanged(aURI, aTitle, aGUID));
 }
 
 void
-nsNavHistory::NotifyFrecencyChanged(nsIURI* aURI,
+nsNavHistory::NotifyFrecencyChanged(const nsACString& aSpec,
                                     int32_t aNewFrecency,
                                     const nsACString& aGUID,
                                     bool aHidden,
                                     PRTime aLastVisitDate)
 {
   MOZ_ASSERT(!aGUID.IsEmpty());
+
+  nsCOMPtr<nsIURI> uri;
+  Unused << NS_NewURI(getter_AddRefs(uri), aSpec);
+  // We cannot assert since some automated tests are checking this path.
+  NS_WARNING_ASSERTION(uri, "Invalid URI in nsNavHistory::NotifyFrecencyChanged");
+  // Notify a frecency change only if we have a valid uri, otherwise
+  // the observer couldn't gather any useful data from the notification.
+  if (!uri) {
+    return;
+  }
   NOTIFY_OBSERVERS(mCanNotify, mObservers, nsINavHistoryObserver,
-                   OnFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden,
+                   OnFrecencyChanged(uri, aNewFrecency, aGUID, aHidden,
                                      aLastVisitDate));
 }
 
 void
 nsNavHistory::NotifyManyFrecenciesChanged()
 {
   NOTIFY_OBSERVERS(mCanNotify, mObservers, nsINavHistoryObserver,
                    OnManyFrecenciesChanged());
 }
 
-namespace {
-
-class FrecencyNotification : public Runnable
-{
-public:
-  FrecencyNotification(const nsACString& aSpec,
-                       int32_t aNewFrecency,
-                       const nsACString& aGUID,
-                       bool aHidden,
-                       PRTime aLastVisitDate)
-    : mozilla::Runnable("FrecencyNotification")
-    , mSpec(aSpec)
-    , mNewFrecency(aNewFrecency)
-    , mGUID(aGUID)
-    , mHidden(aHidden)
-    , mLastVisitDate(aLastVisitDate)
-  {
-  }
-
-  NS_IMETHOD Run() override
-  {
-    MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
-    nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
-    if (navHistory) {
-      nsCOMPtr<nsIURI> uri;
-      (void)NS_NewURI(getter_AddRefs(uri), mSpec);
-      // We cannot assert since some automated tests are checking this path.
-      NS_WARNING_ASSERTION(uri, "Invalid URI in FrecencyNotification");
-      // Notify a frecency change only if we have a valid uri, otherwise
-      // the observer couldn't gather any useful data from the notification.
-      if (uri) {
-        navHistory->NotifyFrecencyChanged(uri, mNewFrecency, mGUID, mHidden,
-                                          mLastVisitDate);
-      }
-    }
-    return NS_OK;
-  }
-
-private:
-  nsCString mSpec;
-  int32_t mNewFrecency;
-  nsCString mGUID;
-  bool mHidden;
-  PRTime mLastVisitDate;
-};
-
-} // namespace
-
 void
 nsNavHistory::DispatchFrecencyChangedNotification(const nsACString& aSpec,
                                                   int32_t aNewFrecency,
                                                   const nsACString& aGUID,
                                                   bool aHidden,
                                                   PRTime aLastVisitDate) const
 {
-  nsCOMPtr<nsIRunnable> notif = new FrecencyNotification(aSpec, aNewFrecency,
-                                                         aGUID, aHidden,
-                                                         aLastVisitDate);
-  (void)NS_DispatchToMainThread(notif);
+  Unused << NS_DispatchToMainThread(
+    NewRunnableMethod<nsCString, int32_t, nsCString, bool, PRTime>(
+      "nsNavHistory::NotifyFrecencyChanged",
+      const_cast<nsNavHistory*>(this),
+      &nsNavHistory::NotifyFrecencyChanged,
+      aSpec, aNewFrecency, aGUID, aHidden, aLastVisitDate
+    )
+  );
+}
+
+
+void
+nsNavHistory::DispatchFrecencyStatsUpdate(int64_t aPlaceId,
+                                          int32_t aOldFrecency,
+                                          int32_t aNewFrecency) const
+{
+  MOZ_ASSERT(aPlaceId >= 0);
+  Unused << NS_DispatchToMainThread(
+    NewRunnableMethod<int64_t, int32_t, int32_t>(
+      "nsNavHistory::UpdateFrecencyStats",
+      const_cast<nsNavHistory*>(this),
+      &nsNavHistory::UpdateFrecencyStats,
+      aPlaceId, aOldFrecency, aNewFrecency
+    )
+  );
+}
+
+void
+nsNavHistory::UpdateFrecencyStats(int64_t aPlaceId,
+                                  int32_t aOldFrecency,
+                                  int32_t aNewFrecency)
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
+  MOZ_ASSERT(aPlaceId >= 0);
+
+  if (aOldFrecency > 0) {
+    MOZ_ASSERT(mFrecencyStatsCount > 0);
+    mFrecencyStatsCount--;
+    uint64_t uOld = static_cast<uint64_t>(aOldFrecency);
+    MOZ_ASSERT(mFrecencyStatsSum >= uOld);
+    mFrecencyStatsSum -= uOld;
+    uint64_t square = uOld * uOld;
+    MOZ_ASSERT(mFrecencyStatsSumOfSquares >= square);
+    mFrecencyStatsSumOfSquares -= square;
+  }
+  if (aNewFrecency > 0) {
+    mFrecencyStatsCount++;
+    uint64_t uNew = static_cast<uint64_t>(aNewFrecency);
+    mFrecencyStatsSum += uNew;
+    mFrecencyStatsSumOfSquares += uNew * uNew;
+  }
+
+  // This method can be called many times very quickly when many frecencies
+  // change at once.  (Note though that it is *not* called when frecencies
+  // decay.)  To avoid hammering preferences, update them only every so often.
+  // There's actually a browser mochitest that makes sure preferences aren't
+  // accessed too much, and it fails without throttling like this.
+  if (!mUpdateFrecencyStatsPrefsTimer) {
+    Unused << NS_NewTimerWithFuncCallback(
+      getter_AddRefs(mUpdateFrecencyStatsPrefsTimer),
+      &UpdateFrecencyStatsPrefs,
+      this,
+      5000, // ms
+      nsITimer::TYPE_ONE_SHOT,
+      "nsNavHistory::UpdateFrecencyStatsPrefs",
+      nullptr
+    );
+  }
+}
+
+void // static
+nsNavHistory::UpdateFrecencyStatsPrefs(nsITimer *aTimer,
+                                       void *aClosure)
+{
+  nsNavHistory *history = static_cast<nsNavHistory *>(aClosure);
+  if (!history) {
+    return;
+  }
+  SetUInt64Pref(PREF_FREC_STATS_COUNT, history->mFrecencyStatsCount);
+  SetUInt64Pref(PREF_FREC_STATS_SUM, history->mFrecencyStatsSum);
+  SetUInt64Pref(PREF_FREC_STATS_SQUARES, history->mFrecencyStatsSumOfSquares);
+  history->mUpdateFrecencyStatsPrefsTimer = nullptr;
+
+  // This is only so that tests can know when the prefs are written.  Observing
+  // nsPref:changed isn't sufficient because that's not fired when a pref value
+  // is the same as the previous value.
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    MOZ_ALWAYS_SUCCEEDS(obs->NotifyObservers(
+      nullptr,
+      "places-frecency-stats-prefs-updated",
+      nullptr
+    ));
+  }
+}
+
+NS_IMETHODIMP
+nsNavHistory::GetFrecencyMean(double *_retval)
+{
+  NS_ENSURE_ARG_POINTER(_retval);
+  if (mFrecencyStatsCount == 0) {
+    *_retval = 0.0;
+    return NS_OK;
+  }
+  *_retval =
+    static_cast<double>(mFrecencyStatsSum) /
+    static_cast<double>(mFrecencyStatsCount);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistory::GetFrecencyStandardDeviation(double *_retval)
+{
+  NS_ENSURE_ARG_POINTER(_retval);
+  if (mFrecencyStatsCount <= 1) {
+    *_retval = 0.0;
+    return NS_OK;
+  }
+  double squares = static_cast<double>(mFrecencyStatsSumOfSquares);
+  double sum = static_cast<double>(mFrecencyStatsSum);
+  double count = static_cast<double>(mFrecencyStatsCount);
+  *_retval = sqrt((squares - ((sum * sum) / count)) / count);
+  return NS_OK;
 }
 
 Atomic<int64_t> nsNavHistory::sLastInsertedPlaceId(0);
 Atomic<int64_t> nsNavHistory::sLastInsertedVisitId(0);
 
 void // static
 nsNavHistory::StoreLastInsertedId(const nsACString& aTable,
                                   const int64_t aLastInsertedId) {
--- a/toolkit/components/places/nsNavHistory.h
+++ b/toolkit/components/places/nsNavHistory.h
@@ -438,17 +438,17 @@ public:
    */
   void NotifyTitleChange(nsIURI* aURI,
                          const nsString& title,
                          const nsACString& aGUID);
 
   /**
    * Fires onFrecencyChanged event to nsINavHistoryService observers
    */
-  void NotifyFrecencyChanged(nsIURI* aURI,
+  void NotifyFrecencyChanged(const nsACString& aSpec,
                              int32_t aNewFrecency,
                              const nsACString& aGUID,
                              bool aHidden,
                              PRTime aLastVisitDate);
 
   /**
    * Fires onManyFrecenciesChanged event to nsINavHistoryService observers
    */
@@ -466,16 +466,45 @@ public:
   /**
    * Returns true if frecency is currently being decayed.
    *
    * @return True if frecency is being decayed, false if not.
    */
   bool IsFrecencyDecaying() const;
 
   /**
+   * Updates frecencyMean and frecencyStandardDeviation given a change in
+   * frecency of a particular moz_places row.
+   *
+   * @param  aPlaceId
+   *         The moz_places row ID.
+   * @param  aOldFrecency
+   *         The old value of the frecency.
+   * @param  aNewFrecency
+   *         The new value of the frecency.
+   */
+  void UpdateFrecencyStats(int64_t aPlaceId,
+                           int32_t aOldFrecency,
+                           int32_t aNewFrecency);
+
+  /**
+   * Dispatches a runnable to the main thread that calls UpdateFrecencyStats.
+   *
+   * @param  aPlaceId
+   *         The moz_places row ID.
+   * @param  aOldFrecency
+   *         The old value of the frecency.
+   * @param  aNewFrecency
+   *         The new value of the frecency.
+   */
+  void DispatchFrecencyStatsUpdate(int64_t aPlaceId,
+                                   int32_t aOldFrecency,
+                                   int32_t aNewFrecency) const;
+
+  /**
    * Store last insterted id for a table.
    */
   static mozilla::Atomic<int64_t> sLastInsertedPlaceId;
   static mozilla::Atomic<int64_t> sLastInsertedVisitId;
 
   static void StoreLastInsertedId(const nsACString& aTable,
                                   const int64_t aLastInsertedId);
 
@@ -626,16 +655,23 @@ protected:
   int32_t mDefaultVisitBonus;
   int32_t mUnvisitedBookmarkBonus;
   int32_t mUnvisitedTypedBonus;
   int32_t mReloadVisitBonus;
 
   void DecayFrecencyCompleted(uint16_t reason);
   uint32_t mDecayFrecencyPendingCount;
 
+  uint64_t mFrecencyStatsCount;
+  uint64_t mFrecencyStatsSum;
+  uint64_t mFrecencyStatsSumOfSquares;
+  nsCOMPtr<nsITimer> mUpdateFrecencyStatsPrefsTimer;
+  static void UpdateFrecencyStatsPrefs(nsITimer *aTimer,
+                                       void *aClosure);
+
   // in nsNavHistoryQuery.cpp
   nsresult TokensToQuery(const nsTArray<QueryKeyValuePair>& aTokens,
                          nsNavHistoryQuery* aQuery,
                          nsNavHistoryQueryOptions* aOptions);
 
   int64_t mTagsFolder;
 
   int32_t mDaysOfHistory;
--- a/toolkit/components/places/nsPlacesTriggers.h
+++ b/toolkit/components/places/nsPlacesTriggers.h
@@ -74,16 +74,18 @@
 // from moz_updateoriginsdelete_temp, allowing us to run a trigger only once
 // per origin.
 #define CREATE_PLACES_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_places_afterdelete_trigger " \
   "AFTER DELETE ON moz_places FOR EACH ROW " \
   "BEGIN " \
     "INSERT OR IGNORE INTO moz_updateoriginsdelete_temp (origin_id, host) " \
     "VALUES (OLD.origin_id, get_host_and_port(OLD.url)); " \
+    "SELECT update_frecency_stats(OLD.id, OLD.frecency, -1) " \
+    "WHERE OLD.id >= 0; " \
   "END" \
 )
 
 // See CREATE_PLACES_AFTERINSERT_TRIGGER. This is the trigger that we want
 // to ensure gets run for each origin that we insert into moz_places.
 #define CREATE_UPDATEORIGINSINSERT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_updateoriginsinsert_afterdelete_trigger " \
   "AFTER DELETE ON moz_updateoriginsinsert_temp FOR EACH ROW " \
@@ -142,16 +144,18 @@
   "BEGIN " \
     "UPDATE moz_origins " \
     "SET frecency = ( " \
       "SELECT IFNULL(MAX(frecency), 0) " \
       "FROM moz_places " \
       "WHERE moz_places.origin_id = moz_origins.id " \
     ") " \
     "WHERE id = NEW.origin_id; " \
+    "SELECT update_frecency_stats(NEW.id, OLD.frecency, NEW.frecency) " \
+    "WHERE NEW.id >= 0; " \
   "END" \
 )
 
 /**
  * This trigger removes a row from moz_openpages_temp when open_count reaches 0.
  *
  * @note this should be kept up-to-date with the definition in
  *       nsPlacesAutoComplete.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_stats.js
@@ -0,0 +1,173 @@
+/* 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/. */
+
+"use strict";
+
+add_task(async function init() {
+  await cleanUp();
+});
+
+
+// Adds/removes some visits and bookmarks and makes sure the stats are updated.
+add_task(async function basic() {
+  Assert.equal(PlacesUtils.history.frecencyMean, 0);
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation, 0);
+
+  let frecenciesByURL = {};
+  let urls = [0, 1, 2].map(i => "http://example.com/" + i);
+
+  // Add a URL 0 visit.
+  await PlacesTestUtils.addVisits([{ uri: urls[0] }]);
+  frecenciesByURL[urls[0]] = frecencyForUrl(urls[0]);
+  Assert.ok(frecenciesByURL[urls[0]] > 0, "Sanity check");
+  Assert.equal(PlacesUtils.history.frecencyMean,
+               mean(Object.values(frecenciesByURL)));
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation,
+               stddev(Object.values(frecenciesByURL)));
+
+  // Add a URL 1 visit.
+  await PlacesTestUtils.addVisits([{ uri: urls[1] }]);
+  frecenciesByURL[urls[1]] = frecencyForUrl(urls[1]);
+  Assert.ok(frecenciesByURL[urls[1]] > 0, "Sanity check");
+  Assert.equal(PlacesUtils.history.frecencyMean,
+               mean(Object.values(frecenciesByURL)));
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation,
+               stddev(Object.values(frecenciesByURL)));
+
+  // Add a URL 2 visit.
+  await PlacesTestUtils.addVisits([{ uri: urls[2] }]);
+  frecenciesByURL[urls[2]] = frecencyForUrl(urls[2]);
+  Assert.ok(frecenciesByURL[urls[2]] > 0, "Sanity check");
+  Assert.equal(PlacesUtils.history.frecencyMean,
+               mean(Object.values(frecenciesByURL)));
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation,
+               stddev(Object.values(frecenciesByURL)));
+
+  // Add another URL 2 visit.
+  await PlacesTestUtils.addVisits([{ uri: urls[2] }]);
+  frecenciesByURL[urls[2]] = frecencyForUrl(urls[2]);
+  Assert.ok(frecenciesByURL[urls[2]] > 0, "Sanity check");
+  Assert.equal(PlacesUtils.history.frecencyMean,
+               mean(Object.values(frecenciesByURL)));
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation,
+               stddev(Object.values(frecenciesByURL)));
+
+  // Remove URL 2's visits.
+  await PlacesUtils.history.remove([urls[2]]);
+  delete frecenciesByURL[urls[2]];
+  Assert.equal(PlacesUtils.history.frecencyMean,
+               mean(Object.values(frecenciesByURL)));
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation,
+               stddev(Object.values(frecenciesByURL)));
+
+  // Bookmark URL 1.
+  let parentGuid =
+    await PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
+  let bookmark = await PlacesUtils.bookmarks.insert({
+    parentGuid,
+    title: "A bookmark",
+    url: NetUtil.newURI(urls[1]),
+  });
+  await PlacesUtils.promiseItemId(bookmark.guid);
+
+  frecenciesByURL[urls[1]] = frecencyForUrl(urls[1]);
+  Assert.ok(frecenciesByURL[urls[1]] > 0, "Sanity check");
+  Assert.equal(PlacesUtils.history.frecencyMean,
+               mean(Object.values(frecenciesByURL)));
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation,
+               stddev(Object.values(frecenciesByURL)));
+
+  // Remove URL 1's visit.
+  await PlacesUtils.history.remove([urls[1]]);
+  frecenciesByURL[urls[1]] = frecencyForUrl(urls[1]);
+  Assert.ok(frecenciesByURL[urls[1]] > 0, "Sanity check");
+  Assert.equal(PlacesUtils.history.frecencyMean,
+               mean(Object.values(frecenciesByURL)));
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation,
+               stddev(Object.values(frecenciesByURL)));
+
+  // Remove URL 1's bookmark.  Also need to call history.remove() again to
+  // remove the URL from moz_places.  Otherwise it sticks around and keeps
+  // contributing to the frecency stats.
+  await PlacesUtils.bookmarks.remove(bookmark);
+  await PlacesUtils.history.remove(urls[1]);
+  delete frecenciesByURL[urls[1]];
+  Assert.equal(PlacesUtils.history.frecencyMean,
+               mean(Object.values(frecenciesByURL)));
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation,
+               stddev(Object.values(frecenciesByURL)));
+
+  // Remove URL 0.
+  await PlacesUtils.history.remove([urls[0]]);
+  delete frecenciesByURL[urls[0]];
+  Assert.equal(PlacesUtils.history.frecencyMean, 0);
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation, 0);
+
+  await cleanUp();
+});
+
+
+// Makes sure the prefs that store the stats are updated.
+add_task(async function preferences() {
+  Assert.equal(PlacesUtils.history.frecencyMean, 0);
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation, 0);
+
+  let url = "http://example.com/";
+  await PlacesTestUtils.addVisits([{ uri: url }]);
+  let frecency = frecencyForUrl(url);
+  Assert.ok(frecency > 0, "Sanity check");
+  Assert.equal(PlacesUtils.history.frecencyMean, frecency);
+  Assert.equal(PlacesUtils.history.frecencyStandardDeviation, 0);
+
+  let expectedValuesByName = {
+    "places.frecency.stats.count": "1",
+    "places.frecency.stats.sum": String(frecency),
+    "places.frecency.stats.sumOfSquares": String(frecency * frecency),
+  };
+
+  info("Waiting for preferences to be updated...");
+  await TestUtils.topicObserved("places-frecency-stats-prefs-updated", () => {
+    return Object.entries(expectedValuesByName).every(([name, expected]) => {
+      let actual = Services.prefs.getCharPref(name, "");
+      info(`${name} => ${actual} (expected=${expected})`);
+      return actual == expected;
+    });
+  });
+  Assert.ok(true, "Preferences updated as expected");
+
+  await cleanUp();
+});
+
+
+function mean(values) {
+  if (values.length == 0) {
+    return 0;
+  }
+  return values.reduce((sum, value) => {
+    sum += value;
+    return sum;
+  }, 0) / values.length;
+}
+
+function stddev(values) {
+  if (values.length <= 1) {
+    return 0;
+  }
+  let sum = values.reduce((memo, value) => {
+    memo += value;
+    return memo;
+  }, 0);
+  let sumOfSquares = values.reduce((memo, value) => {
+    memo += value * value;
+    return memo;
+  }, 0);
+  return Math.sqrt(
+    (sumOfSquares - ((sum * sum) / values.length)) / values.length
+  );
+}
+
+async function cleanUp() {
+  await PlacesUtils.bookmarks.eraseEverything();
+  await PlacesUtils.history.clear();
+}
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -63,16 +63,17 @@ skip-if = (os == "win" && os_version == 
 [test_browserhistory.js]
 [test_bug636917_isLivemark.js]
 [test_childlessTags.js]
 [test_corrupt_telemetry.js]
 [test_database_replaceOnStartup.js]
 [test_download_history.js]
 [test_frecency.js]
 [test_frecency_decay.js]
+[test_frecency_stats.js]
 [test_frecency_zero_updated.js]
 [test_getChildIndex.js]
 [test_hash.js]
 [test_history.js]
 [test_history_clear.js]
 [test_history_notifications.js]
 [test_history_observer.js]
 [test_history_sidebar.js]