--- 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]