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 }