commit b97136c310eac98ee162bcdc1de1c446368bfaca
parent 25d2f39baf24c984167c9bc7ae5035beecbc6756
Author: James Teow <jteow@mozilla.com>
Date: Tue, 11 Nov 2025 02:37:12 +0000
Bug 1986285 - Part 1: Update calculate_frecency with interactions - r=mak,places-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D263266
Diffstat:
11 files changed, 474 insertions(+), 231 deletions(-)
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
@@ -15888,24 +15888,81 @@
value: 2 * 60
mirror: once
-# Minimum time required to upgrade a visit score.
+# Minimum view time required to upgrade a visit score.
- name: places.frecency.pages.alternative.interactions.viewTimeSeconds
type: uint32_t
value: 60
mirror: once
-# Minimum time required to upgrade a visit score of a visit
+# Minimum view time required to upgrade a visit score of a visit
# provided there is a minimum threshold of keypresses.
- name: places.frecency.pages.alternative.interactions.viewTimeIfManyKeypressesSeconds
type: uint32_t
value: 20
mirror: once
+# Minimum keypresses of a visit required to upgrade a visit score.
- name: places.frecency.pages.alternative.interactions.manyKeypresses
type: uint32_t
value: 50
mirror: once
+# Preferences related to interaction based frecency.
+- name: places.frecency.pages.veryHighWeight
+ type: uint32_t
+ value: 200
+ mirror: once
+
+- name: places.frecency.pages.highWeight
+ type: uint32_t
+ value: 100
+ mirror: once
+
+- name: places.frecency.pages.mediumWeight
+ type: uint32_t
+ value: 50
+ mirror: once
+
+- name: places.frecency.pages.lowWeight
+ type: uint32_t
+ value: 20
+ mirror: once
+
+- name: places.frecency.pages.halfLifeDays
+ type: uint32_t
+ value: 30
+ mirror: once
+
+- name: places.frecency.pages.numSampledVisits
+ type: uint32_t
+ value: 10
+ mirror: once
+
+# Max difference allowed between a visit and an interaction.
+- name: places.frecency.pages.interactions.maxVisitGapSeconds
+ type: uint32_t
+ value: 2 * 60
+ mirror: once
+
+# Minimum view time required to upgrade a visit score.
+- name: places.frecency.pages.interactions.viewTimeSeconds
+ type: uint32_t
+ value: 60
+ mirror: once
+
+# Minimum view time required to upgrade a visit score of a visit
+# provided there is a minimum threshold of keypresses.
+- name: places.frecency.pages.interactions.viewTimeIfManyKeypressesSeconds
+ type: uint32_t
+ value: 20
+ mirror: once
+
+# Minimum keypresses of a visit required to upgrade a visit score.
+- name: places.frecency.pages.interactions.manyKeypresses
+ type: uint32_t
+ value: 50
+ mirror: once
+
# Whether flooding prevention feature is enabled or not.
- name: places.history.floodingPrevention.enabled
type: bool
diff --git a/toolkit/components/places/Database.cpp b/toolkit/components/places/Database.cpp
@@ -1348,6 +1348,13 @@ nsresult Database::InitSchema(bool* aDatabaseMigrated) {
// Firefox 141 uses schema version 82
+ if (currentSchemaVersion < 83) {
+ rv = MigrateV83Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 145 uses schema version 83
+
// Schema Upgrades must add migration code here.
// >>> IMPORTANT! <<<
// NEVER MIX UP SYNC AND ASYNC EXECUTION IN MIGRATORS, YOU MAY LOCK THE
@@ -1808,26 +1815,38 @@ nsresult Database::InitTempEntities() {
rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_METADATA_AFTERDELETE_TRIGGER);
NS_ENSURE_SUCCESS(rv, rv);
- if (StaticPrefs::places_frecency_pages_alternative_featureGate_AtStartup()) {
- int32_t viewTimeMs =
- StaticPrefs::
- places_frecency_pages_alternative_interactions_viewTimeSeconds_AtStartup() *
- 1000;
- int32_t viewTimeIfManyKeypressesMs =
- StaticPrefs::
- places_frecency_pages_alternative_interactions_viewTimeIfManyKeypressesSeconds_AtStartup() *
- 1000;
- int32_t manyKeypresses = StaticPrefs::
- places_frecency_pages_alternative_interactions_manyKeypresses_AtStartup();
-
- rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_METADATA_AFTERINSERT_TRIGGER(
- viewTimeMs, viewTimeIfManyKeypressesMs, manyKeypresses));
- NS_ENSURE_SUCCESS(rv, rv);
-
- rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_METADATA_AFTERUPDATE_TRIGGER(
- viewTimeMs, viewTimeIfManyKeypressesMs, manyKeypresses));
- NS_ENSURE_SUCCESS(rv, rv);
- }
+ // Thresholds chosen for elevating a visit to a higher bucket.
+ bool useAlternative =
+ StaticPrefs::places_frecency_pages_alternative_featureGate_AtStartup();
+ int32_t viewTimeMs =
+ (useAlternative
+ ? StaticPrefs::
+ places_frecency_pages_alternative_interactions_viewTimeSeconds_AtStartup() *
+ 1000
+ : StaticPrefs::
+ places_frecency_pages_interactions_viewTimeSeconds_AtStartup() *
+ 1000);
+ int32_t viewTimeIfManyKeypressesMs =
+ (useAlternative
+ ? StaticPrefs::
+ places_frecency_pages_alternative_interactions_viewTimeIfManyKeypressesSeconds_AtStartup() *
+ 1000
+ : StaticPrefs::
+ places_frecency_pages_interactions_viewTimeIfManyKeypressesSeconds_AtStartup() *
+ 1000);
+ int32_t manyKeypresses =
+ (useAlternative
+ ? StaticPrefs::
+ places_frecency_pages_alternative_interactions_manyKeypresses_AtStartup()
+ : StaticPrefs::
+ places_frecency_pages_interactions_manyKeypresses_AtStartup());
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_METADATA_AFTERINSERT_TRIGGER(
+ viewTimeMs, viewTimeIfManyKeypressesMs, manyKeypresses));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_METADATA_AFTERUPDATE_TRIGGER(
+ viewTimeMs, viewTimeIfManyKeypressesMs, manyKeypresses));
+ NS_ENSURE_SUCCESS(rv, rv);
// Create triggers to remove rows with empty json
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_PLACES_EXTRA_AFTERUPDATE_TRIGGER);
@@ -2255,6 +2274,14 @@ nsresult Database::MigrateV82Up() {
return NS_OK;
}
+nsresult Database::MigrateV83Up() {
+ // Recalculate frecency due to changing calculate_frecency.
+ nsresult rv = mMainConn->ExecuteSimpleSQL(
+ "UPDATE moz_places SET recalc_frecency = 1 WHERE frecency > 0"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
int64_t Database::CreateMobileRoot() {
MOZ_ASSERT(NS_IsMainThread());
diff --git a/toolkit/components/places/Database.h b/toolkit/components/places/Database.h
@@ -331,6 +331,7 @@ class Database final : public nsIObserver, public nsSupportsWeakReference {
nsresult MigrateV80Up();
nsresult MigrateV81Up();
nsresult MigrateV82Up();
+ nsresult MigrateV83Up();
nsresult UpdateBookmarkRootTitles();
diff --git a/toolkit/components/places/SQLFunctions.cpp b/toolkit/components/places/SQLFunctions.cpp
@@ -571,197 +571,234 @@ CalculateFrecencyFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
return NS_OK;
}
- enum RedirectBonus { eUnknown, eRedirect, eNormal };
-
- RedirectBonus mostRecentVisitBonus = eUnknown;
-
+ int32_t isRedirect = 0;
if (numEntries > 1) {
- mostRecentVisitBonus = aArguments->AsInt32(1) ? eRedirect : eNormal;
+ isRedirect = aArguments->AsInt32(1);
}
-
- int32_t typed = 0;
- int32_t visitCount = 0;
- PRTime mostRecentBookmarkTime = 0;
- int32_t isQuery = 0;
- float pointsForSampledVisits = 0.0f;
- int32_t numSampledVisits = 0;
- int32_t bonus = 0;
-
// This is a const version of the history object for thread-safety.
const nsNavHistory* history = nsNavHistory::GetConstHistoryService();
NS_ENSURE_STATE(history);
RefPtr<Database> DB = Database::GetDatabase();
NS_ENSURE_STATE(DB);
- // Fetch the page stats from the database.
- {
- nsCOMPtr<mozIStorageStatement> getPageInfo = DB->GetStatement(
- "SELECT typed, visit_count, MAX(dateAdded), "
- "(substr(url, 0, 7) = 'place:') "
- "FROM moz_places h "
- "LEFT JOIN moz_bookmarks ON fk = h.id "
- "WHERE h.id = :page_id");
- NS_ENSURE_STATE(getPageInfo);
- mozStorageStatementScoper infoScoper(getPageInfo);
-
- rv = getPageInfo->BindInt64ByName("page_id"_ns, pageId);
- NS_ENSURE_SUCCESS(rv, rv);
-
- bool hasResult = false;
- rv = getPageInfo->ExecuteStep(&hasResult);
- NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_UNEXPECTED);
-
- rv = getPageInfo->GetInt32(0, &typed);
- NS_ENSURE_SUCCESS(rv, rv);
- rv = getPageInfo->GetInt32(1, &visitCount);
- NS_ENSURE_SUCCESS(rv, rv);
- rv = getPageInfo->GetInt64(2, &mostRecentBookmarkTime);
- NS_ENSURE_SUCCESS(rv, rv);
- rv = getPageInfo->GetInt32(3, &isQuery);
- NS_ENSURE_SUCCESS(rv, rv);
- }
-
- if (visitCount > 0) {
- // Get a sample of the last visits to the page, to calculate its weight.
- // In case the visit is a redirect target, calculate the frecency
- // as if the original page was visited.
- // If it's a redirect source, we may want to use a lower bonus.
- nsCString redirectsTransitionFragment = nsPrintfCString(
- "%d AND %d ", nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
- nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY);
- nsCOMPtr<mozIStorageStatement> getVisits = DB->GetStatement(
- nsLiteralCString(
- "/* do not warn (bug 659740 - SQLite may ignore index if few "
- "visits exist) */"
- "SELECT "
- "IFNULL(origin.visit_type, v.visit_type) AS visit_type, "
- "target.visit_type AS target_visit_type, "
- "ROUND((strftime('%s','now','localtime','utc') - "
- "v.visit_date/1000000)/86400) AS age_in_days, "
- "v.source AS visit_source "
- "FROM moz_historyvisits v "
- "LEFT JOIN moz_historyvisits origin ON origin.id = v.from_visit "
- "AND v.visit_type BETWEEN ") +
- redirectsTransitionFragment +
- nsLiteralCString(
- "LEFT JOIN moz_historyvisits target ON v.id = target.from_visit "
- "AND target.visit_type BETWEEN ") +
- redirectsTransitionFragment +
- nsLiteralCString("WHERE v.place_id = :page_id "
- "ORDER BY v.visit_date DESC "
- "LIMIT :max_visits "));
- NS_ENSURE_STATE(getVisits);
- mozStorageStatementScoper visitsScoper(getVisits);
- rv = getVisits->BindInt64ByName("page_id"_ns, pageId);
- NS_ENSURE_SUCCESS(rv, rv);
- rv = getVisits->BindInt32ByName("max_visits"_ns,
- history->GetNumVisitsForFrecency());
- NS_ENSURE_SUCCESS(rv, rv);
-
- // Fetch only a limited number of recent visits.
- bool hasResult = false;
- while (NS_SUCCEEDED(getVisits->ExecuteStep(&hasResult)) && hasResult) {
- // If this is a redirect target, we'll use the visitType of the source,
- // otherwise the actual visitType.
- int32_t visitType = getVisits->AsInt32(0);
-
- // When adding a new visit, we should haved passed-in whether we should
- // use the redirect bonus. We can't fetch this information from the
- // database, because we only store redirect targets.
- // For older visits we extract the value from the database.
- bool useRedirectBonus = mostRecentVisitBonus == eRedirect;
- if (mostRecentVisitBonus == eUnknown || numSampledVisits > 0) {
- int32_t targetVisitType = getVisits->AsInt32(1);
- useRedirectBonus =
- targetVisitType ==
- nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT ||
- (targetVisitType ==
- nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY &&
- visitType != nsINavHistoryService::TRANSITION_TYPED);
- }
-
- uint32_t visitSource = getVisits->AsInt32(3);
- if (mostRecentBookmarkTime) {
- // For bookmarked visit, add full bonus.
- bonus = history->GetFrecencyTransitionBonus(visitType, true,
- useRedirectBonus);
- bonus += history->GetFrecencyTransitionBonus(
- nsINavHistoryService::TRANSITION_BOOKMARK, true);
- } else if (visitSource == nsINavHistoryService::VISIT_SOURCE_ORGANIC) {
- bonus = history->GetFrecencyTransitionBonus(visitType, true,
- useRedirectBonus);
- } else if (visitSource == nsINavHistoryService::VISIT_SOURCE_SEARCHED) {
- bonus = history->GetFrecencyTransitionBonus(
- nsINavHistoryService::TRANSITION_LINK, true, useRedirectBonus);
- }
+ /*
+ Exponentially decay each visit with an half-life of halfLifeDays.
+ Score per each visit is a weight exponentially decayed depending on how
+ far away is from a reference date, that is the most recent visit date.
+ The weight for each visit is assigned depending on the visit type and other
+ information (bookmarked, a redirect, a typed entry).
+ If a page has no visits, consider a single visit with an high weight and
+ decay its score using the bookmark date as reference time.
+ Frecency is the sum of all the scores / number of samples.
+ The final score is further decayed using the same half-life.
+ To avoid having to decay the score manually, the stored value is the number
+ of days after which the score would become 1.
- // If bonus was zero, we can skip the work to determine the weight.
- if (bonus) {
- int32_t ageInDays = getVisits->AsInt32(2);
- int32_t weight = history->GetFrecencyAgedWeight(ageInDays);
- pointsForSampledVisits += ((float)weight * ((float)bonus / 100.0f));
- }
+ TODO: Add reference link to source docs here.
+ */
+ nsCOMPtr<mozIStorageStatement> stmt = DB->GetStatement(
+ "WITH "
+ "lambda (lambda) AS ( "
+ " SELECT ln(2) / :halfLifeDays "
+ "), "
+ "interactions AS ( "
+ " SELECT "
+ " place_id, "
+ " created_at * 1000 AS visit_date "
+ " FROM "
+ " moz_places_metadata "
+ " WHERE "
+ " place_id = :pageId "
+ // The view time preferences are in seconds while the total view time is
+ // in milliseconds.
+ " AND (total_view_time >= :viewTimeSeconds * 1000 "
+ " OR (total_view_time >= :viewTimeIfManyKeypressesSeconds * 1000 "
+ " AND key_presses >= :manyKeypresses)) "
+ " ORDER BY created_at DESC "
+ " LIMIT :numSampledVisits "
+ "), "
+ "sampled_visits AS ( "
+ " SELECT "
+ " vs.id, "
+ " vs.from_visit, "
+ " vs.place_id, "
+ " vs.visit_date, "
+ " vs.visit_type, "
+ " vs.source, "
+ " ( "
+ " SELECT EXISTS ( "
+ " SELECT 1 "
+ " FROM interactions i "
+ " WHERE vs.visit_date BETWEEN "
+ // Visit dates are in microseconds while the visit gap is in seconds.
+ " i.visit_date - :maxVisitGapSeconds * 1000000 "
+ " AND i.visit_date + :maxVisitGapSeconds * 1000000 "
+ " ) "
+ " ) AS is_interesting "
+ " FROM moz_historyvisits vs "
+ " WHERE place_id = :pageId "
+ // Ignore Downloads, Framed Links, and Reloads.
+ " AND vs.visit_type NOT IN (7, 8, 9) "
+ " ORDER BY visit_date DESC "
+ " LIMIT :numSampledVisits "
+ "), "
+ "virtual_visits AS ( "
+ " SELECT "
+ " NULL AS id, "
+ " 0 AS from_visit, "
+ " i.place_id, "
+ " i.visit_date, "
+ " 1 AS visit_type, "
+ " 0 AS source, "
+ " 1 AS is_interesting "
+ " FROM interactions i "
+ " WHERE NOT EXISTS ( "
+ " SELECT 1 FROM moz_historyvisits vs "
+ " WHERE place_id = :pageId "
+ " AND vs.visit_date BETWEEN "
+ " i.visit_date - :maxVisitGapSeconds * 1000000 "
+ " AND i.visit_date + :maxVisitGapSeconds * 1000000 "
+ " ) "
+ "), "
+ "visit_interaction AS ( "
+ " SELECT * FROM sampled_visits "
+ " UNION ALL "
+ " SELECT * FROM virtual_visits "
+ " ORDER BY visit_date DESC "
+ " LIMIT :numSampledVisits "
+ "), "
+ "visits (days, weight) AS ( "
+ " SELECT "
+ " v.visit_date / 86400000000, "
+ // A visit is given a score based on a variety of factors, such as
+ // whether it was a bookmark, how the user got to the page, and whether
+ // or not it was a redirect. The score will be boosted if the visit was
+ // interesting and it was not a redirect. A visit is interesting if a user
+ // spent a lot of time viewing the page or they typed a lot of keypresses.
+ " (SELECT CASE "
+ " WHEN IFNULL(s.visit_type, v.visit_type) = 3 " // from a bookmark
+ " OR v.source = 2 " // is a bookmark
+ " OR ( IFNULL(s.visit_type, v.visit_type) = 2 " // is typed
+ " AND v.source NOT IN (1, 3) " // not search or sponsored
+ " AND t.id IS NULL AND NOT :isRedirect " // not a redirect
+ " ) "
+ " THEN "
+ " CASE "
+ " WHEN v.is_interesting = 1 THEN :veryHighWeight "
+ " ELSE :highWeight "
+ " END "
+ " WHEN t.id IS NULL AND NOT :isRedirect " // not a redirect
+ " AND IFNULL(s.visit_type, v.visit_type) NOT IN (4, 8, 9) "
+ " AND v.source <> 1 " // Not a sponsored source.
+ " THEN "
+ " CASE "
+ " WHEN v.is_interesting = 1 THEN :highWeight "
+ " ELSE :mediumWeight "
+ " END "
+ " ELSE :lowWeight "
+ " END) "
+ " FROM visit_interaction v "
+ // If it's a redirect target, use the visit_type of the source.
+ " LEFT JOIN moz_historyvisits s ON s.id = v.from_visit "
+ " AND v.visit_type IN (5,6) "
+ // If it's a redirect, use a low weight.
+ " LEFT JOIN moz_historyvisits t ON t.from_visit = v.id "
+ " AND t.visit_type IN (5,6) "
+ "), "
+ "bookmark (days, weight) AS ( "
+ " SELECT dateAdded / 86400000000, :highWeight "
+ " FROM moz_bookmarks "
+ " WHERE fk = :pageId "
+ " ORDER BY dateAdded DESC "
+ " LIMIT 1 "
+ "), "
+ "samples (days, weight) AS ( "
+ " SELECT * FROM bookmark WHERE (SELECT count(*) FROM visits) = 0 "
+ " UNION ALL "
+ " SELECT * FROM visits "
+ "), "
+ "reference (days, samples_count) AS ( "
+ " SELECT max(samples.days), count(*) FROM samples "
+ "), "
+ "scores (score) AS ( "
+ " SELECT (weight * exp(-lambda * (reference.days - samples.days))) "
+ " FROM samples, reference, lambda "
+ ") "
+ "SELECT CASE "
+ "WHEN (substr(url, 0, 7) = 'place:') THEN 0 "
+ "ELSE "
+ " reference.days + CAST (( "
+ " ln( "
+ " sum(score) / samples_count * MAX(visit_count, samples_count) "
+ " ) / lambda "
+ " ) AS INTEGER) "
+ "END "
+ "FROM moz_places h, reference, lambda, scores "
+ "WHERE h.id = :pageId");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper infoScoper(stmt);
- numSampledVisits++;
- }
- }
+ rv = stmt->BindInt64ByName("pageId"_ns, pageId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName("isRedirect"_ns, isRedirect);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "halfLifeDays"_ns,
+ StaticPrefs::places_frecency_pages_halfLifeDays_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "numSampledVisits"_ns,
+ StaticPrefs::places_frecency_pages_numSampledVisits_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "lowWeight"_ns, StaticPrefs::places_frecency_pages_lowWeight_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "mediumWeight"_ns,
+ StaticPrefs::places_frecency_pages_mediumWeight_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "highWeight"_ns,
+ StaticPrefs::places_frecency_pages_highWeight_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "veryHighWeight"_ns,
+ StaticPrefs::places_frecency_pages_veryHighWeight_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "maxVisitGapSeconds"_ns,
+ StaticPrefs::
+ places_frecency_pages_interactions_maxVisitGapSeconds_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "viewTimeSeconds"_ns,
+ StaticPrefs::
+ places_frecency_pages_interactions_viewTimeSeconds_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "manyKeypresses"_ns,
+ StaticPrefs::
+ places_frecency_pages_interactions_manyKeypresses_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "viewTimeIfManyKeypressesSeconds"_ns,
+ StaticPrefs::
+ places_frecency_pages_interactions_viewTimeIfManyKeypressesSeconds_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
- // If we sampled some visits for this page, use the calculated weight.
- if (numSampledVisits) {
- // We were unable to calculate points, maybe cause all the visits in the
- // sample had a zero bonus. Though, we know the page has some past valid
- // visit, or visit_count would be zero. Thus we set the frecency to
- // -1, so they are still shown in autocomplete.
- if (pointsForSampledVisits == 0.0f) {
- *_result = MakeAndAddRef<IntegerVariant>(-1).take();
- } else {
- // Estimate frecency using the sampled visits.
- // Use ceilf() so that we don't round down to 0, which
- // would cause us to completely ignore the place during autocomplete.
- *_result =
- MakeAndAddRef<IntegerVariant>(
- (int32_t)ceilf((float)visitCount * ceilf(pointsForSampledVisits) /
- (float)numSampledVisits))
- .take();
- }
- return NS_OK;
- }
+ bool hasResult = false;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_UNEXPECTED);
- // Otherwise this page has no visits, it may be bookmarked.
- if (!mostRecentBookmarkTime || isQuery) {
+ bool isNull;
+ if (NS_SUCCEEDED(stmt->GetIsNull(0, &isNull)) && isNull) {
*_result = MakeAndAddRef<IntegerVariant>(0).take();
- return NS_OK;
- }
-
- MOZ_ASSERT(bonus == 0, "Pages should arrive here with 0 bonus");
- MOZ_ASSERT(mostRecentBookmarkTime > 0, "This should be a bookmarked page");
-
- // For unvisited bookmarks, produce a non-zero frecency, so that they show
- // up in URL bar autocomplete.
- // Make it so something bookmarked and typed will have a higher frecency
- // than something just typed or just bookmarked.
- bonus += history->GetFrecencyTransitionBonus(
- nsINavHistoryService::TRANSITION_BOOKMARK, false);
- if (typed) {
- bonus += history->GetFrecencyTransitionBonus(
- nsINavHistoryService::TRANSITION_TYPED, false);
+ } else {
+ int32_t score;
+ rv = stmt->GetInt32(0, &score);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *_result = MakeAndAddRef<IntegerVariant>(score).take();
}
-
- // Use an appropriate bucket depending on the bookmark creation date.
- int32_t bookmarkAgeInDays =
- static_cast<int32_t>((PR_Now() - mostRecentBookmarkTime) /
- ((PRTime)SECONDS_PER_DAY * (PRTime)PR_USEC_PER_SEC));
-
- pointsForSampledVisits =
- (float)history->GetFrecencyAgedWeight(bookmarkAgeInDays) *
- ((float)bonus / 100.0f);
-
- // use ceilf() so that we don't round down to 0, which
- // would cause us to completely ignore the place during autocomplete
- *_result =
- MakeAndAddRef<IntegerVariant>((int32_t)ceilf(pointsForSampledVisits))
- .take();
-
return NS_OK;
}
diff --git a/toolkit/components/places/nsINavHistoryService.idl b/toolkit/components/places/nsINavHistoryService.idl
@@ -910,7 +910,7 @@ interface nsINavHistoryService : nsISupports
// The current database schema version.
// To migrate to a new version bump this, add a MigrateVXXUp function to
// Database.cpp/h, and a test into tests/migration/
- const unsigned long DATABASE_SCHEMA_VERSION = 82;
+ const unsigned long DATABASE_SCHEMA_VERSION = 83;
/**
* System Notifications:
diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp
@@ -135,6 +135,8 @@ using namespace mozilla::places;
#define TOPIC_PROFILE_CHANGE "profile-before-change"
#define TOPIC_APP_LOCALES_CHANGED "intl:app-locales-changed"
+#define USEC_PER_DAY 86400000000LL
+
static const char* kObservedPrefs[] = {PREF_HISTORY_ENABLED,
PREF_MATCH_DIACRITICS,
PREF_FREC_NUM_VISITS,
@@ -1865,30 +1867,62 @@ nsNavHistory::PageFrecencyThreshold(int32_t aVisitAgeInDays, int32_t aNumVisits,
int64_t nsNavHistory::CalculateFrecency(int32_t aVisitAgeInDays,
int32_t aNumVisits,
bool aBookmarked) const {
- int32_t weight = this->GetFrecencyAgedWeight(aVisitAgeInDays);
-
- if (aNumVisits) {
- int32_t visitScore = this->GetFrecencyTransitionBonus(
- nsINavHistoryService::TRANSITION_LINK, true, false);
- if (aBookmarked) {
- visitScore += this->GetFrecencyTransitionBonus(
- nsINavHistoryService::TRANSITION_BOOKMARK, true);
- }
- int64_t perVisitScore =
- static_cast<int64_t>(ceilf(static_cast<float>(weight) *
- (static_cast<float>(visitScore) / 100.0f)));
- return aNumVisits * perVisitScore;
- }
-
- if (aBookmarked) {
- // Unvisited bookmark.
- int32_t bookmarkScore = this->GetFrecencyTransitionBonus(
- nsINavHistoryService::TRANSITION_BOOKMARK, true);
- return static_cast<int64_t>(
- ceilf(static_cast<float>(weight) *
- (static_cast<float>(bookmarkScore) / 100.0f)));
- }
- return 0;
+ bool useAlternative =
+ StaticPrefs::places_frecency_pages_alternative_featureGate_AtStartup();
+ int32_t halfLifeDays =
+ (useAlternative
+ ? StaticPrefs::
+ places_frecency_pages_alternative_halfLifeDays_AtStartup()
+ : StaticPrefs::places_frecency_pages_halfLifeDays_AtStartup());
+ int32_t maxSamples =
+ (useAlternative
+ ? StaticPrefs::
+ places_frecency_pages_alternative_numSampledVisits_AtStartup()
+ : StaticPrefs::places_frecency_pages_numSampledVisits_AtStartup());
+ int32_t highWeight =
+ (useAlternative
+ ? StaticPrefs::
+ places_frecency_pages_alternative_highWeight_AtStartup()
+ : StaticPrefs::places_frecency_pages_highWeight_AtStartup());
+ int32_t mediumWeight =
+ (useAlternative
+ ? StaticPrefs::
+ places_frecency_pages_alternative_mediumWeight_AtStartup()
+ : StaticPrefs::places_frecency_pages_mediumWeight_AtStartup());
+
+ int32_t samplesCount = 0;
+ if (aNumVisits > 0) {
+ // The frecency algorithm only samples a maximum number of visits.
+ samplesCount = std::min(aNumVisits, maxSamples);
+ } else if (aBookmarked) {
+ // An unvisited bookmark is considered a single sample.
+ samplesCount = 1;
+ }
+
+ if (samplesCount == 0) {
+ return 0;
+ }
+
+ PRTime now = PR_Now();
+ int32_t todayInDaysFromEpoch = static_cast<int32_t>(now / USEC_PER_DAY);
+ int32_t refTimeInDaysFromEpoch = todayInDaysFromEpoch - aVisitAgeInDays;
+
+ int32_t visitWeight = aBookmarked ? highWeight : mediumWeight;
+ double lambda = log(2.0) / static_cast<double>(halfLifeDays);
+ double decayedWeight =
+ static_cast<double>(visitWeight) *
+ exp(-lambda *
+ static_cast<double>(todayInDaysFromEpoch - refTimeInDaysFromEpoch));
+
+ // Note: Since all samples have equal weight in this simplified version,
+ // we can use decayedWeight directly instead of computing the average.
+ double logCountAdjustedScore =
+ log(decayedWeight * std::max(samplesCount, aNumVisits));
+ // The future date when the score would decay to a value of 1.
+ int32_t frecency = refTimeInDaysFromEpoch +
+ static_cast<int32_t>(logCountAdjustedScore / lambda);
+
+ return static_cast<int64_t>(std::max(frecency, 0));
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/toolkit/components/places/nsPlacesTriggers.h b/toolkit/components/places/nsPlacesTriggers.h
@@ -353,8 +353,8 @@
"AND NEW.key_presses >= %d) " \
"BEGIN " \
"UPDATE moz_places " \
- "SET recalc_alt_frecency = 1 " \
- "WHERE id = NEW.place_id; " \
+ "SET recalc_frecency = 1, recalc_alt_frecency = 1 " \
+ "WHERE id = NEW.place_id AND frecency > 0; " \
"END", \
TOTAL_VIEW_TIME, TOTAL_VIEW_TIME_IF_MANY_KEYPRESSES, MANY_KEY_PRESSES)
@@ -374,8 +374,8 @@
" AND OLD.key_presses < %d AND NEW.key_presses >= %d) " \
"BEGIN " \
"UPDATE moz_places " \
- "SET recalc_alt_frecency = 1 " \
- "WHERE id = NEW.place_id; " \
+ "SET recalc_frecency = 1, recalc_alt_frecency = 1 " \
+ "WHERE id = NEW.place_id AND frecency > 0; " \
"END", \
TOTAL_VIEW_TIME, TOTAL_VIEW_TIME, TOTAL_VIEW_TIME_IF_MANY_KEYPRESSES, \
TOTAL_VIEW_TIME_IF_MANY_KEYPRESSES, MANY_KEY_PRESSES, \
diff --git a/toolkit/components/places/tests/migration/places_v83.sqlite b/toolkit/components/places/tests/migration/places_v83.sqlite
Binary files differ.
diff --git a/toolkit/components/places/tests/migration/test_current_from_v82.js b/toolkit/components/places/tests/migration/test_current_from_v82.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function setup() {
+ let path = await setupPlacesDatabase("places_v82.sqlite");
+ let db = await Sqlite.openConnection({ path });
+
+ await db.execute(`
+ INSERT INTO moz_places (url, guid, url_hash, frecency, recalc_frecency)
+ VALUES
+ -- Zero frecency
+ ('https://example1.com', '___________1', '123456', 0, 0),
+ -- Positive frecency
+ ('https://example2.com', '___________2', '123456', 1, 0),
+ -- Negative frecency
+ ('https://example3.com', '___________3', '123456', -1, 0)
+ `);
+
+ let rows = await db.execute(`
+ SELECT frecency, recalc_frecency
+ FROM moz_places
+ WHERE url_hash = '123456'
+ `);
+
+ Assert.equal(rows.length, 3, "There should be three rows.");
+ for (let row of rows) {
+ Assert.equal(
+ row.getResultByName("recalc_frecency"),
+ 0,
+ "Row should have recalc_frecency equal to 0."
+ );
+ }
+
+ Assert.equal(
+ rows[0].getResultByName("frecency"),
+ 0,
+ "Should have zero frecency."
+ );
+ Assert.greater(
+ rows[1].getResultByName("frecency"),
+ 0,
+ "Should have a frecency greater than 0."
+ );
+ Assert.less(
+ rows[2].getResultByName("frecency"),
+ 0,
+ "Should have a frecency less than 0."
+ );
+
+ await db.close();
+});
+
+add_task(async function database_is_valid() {
+ // trigger migration
+ Assert.equal(
+ PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED
+ );
+
+ const db = await PlacesUtils.promiseDBConnection();
+ Assert.equal(await db.getSchemaVersion(), CURRENT_SCHEMA_VERSION);
+
+ let rows = await db.execute(`
+ SELECT recalc_frecency FROM moz_places
+ WHERE url_hash = '123456'
+ `);
+ Assert.equal(
+ rows[0].getResultByName("recalc_frecency"),
+ 0,
+ "Recalc frecency should still be 0."
+ );
+ Assert.equal(
+ rows[1].getResultByName("recalc_frecency"),
+ 1,
+ "Recalc frecency should be 1."
+ );
+ Assert.equal(
+ rows[2].getResultByName("recalc_frecency"),
+ 0,
+ "Recalc frecency should still be 0."
+ );
+});
diff --git a/toolkit/components/places/tests/migration/xpcshell.toml b/toolkit/components/places/tests/migration/xpcshell.toml
@@ -20,6 +20,7 @@ support-files = [
"places_v79.sqlite",
"places_v81.sqlite",
"places_v82.sqlite",
+ "places_v83.sqlite",
]
["test_current_from_downgraded.js"]
@@ -49,3 +50,5 @@ support-files = [
["test_current_from_v79.js"]
["test_current_from_v81.js"]
+
+["test_current_from_v82.js"]
diff --git a/toolkit/components/places/tests/unit/test_frecency_threshold.js b/toolkit/components/places/tests/unit/test_frecency_threshold.js
@@ -28,11 +28,11 @@ add_task(async function test_basic_invariants() {
"Unvisited bookmark should have a non-zero frecency threshold."
);
- let visitedBookmark = PlacesUtils.history.pageFrecencyThreshold(1, 1, true);
- Assert.greater(
+ let visitedBookmark = PlacesUtils.history.pageFrecencyThreshold(0, 1, true);
+ Assert.equal(
visitedBookmark,
unvisitedBookmark,
- "Visited bookmark should have higher frecency threshold than unvisited bookmark."
+ "Visited bookmark on the same day should have an equal frecency threshold to an unvisited bookmark."
);
let visitedNonBookmark = PlacesUtils.history.pageFrecencyThreshold(