Source code

Revision control

Copy as Markdown

Other Tools

/* 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 lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
const RECIPE_NAME = "personality-provider-recipe";
const MODELS_NAME = "personality-provider-models";
export class PersonalityProvider {
constructor(modelKeys) {
this.modelKeys = modelKeys;
this.onSync = this.onSync.bind(this);
this.setup();
}
setScores(scores) {
this.scores = scores || {};
this.interestConfig = this.scores.interestConfig;
this.interestVector = this.scores.interestVector;
}
get personalityProviderWorker() {
if (this._personalityProviderWorker) {
return this._personalityProviderWorker;
}
this._personalityProviderWorker = new lazy.BasePromiseWorker(
{ type: "module" }
);
return this._personalityProviderWorker;
}
get baseAttachmentsURL() {
// Returning a promise, so we can have an async getter.
return this._getBaseAttachmentsURL();
}
async _getBaseAttachmentsURL() {
if (this._baseAttachmentsURL) {
return this._baseAttachmentsURL;
}
const server = lazy.Utils.SERVER_URL;
const serverInfo = await (
await fetch(`${server}/`, {
credentials: "omit",
})
).json();
const {
capabilities: {
attachments: { base_url },
},
} = serverInfo;
this._baseAttachmentsURL = base_url;
return this._baseAttachmentsURL;
}
setup() {
this.setupSyncAttachment(RECIPE_NAME);
this.setupSyncAttachment(MODELS_NAME);
}
teardown() {
this.teardownSyncAttachment(RECIPE_NAME);
this.teardownSyncAttachment(MODELS_NAME);
if (this._personalityProviderWorker) {
this._personalityProviderWorker.terminate();
}
}
setupSyncAttachment(collection) {
lazy.RemoteSettings(collection).on("sync", this.onSync);
}
teardownSyncAttachment(collection) {
lazy.RemoteSettings(collection).off("sync", this.onSync);
}
onSync(event) {
this.personalityProviderWorker.post("onSync", [event]);
}
/**
* Gets contents of the attachment if it already exists on file,
* and if not attempts to download it.
*/
getAttachment(record) {
return this.personalityProviderWorker.post("getAttachment", [record]);
}
/**
* Returns a Recipe from remote settings to be consumed by a RecipeExecutor.
* A Recipe is a set of instructions on how to processes a RecipeExecutor.
*/
async getRecipe() {
if (!this.recipes || !this.recipes.length) {
const result = await lazy.RemoteSettings(RECIPE_NAME).get();
this.recipes = await Promise.all(
result.map(async record => ({
...(await this.getAttachment(record)),
recordKey: record.key,
}))
);
}
return this.recipes[0];
}
/**
* Grabs a slice of browse history for building a interest vector
*/
async fetchHistory(columns, beginTimeSecs, endTimeSecs) {
let sql = `SELECT url, title, visit_count, frecency, last_visit_date, description
FROM moz_places
WHERE last_visit_date >= ${beginTimeSecs * 1000000}
AND last_visit_date < ${endTimeSecs * 1000000}`;
columns.forEach(requiredColumn => {
sql += ` AND IFNULL(${requiredColumn}, '') <> ''`;
});
sql += " LIMIT 30000";
const { activityStreamProvider } = lazy.NewTabUtils;
const history = await activityStreamProvider.executePlacesQuery(sql, {
columns,
params: {},
});
return history;
}
/**
* Handles setup and metrics of history fetch.
*/
async getHistory() {
let endTimeSecs = new Date().getTime() / 1000;
let beginTimeSecs = endTimeSecs - this.interestConfig.history_limit_secs;
if (
!this.interestConfig ||
!this.interestConfig.history_required_fields ||
!this.interestConfig.history_required_fields.length
) {
return [];
}
let history = await this.fetchHistory(
this.interestConfig.history_required_fields,
beginTimeSecs,
endTimeSecs
);
return history;
}
async setBaseAttachmentsURL() {
await this.personalityProviderWorker.post("setBaseAttachmentsURL", [
await this.baseAttachmentsURL,
]);
}
async setInterestConfig() {
this.interestConfig = this.interestConfig || (await this.getRecipe());
await this.personalityProviderWorker.post("setInterestConfig", [
this.interestConfig,
]);
}
async setInterestVector() {
await this.personalityProviderWorker.post("setInterestVector", [
this.interestVector,
]);
}
async fetchModels() {
const models = await lazy.RemoteSettings(MODELS_NAME).get();
return this.personalityProviderWorker.post("fetchModels", [models]);
}
async generateTaggers() {
await this.personalityProviderWorker.post("generateTaggers", [
this.modelKeys,
]);
}
async generateRecipeExecutor() {
await this.personalityProviderWorker.post("generateRecipeExecutor");
}
async createInterestVector() {
const history = await this.getHistory();
const interestVectorResult = await this.personalityProviderWorker.post(
"createInterestVector",
[history]
);
return interestVectorResult;
}
async init(callback) {
await this.setBaseAttachmentsURL();
await this.setInterestConfig();
if (!this.interestConfig) {
return;
}
// We always generate a recipe executor, no cache used here.
// This is because the result of this is an object with
// functions (taggers) so storing it in cache is not possible.
// Thus we cannot use it to rehydrate anything.
const fetchModelsResult = await this.fetchModels();
// If this fails, log an error and return.
if (!fetchModelsResult.ok) {
return;
}
await this.generateTaggers();
await this.generateRecipeExecutor();
// If we don't have a cached vector, create a new one.
if (!this.interestVector) {
const interestVectorResult = await this.createInterestVector();
// If that failed, log an error and return.
if (!interestVectorResult.ok) {
return;
}
this.interestVector = interestVectorResult.interestVector;
}
// This happens outside the createInterestVector call above,
// because create can be skipped if rehydrating from cache.
// In that case, the interest vector is provided and not created, so we just set it.
await this.setInterestVector();
this.initialized = true;
if (callback) {
callback();
}
}
async calculateItemRelevanceScore(pocketItem) {
if (!this.initialized) {
return pocketItem.item_score || 1;
}
const itemRelevanceScore = await this.personalityProviderWorker.post(
"calculateItemRelevanceScore",
[pocketItem]
);
if (!itemRelevanceScore) {
return -1;
}
const { scorableItem, rankingVector } = itemRelevanceScore;
// Put the results on the item for debugging purposes.
pocketItem.scorableItem = scorableItem;
pocketItem.rankingVector = rankingVector;
return rankingVector.score;
}
/**
* Returns an object holding the personalization scores of this provider instance.
*/
getScores() {
return {
// We cannot return taggers here.
// What we return here goes into persistent cache, and taggers have functions on it.
// If we attempted to save taggers into persistent cache, it would store it to disk,
// and the next time we load it, it would start thowing function is not defined.
interestConfig: this.interestConfig,
interestVector: this.interestVector,
};
}
}