зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1824838 - Add proper Wasm SIMD detection to translations; r=nordzilla
Differential Revision: https://phabricator.services.mozilla.com/D174643
This commit is contained in:
Родитель
1e06c8368f
Коммит
d18c74da1a
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче