Backed out changeset 1bfb1774ed03 (bug 1889404) for causing bc failures @ browser_con...
[gecko.git] / toolkit / components / contentrelevancy / ContentRelevancyManager.sys.mjs
blobea3f2a78a2f114f6a241a62253d1ed593d7db94b
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/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   getFrecentRecentCombinedUrls:
11     "resource://gre/modules/contentrelevancy/private/InputUtils.sys.mjs",
12   NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
13   RelevancyStore: "resource://gre/modules/RustRelevancy.sys.mjs",
14 });
16 XPCOMUtils.defineLazyServiceGetter(
17   lazy,
18   "timerManager",
19   "@mozilla.org/updates/timer-manager;1",
20   "nsIUpdateTimerManager"
23 // Constants used by `nsIUpdateTimerManager` for a cross-session timer.
24 const TIMER_ID = "content-relevancy-timer";
25 const PREF_TIMER_LAST_UPDATE = `app.update.lastUpdateTime.${TIMER_ID}`;
26 const PREF_TIMER_INTERVAL = "toolkit.contentRelevancy.timerInterval";
27 // Set the timer interval to 1 day for validation.
28 const DEFAULT_TIMER_INTERVAL_SECONDS = 1 * 24 * 60 * 60;
30 // Default maximum input URLs to fetch from Places.
31 const DEFAULT_MAX_URLS = 100;
32 // Default minimal input URLs for clasification.
33 const DEFAULT_MIN_URLS = 0;
35 // File name of the relevancy database
36 const RELEVANCY_STORE_FILENAME = "content-relevancy.sqlite";
38 // Nimbus variables
39 const NIMBUS_VARIABLE_ENABLED = "enabled";
40 const NIMBUS_VARIABLE_MAX_INPUT_URLS = "maxInputUrls";
41 const NIMBUS_VARIABLE_MIN_INPUT_URLS = "minInputUrls";
42 const NIMBUS_VARIABLE_TIMER_INTERVAL = "timerInterval";
44 ChromeUtils.defineLazyGetter(lazy, "log", () => {
45   return console.createInstance({
46     prefix: "ContentRelevancyManager",
47     maxLogLevel: Services.prefs.getBoolPref(
48       "toolkit.contentRelevancy.log",
49       false
50     )
51       ? "Debug"
52       : "Error",
53   });
54 });
56 class RelevancyManager {
57   get initialized() {
58     return this.#initialized;
59   }
61   /**
62    * Init the manager. An update timer is registered if the feature is enabled.
63    * The pref observer is always set so we can toggle the feature without restarting
64    * the browser.
65    *
66    * Note that this should be called once only. `#enable` and `#disable` can be
67    * used to toggle the feature once the manager is initialized.
68    */
69   async init() {
70     if (this.initialized) {
71       return;
72     }
74     lazy.log.info("Initializing the manager");
76     if (this.shouldEnable) {
77       await this.#enable();
78     }
80     this._nimbusUpdateCallback = this.#onNimbusUpdate.bind(this);
81     // This will handle both Nimbus updates and pref changes.
82     lazy.NimbusFeatures.contentRelevancy.onUpdate(this._nimbusUpdateCallback);
83     this.#initialized = true;
84   }
86   uninit() {
87     if (!this.initialized) {
88       return;
89     }
91     lazy.log.info("Uninitializing the manager");
93     lazy.NimbusFeatures.contentRelevancy.offUpdate(this._nimbusUpdateCallback);
94     this.#disable();
96     this.#initialized = false;
97   }
99   /**
100    * Determine whether the feature should be enabled based on prefs and Nimbus.
101    */
102   get shouldEnable() {
103     return (
104       lazy.NimbusFeatures.contentRelevancy.getVariable(
105         NIMBUS_VARIABLE_ENABLED
106       ) ?? false
107     );
108   }
110   #startUpTimer() {
111     // Log the last timer tick for debugging.
112     const lastTick = Services.prefs.getIntPref(PREF_TIMER_LAST_UPDATE, 0);
113     if (lastTick) {
114       lazy.log.debug(
115         `Last timer tick: ${lastTick}s (${
116           Math.round(Date.now() / 1000) - lastTick
117         })s ago`
118       );
119     } else {
120       lazy.log.debug("Last timer tick: none");
121     }
123     const interval =
124       lazy.NimbusFeatures.contentRelevancy.getVariable(
125         NIMBUS_VARIABLE_TIMER_INTERVAL
126       ) ??
127       Services.prefs.getIntPref(
128         PREF_TIMER_INTERVAL,
129         DEFAULT_TIMER_INTERVAL_SECONDS
130       );
131     lazy.timerManager.registerTimer(
132       TIMER_ID,
133       this,
134       interval,
135       interval != 0 // Do not skip the first timer tick for a zero interval for testing
136     );
137   }
139   get #storePath() {
140     return PathUtils.join(
141       Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
142       RELEVANCY_STORE_FILENAME
143     );
144   }
146   async #enable() {
147     if (!this.#_store) {
148       // Init the relevancy store.
149       const path = this.#storePath;
150       lazy.log.info(`Initializing RelevancyStore: ${path}`);
152       try {
153         this.#_store = await lazy.RelevancyStore.init(path);
154       } catch (error) {
155         lazy.log.error(`Error initializing RelevancyStore: ${error}`);
156         return;
157       }
158     }
160     this.#startUpTimer();
161   }
163   /**
164    * The reciprocal of `#enable()`, ensure this is safe to call when you add
165    * new disabling code here. It should be so even if `#enable()` hasn't been
166    * called.
167    */
168   #disable() {
169     this.#_store = null;
170     lazy.timerManager.unregisterTimer(TIMER_ID);
171   }
173   async #toggleFeature() {
174     if (this.shouldEnable) {
175       await this.#enable();
176     } else {
177       this.#disable();
178     }
179   }
181   /**
182    * nsITimerCallback
183    */
184   notify() {
185     lazy.log.info("Background job timer fired");
186     this.#doClassification();
187   }
189   get isInProgress() {
190     return this.#isInProgress;
191   }
193   /**
194    * Perform classification based on browsing history.
195    *
196    * It will fetch up to `DEFAULT_MAX_URLS` (or the corresponding Nimbus value)
197    * URLs from top frecent URLs and use most recent URLs as a fallback if the
198    * former is insufficient. The returned URLs might be fewer than requested.
199    *
200    * The classification will not be performed if the total number of input URLs
201    * is less than `DEFAULT_MIN_URLS` (or the corresponding Nimbus value).
202    */
203   async #doClassification() {
204     if (this.isInProgress) {
205       lazy.log.info(
206         "Another classification is in progress, aborting interest classification"
207       );
208       return;
209     }
211     // Set a flag indicating this classification. Ensure it's cleared upon early
212     // exit points & success.
213     this.#isInProgress = true;
215     try {
216       lazy.log.info("Fetching input data for interest classification");
218       const maxUrls =
219         lazy.NimbusFeatures.contentRelevancy.getVariable(
220           NIMBUS_VARIABLE_MAX_INPUT_URLS
221         ) ?? DEFAULT_MAX_URLS;
222       const minUrls =
223         lazy.NimbusFeatures.contentRelevancy.getVariable(
224           NIMBUS_VARIABLE_MIN_INPUT_URLS
225         ) ?? DEFAULT_MIN_URLS;
226       const urls = await lazy.getFrecentRecentCombinedUrls(maxUrls);
227       if (urls.length < minUrls) {
228         lazy.log.info("Aborting interest classification: insufficient input");
229         return;
230       }
232       lazy.log.info("Starting interest classification");
233       await this.#doClassificationHelper(urls);
234     } catch (error) {
235       if (error instanceof StoreNotAvailableError) {
236         lazy.log.error("#store became null, aborting interest classification");
237       } else {
238         lazy.log.error("Classification error: " + (error.reason ?? error));
239       }
240     } finally {
241       this.#isInProgress = false;
242     }
244     lazy.log.info("Finished interest classification");
245   }
247   /**
248    * Classification helper. Use the getter `this.#store` rather than `#_store`
249    * to access the store so that when it becomes null, a `StoreNotAvailableError`
250    * will be raised. Likewise, other store related errors should be propagated
251    * to the caller if you want to perform custom error handling in this helper.
252    *
253    * @param {Array} urls
254    *   An array of URLs.
255    * @throws {StoreNotAvailableError}
256    *   Thrown when the store became unavailable (i.e. set to null elsewhere).
257    * @throws {RelevancyAPIError}
258    *   Thrown for other API errors on the store.
259    */
260   async #doClassificationHelper(urls) {
261     // The following logs are unnecessary, only used to suppress the linting error.
262     // TODO(nanj): delete me once the following TODO is done.
263     if (!this.#store) {
264       lazy.log.error("#store became null, aborting interest classification");
265     }
266     lazy.log.info("Classification input: " + urls);
268     // TODO(nanj): uncomment the following once `ingest()` is implemented.
269     // await this.#store.ingest(urls);
270   }
272   /**
273    * Exposed for testing.
274    */
275   async _test_doClassification(urls) {
276     await this.#doClassificationHelper(urls);
277   }
279   /**
280    * Internal getter for `#_store` used by for classification. It will throw
281    * a `StoreNotAvailableError` is the store is not ready.
282    */
283   get #store() {
284     if (!this._isStoreReady) {
285       throw new StoreNotAvailableError("Store is not available");
286     }
288     return this.#_store;
289   }
291   /**
292    * Whether or not the store is ready (i.e. not null).
293    */
294   get _isStoreReady() {
295     return !!this.#_store;
296   }
298   /**
299    * Nimbus update listener.
300    */
301   #onNimbusUpdate(_event, _reason) {
302     this.#toggleFeature();
303   }
305   // The `RustRelevancy` store.
306   #_store;
308   // Whether or not the module is initialized.
309   #initialized = false;
311   // Whether or not there is an in-progress classification. Used to prevent
312   // duplicate classification tasks.
313   #isInProgress = false;
317  * Error raised when attempting to access a null store.
318  */
319 class StoreNotAvailableError extends Error {
320   constructor(message, ...params) {
321     super(message, ...params);
322     this.name = "StoreNotAvailableError";
323   }
326 export var ContentRelevancyManager = new RelevancyManager();