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:
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",