Dans ce guide, destiné aux développeurs Web qui souhaitent bénéficier de WebAssembly, vous apprendrez à utiliser Wasm pour externaliser les tâches gourmandes en ressources de processeur avec à l'aide d'un exemple exécuté. Ce guide couvre tous les sujets, des bonnes pratiques le chargement des modules Wasm pour optimiser leur compilation et leur instanciation. Il approfondira le transfert des tâches exigeantes en ressources processeur vers les Web Workers, les décisions d'implémentation auxquelles vous serez confronté, par exemple pour créer le Web Nœud de calcul, et s'il faut le maintenir actif de manière permanente ou le lancer en cas de besoin. La développe l'approche de manière itérative et introduit un modèle de performances à la fois, jusqu’à ce que l’on suggère la meilleure solution au problème.
Hypothèses
Supposons que vous souhaitiez externaliser une tâche nécessitant une utilisation intensive du processeur
WebAssembly (Wasm) pour ses performances proches du format natif. La tâche nécessitant une utilisation intensive du processeur
utilisé à titre d'exemple dans ce guide calcule le factoriel d'un nombre. La
factoriel est le produit d'un nombre
entier et de tous les entiers en dessous. Pour
Par exemple, la factorielle de quatre (écrite sous la forme 4!
) est égale à 24
(c'est-à-dire
4 * 3 * 2 * 1
). Les chiffres deviennent
rapides et élevés. Par exemple, 16!
est
2,004,189,184
Voici un exemple plus réaliste d'une tâche nécessitant une utilisation intensive du processeur :
scanner un code-barres
traçage d'une image matricielle.
Une implémentation itérative performante (plutôt que récursive) d'un factorial()
.
est présentée dans l'exemple de code suivant écrit en C++.
#include <stdint.h>
extern "C" {
// Calculates the factorial of a non-negative integer n.
uint64_t factorial(unsigned int n) {
uint64_t result = 1;
for (unsigned int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
}
Pour la suite de l'article, supposons qu'il existe un module Wasm basé sur la compilation
cette fonction factorial()
avec Emscripten dans un fichier nommé factorial.wasm
en utilisant tous les
bonnes pratiques d'optimisation du code.
Pour un rappel sur
la façon de procéder, consultez
Appeler des fonctions C compilées à partir de JavaScript à l'aide de ccall/cwrap
La commande suivante a été utilisée pour compiler factorial.wasm
en tant que
Wasm autonome.
emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]' --no-entry
En HTML, il existe un élément form
avec un élément input
associé à un élément output
et à un élément Submit
button
Ces éléments sont référencés à partir de JavaScript en fonction de leur nom.
<form>
<label>The factorial of <input type="text" value="12" /></label> is
<output>479001600</output>.
<button type="submit">Calculate</button>
</form>
const input = document.querySelector('input');
const output = document.querySelector('output');
const button = document.querySelector('button');
Chargement, compilation et instanciation du module
Avant de pouvoir utiliser un module Wasm, vous devez le charger. Sur le Web, cela se produit
via le
fetch()
API. Comme vous le savez, votre application Web dépend du module Wasm pour le
une tâche nécessitant une utilisation intensive du processeur, vous devez précharger le fichier Wasm dès que possible. Toi
faites-le avec une
Récupération compatible CORS
dans la section <head>
de votre application.
<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />
En réalité, l'API fetch()
est asynchrone et vous devez await
résultat.
fetch('factorial.wasm');
Ensuite, compilez et instanciez le module Wasm. Il y a des noms
appelées
WebAssembly.compile()
(plus
WebAssembly.compileStreaming()
)
et
WebAssembly.instantiate()
pour ces tâches, mais
WebAssembly.instantiateStreaming()
compile et instancie un module Wasm directement à partir d'un flux
source sous-jacente telle que fetch()
: aucun await
n'est nécessaire. C'est l'approche la plus efficace
et optimisé pour charger du code Wasm. En supposant que le module Wasm exporte un
factorial()
, vous pouvez l'utiliser immédiatement.
const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
const factorial = resultObject.instance.exports.factorial;
button.addEventListener('click', (e) => {
e.preventDefault();
output.textContent = factorial(parseInt(input.value, 10));
});
Déplacer la tâche vers un nœud de calcul Web
Si vous l'exécutez sur le thread principal, avec des tâches vraiment gourmandes en ressources de processeur, vous risquez bloquer toute l'application. Une pratique courante consiste à transférer ces tâches Nœud de calcul.
Restructurer le thread principal
Pour transférer une tâche nécessitant une utilisation intensive des processeurs vers un Web Worker, la première étape consiste à restructurer
l'application. Le thread principal crée maintenant un Worker
. En plus de cela,
ne traite que de l'envoi de l'entrée au Web Worker, puis de la réception des
et de l'afficher.
/* Main thread. */
let worker = null;
// When the button is clicked, submit the input value
// to the Web Worker.
button.addEventListener('click', (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker('worker.js');
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({ integer: parseInt(input.value, 10) });
});
Mauvaise réponse: la tâche s'exécute dans Web Worker, mais le code est trop ancien.
Le composant Web Worker instancie le module Wasm et, à la réception d'un message,
effectue la tâche nécessitant une utilisation intensive du processeur et renvoie le résultat au thread principal.
Le problème de cette approche est que l'instanciation d'un module Wasm avec
WebAssembly.instantiateStreaming()
est une opération asynchrone. Cela signifie
que le code est pour adultes. Dans le pire des cas, le thread principal envoie des données lorsque le
Web Worker n'est pas encore prêt, et il ne reçoit jamais le message.
/* Worker thread. */
// Instantiate the Wasm module.
// 🚫 This code is racy! If a message comes in while
// the promise is still being awaited, it's lost.
const importObject = {};
const resultObject = await WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
const factorial = resultObject.instance.exports.factorial;
// Listen for incoming messages, run the task,
// and post the result.
self.addEventListener('message', (e) => {
const { integer } = e.data;
self.postMessage({ result: factorial(integer) });
});
Mieux: la tâche s'exécute dans Web Worker, mais avec un chargement et une compilation potentiellement redondants.
Une solution de contournement au problème de l'instanciation du module Wasm asynchrone consiste à déplacer le chargement, la compilation et l'instanciation du module Wasm dans l'événement d'audit, mais cela signifierait que ce travail doit avoir lieu message reçu. Grâce à la mise en cache HTTP et au cache HTTP, le cache le bytecode Wasm compilé, ce n'est pas la pire solution, mais il existe une meilleure de la même façon.
En déplaçant le code asynchrone au début de Web Worker et non attendr que la promesse soit tenue, mais plutôt la stocker dans un , le programme passe immédiatement à la partie écouteur d'événements du et aucun message du fil de discussion principal ne sera perdu. Dans l'événement l'écouteur, la promesse peut alors être attendue.
/* Worker thread. */
const importObject = {};
// Instantiate the Wasm module.
// 🚫 If the `Worker` is spun up frequently, the loading
// compiling, and instantiating work will happen every time.
const wasmPromise = WebAssembly.instantiateStreaming(
fetch('factorial.wasm'),
importObject,
);
// Listen for incoming messages
self.addEventListener('message', async (e) => {
const { integer } = e.data;
const resultObject = await wasmPromise;
const factorial = resultObject.instance.exports.factorial;
const result = factorial(integer);
self.postMessage({ result });
});
Bon: la tâche s'exécute dans Web Worker, et ne se charge et n'est compilée qu'une seule fois.
Le résultat de l'analyse
WebAssembly.compileStreaming()
est une promesse qui se résout en
WebAssembly.Module
Cet objet peut être transféré à l'aide d'un câble
postMessage()
Le module Wasm ne peut donc être chargé et compilé qu'une seule fois dans le
(ou d'un autre Web Worker uniquement dédié au chargement et à la compilation),
avant d'être transférées vers le travailleur Web responsable du traitement
tâche. Le code suivant illustre ce flux.
/* Main thread. */
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
let worker = null;
// When the button is clicked, submit the input value
// and the Wasm module to the Web Worker.
button.addEventListener('click', async (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker('worker.js');
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
Du côté de Web Worker, il ne reste plus qu'à extraire le fichier WebAssembly.Module
.
et de l'instancier. Puisque le message avec WebAssembly.Module
n'est pas
en flux continu, le code de Web Worker
WebAssembly.instantiate()
au lieu de la variante instantiateStreaming()
d'avant. La classe instanciée
module est mis en cache dans une variable, de sorte que le travail d'instanciation ne doit avoir lieu
une fois lors de la mise en route
de Web Worker.
/* Worker thread. */
let instance = null;
// Listen for incoming messages
self.addEventListener('message', async (e) => {
// Extract the `WebAssembly.Module` from the message.
const { integer, module } = e.data;
const importObject = {};
// Instantiate the Wasm module that came via `postMessage()`.
instance = instance || (await WebAssembly.instantiate(module, importObject));
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({ result });
});
Parfait: la tâche s'exécute dans un composant Web Worker intégré, et ne se charge et n'est compilée qu'une seule fois.
Même avec la mise en cache HTTP, l'obtention du code (idéalement) Web Worker en cache et
potentiellement atteindre
le réseau est coûteux. Une astuce courante consiste à
Intégrez Web Worker et chargez-le en tant qu'URL blob:
. Pour cela, il faut toujours
un module Wasm compilé pour être transmis à Web Worker pour une instanciation, en tant que
les contextes du nœud de calcul Web et du thread principal sont différents, même s'ils sont
à partir du même fichier source JavaScript.
/* Main thread. */
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
let worker = null;
const blobURL = URL.createObjectURL(
new Blob(
[
`
let instance = null;
self.addEventListener('message', async (e) => {
// Extract the \`WebAssembly.Module\` from the message.
const {integer, module} = e.data;
const importObject = {};
// Instantiate the Wasm module that came via \`postMessage()\`.
instance = instance || await WebAssembly.instantiate(module, importObject);
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({result});
});
`,
],
{ type: 'text/javascript' },
),
);
button.addEventListener('click', async (e) => {
e.preventDefault();
// Create the Web Worker lazily on-demand.
if (!worker) {
worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
}
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
Création de workers Web paresseuse ou impatiente
Jusqu'à présent, tous les exemples de code ont lancé Web Worker à la demande, de manière différée, lorsque l'utilisateur a appuyé sur le bouton. Selon votre application, il peut être judicieux créer le Web Worker de manière plus hâtive, par exemple, lorsque l'application est inactive ou du processus d'amorçage de l'application. Par conséquent, déplacez la création de nœud de calcul Web code en dehors de l'écouteur d'événements du bouton.
const worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
Maintenir le Web Worker à portée de main
Vous vous demandez peut-être si vous devez garder Web Worker en permanence, ou la recréer chaque fois que vous en avez besoin. Les deux approches sont possible et présenter leurs avantages et leurs inconvénients. Par exemple, conserver un serveur Web Travailler en permanence peut augmenter l'espace mémoire utilisé par votre appli gérer les tâches simultanées plus difficile, car vous devez, d'une manière ou d'une autre, mapper les résultats en retour aux requêtes. D'un autre côté, vos données Le code d'amorçage d'un nœud de calcul peut être assez complexe. Il peut donc y avoir si vous en créez un à chaque fois. Heureusement, vous pouvez mesurer avec API User Timing :
Jusqu'à présent, les exemples de code ont conservé un Web worker permanent. Les éléments suivants : crée un nouveau nœud de calcul Web ad hoc si nécessaire. Notez que vous devez pour suivre arrêt du Worker Web Worker vous-même. (L'extrait de code ignore la gestion des erreurs, mais si une action incorrect, assurez-vous de résilier dans tous les cas, qu'il s'agisse d'une réussite ou d'un échec.)
/* Main thread. */
let worker = null;
const modulePromise = WebAssembly.compileStreaming(fetch('factorial.wasm'));
const blobURL = URL.createObjectURL(
new Blob(
[
`
// Caching the instance means you can switch between
// throw-away and permanent Web Worker freely.
let instance = null;
self.addEventListener('message', async (e) => {
// Extract the \`WebAssembly.Module\` from the message.
const {integer, module} = e.data;
const importObject = {};
// Instantiate the Wasm module that came via \`postMessage()\`.
instance = instance || await WebAssembly.instantiate(module, importObject);
const factorial = instance.exports.factorial;
const result = factorial(integer);
self.postMessage({result});
});
`,
],
{ type: 'text/javascript' },
),
);
button.addEventListener('click', async (e) => {
e.preventDefault();
// Terminate a potentially running Web Worker.
if (worker) {
worker.terminate();
}
// Create the Web Worker lazily on-demand.
worker = new Worker(blobURL);
worker.addEventListener('message', (e) => {
worker.terminate();
worker = null;
output.textContent = e.data.result;
});
worker.postMessage({
integer: parseInt(input.value, 10),
module: await modulePromise,
});
});
Démonstrations
Vous avez deux versions de démonstration. Un avec un
nœud de calcul Web ad hoc
(code source)
et l'autre avec
travailleur Web permanent
(code source).
Si vous ouvrez les outils pour les développeurs Chrome et que vous consultez la console, vous pouvez voir
Journaux de l'API Timing qui mesurent le temps écoulé entre le clic sur le bouton et l'événement
résultat affiché à l'écran. L'onglet "Réseau" affiche l'URL blob:
requête(s) en attente. Dans cet exemple, la différence temporelle
entre ad hoc et permanentes
est d’environ 3×. En pratique, à l'œil humain, ces deux-là ne sont pas différenciables
. Il est très probable que les résultats de votre application réelle varieront.
Conclusions
Ce post explore quelques schémas de performances pour la gestion de Wasm.
- En règle générale, préférez les méthodes de traitement par flux
(
WebAssembly.compileStreaming()
etWebAssembly.instantiateStreaming()
) par rapport à leurs homologues qui ne diffusent pas d'annonces en streaming (WebAssembly.compile()
etWebAssembly.instantiate()
). - Si possible, externalisez les tâches gourmandes en performances à un Web Worker et
les tâches de chargement et de compilation ne sont effectuées qu'une seule fois en dehors de Web Worker. De cette façon, le
Il suffit d'instancier le module Wasm qu'il reçoit de la
thread dans lequel le chargement et la compilation ont eu lieu
WebAssembly.instantiate()
, ce qui signifie que l'instance peut être mise en cache si vous qui maintiennent le nœud de calcul Web en permanence à portée de main. - Déterminez avec soin s'il est judicieux de conserver un Web Worker permanent ou créer des Web Workers ad hoc dès qu'ils en ont besoin. Aussi à réfléchir au meilleur moment pour créer le Web Worker. Éléments à prendre en compte la consommation de mémoire, la durée d'instanciation du nœud de calcul Web, mais aussi la complexité de la nécessité de traiter des requêtes simultanées.
Si vous tenez compte de ces tendances, vous êtes sur la bonne voie pour optimiser Performances de Wasm
Remerciements
Ce guide a été examiné par Andreas Haas, Jakob Kummerow, Deepti Gandluri, Alon Zakai, Francis McCabe, François Beaufort et Rachel Andrew.