Loading browser/app/profile/firefox.js +10 −0 Original line number Diff line number Diff line Loading @@ -2652,3 +2652,13 @@ pref("svg.context-properties.content.allowed-domains", "profile.accounts.firefox #ifdef NIGHTLY_BUILD pref("extensions.translations.disabled", true); #endif // A set of scores for rating the relevancy of snapshots. The suffixes after the // last decimal are prefixed by `_score` and reference the functions called in // SnapshotScorer. pref("browser.snapshots.score.Visit", 1); pref("browser.snapshots.score.CurrentSession", 1); pref("browser.snapshots.score.InNavigation", 3); pref("browser.snapshots.score.IsOverlappingVisit", 3); pref("browser.snapshots.score.IsUserPersisted", 1); pref("browser.snapshots.score.IsUsedRemoved", -10); browser/components/places/SnapshotScorer.jsm 0 → 100644 +209 −0 Original line number Diff line number Diff line /* 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/. */ const EXPORTED_SYMBOLS = ["SnapshotScorer"]; const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); ChromeUtils.defineModuleGetter( this, "Services", "resource://gre/modules/Services.jsm" ); XPCOMUtils.defineLazyGetter(this, "logConsole", function() { return console.createInstance({ prefix: "SnapshotSelector", maxLogLevel: Services.prefs.getBoolPref( "browser.snapshots.scorer.log", false ) ? "Debug" : "Warn", }); }); /** * The snapshot scorer receives sets of snapshots and scores them based on the * expected relevancy to the user. This order is subsequently used to display * the candidates. */ const SnapshotScorer = new (class SnapshotScorer { /** * @type {Map} * A map of function suffixes to relevancy points. The suffixes are prefixed * with `_score`. Each function will be called in turn to obtain the score * for that item with the result multiplied by the relevancy points. * This map is filled from the `browser.snapshots.score.` preferences. */ #RELEVANCY_POINTS = new Map(); /** * @type {Date|null} * Used to override the current date for tests. */ #dateOverride = null; constructor() { XPCOMUtils.defineLazyPreferenceGetter( this, "snapshotThreshold", "browser.places.snapshots.threshold", 4 ); let branch = Services.prefs.getBranch("browser.snapshots.score."); for (let name of branch.getChildList("")) { this.#RELEVANCY_POINTS.set(name, branch.getIntPref(name, 0)); } } /** * Combines groups of snapshots into one group, and scoring their relevance. * If snapshots are present in multiple groups, the snapshot with the highest * score is used. * A snapshot score must meet the `snapshotThreshold` to be included in the * results. * * @param {Set} currentSessionUrls * A set of urls that are in the current session. * @param {Snapshot[]} snapshotGroups * One or more arrays of snapshot groups to combine. * @returns {Snapshot[]} * The combined snapshot array in descending order of relevancy. */ combineAndScore(currentSessionUrls, ...snapshotGroups) { let combined = new Map(); let currentDate = this.#dateOverride ?? Date.now(); for (let group of snapshotGroups) { for (let snapshot of group) { let existing = combined.get(snapshot.url); let score = this.#score(snapshot, currentDate, currentSessionUrls); logConsole.debug("Scored", score, "for", snapshot.url); if (existing) { if (score > existing.relevancyScore) { snapshot.relevancyScore = score; combined.set(snapshot.url, snapshot); } } else if (score >= this.snapshotThreshold) { snapshot.relevancyScore = score; combined.set(snapshot.url, snapshot); } } } return [...combined.values()].sort( (a, b) => b.relevancyScore - a.relevancyScore ); } /** * Test-only. Overrides the time used in the scoring algorithm with a * specific time which allows for deterministic tests. * * @param {number} date * Epoch time to set the date to. */ overrideCurrentTimeForTests(date) { this.#dateOverride = date; } /** * Scores a snapshot based on its relevancy. * * @param {Snapshot} snapshot * The snapshot to score. * @param {number} currentDate * The current time in milliseconds from the epoch. * @param {Set} currentSessionUrls * The urls of the current session. * @returns {number} * The relevancy score for the snapshot. */ #score(snapshot, currentDate, currentSessionUrls) { let points = 0; for (let [item, value] of this.#RELEVANCY_POINTS.entries()) { let fnName = `_score${item}`; if (!(fnName in this)) { console.error("Could not find function", fnName, "in SnapshotScorer"); continue; } points += this[fnName](snapshot, currentSessionUrls) * value; } let timeAgo = currentDate - snapshot.lastInteractionAt; timeAgo = timeAgo / (24 * 60 * 60 * 1000); return points * Math.exp(timeAgo / -7); } /** * Calculates points based on how many times the snapshot has been visited. * * @param {Snapshot} snapshot * @returns {number} */ _scoreVisit(snapshot) { // Protect against cases where a bookmark was created without a visit. if (snapshot.visitCount == 0) { return 0; } return 2 - 1 / snapshot.visitCount; } /** * Calculates points based on if the snapshot has already been visited in * the current session. * * @param {Snapshot} snapshot * @param {Set} currentSessionUrls * @returns {number} */ _scoreCurrentSession(snapshot, currentSessionUrls) { return currentSessionUrls.has(snapshot.url) ? 1 : 0; } /** * Not currently used. * * @param {Snapshot} snapshot * @returns {number} */ _scoreInNavigation(snapshot) { // In Navigation is not currently implemented. return 0; } /** * Calculates points based on if the snapshot has been visited within a * certain time period of another website. * * @param {Snapshot} snapshot * @returns {number} */ _scoreIsOverlappingVisit(snapshot) { return snapshot.overlappingVisitScore ?? 0; } /** * Calculates points based on if the user persisted the snapshot. * * @param {Snapshot} snapshot * @returns {number} */ _scoreIsUserPersisted(snapshot) { return snapshot.userPersisted ? 1 : 0; } /** * Calculates points based on if the user removed the snapshot. * * @param {Snapshot} snapshot * @returns {number} */ _scoreIsUsedRemoved(snapshot) { return snapshot.removedAt ? 1 : 0; } })(); browser/components/places/SnapshotSelector.jsm +35 −6 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { FilterAdult: "resource://activity-stream/lib/FilterAdult.jsm", Services: "resource://gre/modules/Services.jsm", Snapshots: "resource:///modules/Snapshots.jsm", SnapshotScorer: "resource:///modules/SnapshotScorer.jsm", }); XPCOMUtils.defineLazyGetter(this, "logConsole", function() { Loading Loading @@ -87,6 +88,12 @@ class SnapshotSelector extends EventEmitter { * @type {PageDataCollector.DATA_TYPE | undefined} */ type: undefined, /** * A function that returns a Set containing the urls for the current session. * @type {function} */ getCurrentSessionUrls: undefined, }; /** Loading @@ -95,21 +102,33 @@ class SnapshotSelector extends EventEmitter { #task = null; /** * @param {number} count * @param {object} options * @param {number} [options.count] * The maximum number of snapshots we ever need to generate. This should not * affect the actual snapshots generated and their order but may speed up * calculations. * @param {boolean} filterAdult * @param {boolean} [options.filterAdult] * Whether adult sites should be filtered from the snapshots. * @param {boolean} selectOverlappingVisits * @param {boolean} [options.selectOverlappingVisits] * Whether to select snapshots where visits overlapped the current context url * @param {function} [options.getCurrentSessionUrls] * A function that returns a Set containing the urls for the current session. */ constructor(count = 5, filterAdult = false, selectOverlappingVisits = false) { constructor({ count = 5, filterAdult = false, selectOverlappingVisits = false, getCurrentSessionUrls = () => new Set(), }) { super(); this.#task = new DeferredTask(() => this.#buildSnapshots(), 500); this.#task = new DeferredTask( () => this.#buildSnapshots().catch(console.error), 500 ); this.#context.count = count; this.#context.filterAdult = filterAdult; this.#context.selectOverlappingVisits = selectOverlappingVisits; this.#context.getCurrentSessionUrls = getCurrentSessionUrls; SnapshotSelector.#selectors.add(this); } Loading Loading @@ -212,10 +231,20 @@ class SnapshotSelector extends EventEmitter { return !context.filterAdult || !FilterAdult.isAdultUrl(snapshot.url); }); logConsole.debug( "Found overlapping snapshots:", snapshots.map(s => s.url) ); snapshots = SnapshotScorer.combineAndScore( this.#context.getCurrentSessionUrls(), snapshots ); snapshots = snapshots.slice(0, context.count); logConsole.debug( "Found overlapping snapshots: ", "Reduced final candidates:", snapshots.map(s => s.url) ); Loading browser/components/places/Snapshots.jsm +11 −6 Original line number Diff line number Diff line Loading @@ -114,7 +114,8 @@ XPCOMUtils.defineLazyPreferenceGetter( * Collection of PageData by type. See PageDataService.jsm * @property {Number} overlappingVisitScore * Calculated score based on overlapping visits to the context url. In the range [0.0, 1.0] * @property {number} [relevancyScore] * The relevancy score associated with the snapshot. */ /** Loading Loading @@ -436,7 +437,8 @@ const Snapshots = new (class Snapshots { SELECT h.url AS url, h.title AS title, created_at, removed_at, document_type, first_interaction_at, last_interaction_at, user_persisted, description, site_name, preview_image_url, group_concat('[' || e.type || ', ' || e.data || ']') AS page_data group_concat('[' || e.type || ', ' || e.data || ']') AS page_data, h.visit_count FROM moz_places_metadata_snapshots s JOIN moz_places h ON h.id = s.place_id LEFT JOIN moz_places_metadata_snapshots_extra e ON e.place_id = s.place_id Loading Loading @@ -494,7 +496,8 @@ const Snapshots = new (class Snapshots { SELECT h.url AS url, h.title AS title, created_at, removed_at, document_type, first_interaction_at, last_interaction_at, user_persisted, description, site_name, preview_image_url, group_concat('[' || e.type || ', ' || e.data || ']') AS page_data group_concat('[' || e.type || ', ' || e.data || ']') AS page_data, h.visit_count FROM moz_places_metadata_snapshots s JOIN moz_places h ON h.id = s.place_id LEFT JOIN moz_places_metadata_snapshots_extra e ON e.place_id = s.place_id Loading Loading @@ -557,7 +560,8 @@ const Snapshots = new (class Snapshots { let rows = await db.executeCached( `SELECT h.url AS url, h.title AS title, o.overlappingVisitScore, created_at, removed_at, document_type, first_interaction_at, last_interaction_at, user_persisted, description, site_name, preview_image_url, group_concat(e.data, ",") AS page_data user_persisted, description, site_name, preview_image_url, group_concat(e.data, ",") AS page_data, h.visit_count FROM moz_places_metadata_snapshots s JOIN moz_places h ON h.id = s.place_id JOIN ( SELECT place_id, 1.0 AS overlappingVisitScore FROM Loading Loading @@ -665,6 +669,7 @@ const Snapshots = new (class Snapshots { userPersisted: !!row.getResultByName("user_persisted"), overlappingVisitScore, pageData: pageData ?? new Map(), visitCount: row.getResultByName("visit_count"), }; snapshot.commonName = CommonNames.getName(snapshot); Loading browser/components/places/moz.build +1 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ EXTRA_JS_MODULES += [ "InteractionsBlocklist.jsm", "PlacesUIUtils.jsm", "Snapshots.jsm", "SnapshotScorer.jsm", "SnapshotSelector.jsm", ] Loading Loading
browser/app/profile/firefox.js +10 −0 Original line number Diff line number Diff line Loading @@ -2652,3 +2652,13 @@ pref("svg.context-properties.content.allowed-domains", "profile.accounts.firefox #ifdef NIGHTLY_BUILD pref("extensions.translations.disabled", true); #endif // A set of scores for rating the relevancy of snapshots. The suffixes after the // last decimal are prefixed by `_score` and reference the functions called in // SnapshotScorer. pref("browser.snapshots.score.Visit", 1); pref("browser.snapshots.score.CurrentSession", 1); pref("browser.snapshots.score.InNavigation", 3); pref("browser.snapshots.score.IsOverlappingVisit", 3); pref("browser.snapshots.score.IsUserPersisted", 1); pref("browser.snapshots.score.IsUsedRemoved", -10);
browser/components/places/SnapshotScorer.jsm 0 → 100644 +209 −0 Original line number Diff line number Diff line /* 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/. */ const EXPORTED_SYMBOLS = ["SnapshotScorer"]; const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); ChromeUtils.defineModuleGetter( this, "Services", "resource://gre/modules/Services.jsm" ); XPCOMUtils.defineLazyGetter(this, "logConsole", function() { return console.createInstance({ prefix: "SnapshotSelector", maxLogLevel: Services.prefs.getBoolPref( "browser.snapshots.scorer.log", false ) ? "Debug" : "Warn", }); }); /** * The snapshot scorer receives sets of snapshots and scores them based on the * expected relevancy to the user. This order is subsequently used to display * the candidates. */ const SnapshotScorer = new (class SnapshotScorer { /** * @type {Map} * A map of function suffixes to relevancy points. The suffixes are prefixed * with `_score`. Each function will be called in turn to obtain the score * for that item with the result multiplied by the relevancy points. * This map is filled from the `browser.snapshots.score.` preferences. */ #RELEVANCY_POINTS = new Map(); /** * @type {Date|null} * Used to override the current date for tests. */ #dateOverride = null; constructor() { XPCOMUtils.defineLazyPreferenceGetter( this, "snapshotThreshold", "browser.places.snapshots.threshold", 4 ); let branch = Services.prefs.getBranch("browser.snapshots.score."); for (let name of branch.getChildList("")) { this.#RELEVANCY_POINTS.set(name, branch.getIntPref(name, 0)); } } /** * Combines groups of snapshots into one group, and scoring their relevance. * If snapshots are present in multiple groups, the snapshot with the highest * score is used. * A snapshot score must meet the `snapshotThreshold` to be included in the * results. * * @param {Set} currentSessionUrls * A set of urls that are in the current session. * @param {Snapshot[]} snapshotGroups * One or more arrays of snapshot groups to combine. * @returns {Snapshot[]} * The combined snapshot array in descending order of relevancy. */ combineAndScore(currentSessionUrls, ...snapshotGroups) { let combined = new Map(); let currentDate = this.#dateOverride ?? Date.now(); for (let group of snapshotGroups) { for (let snapshot of group) { let existing = combined.get(snapshot.url); let score = this.#score(snapshot, currentDate, currentSessionUrls); logConsole.debug("Scored", score, "for", snapshot.url); if (existing) { if (score > existing.relevancyScore) { snapshot.relevancyScore = score; combined.set(snapshot.url, snapshot); } } else if (score >= this.snapshotThreshold) { snapshot.relevancyScore = score; combined.set(snapshot.url, snapshot); } } } return [...combined.values()].sort( (a, b) => b.relevancyScore - a.relevancyScore ); } /** * Test-only. Overrides the time used in the scoring algorithm with a * specific time which allows for deterministic tests. * * @param {number} date * Epoch time to set the date to. */ overrideCurrentTimeForTests(date) { this.#dateOverride = date; } /** * Scores a snapshot based on its relevancy. * * @param {Snapshot} snapshot * The snapshot to score. * @param {number} currentDate * The current time in milliseconds from the epoch. * @param {Set} currentSessionUrls * The urls of the current session. * @returns {number} * The relevancy score for the snapshot. */ #score(snapshot, currentDate, currentSessionUrls) { let points = 0; for (let [item, value] of this.#RELEVANCY_POINTS.entries()) { let fnName = `_score${item}`; if (!(fnName in this)) { console.error("Could not find function", fnName, "in SnapshotScorer"); continue; } points += this[fnName](snapshot, currentSessionUrls) * value; } let timeAgo = currentDate - snapshot.lastInteractionAt; timeAgo = timeAgo / (24 * 60 * 60 * 1000); return points * Math.exp(timeAgo / -7); } /** * Calculates points based on how many times the snapshot has been visited. * * @param {Snapshot} snapshot * @returns {number} */ _scoreVisit(snapshot) { // Protect against cases where a bookmark was created without a visit. if (snapshot.visitCount == 0) { return 0; } return 2 - 1 / snapshot.visitCount; } /** * Calculates points based on if the snapshot has already been visited in * the current session. * * @param {Snapshot} snapshot * @param {Set} currentSessionUrls * @returns {number} */ _scoreCurrentSession(snapshot, currentSessionUrls) { return currentSessionUrls.has(snapshot.url) ? 1 : 0; } /** * Not currently used. * * @param {Snapshot} snapshot * @returns {number} */ _scoreInNavigation(snapshot) { // In Navigation is not currently implemented. return 0; } /** * Calculates points based on if the snapshot has been visited within a * certain time period of another website. * * @param {Snapshot} snapshot * @returns {number} */ _scoreIsOverlappingVisit(snapshot) { return snapshot.overlappingVisitScore ?? 0; } /** * Calculates points based on if the user persisted the snapshot. * * @param {Snapshot} snapshot * @returns {number} */ _scoreIsUserPersisted(snapshot) { return snapshot.userPersisted ? 1 : 0; } /** * Calculates points based on if the user removed the snapshot. * * @param {Snapshot} snapshot * @returns {number} */ _scoreIsUsedRemoved(snapshot) { return snapshot.removedAt ? 1 : 0; } })();
browser/components/places/SnapshotSelector.jsm +35 −6 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ XPCOMUtils.defineLazyModuleGetters(this, { FilterAdult: "resource://activity-stream/lib/FilterAdult.jsm", Services: "resource://gre/modules/Services.jsm", Snapshots: "resource:///modules/Snapshots.jsm", SnapshotScorer: "resource:///modules/SnapshotScorer.jsm", }); XPCOMUtils.defineLazyGetter(this, "logConsole", function() { Loading Loading @@ -87,6 +88,12 @@ class SnapshotSelector extends EventEmitter { * @type {PageDataCollector.DATA_TYPE | undefined} */ type: undefined, /** * A function that returns a Set containing the urls for the current session. * @type {function} */ getCurrentSessionUrls: undefined, }; /** Loading @@ -95,21 +102,33 @@ class SnapshotSelector extends EventEmitter { #task = null; /** * @param {number} count * @param {object} options * @param {number} [options.count] * The maximum number of snapshots we ever need to generate. This should not * affect the actual snapshots generated and their order but may speed up * calculations. * @param {boolean} filterAdult * @param {boolean} [options.filterAdult] * Whether adult sites should be filtered from the snapshots. * @param {boolean} selectOverlappingVisits * @param {boolean} [options.selectOverlappingVisits] * Whether to select snapshots where visits overlapped the current context url * @param {function} [options.getCurrentSessionUrls] * A function that returns a Set containing the urls for the current session. */ constructor(count = 5, filterAdult = false, selectOverlappingVisits = false) { constructor({ count = 5, filterAdult = false, selectOverlappingVisits = false, getCurrentSessionUrls = () => new Set(), }) { super(); this.#task = new DeferredTask(() => this.#buildSnapshots(), 500); this.#task = new DeferredTask( () => this.#buildSnapshots().catch(console.error), 500 ); this.#context.count = count; this.#context.filterAdult = filterAdult; this.#context.selectOverlappingVisits = selectOverlappingVisits; this.#context.getCurrentSessionUrls = getCurrentSessionUrls; SnapshotSelector.#selectors.add(this); } Loading Loading @@ -212,10 +231,20 @@ class SnapshotSelector extends EventEmitter { return !context.filterAdult || !FilterAdult.isAdultUrl(snapshot.url); }); logConsole.debug( "Found overlapping snapshots:", snapshots.map(s => s.url) ); snapshots = SnapshotScorer.combineAndScore( this.#context.getCurrentSessionUrls(), snapshots ); snapshots = snapshots.slice(0, context.count); logConsole.debug( "Found overlapping snapshots: ", "Reduced final candidates:", snapshots.map(s => s.url) ); Loading
browser/components/places/Snapshots.jsm +11 −6 Original line number Diff line number Diff line Loading @@ -114,7 +114,8 @@ XPCOMUtils.defineLazyPreferenceGetter( * Collection of PageData by type. See PageDataService.jsm * @property {Number} overlappingVisitScore * Calculated score based on overlapping visits to the context url. In the range [0.0, 1.0] * @property {number} [relevancyScore] * The relevancy score associated with the snapshot. */ /** Loading Loading @@ -436,7 +437,8 @@ const Snapshots = new (class Snapshots { SELECT h.url AS url, h.title AS title, created_at, removed_at, document_type, first_interaction_at, last_interaction_at, user_persisted, description, site_name, preview_image_url, group_concat('[' || e.type || ', ' || e.data || ']') AS page_data group_concat('[' || e.type || ', ' || e.data || ']') AS page_data, h.visit_count FROM moz_places_metadata_snapshots s JOIN moz_places h ON h.id = s.place_id LEFT JOIN moz_places_metadata_snapshots_extra e ON e.place_id = s.place_id Loading Loading @@ -494,7 +496,8 @@ const Snapshots = new (class Snapshots { SELECT h.url AS url, h.title AS title, created_at, removed_at, document_type, first_interaction_at, last_interaction_at, user_persisted, description, site_name, preview_image_url, group_concat('[' || e.type || ', ' || e.data || ']') AS page_data group_concat('[' || e.type || ', ' || e.data || ']') AS page_data, h.visit_count FROM moz_places_metadata_snapshots s JOIN moz_places h ON h.id = s.place_id LEFT JOIN moz_places_metadata_snapshots_extra e ON e.place_id = s.place_id Loading Loading @@ -557,7 +560,8 @@ const Snapshots = new (class Snapshots { let rows = await db.executeCached( `SELECT h.url AS url, h.title AS title, o.overlappingVisitScore, created_at, removed_at, document_type, first_interaction_at, last_interaction_at, user_persisted, description, site_name, preview_image_url, group_concat(e.data, ",") AS page_data user_persisted, description, site_name, preview_image_url, group_concat(e.data, ",") AS page_data, h.visit_count FROM moz_places_metadata_snapshots s JOIN moz_places h ON h.id = s.place_id JOIN ( SELECT place_id, 1.0 AS overlappingVisitScore FROM Loading Loading @@ -665,6 +669,7 @@ const Snapshots = new (class Snapshots { userPersisted: !!row.getResultByName("user_persisted"), overlappingVisitScore, pageData: pageData ?? new Map(), visitCount: row.getResultByName("visit_count"), }; snapshot.commonName = CommonNames.getName(snapshot); Loading
browser/components/places/moz.build +1 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ EXTRA_JS_MODULES += [ "InteractionsBlocklist.jsm", "PlacesUIUtils.jsm", "Snapshots.jsm", "SnapshotScorer.jsm", "SnapshotSelector.jsm", ] Loading