tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mmodules/libpref/init/StaticPrefList.yaml | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtoolkit/components/places/Database.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mtoolkit/components/places/Database.h | 1+
Mtoolkit/components/places/SQLFunctions.cpp | 391+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mtoolkit/components/places/nsINavHistoryService.idl | 2+-
Mtoolkit/components/places/nsNavHistory.cpp | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mtoolkit/components/places/nsPlacesTriggers.h | 8++++----
Atoolkit/components/places/tests/migration/places_v83.sqlite | 0
Atoolkit/components/places/tests/migration/test_current_from_v82.js | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/places/tests/migration/xpcshell.toml | 3+++
Mtoolkit/components/places/tests/unit/test_frecency_threshold.js | 6+++---
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(