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.defineLazyGetter(lazy, "console", () => {
return console.createInstance({
maxLogLevelPref: "browser.translations.logLevel",
prefix: "Translations",
});
});
ChromeUtils.defineESModuleGetters(lazy, {
handleActorMessage:
"chrome://global/content/translations/translations-engine.sys.mjs",
});
/**
* @typedef {import("../translations").LanguagePair} LanguagePair
* @typedef {import("../translations").TranslationsEnginePayload} TranslationsEnginePayload
*/
/**
* Decompresses a blob of zstd-compressed data.
*
* The Translations models are hosted and served from Remote Settings in zstd format.
* They need to be decompressed before they can be loaded into the WASM memory.
*
* @param {Blob} blob
* @param {boolean} isMocked - Whether the TranslationsEngine payload is mocked for testing.
* @returns {ArrayBuffer}
*/
async function decompressFromZstdBlob(blob, isMocked) {
if (isMocked) {
return new ArrayBuffer();
}
/* global DecompressionStream */
const decompressionStream = new DecompressionStream("zstd");
const buffer = await new Response(
blob.stream().pipeThrough(decompressionStream)
).arrayBuffer();
return buffer;
}
/**
* The engine child is responsible for exposing privileged code to the un-privileged
* space the engine runs in.
*/
export class TranslationsEngineChild extends JSProcessActorChild {
/**
* The resolve function for the Promise returned by the
* "TranslationsEngine:ForceShutdown" message.
*
* @type {null | () => {}}
*/
#resolveForceShutdown = null;
#isDestroyed = false;
// eslint-disable-next-line consistent-return
async receiveMessage({ name, data }) {
if (this.#isDestroyed) {
return undefined;
}
switch (name) {
case "TranslationsEngine:StartTranslation": {
const { languagePair, innerWindowId, port } = data;
const message = {
type: "StartTranslation",
languagePair,
innerWindowId,
port,
};
lazy.handleActorMessage(message);
break;
}
case "TranslationsEngine:DiscardTranslations": {
const { innerWindowId } = data;
lazy.handleActorMessage({
type: "DiscardTranslations",
innerWindowId,
});
break;
}
case "TranslationsEngine:ForceShutdown": {
lazy.handleActorMessage({
type: "ForceShutdown",
});
return new Promise(resolve => {
this.#resolveForceShutdown = resolve;
});
}
default: {
console.error("Unknown message received", name);
}
}
}
/**
* @param {object} options
* @param {number?} options.startTime
* @param {string?} options.type
* @param {string} options.message
* @param {number} options.innerWindowId
*/
TE_addProfilerMarker({ startTime, type, message, innerWindowId }) {
ChromeUtils.addProfilerMarker(
type ? `TranslationsEngine ${type}` : "TranslationsEngine",
{ startTime, innerWindowId },
message
);
}
/**
* Pass the message from content that the engines were shut down.
*/
TE_resolveForceShutdown() {
this.#resolveForceShutdown();
this.#resolveForceShutdown = null;
}
/**
* @returns {string}
*/
TE_getLogLevel() {
return Services.prefs.getCharPref("browser.translations.logLevel");
}
/**
* Log messages if "browser.translations.logLevel" is set to "All".
*
* @param {...any} args
*/
TE_log(...args) {
lazy.console.log(...args);
}
/**
* Report an error to the console.
*
* @param {...any} args
*/
TE_logError(...args) {
lazy.console.error(...args);
}
/**
* Reports translation engine performance data to telemetry.
*
* @param {object} data
* @param {string} data.sourceLanguage - The BCP-47 language tag of the source text.
* @param {string} data.targetLanguage - The BCP-47 language tag of the target text.
* @param {number} data.totalInferenceSeconds - Total total seconds spent in active translation inference.
* @param {number} data.totalTranslatedWords - Total total count of words that were translated.
* @param {number} data.totalCompletedRequests - Total total count of completed translation requests.
*/
TE_reportEnginePerformance({
sourceLanguage,
targetLanguage,
totalInferenceSeconds,
totalTranslatedWords,
totalCompletedRequests,
}) {
if (this.#isDestroyed) {
return;
}
this.sendAsyncMessage("TranslationsEngine:ReportEnginePerformance", {
sourceLanguage,
targetLanguage,
totalInferenceSeconds,
totalTranslatedWords,
totalCompletedRequests,
});
}
/**
* @param {LanguagePair} languagePair
*
* @returns {Promise<TranslationsEnginePayload> | undefined}
*/
async TE_requestEnginePayload(languagePair) {
if (this.#isDestroyed) {
return undefined;
}
/** @type {TranslationsEnginePayload} */
const payload = await this.sendQuery(
"TranslationsEngine:RequestEnginePayload",
{
languagePair,
}
);
const wasmPromise = decompressFromZstdBlob(
payload.bergamotWasmBlob,
payload.isMocked
).then(buffer => {
payload.bergamotWasmArrayBuffer = buffer;
payload.bergamotWasmBlob = null;
});
payload.translationModelPayloads = await Promise.all(
payload.translationModelPayloads.map(async modelPayload => {
const entries = Object.values(modelPayload.languageModelFiles);
await Promise.all(
entries.map(async entry => {
entry.buffer = await decompressFromZstdBlob(
entry.blob,
payload.isMocked
);
entry.blob = null;
})
);
return modelPayload;
})
);
await wasmPromise;
return payload;
}
/**
* @param {number} innerWindowId
* @param {"ready" | "error"} status
*/
TE_reportEngineStatus(innerWindowId, status) {
if (this.#isDestroyed) {
return;
}
this.sendAsyncMessage("TranslationsEngine:ReportEngineStatus", {
innerWindowId,
status,
});
}
/**
* No engines are still alive, signal that the process can be destroyed.
*/
TE_destroyEngineProcess() {
if (this.#isDestroyed) {
return;
}
this.sendAsyncMessage("TranslationsEngine:DestroyEngineProcess");
}
didDestroy() {
this.#isDestroyed = true;
}
}