PersonalityProvider.sys.mjs (7511B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs", 9 NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", 10 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", 11 Utils: "resource://services-settings/Utils.sys.mjs", 12 }); 13 14 const RECIPE_NAME = "personality-provider-recipe"; 15 const MODELS_NAME = "personality-provider-models"; 16 17 export class PersonalityProvider { 18 constructor(modelKeys) { 19 this.modelKeys = modelKeys; 20 this.onSync = this.onSync.bind(this); 21 this.setup(); 22 } 23 24 setScores(scores) { 25 this.scores = scores || {}; 26 this.interestConfig = this.scores.interestConfig; 27 this.interestVector = this.scores.interestVector; 28 } 29 30 get personalityProviderWorker() { 31 if (this._personalityProviderWorker) { 32 return this._personalityProviderWorker; 33 } 34 35 this._personalityProviderWorker = new lazy.BasePromiseWorker( 36 "resource://newtab/lib/PersonalityProvider/PersonalityProvider.worker.mjs", 37 { type: "module" } 38 ); 39 40 return this._personalityProviderWorker; 41 } 42 43 setup() { 44 this.setupSyncAttachment(RECIPE_NAME); 45 this.setupSyncAttachment(MODELS_NAME); 46 } 47 48 teardown() { 49 this.teardownSyncAttachment(RECIPE_NAME); 50 this.teardownSyncAttachment(MODELS_NAME); 51 if (this._personalityProviderWorker) { 52 this._personalityProviderWorker.terminate(); 53 } 54 } 55 56 setupSyncAttachment(collection) { 57 lazy.RemoteSettings(collection).on("sync", this.onSync); 58 } 59 60 teardownSyncAttachment(collection) { 61 lazy.RemoteSettings(collection).off("sync", this.onSync); 62 } 63 64 onSync(event) { 65 this.personalityProviderWorker.post("onSync", [event]); 66 } 67 68 /** 69 * Gets contents of the attachment if it already exists on file, 70 * and if not attempts to download it. 71 */ 72 getAttachment(record) { 73 return this.personalityProviderWorker.post("getAttachment", [record]); 74 } 75 76 /** 77 * Returns a Recipe from remote settings to be consumed by a RecipeExecutor. 78 * A Recipe is a set of instructions on how to processes a RecipeExecutor. 79 */ 80 async getRecipe() { 81 if (!this.recipes || !this.recipes.length) { 82 const result = await lazy.RemoteSettings(RECIPE_NAME).get(); 83 this.recipes = await Promise.all( 84 result.map(async record => ({ 85 ...(await this.getAttachment(record)), 86 recordKey: record.key, 87 })) 88 ); 89 } 90 return this.recipes[0]; 91 } 92 93 /** 94 * Grabs a slice of browse history for building a interest vector 95 */ 96 async fetchHistory(columns, beginTimeSecs, endTimeSecs) { 97 let sql = `SELECT url, title, visit_count, frecency, last_visit_date, description 98 FROM moz_places 99 WHERE last_visit_date >= ${beginTimeSecs * 1000000} 100 AND last_visit_date < ${endTimeSecs * 1000000}`; 101 columns.forEach(requiredColumn => { 102 sql += ` AND IFNULL(${requiredColumn}, '') <> ''`; 103 }); 104 sql += " LIMIT 30000"; 105 106 const { activityStreamProvider } = lazy.NewTabUtils; 107 const history = await activityStreamProvider.executePlacesQuery(sql, { 108 columns, 109 params: {}, 110 }); 111 112 return history; 113 } 114 115 /** 116 * Handles setup and metrics of history fetch. 117 */ 118 async getHistory() { 119 let endTimeSecs = new Date().getTime() / 1000; 120 let beginTimeSecs = endTimeSecs - this.interestConfig.history_limit_secs; 121 if ( 122 !this.interestConfig || 123 !this.interestConfig.history_required_fields || 124 !this.interestConfig.history_required_fields.length 125 ) { 126 return []; 127 } 128 let history = await this.fetchHistory( 129 this.interestConfig.history_required_fields, 130 beginTimeSecs, 131 endTimeSecs 132 ); 133 134 return history; 135 } 136 137 async setBaseAttachmentsURL() { 138 await this.personalityProviderWorker.post("setBaseAttachmentsURL", [ 139 await lazy.Utils.baseAttachmentsURL(), 140 ]); 141 } 142 143 async setInterestConfig() { 144 this.interestConfig = this.interestConfig || (await this.getRecipe()); 145 await this.personalityProviderWorker.post("setInterestConfig", [ 146 this.interestConfig, 147 ]); 148 } 149 150 async setInterestVector() { 151 await this.personalityProviderWorker.post("setInterestVector", [ 152 this.interestVector, 153 ]); 154 } 155 156 async fetchModels() { 157 const models = await lazy.RemoteSettings(MODELS_NAME).get(); 158 return this.personalityProviderWorker.post("fetchModels", [models]); 159 } 160 161 async generateTaggers() { 162 await this.personalityProviderWorker.post("generateTaggers", [ 163 this.modelKeys, 164 ]); 165 } 166 167 async generateRecipeExecutor() { 168 await this.personalityProviderWorker.post("generateRecipeExecutor"); 169 } 170 171 async createInterestVector() { 172 const history = await this.getHistory(); 173 174 const interestVectorResult = await this.personalityProviderWorker.post( 175 "createInterestVector", 176 [history] 177 ); 178 179 return interestVectorResult; 180 } 181 182 async init(callback) { 183 await this.setBaseAttachmentsURL(); 184 await this.setInterestConfig(); 185 if (!this.interestConfig) { 186 return; 187 } 188 189 // We always generate a recipe executor, no cache used here. 190 // This is because the result of this is an object with 191 // functions (taggers) so storing it in cache is not possible. 192 // Thus we cannot use it to rehydrate anything. 193 const fetchModelsResult = await this.fetchModels(); 194 // If this fails, log an error and return. 195 if (!fetchModelsResult.ok) { 196 return; 197 } 198 await this.generateTaggers(); 199 await this.generateRecipeExecutor(); 200 201 // If we don't have a cached vector, create a new one. 202 if (!this.interestVector) { 203 const interestVectorResult = await this.createInterestVector(); 204 // If that failed, log an error and return. 205 if (!interestVectorResult.ok) { 206 return; 207 } 208 this.interestVector = interestVectorResult.interestVector; 209 } 210 211 // This happens outside the createInterestVector call above, 212 // because create can be skipped if rehydrating from cache. 213 // In that case, the interest vector is provided and not created, so we just set it. 214 await this.setInterestVector(); 215 216 this.initialized = true; 217 if (callback) { 218 callback(); 219 } 220 } 221 222 async calculateItemRelevanceScore(pocketItem) { 223 if (!this.initialized) { 224 return pocketItem.item_score || 1; 225 } 226 const itemRelevanceScore = await this.personalityProviderWorker.post( 227 "calculateItemRelevanceScore", 228 [pocketItem] 229 ); 230 if (!itemRelevanceScore) { 231 return -1; 232 } 233 const { scorableItem, rankingVector } = itemRelevanceScore; 234 // Put the results on the item for debugging purposes. 235 pocketItem.scorableItem = scorableItem; 236 pocketItem.rankingVector = rankingVector; 237 return rankingVector.score; 238 } 239 240 /** 241 * Returns an object holding the personalization scores of this provider instance. 242 */ 243 getScores() { 244 return { 245 // We cannot return taggers here. 246 // What we return here goes into persistent cache, and taggers have functions on it. 247 // If we attempted to save taggers into persistent cache, it would store it to disk, 248 // and the next time we load it, it would start thowing function is not defined. 249 interestConfig: this.interestConfig, 250 interestVector: this.interestVector, 251 }; 252 } 253 }