Acelerar los gráficos con OffscreenCanvas

La representación de gráficos puede afectar seriamente al navegador. Especialmente cuando se trata de generar una multitud de elementos que representan diagramas en la interfaz de una aplicación compleja. Puede intentar mejorar la situación utilizando la interfaz OffscreenCanvas, cuyo nivel de soporte está aumentando gradualmente los navegadores. Permite, utilizando un trabajador web, pasar a las tareas de formar una imagen mostrada en un elemento visible <canvas>. El artículo, cuya traducción publicamos hoy, está dedicado al uso de la interfaz . Aquí hablaremos sobre por qué podría ser necesaria esta interfaz, sobre lo que realmente se puede esperar de su uso y sobre las dificultades que pueden surgir al trabajar con ella.



OffscreenCanvas

¿Por qué prestar atención a OffscreenCanvas?


El código involucrado en la representación del diagrama puede tener una complejidad computacional suficientemente alta. En muchos lugares, puede encontrar historias detalladas sobre cómo generar una animación fluida y para garantizar la interacción conveniente del usuario con la página, es necesario que la representación de un cuadro se ajuste a unos 10 ms, lo que le permite alcanzar 60 cuadros por segundo. Si los cálculos necesarios para generar un cuadro tardan más de 10 ms, esto dará como resultado "ralentizaciones" notables de la página.

Sin embargo, cuando se muestran diagramas, la situación se ve agravada por lo siguiente:

  • — - (SVG, canvas, WebGL) , , .
  • , , , , 10 . ( — ) .

Estos problemas son candidatos ideales para un enfoque de optimización clásico llamado divide y vencerás. En este caso, estamos hablando de la distribución de la carga informática a través de múltiples hilos. Sin embargo, antes de la aparición de la interfaz, OffscreenCanvastodo el código de representación debía ejecutarse en el hilo principal. La única forma en que este código podría usar las API que necesitaba.

Desde un punto de vista técnico, los cálculos "pesados" podrían enviarse a un hilo de trabajo web antes. Pero, dado que las llamadas responsables de la representación tenían que hacerse desde el flujo principal, esto requería el uso de esquemas complejos de intercambio de mensajes entre el flujo de trabajo y el flujo principal en el código de salida del gráfico. Además, este enfoque a menudo solo dio un ligero aumento de rendimiento.

EspecificaciónOffscreenCanvasnos proporciona un mecanismo para transferir el control de superficie para mostrar gráficos de elementos <canvas>en un trabajador web. Actualmente, esta especificación es compatible con los navegadores Chrome y Edge (después de que Edge se haya migrado a Chromium). Se espera que en Firefox el soporte OffscreenCanvasaparezca dentro de seis meses. Si hablamos de Safari, aún se desconoce si el soporte para esta tecnología está planeado en este navegador.

Gráfico de salida usando OffscreenCanvas



Un ejemplo de un gráfico que muestra 100,000 puntos multicolores

Para evaluar mejor las posibles ventajas y desventajasOffscreenCanvas, veamos un ejemplo del resultado del diagrama "pesado" que se muestra en la figura anterior.

const offscreenCanvas = canvasContainer
  .querySelector('canvas')
  .transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);

Primero, consultamos OffscreenCanvasel elemento canvasusando el nuevo método transferControlToOffscreen(). Luego llamamos al método worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas]), haciendo esto para enviar un mensaje al trabajador que contiene un enlace offscreenCanvas. Es muy importante que aquí, como segundo argumento, necesite usar [offscreenCanvas]. Esto le permite hacer que el propietario de este objeto trabaje, lo que le permitirá administrar solo OffscreenCanvas.

canvasContainer.addEventListener('measure', ({ detail }) => {
  const { width, height } = detail;
  worker.postMessage({ width, height });
});
canvasContainer.requestRedraw();

Teniendo en cuenta que los tamaños se OffscreenCanvasheredaron originalmente de los atributos widthy el heightelemento canvas, es nuestra responsabilidad mantener estos valores actualizados cuando se cambia el tamaño del elemento canvas. Aquí usamos el evento measurede d3fc-canvas, que nos dará la oportunidad, de acuerdo con requestAnimationFrame, de transmitir al trabajador información sobre las nuevas dimensiones del elemento canvas.

Para simplificar el ejemplo, utilizaremos componentes de la biblioteca d3fc . Este es un conjunto de componentes auxiliares que agregan componentes d3 o complementan su funcionalidad. Puede trabajar OffscreenCanvassin componentes d3fc. Todo lo que se discutirá se puede hacer utilizando características de JavaScript exclusivamente estándar.

Ahora ve al código del archivo worker.js. En este ejemplo, en aras de un aumento real en el rendimiento de representación, vamos a utilizar WebGL.

addEventListener('message', ({ data: { offscreenCanvas, width, height } }) => {
    if (offscreenCanvas != null) {
        const gl = offscreenCanvas.getContext('webgl');
        series.context(gl);
        series(data);
    }

    if (width != null && height != null) {
        const gl = series.context();
        gl.canvas.width = width;
        gl.canvas.height = height;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    }
});

Cuando recibimos un mensaje que contiene una propiedad canvas, asumimos que el mensaje proviene del hilo principal. Luego, obtenemos el canvascontexto del elemento webgly lo pasamos al componente series. Luego llamamos al componente series, pasándole los datos ( data), que queremos representar con él (a continuación hablaremos de dónde provienen las variables correspondientes).

Además, verificamos las propiedades widthy height, que aparecieron en el mensaje, y las usamos para establecer las dimensiones offscreenCanvasy el área de visualización de WebGL. El enlace offscreenCanvasno se usa directamente. El hecho es que el mensaje contiene una propiedad offscreenCanvaso propiedades widthy height.

El código de trabajo restante es responsable de configurar la representación de lo que queremos generar. Aquí no hay nada especial, por lo que si todo esto le resulta familiar, puede pasar inmediatamente a la siguiente sección en la que hablaremos sobre el rendimiento.

const randomNormal = d3.randomNormal(0, 1);
const randomLogNormal = d3.randomLogNormal();

const data = Array.from({ length: 1e5 }, () => ({
    x: randomNormal(),
    y: randomNormal(),
    size: randomLogNormal() * 10
}));

const xScale = d3.scaleLinear().domain([-5, 5]);

const yScale = d3.scaleLinear().domain([-5, 5]);

Primero, creamos un conjunto de datos que contiene las coordenadas de puntos distribuidos aleatoriamente alrededor del origen x / y, y ajustamos la escala. No establecemos el rango de valores xey usando el método range, ya que la serie WebGL se muestra en tamaño completo del elemento canvas (-1 -> +1en las coordenadas normalizadas del dispositivo).

const series = fc
    .seriesWebglPoint()
    .xScale(xScale)
    .yScale(yScale)
    .crossValue(d => d.x)
    .mainValue(d => d.y)
    .size(d => d.size)
    .equals((previousData, data) => previousData.length > 0);

A continuación, configuramos una serie de puntos usando xScaley yScale. Luego configuramos los medios de acceso apropiados que le permiten leer datos.

Además, definimos nuestra propia función de comprobación de igualdad, que está diseñada para garantizar que el componente no transmita dataGPU en cada representación. Debemos expresar esto explícitamente, porque incluso si sabemos que no modificaremos estos datos, el componente no puede saberlo sin realizar una verificación "sucia" de uso intensivo de recursos.

const colorScale = d3.scaleOrdinal(d3.schemeAccent);

const webglColor = color => {
    const { r, g, b, opacity } = d3.color(color).rgb();
    return [r / 255, g / 255, b / 255, opacity];
};

const fillColor = fc
    .webglFillColor()
    .value((d, i) => webglColor(colorScale(i)))
    .data(data);

series.decorate(program => {
    fillColor(program);
});

Este código le permite colorear el gráfico. Usamos el índice del punto en el conjunto de datos para seleccionar un color adecuado colorScale, luego lo convertimos al formato requerido y lo usamos para decorar el punto de salida.

function render() {
    const ease = 5 * (0.51 + 0.49 * Math.sin(Date.now() / 1e3));
    xScale.domain([-ease, ease]);
    yScale.domain([-ease, ease]);
    series(data);
    requestAnimationFrame(render);
}

Ahora que los puntos están coloreados, todo lo que queda es animar el gráfico. Debido a esto, necesitaremos una llamada al método constante render(). Esto, además, creará algo de carga en el sistema. Simulamos el aumento y la disminución de la escala del gráfico requestAnimationFramepara modificar las propiedades xScale.domainy yScale.domaincada cuadro. Aquí, los valores dependientes del tiempo se aplican y se calculan para que la escala del gráfico cambie sin problemas. Además, modificamos el encabezado del mensaje para que se llame a un método para comenzar el ciclo de representación render()y para que no tengamos que llamar directamente series(data).

importScripts(
    './node_modules/d3-array/dist/d3-array.js',
    './node_modules/d3-collection/dist/d3-collection.js',
    './node_modules/d3-color/dist/d3-color.js',
    './node_modules/d3-interpolate/dist/d3-interpolate.js',
    './node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js',
    './node_modules/d3-random/dist/d3-random.js',
    './node_modules/d3-scale/dist/d3-scale.js',
    './node_modules/d3-shape/dist/d3-shape.js',
    './node_modules/d3fc-extent/build/d3fc-extent.js',
    './node_modules/d3fc-random-data/build/d3fc-random-data.js',
    './node_modules/d3fc-rebind/build/d3fc-rebind.js',
    './node_modules/d3fc-series/build/d3fc-series.js',
    './node_modules/d3fc-webgl/build/d3fc-webgl.js'
);

Para que este ejemplo funcione, solo queda importar las bibliotecas necesarias al trabajador. Usamos para esto importScripts, y también, para no usar las herramientas para construir proyectos, usamos una lista de dependencias compiladas manualmente. Desafortunadamente, no podemos descargar las compilaciones d3 / d3fc completas, ya que dependen del DOM y lo que necesitan en el trabajador no está disponible.

→ El código completo para este ejemplo se puede encontrar en GitHub

Offscreen: lienzo y rendimiento


La animación en nuestro proyecto funciona gracias al uso requestAnimationFramede un trabajador. Esto le permite al trabajador renderizar páginas incluso cuando el hilo principal está ocupado con otras cosas. Si mira la página del proyecto descrita en la sección anterior y hace clic en el botón Stop main thread, puede prestar atención al hecho de que cuando se muestra el cuadro de mensaje, la actualización de la información de la marca de tiempo se detiene. El hilo principal está bloqueado, pero la animación del gráfico no se detiene.


Ventana del proyecto

Podríamos organizar una representación iniciada al recibir mensajes de la transmisión principal. Por ejemplo, dichos eventos podrían enviarse cuando un usuario interactúa con un gráfico o cuando recibe datos nuevos a través de la red. Pero con este enfoque, si el hilo principal está ocupado con algo y no se reciben mensajes en el trabajador, la representación se detendrá.

La representación continua de la tabla en condiciones en las que no sucede nada es una excelente manera de molestar al usuario con un zumbido de ventilador y batería baja. Como resultado, resulta que en el mundo real, la decisión sobre cómo dividir las responsabilidades entre el hilo principal y el hilo del trabajador depende de si el trabajador puede hacer algo útil cuando no recibe mensajes sobre un cambio en la situación del hilo principal.

Si hablamos de la transferencia de mensajes entre hilos, debe tenerse en cuenta que aquí aplicamos un enfoque muy simple. Honestamente, necesitamos transmitir muy pocos mensajes entre hilos, por lo que preferiría llamar a nuestro enfoque una consecuencia del pragmatismo, no de la pereza. Pero si hablamos de aplicaciones reales, se puede observar que el esquema de transferencia de mensajes entre los flujos se volverá más complicado si la interacción del usuario con el diagrama y la actualización de los datos visualizados dependerán de ellos.

Puede usar la interfaz estándar MessageChannel para crear canales de mensajes separados entre el hilo principal y el hilo de trabajo .. Cada uno de estos canales se puede utilizar para una categoría específica de mensajes, lo que facilita el procesamiento de mensajes. Como alternativa a los mecanismos estándar, pueden actuar bibliotecas de terceros como Comlink . Esta biblioteca oculta detalles de bajo nivel detrás de una interfaz de alto nivel que utiliza objetos proxy .

Otra característica interesante de nuestro ejemplo, que se vuelve claramente visible en retrospectiva, es el hecho de que tiene en cuenta el hecho de que los recursos de la GPU, como otros recursos del sistema, están lejos de ser infinitos. La traducción del renderizado en un trabajador permite que el hilo principal resuelva otros problemas. Pero el navegador, de todos modos, necesita acceder a la GPU para representar el DOM y lo que se generó por los medios OffscreenCanvas.

Si el trabajador consume todos los recursos de la GPU, la ventana principal de la aplicación experimentará problemas de rendimiento, independientemente de dónde se realice el renderizado. Preste atención a cómo disminuye la velocidad de actualización de la marca de tiempo en el ejemplo a medida que aumenta el número de puntos mostrados. Si tiene una tarjeta gráfica potente, es posible que deba aumentar el número de puntos transmitidos a la página en la cadena de consulta. Para hacer esto, puede usar un valor que exceda el valor máximo de 100000, especificado usando uno de los enlaces en la página de ejemplo.

Cabe señalar que aquí no exploramos el mundo secreto de los atributos de contexto.. Tal estudio podría ayudarnos a aumentar la productividad de la solución. Sin embargo, no hice esto al hacerlo debido al bajo nivel de soporte para estos atributos.

Si hablamos de renderizar usando OffscreenCanvas, entonces el atributo se ve más interesante aquí desynchronized. Este, donde es compatible, y teniendo en cuenta algunas restricciones, le permite deshacerse de la sincronización entre el bucle de eventos y el bucle de representación que se ejecuta en el trabajador. Esto minimiza el retraso en la actualización de la imagen. Los detalles sobre esto se pueden encontrar aquí .

Resumen


La interfaz OffscreenCanvasbrinda a los desarrolladores la oportunidad de mejorar el rendimiento de la representación gráfica, pero su uso requiere un enfoque reflexivo. Al usarlo, debe considerar lo siguiente:

  • (, , ) - .

    • , . , , , postMessage.
  • , (, SVG/HTML- <canvas>), , , .

    • , , , , , , , , .
  • , GPU, OffscreenCanvas .

    • OffscreenCanvas, GPU-, , . , , .

Aquí hay un código de ejemplo, y aquí está su versión de trabajo.

¡Queridos lectores! ¿Has utilizado OffscreenCanvas para acelerar la visualización de gráficos en páginas web?


All Articles