/* 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 | 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; } }