Hola, mi nombre es Dmitry Karlovsky y yo ... desempleado. Por lo tanto, tengo mucho tiempo libre para tocar música, deportes, creatividad, idiomas, conferencias JS y ciencias de la computación. Te contaré sobre las últimas investigaciones en el campo de la división semiautomática de cálculos largos en pequeños cuantos de varios milisegundos, lo que resultó en una biblioteca en miniatura $mol_fiber
. Pero primero, describamos los problemas que resolveremos ...

Esta es una versión de texto del rendimiento homónimo en HolyJS 2018 Piter . Puede leerlo como un artículo , abrirlo en la interfaz de presentación o ver un video .
Problema: baja capacidad de respuesta
Si queremos tener 60 cuadros estables por segundo, entonces solo tenemos 16 con un poco de milisegundos para hacer todo el trabajo, incluido lo que hace el navegador para mostrar los resultados en la pantalla.
Pero, ¿qué pasa si tomamos el flujo por más tiempo? Luego, el usuario observará una interfaz retrasada, inhibiendo la animación y similares de la degradación de UX.

Problema: sin escape
Sucede que mientras realizamos los cálculos, el resultado ya no nos interesa. Por ejemplo, tenemos un desplazamiento virtual, el usuario lo tira activamente, pero no podemos seguirlo y no podemos representar el área real hasta que la representación anterior devuelva el control para procesar los eventos del usuario.

Idealmente, no importa cuánto tiempo trabajemos, debemos continuar procesando eventos y poder cancelar en cualquier momento el trabajo que hemos comenzado, pero que aún no hemos completado.
Soy rapido y lo se
Pero, ¿qué pasa si nuestro trabajo no es uno, sino varios, sino un flujo? Imagine que conduce en su loto amarillo recién comprado y conduce hasta el cruce del ferrocarril. Cuando es gratis, puedes deslizarlo en una fracción de segundo. Pero ..

Problema: sin concurrencia
Cuando el cruce está ocupado por un tren de un kilómetro, debes pararte y esperar diez minutos hasta que pase. No es por eso que compraste un auto deportivo, ¿verdad?

¡Y qué genial sería si este tren se dividiera en 10 trenes de 100 metros cada uno y hubiera varios minutos entre ellos para pasar! No llegarías tan tarde entonces.
Entonces, ¿cuáles son las soluciones a estos problemas en el mundo JS ahora?
Solución: trabajadores
Lo primero que viene a la mente: ¿vamos a poner todos los cálculos complejos en un hilo separado? Para hacer esto, tenemos un mecanismo para WebWorkers.

Los eventos de la secuencia de IU se pasan al trabajador. Allí se procesan y las instrucciones sobre qué y cómo cambiar en la página ya se devuelven. Por lo tanto, guardamos el flujo de la interfaz de usuario de una gran capa de computación, pero no todos los problemas se resuelven de esta manera, y además se agregan otros nuevos.
Trabajadores: Problemas: (De) Serialización
La comunicación entre flujos se produce mediante el envío de mensajes que se serializan en un flujo de bytes, se transfieren a otro flujo y allí se analizan en objetos. Todo esto es mucho más lento que una llamada a método directo dentro de un solo hilo.

Trabajadores: Problemas: solo asíncrono
Los mensajes se transmiten estrictamente asincrónicamente. Y esto significa que algunas características que le pido no están disponibles. Por ejemplo, no puede detener el ascenso de un evento ui de un trabajador, ya que para cuando se inicie el controlador, el evento en el hilo de la IU ya completará su ciclo de vida.

Trabajadores: Problemas: API limitadas
Las siguientes API no están disponibles para nosotros en los trabajadores.
- DOM, CSSOM
- Lienzo
- GeoLocation
- Historia y ubicacion
- Sincronizar solicitudes http
- XMLHttpRequest.responseXML
- Ventana
Trabajadores: Problemas: no se puede cancelar
Y de nuevo, no tenemos forma de detener los cálculos en woker.

Sí, podemos detener a todo el trabajador, pero eso detendrá todas las tareas en él.
Sí, puede ejecutar cada tarea en un trabajador independiente, pero requiere muchos recursos.
Solución: Reaccionar fibra
Seguramente muchos escucharon que FaceBook reescribía heroicamente React, dividiendo todos los cálculos en un conjunto de pequeñas funciones lanzadas por un programador especial.

No entraré en detalles sobre su implementación, ya que este es un gran tema separado. Notaré solo algunas características, por lo que puede no ser adecuado para usted ...
Reaccionar fibra: se requiere reacción
Obviamente, si usa Angular, Vue u otro marco que no sea React, entonces React Fiber es inútil para usted.

Reaccionar fibra: solo renderizado
Reaccionar: solo cubre la capa de representación. Todas las demás capas de la aplicación quedan sin cuantificación.

React Fiber no lo salvará cuando necesite, por ejemplo, filtrar un gran bloque de datos por condiciones difíciles.
Reaccionar fibra: la cuantización está desactivada
A pesar del soporte reclamado para la cuantización, todavía está desactivado de forma predeterminada, ya que rompe la compatibilidad con versiones anteriores.

La cuantización en React sigue siendo algo experimental. Ten cuidado
Reaccionar fibra: la depuración es dolorosa
Cuando activa la cuantización, la pila de llamadas ya no coincide con su código, lo que complica enormemente la depuración. Pero volveremos a este tema.

Solución: cuantización
Intentemos generalizar el enfoque React Fiber para deshacernos de las desventajas mencionadas. Queremos permanecer en el marco de una secuencia, pero dividimos los cálculos largos en pequeños cuantos, entre los cuales el navegador puede representar los cambios que ya se han realizado en la página, y responderemos a los eventos.

Arriba ves un cálculo largo que detuvo al mundo entero en más de 100 ms. Y desde abajo, el mismo cálculo, pero desglosado en segmentos de tiempo de aproximadamente 16 ms, que dieron un promedio de 60 fotogramas por segundo. Como generalmente no sabemos cuánto tiempo llevarán los cálculos, no podemos dividirlo manualmente en trozos de 16 ms por adelantado. por lo tanto, necesitamos algún tipo de mecanismo de tiempo de ejecución que mida el tiempo que lleva completar la tarea y cuándo se excede el cuanto, lo que detiene la ejecución hasta el siguiente cuadro de animación. Pensemos qué mecanismos tenemos implementados para implementar tareas suspendidas aquí.
Concurrencia: fibras - corutinas apiladas
En lenguajes como Go y D existe un modismo como "corutina con una pila", también es una "fibra" o "fibra".
import { Future } from 'node-fibers' const one = ()=> Future.wait( future => setTimeout( future.return ) ) const two = ()=> one() + 1 const three = ()=> two() + 1 const four = ()=> three() + 1 Future.task( four ).detach()
En el ejemplo de código, verá one
función, que puede pausar la fibra actual, pero en sí misma tiene una interfaz completamente sincrónica. Las two
, three
y four
funciones son funciones síncronas regulares que no saben nada acerca de la fibra. En ellos puedes usar todas las características de javascript en su totalidad. Y finalmente, en la última línea, simplemente ejecutamos las four
funciones en una fibra separada.
El uso de fibras es bastante conveniente, pero para admitirlas, necesita soporte en tiempo de ejecución, que la mayoría de los intérpretes de JS no tienen. Sin embargo, para NodeJS hay una extensión nativa node-fibers
que agrega este soporte. Desafortunadamente, no hay navegadores disponibles en ningún navegador.
Concurrencia: FSM - corutinas apiladas
En lenguajes como C # y ahora JS hay soporte para "corutinas sin pila" o "funciones asincrónicas". Dichas funciones son una máquina de estado bajo el capó y no saben nada sobre la pila, por lo que deben marcarse con la palabra clave especial "async", y los lugares donde se pueden pausar están "en espera".
const one = ()=> new Promise( done => setTimeout( done ) ) const two = async ()=> ( await one() ) + 1 const three = async ()=> ( await two() ) + 1 const four = async ()=> ( await three() ) + 1 four()
Como es posible que tengamos que posponer el cálculo en cualquier momento, resulta que casi todas las funciones de la aplicación tendrán que hacerse asincrónicas. Esto no es solo que la complejidad del código, sino que también afecta en gran medida el rendimiento. Además, muchas API que aceptan devolución de llamada aún no admiten devoluciones de llamada asincrónicas. Un ejemplo sorprendente es el método de reduce
de cualquier matriz.
Concurrencia: semi-fibras - reinicios
Intentemos hacer algo similar a la fibra, utilizando solo las funciones que están disponibles para nosotros en cualquier navegador moderno.
import { $mol_fiber_async , $mol_fiber_start } from 'mol_fiber/web' const one = ()=> $mol_fiber_async( back => setTimeout( back ) ) const two = ()=> one() + 1 const three = ()=> two() + 1 const four = ()=> three() + 1 $mol_fiber_start( four )
Como puede ver, las funciones intermedias no saben nada acerca de la interrupción; esto es JS normal. Solo one
función conoce la posibilidad de suspensión. Para abortar el cálculo, ella simplemente lanza Promise
como una excepción. En la última línea, ejecutamos las four
funciones en una pseudo fibra separada, que monitorea las excepciones lanzadas dentro, y si llega Promise
, se suscribe a su resolve
y luego reinicia la fibra.
Para mostrar cómo funcionan las pseudo-fibras, escribiremos un código complicado ...

Imaginemos que la función de step
aquí escribe algo en la consola y hace otro trabajo duro durante 20 ms. Y la función de walk
llama al step
dos veces, registrando todo el proceso. En el medio, mostrará lo que ahora se muestra en la consola. Y a la derecha está el estado del árbol de pseudofibra.
$ mol_fiber: sin cuantización
Ejecutemos este código y veamos qué sucede.

Hasta ahora, todo es simple y obvio. El árbol de pseudo-fibra, por supuesto, no está involucrado. Y todo estaría bien, pero este código se ejecuta durante más de 40 ms, lo que no tiene valor.
$ mol_fiber: caché primero
Envuelvamos ambas funciones en un contenedor especial que lo ejecute en una pseudo fibra y veamos qué sucede.

Aquí vale la pena prestar atención al hecho de que para cada lugar de invocación de one
función dentro de la fibra de walk
, se creó una fibra separada. El resultado de la primera llamada se almacenó en caché, pero en lugar de la segunda, se lanzó Promise
, ya que habíamos agotado nuestro tiempo.
$ mol_fiber: segundo caché
Lanzado en el primer fotograma, Promise
se resolverá automáticamente en el siguiente, lo que conducirá a un reinicio de la fibra de walk
.

Como puede ver, debido al reinicio, de nuevo enviamos "inicio" y "primer hecho" a la consola, pero "primer comienzo" ya se ha ido, ya que está en la fibra con el caché lleno anteriormente, por lo que su controlador es más no llamado Cuando se llena el caché de la fibra de walk
, se destruyen todas las fibras incrustadas, ya que la ejecución nunca las alcanzará.
Entonces, ¿por qué first begin
imprimir una vez y first done
dos? Se trata de idempotencia. console.log
: operación no idempotente, cuántas veces la llamas, tantas veces agregará una entrada a la consola. Pero la fibra que se está ejecutando en otra fibra es idempotente, solo ejecuta el identificador en la primera llamada y, en las devoluciones posteriores, inmediatamente el resultado de la memoria caché, sin provocar efectos secundarios adicionales.
$ mol_fiber: idempotencia primero
Vamos a envolver console.log
en una fibra, convirtiéndola en idempotente, y veamos cómo se comporta el programa.

Como puede ver, ahora en el árbol de fibra tenemos entradas para cada llamada a la función de log
.
$ mol_fiber: segunda idempotencia
En el próximo reinicio de la fibra de walk
, las llamadas repetidas a la función de log
ya no generan llamadas a la console.log
, pero tan pronto como llegamos a la ejecución de las fibras con un caché vacío, las llamadas a console.log
reanudan.

Tenga en cuenta que en la consola ahora no mostramos nada superfluo, exactamente lo que se mostraría en código síncrono sin fibra ni cuantificación.
$ mol_fiber: descanso
¿Cómo interrumpe el cálculo? Al comienzo del cuanto, se establece una fecha límite. Y antes de comenzar cada fibra, se verifica si la hemos alcanzado. Y si alcanzas, Promise
apresura, lo que se resuelve en el siguiente cuadro y comienza un nuevo cuanto.
if( Date.now() > $mol_fiber.deadline ) { throw new Promise( $mol_fiber.schedule ) }
$ mol_fiber: fecha límite
La fecha límite para el cuanto es fácil de establecer. Se agregan 8 milisegundos a la hora actual. ¿Por qué exactamente 8, porque hay hasta 16 para preparar el tiro? El hecho es que no sabemos de antemano cuánto tiempo tendrá que procesar el navegador, por lo que debemos dejar algo de tiempo para que funcione. Pero a veces sucede que el navegador no necesita renderizar nada, y luego con 8 ms quanta podemos insertar otro cuanto en el mismo marco, lo que dará un paquete denso de cuantos con un tiempo de inactividad mínimo del procesador.
const now = Date.now() const quant = 8 const elapsed = Math.max( 0 , now - $mol_fiber.deadline ) const resistance = Math.min( elapsed , 1000 ) / 10
Pero si solo lanzamos una excepción cada 8 ms, entonces la depuración con la parada de excepción activada se convertirá en una pequeña rama del infierno. Necesitamos algún mecanismo para detectar este modo de depurador. Desafortunadamente, esto solo puede entenderse indirectamente: una persona tarda aproximadamente un segundo en comprender si continúa la ejecución o no. Y esto significa que si el control no volvió al script durante mucho tiempo, entonces el depurador se detuvo o hubo un cálculo pesado. Para sentarnos en ambas sillas, agregamos al cuántico el 10% del tiempo transcurrido, pero no más de 100 ms. Esto no afecta en gran medida a los FPS, pero reduce la frecuencia de detención del depurador en un orden de magnitud debido a la cuantización.
Depurar: probar / atrapar
Ya que estamos hablando de depuración, ¿qué piensas, en qué lugar de este código se detiene el depurador?
function foo() { throw new Error( 'Something wrong' ) // [1] } try { foo() } catch( error ) { handle( error ) throw error // [2] }
Como regla general, debe detenerse donde se lanza la excepción por primera vez, pero la realidad es que se detiene solo donde se lanzó la última vez, que generalmente está muy lejos de donde ocurrió. Por lo tanto, para no complicar la depuración, nunca se deben detectar excepciones a través de try-catch. Pero incluso sin un manejo excepcional es imposible.
Depuración: eventos no controlados
Normalmente, un tiempo de ejecución proporciona un evento global que se produce para cada excepción no detectada.
function foo() { throw new Error( 'Something wrong' ) } window.addEventListener( 'error' , event => handle( event.error ) ) foo()
Además de la molestia, esta solución tiene un inconveniente tal que todas las excepciones caen aquí y es bastante difícil entender de qué fibra y fibra se produjo el evento.
Depurar: promesa
Las promesas son la mejor manera de manejar las excepciones.
function foo() { throw new Error( 'Something wrong' ) } new Promise( ()=> { foo() } ).catch( error => handle( error ) )
La función pasada a Promise se llama de forma inmediata, sincrónica, pero la excepción no se detecta y detiene de forma segura el depurador en el lugar de su aparición. Un poco más tarde, de forma asíncrona, ya llama al controlador de errores, en el que sabemos exactamente qué fibra dio la falla y qué falla. Este es precisamente el mecanismo utilizado en $ mol_fiber.
Seguimiento de pila: reaccionar fibra
Echemos un vistazo a la traza de pila que obtienes en React Fiber.

Como puede ver, tenemos mucha reacción intestinal. De lo útil aquí, solo el punto de ocurrencia de la excepción y los nombres de los componentes son más altos en la jerarquía. No mucho
Seguimiento de pila: $ mol_fiber
En $ mol_fiber, obtenemos un seguimiento de pila mucho más útil: sin agallas, solo puntos específicos en el código de la aplicación a través del cual llegó a una excepción.

Esto se logra mediante el uso de la pila nativa, las promesas y la eliminación automática de los intestinos. Si lo desea, puede expandir el error en la consola, como en la captura de pantalla, y ver las tripas, pero no hay nada interesante.
$ mol_fiber: manejar
Entonces, para interrumpir un cuanto, se lanza Promesa.
limit() { if( Date.now() > $mol_fiber.deadline ) { throw new Promise( $mol_fiber.schedule ) }
Pero, como puede suponer, Promise puede ser absolutamente cualquier cosa: para Fiber, en general, no importa qué esperar: el siguiente marco, la finalización de la carga de datos u otra cosa ...
fail( error : Error ) { if( error instanceof Promise ) { const listener = ()=> self.start() return error.then( listener , listener ) }
Fiber simplemente se suscribe para resolver promesas y reinicia. Pero no es necesario lanzar y atrapar promesas manualmente, porque el paquete incluye varios envoltorios útiles.
$ mol_fiber: funciones
Para convertir cualquier función síncrona en una fibra idempotente, simplemente envuélvala en $mol_fiber_func
..
import { $mol_fiber_func as fiberize } from 'mol_fiber/web' const log = fiberize( console.log ) export const main = fiberize( ()=> { log( getData( 'goo.gl' ).data ) } )
Aquí creamos console.log
idempotent, y main
enseñó a interrumpir mientras esperaba la descarga.
$ mol_fiber: manejo de errores
Pero, ¿cómo responder a las excepciones si no queremos usar try-catch
? Entonces podemos registrar el controlador de errores con $mol_fiber_catch
...
import { $mol_fiber_func as fiberize , $mol_fiber_catch as onError } from 'mol_fiber' const getConfig = fiberize( ()=> { onError( error => ({ user : 'Anonymous' }) ) return getData( '/config' ).data } )
Si devolvemos algo diferente del error, será el resultado de la fibra actual. En este ejemplo, si no es posible descargar la configuración del servidor, la función getConfig
devolverá la configuración de forma predeterminada.
$ mol_fiber: métodos
Por supuesto, puede ajustar no solo las funciones, sino también los métodos que usan un decorador.
import { $mol_fiber_method as action } from 'mol_fiber/web' export class Mover { @action move() { sendData( 'ya.ru' , getData( 'goo.gl' ) ) } }
Aquí, por ejemplo, subimos datos de Google y los subimos a Yandex.
$ mol_fiber: promesas
Para descargar datos del servidor, basta con tomar, por ejemplo, la función de fetch
asíncrona y, con un simple movimiento de la muñeca, la convierte en sincronizada.
import { $mol_fiber_sync as sync } from 'mol_fiber/web' export const getData = sync( fetch )
Esta implementación es buena para todos, pero no admite la cancelación de una solicitud cuando se destruye un árbol de fibras, por lo que debemos utilizar una API
más confusa.
$ mol_fiber: cancelar solicitud
import { $mol_fiber_async as async } from 'mol_fiber/web' function getData( uri : string ) : Response { return async( back => { var controller = new AbortController(); fetch( uri , { signal : controller.signal } ).then( back( res => res ) , back( error => { throw error } ) , ) return ()=> controller.abort() } ) }
La función que se pasa al reiniciador async
se llama solo una vez y el reiniciador se le pasa, en el que debe ajustar las devoluciones de llamada. En consecuencia, en estas devoluciones de llamada, debe devolver el valor o lanzar una excepción. Cualquiera que sea el resultado de la devolución de llamada, también será el resultado de la fibra. Tenga en cuenta que al final devolveremos una función que se llamará en caso de destrucción prematura de la fibra.
$ mol_fiber: cancelar respuesta
Del lado del servidor, también puede ser útil cancelar el cálculo cuando el cliente se ha caído. Implementemos un contenedor sobre midleware
que creará una fibra en la que se ejecutará el midleware
original. , , , .
import { $mol_fiber_make as Fiber } from 'mol_fiber' const middle_fiber = middleware => ( req , res ) => { const fiber = Fiber( ()=> middleware( req , res ) ) req.on( 'close' , ()=> fiber.destructor() ) fiber.start() } app.get( '/foo' , middle_fiber( ( req , res ) => { // do something } ) )
$mol_fiber: concurrency
, . , 3 : , , - ..

: , . . , , .
$mol_fiber: properties
, ..
Pros:
- Runtime support isn't required
- Can be cancelled at any time
- High FPS
- Concurrent execution
- Debug friendly
- ~ 3KB gzipped
Cons:
- Instrumentation is required
- All code should be idempotent
- Longer total execution
$mol_fiber — , . — , . , , . , , , , . , . .
Links
Call back

: , , )
: , .
: . , .
: . , . , .
: , . , )
: , .
: - . , , .
: . , , .
: , . 16ms, ? 16 8 , 8, . , . , «».
: — . Gracias
: . , . !
: , . .
: , , , , , / , .
: , .
: .
: , . mol.
: , , . , , , .
: .
: , . , $mol, , .
: , , . — . .
: - , .
: $mol , . (pdf, ) , .
: , . , .
: , ) .
: . .
: In some places I missed what the reporter was saying. The conversation was about how to use the "Mola" library and "why?". But how it works remains a mystery for me.To smoke an source code is for the overhead.
: , .
: . , . . .
: : . - (, ). , : 16?
Más o menos : no trabajé con fibra. En el informe escuché la teoría del trabajo de la fibra. Pero no entendí absolutamente cómo usar mol_fiber en casa ... Los ejemplos pequeños son geniales, pero ¿cómo se puede aplicar esto en una aplicación grande con 30 fps para acelerar hasta 60 fps? No hubo comprensión. Ahora, si el autor prestara más atención a esto y menos diseño de módulo interno, la calificación sería más alta.