Commit 9b12e17f authored by Mark Banner's avatar Mark Banner
Browse files

Bug 1741868 - Add a scorer for scoring snapshots based on a relevancy score. r=mossop

parent f04b73ba
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -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);
+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;
  }
})();
+35 −6
Original line number Diff line number Diff line
@@ -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() {
@@ -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,
  };

  /**
@@ -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);
  }

@@ -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)
    );

+11 −6
Original line number Diff line number Diff line
@@ -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.
 */

/**
@@ -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
@@ -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
@@ -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
@@ -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);
+1 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ EXTRA_JS_MODULES += [
    "InteractionsBlocklist.jsm",
    "PlacesUIUtils.jsm",
    "Snapshots.jsm",
    "SnapshotScorer.jsm",
    "SnapshotSelector.jsm",
]

Loading