En esta guía, dirigida a desarrolladores web que quieren beneficiarse de WebAssembly, Aprenderás a usar Wasm para externalizar tareas que consumen mucha CPU con el ayuda de un ejemplo de carrera. La guía abarca todo, desde las prácticas recomendadas cargando módulos de Wasm para optimizar su compilación y creación de instancias. Integra analiza en más detalle cómo pasar las tareas que consumen mucha CPU a Web Workers y analiza de implementación a las que te enfrentarás, como cuándo crear la Web Worker y si quieres mantenerlo activo de forma permanente o iniciarlo cuando sea necesario. El desarrolla de forma iterativa el enfoque y presenta un patrón de rendimiento hasta que se sugiera la mejor solución al problema.
Suposiciones
Supongamos que tienes una tarea con un uso intensivo de CPU que quieres subcontratar
WebAssembly (Wasm) por su rendimiento casi nativo. La tarea con uso intensivo de CPU
usado como ejemplo en esta guía calcula el factorial de un número. El
factorial es el producto de un número entero y todos los números enteros debajo de este. Para
Por ejemplo, el factorial de cuatro (escrito como 4!
) es igual a 24
(es decir,
4 * 3 * 2 * 1
). Las cifras aumentan rápidamente. Por ejemplo, 16!
es
2,004,189,184
Un ejemplo más realista de una tarea con uso intensivo de CPU podría ser
escanear un código de barras
el seguimiento de una imagen de trama.
Una implementación iterativa y eficaz (en lugar de recursiva) de un factorial()
función se muestra en la siguiente muestra de código escrita 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;
}
}
Para el resto del artículo, imagina que hay un módulo de Wasm basado en la compilación
esta función factorial()
con Emscripten en un archivo llamado factorial.wasm
utilizando todas
prácticas recomendadas para la optimización de código.
Para repasar cómo hacerlo, lee
Llama a funciones C compiladas desde JavaScript con ccall/cwrap.
Se usó el siguiente comando para compilar factorial.wasm
como
Wasm independiente.
emcc -O3 factorial.cpp -o factorial.wasm -s WASM_BIGINT -s EXPORTED_FUNCTIONS='["_factorial"]' --no-entry
En HTML, hay un form
con un input
vinculado con un output
y un envío
button
JavaScript hace referencia a estos elementos en función de sus nombres.
<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');
Carga, compilación y creación de instancias del módulo
Antes de que puedas usar un módulo de Wasm, debes cargarlo. Esto sucede en la Web
mediante la
fetch()
en la API de Cloud. Como sabes, tu app web depende del módulo de Wasm para el
con mucha CPU, debes cargar previamente el archivo de Wasm lo antes posible. Tú
haz esto con un
Recuperación habilitada para CORS
en la sección <head>
de la app.
<link rel="preload" as="fetch" href="factorial.wasm" crossorigin />
En realidad, la API de fetch()
es asíncrona, por lo que debes aplicar await
al
resultado.
fetch('factorial.wasm');
A continuación, compila el módulo de Wasm y crea una instancia de él. Hay nombres tentadores
funciones llamadas
WebAssembly.compile()
(más
WebAssembly.compileStreaming()
)
y
WebAssembly.instantiate()
para estas tareas, pero, en cambio,
WebAssembly.instantiateStreaming()
compila y crea una instancia de un módulo de Wasm directamente desde una red
fuente subyacente como fetch()
, no se necesita await
. Esta es la solución más eficiente
y optimizada de cargar el código de Wasm. Suponiendo que el módulo de Wasm exporta un
factorial()
, puedes usarla de inmediato.
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));
});
Traslada la tarea a Web Worker
Si lo ejecutas en el subproceso principal, con tareas que hacen un uso intensivo de la CPU, corres el riesgo bloqueando toda la app. Una práctica común es trasladar estas tareas a un Trabajador.
Reestructuración del subproceso principal
Para trasladar la tarea que consume mucha CPU a un Web Worker, el primer paso es reestructurar
la aplicación. El subproceso principal ahora crea un Worker
y, además de eso,
solo se encarga de enviar la entrada al Web Worker y, luego, recibir la
salida y la muestra.
/* 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) });
});
Mala: La tarea se ejecuta en Web Worker, pero el código es subido de tono
El trabajador web crea una instancia del módulo de Wasm y, al recibir un mensaje,
realiza la tarea con uso intensivo de CPU y envía el resultado al subproceso principal.
El problema de este enfoque es que crear una instancia de un módulo de Wasm con
WebAssembly.instantiateStreaming()
es una operación asíncrona. Esto significa
de que el código es subido de tono. En el peor de los casos, el subproceso principal envía datos cuando
El trabajador web aún no está listo y nunca recibe el mensaje.
/* 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) });
});
Mejor: La tarea se ejecuta en Web Worker, pero posiblemente con cargas y compilaciones redundantes
Una solución al problema de la creación asíncrona de instancias del módulo Wasm es mover la carga, la compilación y la creación de instancias del módulo de Wasm al evento oyente, pero esto significaría que este trabajo tendría que ocurrir en cada mensaje recibido. Gracias al almacenamiento en caché de HTTP y a la caché HTTP, el código de bytes de Wasm compilado, esta no es la peor solución, pero hay una mejor de una nueva manera.
Con el traslado del código asíncrono al comienzo del trabajador web y no en realidad esperando a que se cumpla la promesa, sino almacenarla en un variable, el programa pasa inmediatamente a la parte del objeto de escucha de eventos del código, y no se perderá ningún mensaje del subproceso principal. Interior del evento objeto de escucha, se puede esperar la promesa.
/* 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 });
});
Bueno: Task se ejecuta en Web Worker y se carga y compila solo una vez
El resultado de la imagen estática
WebAssembly.compileStreaming()
método es una promesa que se resuelve en un
WebAssembly.Module
Una buena función de este objeto es que se puede transferir
postMessage()
Esto significa que el módulo de Wasm se puede cargar y compilar solo una vez en la
subproceso (o incluso otro trabajador web que solo esté relacionado con la carga y compilación),
y, luego, al trabajador web responsable de las tareas
tarea. El siguiente código muestra este flujo.
/* 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,
});
});
Del lado del trabajador web, solo falta extraer WebAssembly.Module
.
y crear una instancia de él. Dado que el mensaje con WebAssembly.Module
no es
se transmite, el código de Web Worker ahora usa
WebAssembly.instantiate()
en lugar de la variante instantiateStreaming()
de antes. La instancia
módulo se almacena en caché en una variable, por lo que el trabajo de creación de instancias solo tiene que ocurrir
alguna vez al iniciar 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 });
});
Perfecta: La tarea se ejecuta en un Web Worker intercalado, y se carga y compila solo una vez
Incluso con el almacenamiento en caché HTTP, obtener el código de Web Worker (idealmente) almacenado en caché y
lo que podría ser costoso. Un truco de rendimiento común es
intercalar el trabajador web y cargarlo como una URL blob:
Esto todavía requiere
compilado de Wasm para pasarlo a Web Worker para crear una instancia, como
contextos de Web Worker y el subproceso principal son diferentes, incluso si
basado en el mismo archivo fuente 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,
});
});
Creación diferida o entusiasmada de Web Worker
Hasta ahora, todas las muestras de código iniciaron el Web Worker de manera diferida a pedido, es decir, cuando se presionó el botón. Según tu aplicación, puede tener sentido crear el trabajador web con mayor entusiasmo, por ejemplo, cuando la aplicación está inactiva o incluso cuando parte del proceso de arranque de la app. Por lo tanto, mueve la creación de Web Worker fuera del objeto de escucha de eventos del botón.
const worker = new Worker(blobURL);
// Listen for incoming messages and display the result.
worker.addEventListener('message', (e) => {
output.textContent = e.result;
});
Mantén al trabajador web disponible o no
Una pregunta que podrías hacerte es si deberías mantener Web Worker de manera permanente o volver a crearla cuando la necesites. Ambos enfoques son y tienen sus ventajas y desventajas. Por ejemplo, mantener una página web Un trabajador permanente puede aumentar el espacio en memoria de tu app y hacer lidiar con las tareas simultáneas con mayor dificultad, ya que, de alguna manera, necesitas asignar los resultados que provienen del trabajador web de vuelta a las solicitudes. Por otro lado, tu Web El código de arranque de Worker puede ser bastante complejo, por lo que podría haber si creas una nueva cada vez. Por suerte, esto es algo que puedes medir con el API de User Timing.
Las muestras de código hasta ahora mantuvieron un Web Worker permanente cerca. Lo siguiente de código de muestra crea un nuevo trabajador web ad hoc siempre que sea necesario. Ten en cuenta que necesitas para hacer un seguimiento de cómo finalizar Web Worker tú mismo. (El fragmento de código omite el manejo de errores, pero, en caso de que algo salga, incorrecta, asegúrate de resolver en todos los casos, ya sea con éxito o con fracaso).
/* 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,
});
});
Demostraciones
Hay dos demostraciones con las que puedes jugar. Una con un
Trabajador web ad hoc
(código fuente)
y una con una
Trabajador web permanente
(código fuente).
Si abres las Herramientas para desarrolladores de Chrome y revisas la consola, puedes ver la opción Usuario
Registros de API de Timing que miden el tiempo que se tarda desde el clic en el botón hasta
el resultado que se muestra en la pantalla. La pestaña Red muestra la URL blob:
solicitudes. En este ejemplo, la diferencia de tiempo entre ad hoc y permanente
es de aproximadamente 3×. En la práctica, para el ojo humano, ambos no se distinguen en esta
para determinar si este es el caso. Es muy probable que los resultados para tu propia app en la vida real varíen.
Conclusiones
En esta publicación, se exploraron algunos patrones de rendimiento para lidiar con Wasm.
- Como regla general, es preferible usar los métodos de transmisión
(
WebAssembly.compileStreaming()
yWebAssembly.instantiateStreaming()
) en comparación con sus contrapartes que no transmiten (WebAssembly.compile()
yWebAssembly.instantiate()
). - Si puedes, externaliza las tareas de alto rendimiento en un Web Worker y realiza las tareas de Wasm
cargar y compilar el trabajo solo una vez fuera del trabajador web De esta manera,
Web Worker solo necesita crear una instancia del módulo de Wasm que recibe desde la aplicación principal
subproceso en el que se produjo la carga y compilación
WebAssembly.instantiate()
, lo que significa que la instancia puede almacenarse en caché si para mantener al trabajador web de forma permanente. - Mida con cuidado si tiene sentido mantener un trabajador web permanente para siempre, o para crear trabajadores web ad hoc cuando sea necesario. También pensar cuándo es el mejor momento para crear el Web Worker. Aspectos que se deben tener en cuenta son el consumo de memoria, la duración de la creación de instancias de Web Worker pero también la complejidad de tener que lidiar con solicitudes simultáneas.
Si tienes en cuenta estos patrones, vas por buen camino hacia la optimización Rendimiento de Wasm.
Agradecimientos
Esta guía fue revisada por Andreas Haas, Jakob Kummerow, Deepti Gandluri, Alon Zakai, Francis McCabe, François Beaufort y Rachel Andrew.