tor-browser

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

MemoriesHistoryScheduler.sys.mjs (8145B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 */
      5 
      6 const lazy = {};
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  setInterval: "resource://gre/modules/Timer.sys.mjs",
      9  clearInterval: "resource://gre/modules/Timer.sys.mjs",
     10  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     11  MemoriesManager:
     12    "moz-src:///browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs",
     13  MemoriesDriftDetector:
     14    "moz-src:///browser/components/aiwindow/models/memories/MemoriesDriftDetector.sys.mjs",
     15  PREF_GENERATE_MEMORIES:
     16    "moz-src:///browser/components/aiwindow/models/memories/MemoriesConstants.sys.mjs",
     17  DRIFT_EVAL_DELTA_COUNT:
     18    "moz-src:///browser/components/aiwindow/models/memories/MemoriesConstants.sys.mjs",
     19  DRIFT_TRIGGER_QUANTILE:
     20    "moz-src:///browser/components/aiwindow/models/memories/MemoriesConstants.sys.mjs",
     21 });
     22 
     23 ChromeUtils.defineLazyGetter(lazy, "console", function () {
     24  return console.createInstance({
     25    prefix: "MemoriesHistoryScheduler",
     26    maxLogLevelPref: "browser.aiwindow.memoriesLogLevel",
     27  });
     28 });
     29 
     30 // Special case - Minimum number of pages before the first time memories run.
     31 const INITIAL_MEMORIES_PAGES_THRESHOLD = 10;
     32 
     33 // Only run if at least this many pages have been visited.
     34 const MEMORIES_SCHEDULER_PAGES_THRESHOLD = 25;
     35 
     36 // Memories history schedule every 6 hours
     37 const MEMORIES_SCHEDULER_INTERVAL_MS = 6 * 60 * 60 * 1000;
     38 
     39 /**
     40 * Schedules periodic generation of browsing history based memories.
     41 *
     42 * This decides based on the #pagesVisited and periodically evaluates history drift metrics.
     43 * Triggers memories generation when drift exceeds a configured threshold.
     44 *
     45 * E.g. Usage: MemoriesHistoryScheduler.maybeInit()
     46 */
     47 export class MemoriesHistoryScheduler {
     48  #pagesVisited = 0;
     49  #intervalHandle = 0;
     50  #destroyed = false;
     51  #running = false;
     52 
     53  /** @type {MemoriesHistoryScheduler | null} */
     54  static #instance = null;
     55 
     56  /**
     57   * Initializes the scheduler if the relevant pref is enabled.
     58   *
     59   * This should be called from startup/feature initialization code.
     60   *
     61   * @returns {MemoriesHistoryScheduler|null}
     62   *          The scheduler instance if initialized, otherwise null.
     63   */
     64  static maybeInit() {
     65    if (!Services.prefs.getBoolPref(lazy.PREF_GENERATE_MEMORIES, false)) {
     66      return null;
     67    }
     68    if (!this.#instance) {
     69      this.#instance = new MemoriesHistoryScheduler();
     70    }
     71 
     72    return this.#instance;
     73  }
     74 
     75  /**
     76   * Creates a new scheduler instance.
     77   *
     78   * The constructor:
     79   * - Starts the periodic interval timer.
     80   * - Subscribes to Places "page-visited" notifications.
     81   */
     82  constructor() {
     83    this.#startInterval();
     84    lazy.PlacesUtils.observers.addListener(
     85      ["page-visited"],
     86      this.#onPageVisited
     87    );
     88    lazy.console.debug("[MemoriesHistoryScheduler] Initialized");
     89  }
     90 
     91  /**
     92   * Starts the interval that periodically evaluates history drift and
     93   * potentially triggers memory generation.
     94   *
     95   * @throws {Error} If an interval is already running.
     96   */
     97  #startInterval() {
     98    if (this.#intervalHandle) {
     99      throw new Error(
    100        "Attempting to start an interval when one already existed"
    101      );
    102    }
    103    this.#intervalHandle = lazy.setInterval(
    104      this.#onInterval,
    105      MEMORIES_SCHEDULER_INTERVAL_MS
    106    );
    107  }
    108 
    109  /**
    110   * Stops the currently running interval, if any.
    111   */
    112  #stopInterval() {
    113    if (this.#intervalHandle) {
    114      lazy.clearInterval(this.#intervalHandle);
    115      this.#intervalHandle = 0;
    116    }
    117  }
    118 
    119  /**
    120   * Places "page-visited" observer callback.
    121   *
    122   * Increments the internal counter of pages visited since the last
    123   * successful memory generation run.
    124   */
    125  #onPageVisited = () => {
    126    this.#pagesVisited++;
    127  };
    128 
    129  /**
    130   * Periodic interval handler.
    131   *
    132   * - Skips if the scheduler is destroyed or already running.
    133   * - Skips if the minimum pages-visited threshold is not met.
    134   * - Computes history drift metrics and decides whether to run memories.
    135   * - Invokes {@link lazy.MemoriesManager.generateMemoriesFromBrowsingHistory}
    136   *   when appropriate.
    137   *
    138   * @private
    139   * @returns {Promise<void>} Resolves once the interval run completes.
    140   */
    141  #onInterval = async () => {
    142    if (this.#destroyed) {
    143      lazy.console.warn(
    144        "[MemoriesHistoryScheduler] Interval fired after destroy; ignoring."
    145      );
    146      return;
    147    }
    148 
    149    if (this.#running) {
    150      lazy.console.debug(
    151        "[MemoriesHistoryScheduler] Skipping run because a previous run is still in progress."
    152      );
    153      return;
    154    }
    155 
    156    this.#running = true;
    157    this.#stopInterval();
    158 
    159    try {
    160      // Detect whether generated history memories were before.
    161      const lastMemoryTs =
    162        (await lazy.MemoriesManager.getLastHistoryMemoryTimestamp()) ?? 0;
    163      const isFirstRun = lastMemoryTs === 0;
    164      const minPagesThreshold = isFirstRun
    165        ? INITIAL_MEMORIES_PAGES_THRESHOLD
    166        : MEMORIES_SCHEDULER_PAGES_THRESHOLD;
    167 
    168      if (this.#pagesVisited < minPagesThreshold) {
    169        lazy.console.debug(
    170          `[MemoriesHistoryScheduler] Not enough pages visited (${this.#pagesVisited}/${minPagesThreshold}); ` +
    171            `skipping analysis. isFirstRun=${isFirstRun}`
    172        );
    173        return;
    174      }
    175 
    176      if (!isFirstRun) {
    177        lazy.console.debug(
    178          "[MemoriesHistoryScheduler] Computing history drift metrics before running memories..."
    179        );
    180 
    181        const { baselineMetrics, deltaMetrics, trigger } =
    182          await lazy.MemoriesDriftDetector.computeHistoryDriftAndTrigger({
    183            triggerQuantile: lazy.DRIFT_TRIGGER_QUANTILE,
    184            evalDeltaCount: lazy.DRIFT_EVAL_DELTA_COUNT,
    185          });
    186 
    187        if (!baselineMetrics.length || !deltaMetrics.length) {
    188          lazy.console.debug(
    189            "[MemoriesHistoryScheduler] Drift metrics incomplete (no baseline or delta); falling back to non-drift scheduling."
    190          );
    191        } else if (!trigger.triggered) {
    192          lazy.console.debug(
    193            "[MemoriesHistoryScheduler] History drift below threshold; skipping memories run for this interval."
    194          );
    195          // Reset pages so we don’t repeatedly attempt with the same data.
    196          this.#pagesVisited = 0;
    197          return;
    198        } else {
    199          lazy.console.debug(
    200            `[MemoriesHistoryScheduler] Drift triggered (jsThreshold=${trigger.jsThreshold.toFixed(4)}, ` +
    201              `surpriseThreshold=${trigger.surpriseThreshold.toFixed(4)}); sessions=${trigger.triggeredSessionIds.join(
    202                ","
    203              )}`
    204          );
    205        }
    206      }
    207 
    208      lazy.console.debug(
    209        `[MemoriesHistoryScheduler] Generating memories from history with ${this.#pagesVisited} new pages`
    210      );
    211      await lazy.MemoriesManager.generateMemoriesFromBrowsingHistory();
    212      this.#pagesVisited = 0;
    213 
    214      lazy.console.debug(
    215        "[MemoriesHistoryScheduler] History memories generation complete."
    216      );
    217    } catch (error) {
    218      lazy.console.error(
    219        "[MemoriesHistoryScheduler] Failed to generate history memories",
    220        error
    221      );
    222    } finally {
    223      if (!this.#destroyed) {
    224        this.#startInterval();
    225      }
    226      this.#running = false;
    227    }
    228  };
    229 
    230  /**
    231   * Cleans up scheduler resources.
    232   *
    233   * Stops the interval, unsubscribes from Places notifications,
    234   * and marks the scheduler as destroyed so future interval ticks
    235   * are ignored.
    236   */
    237  destroy() {
    238    this.#stopInterval();
    239    lazy.PlacesUtils.observers.removeListener(
    240      ["page-visited"],
    241      this.#onPageVisited
    242    );
    243    this.#destroyed = true;
    244    lazy.console.debug("[MemoriesHistoryScheduler] Destroyed");
    245  }
    246 
    247  /**
    248   * Testing helper: set pagesVisited count.
    249   * Not used in production code.
    250   *
    251   * @param {number} count
    252   */
    253  setPagesVisitedForTesting(count) {
    254    this.#pagesVisited = count;
    255  }
    256 
    257  /**
    258   * Testing helper: runs the interval handler once immediately.
    259   * Not used in production code.
    260   */
    261  async runNowForTesting() {
    262    await this.#onInterval();
    263  }
    264 }