Bug 1824838 - Add proper Wasm SIMD detection to translations; r=nordzilla

Differential Revision: https://phabricator.services.mozilla.com/D174643
This commit is contained in:
Greg Tatum 2023-04-05 12:24:23 +00:00
Родитель 1e06c8368f
Коммит d18c74da1a
9 изменённых файлов: 221 добавлений и 27 удалений

Просмотреть файл

@ -3788,6 +3788,10 @@ pref("browser.translations.useHTML", false);
// so that the page automatically performs a translation if one is detected as being
// required.
pref("browser.translations.autoTranslate", false);
// Simulate the behavior of using a device that does not support the translations engine.
// Requires restart.
pref("browser.translations.simulateUnsupportedEngine", false);
// When a user cancels this number of authentication dialogs coming from
// a single web page in a row, all following authentication dialogs will

Просмотреть файл

@ -174,10 +174,12 @@ export class AboutTranslationsChild extends JSWindowActorChild {
/**
* Does this device support the translation engine?
* @returns {boolean}
* @returns {Promise<boolean>}
*/
AT_isTranslationEngineSupported() {
return this.#getTranslationsChild().isTranslationsEngineSupported();
return this.#convertToContentPromise(
this.#getTranslationsChild().isTranslationsEngineSupported
);
}
/**

Просмотреть файл

@ -679,10 +679,8 @@ export class TranslationsChild extends JSWindowActorChild {
async maybeOfferTranslation() {
const translationsStart = this.docShell.now();
if (!(await this.isTranslationsEngineSupported())) {
lazy.console.log(
"The translations engine is not supported on this device."
);
const isSupported = await this.isTranslationsEngineSupported;
if (!isSupported) {
return;
}
@ -696,13 +694,18 @@ export class TranslationsChild extends JSWindowActorChild {
}
}
async isTranslationsEngineSupported() {
if (await this.#isTranslationsEngineMocked) {
// A mocked engine is always supported.
return true;
}
// Bergamot requires intgemm support.
return Boolean(WebAssembly.mozIntGemm);
/**
* Lazily initialize this value. It doesn't change after being set.
*
* @type {Promise<boolean>}
*/
get isTranslationsEngineSupported() {
// Delete the getter and set the real value directly onto the TranslationsChild's
// prototype. This value never changes while a browser is open.
delete TranslationsChild.isTranslationsEngineSupported;
return (TranslationsChild.isTranslationsEngineSupported = this.sendQuery(
"Translations:GetIsTranslationsEngineSupported"
));
}
/**

Просмотреть файл

@ -40,6 +40,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
"browser.translations.enable"
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"simulateUnsupportedEnginePref",
"browser.translations.simulateUnsupportedEngine"
);
// Do the slow/safe thing of always verifying the signature when the data is
// loaded from the file system. This restriction could be eased in the future if it
// proves to be a performance problem, and the security risk is acceptable.
@ -117,6 +123,57 @@ export class TranslationsParent extends JSWindowActorParent {
*/
static #mockedLanguageIdConfidence = null;
/**
* @type {null | Promise<boolean>}
*/
static #isTranslationsEngineSupported = null;
/**
* Detect if Wasm SIMD is supported, and cache the value. It's better to check
* for support before downloading large binary blobs to a user who can't even
* use the feature. This function also respects mocks and simulating unsupported
* engines.
*
* @type {Promise<boolean>}
*/
static getIsTranslationsEngineSupported() {
if (lazy.simulateUnsupportedEnginePref) {
// Use the non-lazy console.log so that the user is always informed as to why
// the translations engine is not working.
console.log(
"Translations: The translations engine is disabled through the pref " +
'"browser.translations.simulateUnsupportedEngine".'
);
// The user is manually testing unsupported engines.
return Promise.resolve(false);
}
if (TranslationsParent.#mockedLanguagePairs) {
// A mocked translations engine is always supported.
return Promise.resolve(true);
}
if (TranslationsParent.#isTranslationsEngineSupported === null) {
TranslationsParent.#isTranslationsEngineSupported = detectSimdSupport();
TranslationsParent.#isTranslationsEngineSupported.then(
isSupported => () => {
// Use the non-lazy console.log so that the user is always informed as to why
// the translations engine is not working.
if (!isSupported) {
console.log(
"Translations: The translations engine is not supported on your device as " +
"it does not support Wasm SIMD operations."
);
}
}
);
}
return TranslationsParent.#isTranslationsEngineSupported;
}
async receiveMessage({ name, data }) {
switch (name) {
case "Translations:GetBergamotWasmArrayBuffer": {
@ -134,6 +191,9 @@ export class TranslationsParent extends JSWindowActorParent {
case "Translations:GetIsTranslationsEngineMocked": {
return Boolean(TranslationsParent.#mockedLanguagePairs);
}
case "Translations:GetIsTranslationsEngineSupported": {
return TranslationsParent.getIsTranslationsEngineSupported();
}
case "Translations:GetLanguageTranslationModelFiles": {
const { fromLanguage, toLanguage } = data;
const files = await this.getLanguageTranslationModelFiles(
@ -748,3 +808,24 @@ function bypassSignatureVerificationIfDev(client) {
client.verifySignature = false;
}
}
/**
* WebAssembly modules must be instantiated from a Worker, since it's considered
* unsafe eval.
*/
function detectSimdSupport() {
return new Promise(resolve => {
lazy.console.log("Loading wasm simd detector worker.");
const worker = new Worker(
"chrome://global/content/translations/simd-detect-worker.js"
);
// This should pretty much immediately resolve, so it does not need Firefox shutdown
// detection.
worker.addEventListener("message", ({ data }) => {
resolve(data.isSimdSupported);
worker.terminate();
});
});
}

Просмотреть файл

@ -0,0 +1,42 @@
/* 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/. */
let isSimdSupported = false;
/**
* WebAssembly counts as unsafe eval in privileged contexts, so we have to execute this
* code in a ChromeWorker. The code feature detects SIMD support. The comment above
* the binary code is the .wat version of the .wasm binary.
*/
try {
new WebAssembly.Module(
new Uint8Array(
// ```
// ;; Detect SIMD support.
// ;; Compile by running: wat2wasm --enable-all simd-detect.wat
//
// (module
// (func (result v128)
// i32.const 0
// i8x16.splat
// i8x16.popcnt
// )
// )
// ```
// prettier-ignore
[
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00,
0x01, 0x7b, 0x03, 0x02, 0x01, 0x00, 0x0a, 0x0a, 0x01, 0x08, 0x00, 0x41, 0x00,
0xfd, 0x0f, 0xfd, 0x62, 0x0b
]
)
);
isSimdSupported = true;
} catch (error) {
console.error(`Translations: SIMD not supported`, error);
}
postMessage({ isSimdSupported });

Просмотреть файл

@ -65,20 +65,28 @@ class TranslationsState {
*/
translationsEngine = null;
constructor() {
/**
* @param {boolean} isSupported
*/
constructor(isSupported) {
/**
* Is the engine supported by the device?
* @type {boolean}
*/
this.isTranslationEngineSupported = AT_isTranslationEngineSupported();
this.isTranslationEngineSupported = isSupported;
/**
* Allow code to wait for the engine to be created.
* @type {Promise<void>}
*/
this.languageIdEngineCreated = AT_createLanguageIdEngine();
this.languageIdEngineCreated = isSupported
? AT_createLanguageIdEngine()
: Promise.resolve();
this.supportedLanguages = isSupported
? AT_getSupportedLanguages()
: Promise.resolve([]);
this.supportedLanguages = AT_getSupportedLanguages();
this.ui = new TranslationsUI(this);
this.ui.setup();
}
@ -343,12 +351,13 @@ class TranslationsUI {
* Do the initial setup.
*/
setup() {
this.setupDropdowns();
this.setupTextarea();
if (!this.state.isTranslationEngineSupported) {
this.showInfo("about-translations-no-support");
this.disableUI();
return;
}
this.setupDropdowns();
this.setupTextarea();
}
/**
@ -533,6 +542,12 @@ class TranslationsUI {
});
}
disableUI() {
this.translationFrom.disabled = true;
this.languageFrom.disabled = true;
this.languageTo.disabled = true;
}
/**
* @param {string} message
*/
@ -561,7 +576,9 @@ window.addEventListener("AboutTranslationsChromeToContent", ({ detail }) => {
if (window.translationsState) {
throw new Error("about:translations was already initialized.");
}
window.translationsState = new TranslationsState();
AT_isTranslationEngineSupported().then(isSupported => {
window.translationsState = new TranslationsState(isSupported);
});
document.body.style.visibility = "visible";
break;
}

Просмотреть файл

@ -7,6 +7,7 @@ toolkit.jar:
content/global/translations/fasttext.js (fasttext/fasttext.js)
content/global/translations/fasttext_wasm.js (fasttext/fasttext_wasm.js)
content/global/translations/language-id-engine-worker.js (content/language-id-engine-worker.js)
content/global/translations/simd-detect-worker.js (content/simd-detect-worker.js)
content/global/translations/translations-document.sys.mjs (content/translations-document.sys.mjs)
content/global/translations/translations-engine-worker.js (content/translations-engine-worker.js)
content/global/translations/translations.html (content/translations.html)

Просмотреть файл

@ -600,3 +600,48 @@ add_task(async function test_about_translations_language_identification() {
},
});
});
/**
* Test that the page is properly disabled when the engine isn't supported.
*/
add_task(async function test_about_translations_() {
await openAboutTranslations({
prefs: [["browser.translations.simulateUnsupportedEngine", true]],
runInPage: async ({ selectors }) => {
const { document, window } = content;
info('Checking for the "no support" message.');
await ContentTaskUtils.waitForCondition(
() => document.querySelector(selectors.noSupportMessage),
'Waiting for the "no support" message.'
);
/** @type {HTMLSelectElement} */
const fromSelect = document.querySelector(selectors.fromLanguageSelect);
/** @type {HTMLSelectElement} */
const toSelect = document.querySelector(selectors.toLanguageSelect);
/** @type {HTMLTextAreaElement} */
const translationTextarea = document.querySelector(
selectors.translationTextarea
);
ok(fromSelect.disabled, "The from select is disabled");
ok(toSelect.disabled, "The to select is disabled");
ok(translationTextarea.disabled, "The textarea is disabled");
function checkElementIsVisible(expectVisible, name) {
const expected = expectVisible ? "visible" : "hidden";
const element = document.querySelector(selectors[name]);
ok(Boolean(element), `Element ${name} was found.`);
const { visibility } = window.getComputedStyle(element);
is(
visibility,
expected,
`Element ${name} was not ${expected} but should be.`
);
}
checkElementIsVisible(true, "translationInfo");
},
});
});

Просмотреть файл

@ -69,6 +69,8 @@ async function openAboutTranslations({
translationTextarea: "textarea#translation-from",
translationResult: "#translation-to",
translationResultBlank: "#translation-to-blank",
translationInfo: "#translation-info",
noSupportMessage: "[data-l10n-id='about-translations-no-support']",
};
// Start the tab at a blank page.
@ -78,13 +80,10 @@ async function openAboutTranslations({
true // waitForLoad
);
// Before loading about:translations, handle the mocking of the actor.
if (!languagePairs) {
throw new Error(
"Expected language pairs for mocking the translations engine."
);
if (languagePairs) {
// Before loading about:translations, handle the mocking of the actor.
TranslationsParent.mockLanguagePairs(languagePairs);
}
TranslationsParent.mockLanguagePairs(languagePairs);
TranslationsParent.mockLanguageIdentification(
detectedLanguageLabel ?? "en",
detectedLanguageConfidence ?? "0.5"