tor-browser

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

commit 13c5045d6b46456c4246d0ea76d9a5f4014435ac
parent ab4eceb2b570842ab371311bf9380f2371c2a838
Author: scottdowne <sdowne@mozilla.com>
Date:   Tue,  6 Jan 2026 21:09:55 +0000

Bug 2008400 - Newtab frecency boosted topsites caching r=home-newtab-reviewers,nbarrett

Differential Revision: https://phabricator.services.mozilla.com/D277791

Diffstat:
Mbrowser/extensions/newtab/lib/FrecencyBoostProvider/FrecencyBoostProvider.mjs | 38++++++++++++++++++++++++++++++++++----
Mbrowser/extensions/newtab/lib/TopSitesFeed.sys.mjs | 18++++++++++++++----
Mbrowser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyDeduping.js | 22++++++++++++++--------
Mbrowser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyRanking.js | 21++++++++++++---------
4 files changed, 74 insertions(+), 25 deletions(-)

diff --git a/browser/extensions/newtab/lib/FrecencyBoostProvider/FrecencyBoostProvider.mjs b/browser/extensions/newtab/lib/FrecencyBoostProvider/FrecencyBoostProvider.mjs @@ -17,6 +17,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + PersistentCache: "resource://newtab/lib/PersistentCache.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", @@ -44,13 +45,17 @@ ChromeUtils.defineLazyGetter(lazy, "pageFrecencyThreshold", () => { return 101; }); +const CACHE_KEY = "frecency_boost_cache"; const RS_FALLBACK_BASE_URL = "https://firefox-settings-attachments.cdn.mozilla.net/"; const SPONSORED_TILE_PARTNER_FREC_BOOST = "frec-boost"; +const DEFAULT_SOV_NUM_ITEMS = 200; export class FrecencyBoostProvider { constructor(frecentCache) { + this.cache = new lazy.PersistentCache(CACHE_KEY, true); this.frecentCache = frecentCache; + this._links = null; this._frecencyBoostedSponsors = new Map(); this._frecencyBoostRS = null; } @@ -192,12 +197,37 @@ export class FrecencyBoostProvider { return candidates; } - async getLinks(numItems) { - if (this._frecencyBoostedSponsors.size === 0) { + async update(numItems = DEFAULT_SOV_NUM_ITEMS) { + if (!this._frecencyBoostedSponsors.size) { await this._importFrecencyBoostedSponsors(); } - const domainList = Array.from(this._frecencyBoostedSponsors.values()); - return this.buildFrecencyBoostedSpocs(domainList, numItems); + let candidates = []; + if (this._frecencyBoostedSponsors.size) { + const domainList = Array.from(this._frecencyBoostedSponsors.values()); + // Find all matches from the sponsor domains, sorted by frecency + candidates = await this.buildFrecencyBoostedSpocs(domainList, numItems); + } + this._links = candidates; + await this.cache.set("links", this._links); + } + + async fetch(numItems) { + if (!this._links) { + this._links = await this.cache.get("links"); + + // If we still have no links we are likely in first startup. + // In that case, we can fire off a background update. + if (!this._links) { + void this.update(numItems); + } + } + + const links = this._links || []; + + // Apply blocking at read time so it’s always current. + return links.filter( + link => !lazy.NewTabUtils.blockedLinks.isBlocked({ url: link.url }) + ); } } diff --git a/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs b/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs @@ -112,7 +112,6 @@ const PREF_SOV_NAME = "sov.name"; const PREF_SOV_AMP_ALLOCATION = "sov.amp.allocation"; const PREF_SOV_FRECENCY_ALLOCATION = "sov.frecency.allocation"; const DEFAULT_SOV_SLOT_COUNT = 3; -const DEFAULT_SOV_NUM_ITEMS = 200; // Search experiment stuff const FILTER_DEFAULT_SEARCH_PREF = "improvesearch.noDefaultSearchTile"; @@ -1389,10 +1388,9 @@ export class TopSitesFeed { this.store.getState().Prefs.values[SHOW_SPONSORED_PREF] ) { const { values } = this.store.getState().Prefs; - const numItems = - values?.trainhopConfig?.sov?.numItems || DEFAULT_SOV_NUM_ITEMS; + const numItems = values?.trainhopConfig?.sov?.numItems; - candidates = await this.frecencyBoostProvider.getLinks(numItems); + candidates = await this.frecencyBoostProvider.fetch(numItems); // If we have a matched set of candidates, // we can check if it's an exposure event. @@ -1404,6 +1402,15 @@ export class TopSitesFeed { } /** + * Updates frecency boosted topsites spocs cache. + */ + async updateFrecencyBoostedSpocs() { + const { values } = this.store.getState().Prefs; + const numItems = values?.trainhopConfig?.sov?.numItems; + await this.frecencyBoostProvider.update(numItems); + } + + /** * Flip exposure event pref, * if the user is in a SOV experiment, * for both control and treatment, @@ -2321,6 +2328,9 @@ export class TopSitesFeed { case at.SYSTEM_TICK: this.refresh({ broadcast: false }); this._contile.periodicUpdate(); + // We don't need to await on this, + // we can let this update in the background. + void this.updateFrecencyBoostedSpocs(); break; // All these actions mean we need new top sites case at.PLACES_HISTORY_CLEARED: diff --git a/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyDeduping.js b/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyDeduping.js @@ -13,7 +13,10 @@ const PREF_SOV_ENABLED = "sov.enabled"; const SHOW_SPONSORED_PREF = "showSponsoredTopSites"; const ROWS_PREF = "topSitesRows"; -function getTopSitesFeedForTest(sandbox, { frecent = [], contile = [] } = {}) { +async function getTopSitesFeedForTest( + sandbox, + { frecent = [], contile = [] } = {} +) { let feed = new TopSitesFeed(); feed.store = { @@ -87,6 +90,9 @@ function getTopSitesFeedForTest(sandbox, { frecent = [], contile = [] } = {}) { DEFAULT_TOP_SITES.length = 0; feed._readContile(); + // Kick off an update so the cache is populated. + await feed.frecencyBoostProvider.update(); + return feed; } @@ -94,7 +100,7 @@ add_task(async function test_dedupeSponsorsAgainstNothing() { let sandbox = sinon.createSandbox(); { info("TopSitesFeed.getLinksWithDefaults - Should return all defaults"); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://default1.com", frecency: 1000 }, { url: "https://default2.com", frecency: 1000 }, @@ -119,7 +125,7 @@ add_task(async function test_dedupeSponsorsAgainstNothing() { "TopSitesFeed.getLinksWithDefaults - " + "Should return a frecency match in the second position" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://default1.com", frecency: 1000 }, { url: "https://default2.com", frecency: 1000 }, @@ -145,7 +151,7 @@ add_task(async function test_dedupeSponsorsAgainstNothing() { "TopSitesFeed.getLinksWithDefaults - " + "Should return a frecency match with path in the second position" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://default1.com", frecency: 1000 }, { url: "https://default2.com", frecency: 1000 }, @@ -175,7 +181,7 @@ add_task(async function test_dedupeSponsorsAgainstTopsites() { "TopSitesFeed.getLinksWithDefaults - " + "Should dedupe against matching topsite" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://default1.com", frecency: 1000 }, { url: "https://default2.com", frecency: 1000 }, @@ -201,7 +207,7 @@ add_task(async function test_dedupeSponsorsAgainstTopsites() { "TopSitesFeed.getLinksWithDefaults - " + "Should dedupe against matching topsite with path" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://default1.com", frecency: 1000 }, { url: "https://default2.com", frecency: 1000 }, @@ -231,7 +237,7 @@ add_task(async function test_dedupeSponsorsAgainstContile() { "TopSitesFeed.getLinksWithDefaults - " + "Should dedupe against matching contile" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://default1.com", frecency: 1000 }, { url: "https://default2.com", frecency: 1000 }, @@ -257,7 +263,7 @@ add_task(async function test_dedupeSponsorsAgainstContile() { "TopSitesFeed.getLinksWithDefaults - " + "Should dedupe against matching contile label" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://default1.com", frecency: 1000 }, { url: "https://default2.com", frecency: 1000 }, diff --git a/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyRanking.js b/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed_frecencyRanking.js @@ -14,7 +14,7 @@ const PREF_SOV_ENABLED = "sov.enabled"; const SHOW_SPONSORED_PREF = "showSponsoredTopSites"; const ROWS_PREF = "topSitesRows"; -function getTopSitesFeedForTest(sandbox, { frecent = [] } = {}) { +async function getTopSitesFeedForTest(sandbox, { frecent = [] } = {}) { let feed = new TopSitesFeed(); feed.store = { @@ -97,6 +97,9 @@ function getTopSitesFeedForTest(sandbox, { frecent = [] } = {}) { .stub(feed.frecencyBoostProvider, "_frecencyBoostedSponsors") .value(frecencyBoostedSponsors); + // Kick off an update so the cache is populated. + await feed.frecencyBoostProvider.update(); + return feed; } @@ -108,7 +111,7 @@ add_task(async function test_frecency_sponsored_topsites() { "TopSitesFeed.fetchFrecencyBoostedSpocs - " + "Should return an empty array with no history" ); - const feed = getTopSitesFeedForTest(sandbox); + const feed = await getTopSitesFeedForTest(sandbox); const frecencyBoostedSpocs = await feed.fetchFrecencyBoostedSpocs(); Assert.equal(frecencyBoostedSpocs.length, 0); @@ -120,7 +123,7 @@ add_task(async function test_frecency_sponsored_topsites() { "TopSitesFeed.fetchFrecencyBoostedSpocs - " + "Should return a single match with the right format" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://domain1.com", @@ -150,7 +153,7 @@ add_task(async function test_frecency_sponsored_topsites() { "TopSitesFeed.fetchFrecencyBoostedSpocs - " + "Should return multiple matches" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://domain1.com", @@ -175,7 +178,7 @@ add_task(async function test_frecency_sponsored_topsites() { "TopSitesFeed.fetchFrecencyBoostedSpocs - " + "Should return a single match with partial url" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://domain1.com/path", @@ -195,7 +198,7 @@ add_task(async function test_frecency_sponsored_topsites() { "TopSitesFeed.fetchFrecencyBoostedSpocs - " + "Should return a single match with a subdomain" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://www.domain1.com", @@ -215,7 +218,7 @@ add_task(async function test_frecency_sponsored_topsites() { "TopSitesFeed.fetchFrecencyBoostedSpocs - " + "Should not return a match with a different subdomain" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://bus.domain1.com", @@ -234,7 +237,7 @@ add_task(async function test_frecency_sponsored_topsites() { "TopSitesFeed.fetchFrecencyBoostedSpocs - " + "Should return a match with the same subdomain" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://sub.domain1.com", @@ -254,7 +257,7 @@ add_task(async function test_frecency_sponsored_topsites() { "TopSitesFeed.fetchFrecencyBoostedSpocs - " + "Should not match a partial domain" ); - const feed = getTopSitesFeedForTest(sandbox, { + const feed = await getTopSitesFeedForTest(sandbox, { frecent: [ { url: "https://domain12.com",