Bonjour, je m'appelle Dmitry Karlovsky et moi ... sans emploi. Par conséquent, j'ai beaucoup de temps libre pour jouer de la musique, des sports, de la créativité, des langues, des conférences JS et de l'informatique. Je vais vous parler des dernières recherches dans le domaine du fractionnement semi-automatique de longs calculs en petits quanta de plusieurs millisecondes, qui ont abouti à une bibliothèque miniature $mol_fiber
. Mais d'abord, décrivons les problèmes que nous allons résoudre.

Il s'agit d'une version texte de la performance éponyme à HolyJS 2018 Piter . Vous pouvez soit le lire comme un article , soit l' ouvrir dans l'interface de présentation , soit regarder une vidéo .
Problème: faible réactivité
Si nous voulons avoir 60 images stables par seconde, nous n'en avons que 16 avec un peu de millisecondes pour faire tout le travail, y compris ce que le navigateur fait pour afficher les résultats à l'écran.
Mais que se passe-t-il si nous prenons le courant plus longtemps? Ensuite, l'utilisateur observera une interface en retard, inhibant l'animation et similaire de la dégradation UX.

Problème: aucune échappatoire
Il arrive que pendant que nous effectuons les calculs, le résultat ne nous intéresse plus. Par exemple, nous avons un défilement virtuel, l'utilisateur le tire activement, mais nous ne pouvons pas le suivre et ne pouvons pas restituer la zone réelle jusqu'à ce que le rendu précédent renvoie le contrôle pour traiter les événements utilisateur.

Idéalement, quelle que soit la durée de notre travail, nous devons continuer à traiter les événements et pouvoir à tout moment annuler le travail que nous avons commencé, mais pas encore terminé.
Je suis rapide et je le sais
Mais que faire si notre travail n'est pas un, mais plusieurs, mais un flux? Imaginez que vous conduisez sur votre lotus jaune fraîchement acheté et conduisez jusqu'au passage à niveau. Lorsqu'il est gratuit, vous pouvez le glisser en une fraction de seconde. Mais ..

Problème: pas de simultanéité
Lorsque le passage à niveau est occupé par un train d'un kilomètre, vous devez vous lever et attendre dix minutes jusqu'à ce qu'il passe. Pas pour ça que vous avez acheté une voiture de sport, non?

Et comme ce serait cool si ce train était divisé en 10 trains de 100 mètres chacun et qu'il y aurait plusieurs minutes entre eux pour passer! Vous ne seriez pas si tard alors.
Alors, quelles sont les solutions à ces problèmes dans le monde JS maintenant?
Solution: les travailleurs
La première chose qui me vient à l'esprit: mettons simplement tous les calculs complexes dans un thread séparé? Pour ce faire, nous avons un mécanisme pour les WebWorkers.

Les événements du flux d'interface utilisateur sont transmis au travailleur. Là, ils sont traités et les instructions sur quoi et comment changer sur la page sont déjà renvoyées. Ainsi, nous enregistrons le flux d'interface utilisateur à partir d'une grande couche de calcul, mais tous les problèmes ne sont pas résolus de cette manière, et en plus de nouveaux sont ajoutés.
Workers: Issues: (De) Sérialisation
La communication entre les flux se produit en envoyant des messages qui sont sérialisés en un flux d'octets, transférés vers un autre flux, et là, ils sont analysés en objets. Tout cela est beaucoup plus lent qu'un appel de méthode direct dans un seul thread.

Workers: Issues: Asynchronous only
Les messages sont transmis de manière strictement asynchrone. Et cela signifie que certaines fonctionnalités que je vous demande ne sont pas disponibles. Par exemple, vous ne pouvez pas arrêter l'ascension d'un événement ui à partir d'un travailleur, car au moment où le gestionnaire démarre, l'événement dans le thread d'interface utilisateur termine déjà son cycle de vie.

Travailleurs: Problèmes: API limitées
Les API suivantes ne sont pas disponibles pour nous dans les travailleurs.
- DOM, CSSOM
- Toile
- GéoLocalisation
- Histoire et emplacement
- Synchroniser les requêtes http
- XMLHttpRequest.responseXML
- Fenêtre
Travailleurs: problèmes: impossible d'annuler
Et encore une fois, nous n'avons aucun moyen d'arrêter les calculs dans woker.

Oui, nous pouvons arrêter l'ensemble du travailleur, mais cela arrêtera toutes les tâches qu'il contient.
Oui, vous pouvez exécuter chaque tâche dans un travailleur distinct, mais cela consomme beaucoup de ressources.
Solution: réagir la fibre
Beaucoup ont sûrement entendu FaceBook réécrire héroïquement React, brisant tous les calculs qu'il contient en un tas de petites fonctions lancées par un programmateur spécial.

Je n'entrerai pas dans les détails de sa mise en œuvre, car il s'agit d'un grand sujet distinct. Je ne noterai que quelques fonctionnalités, à cause desquelles cela peut ne pas vous convenir ..
React Fibre: React requis
Évidemment, si vous utilisez Angular, Vue ou un autre framework que React, React Fibre est inutile pour vous.

React Fibre: rendu uniquement
Réagir - ne couvre que la couche de rendu. Toutes les autres couches de l'application restent sans quantification.

React Fibre ne vous sauvera pas lorsque vous aurez besoin, par exemple, de filtrer un grand bloc de données dans des conditions difficiles.
React Fibre: la quantification est désactivée
Malgré le support revendiqué pour la quantification, il est toujours désactivé par défaut, car il rompt la compatibilité descendante.

La quantification dans React est encore une chose expérimentale. Faites attention!
React Fiber: Debug is pain
Lorsque vous activez la quantification, callstack ne correspond plus à votre code, ce qui complique considérablement le débogage. Mais nous reviendrons sur cette question.

Solution: quantification
Essayons de généraliser l'approche React Fibre afin de nous débarrasser des inconvénients mentionnés. Nous voulons rester dans le cadre d'un flux, mais diviser les longs calculs en petits quanta, entre lesquels le navigateur peut restituer les modifications qui ont déjà été apportées à la page, et nous répondrons aux événements.

Ci-dessus, vous voyez un long calcul qui a arrêté le monde entier de plus de 100 ms. Et d'en bas - le même calcul, mais décomposé en tranches de temps d'environ 16 ms, ce qui donne une moyenne de 60 images par seconde. Comme nous ne savons généralement pas combien de temps les calculs prendront, nous ne pouvons pas le diviser manuellement en morceaux de 16 ms à l'avance. par conséquent, nous avons besoin d'une sorte de mécanisme d'exécution qui mesure le temps nécessaire pour terminer la tâche et lorsque la taille du quantum est dépassée, ce qui suspend l'exécution jusqu'à la prochaine image d'animation. Réfléchissons aux mécanismes que nous avons en place pour mettre en œuvre de telles tâches suspendues ici.
Concurrence: fibres - coroutines empilables
Dans des langues comme Go et D, il existe un idiome comme une «coroutine avec une pile», c'est aussi une «fibre» ou «fibre».
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()
Dans l'exemple de code, vous voyez one
fonction, qui peut mettre en pause la fibre actuelle, mais elle-même a une interface complètement synchrone. Les fonctions two
, three
et four
sont des fonctions synchrones régulières qui ne connaissent rien à la fibre. En eux, vous pouvez utiliser toutes les fonctionnalités de javascript dans leur intégralité. Et enfin, sur la dernière ligne, nous exécutons simplement la fonction four
dans une fibre distincte.
L'utilisation de fibres est assez pratique, mais pour les prendre en charge, vous avez besoin d'une prise en charge d'exécution, que la plupart des interprètes JS ne possèdent pas. Cependant, pour NodeJS, il existe une extension native node-fibers
qui ajoute cette prise en charge. Malheureusement, aucun navigateur n'est disponible dans aucun navigateur.
Concurrence: FSM - coroutines sans pile
Dans des langages comme C # et maintenant JS, il existe un support pour les «coroutines sans pile» ou les «fonctions asynchrones». De telles fonctions sont une machine d'état sous le capot et ne savent rien de la pile, elles doivent donc être marquées avec le mot-clé spécial "async", et les endroits où elles peuvent être suspendues sont "en attente".
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()
Étant donné que nous devrons peut-être reporter le calcul à tout moment, il s'avère que presque toutes les fonctions de l'application devront être rendues asynchrones. Ce n'est pas seulement que la complexité du code, mais affecte également considérablement les performances. En outre, de nombreuses API acceptant les rappels ne prennent toujours pas en charge les rappels asynchrones. Un exemple frappant est la méthode de reduce
de tout tableau.
Concurrence: semi-fibres - redémarrage
Essayons de faire quelque chose de similaire à la fibre, en utilisant uniquement les fonctionnalités qui sont disponibles pour nous dans n'importe quel navigateur moderne.
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 )
Comme vous pouvez le voir, les fonctions intermédiaires ne savent rien de l'interruption - c'est JS normal. Seule one
fonction connaît la possibilité de suspension. Pour abandonner le calcul, elle lève simplement Promise
comme exception. Sur la dernière ligne, nous exécutons la fonction four
dans une pseudo-fibre distincte, qui surveille les exceptions levées à l'intérieur, et si Promise
arrive, s'abonne à sa resolve
, puis redémarre la fibre.
Pour montrer comment fonctionnent les pseudo-fibres, nous allons écrire un code délicat.

Imaginons que la fonction step
ici écrit quelque chose sur la console et fasse un autre travail dur pendant 20 ms. Et la fonction de walk
appelle l' step
deux fois, enregistrant l'ensemble du processus. Au milieu, il montrera ce qui est maintenant affiché dans la console. Et à droite, l'état de l'arbre pseudofibre.
$ mol_fiber: pas de quantification
Exécutons ce code et voyons ce qui se passe ..

Jusqu'à présent, tout est simple et évident. L'arbre pseudo-fibre, bien sûr, n'est pas impliqué. Et tout irait bien, mais ce code est exécuté pendant plus de 40 ms, ce qui ne vaut rien.
$ mol_fiber: cache d'abord
Emballons les deux fonctions dans un wrapper spécial qui l'exécute dans une pseudo-fibre et voyons ce qui se passe.

Ici, il convient de prêter attention au fait que pour chaque lieu d'appel de la fonction à l'intérieur de la fibre de walk
, une fibre distincte a été créée. Le résultat du premier appel a été mis en cache, mais au lieu du second, Promise
été levée, car nous avions épuisé notre tranche de temps.
$ mol_fiber: cache seconde
Lancée dans la première image, Promise
sera automatiquement résolue dans la suivante, ce qui entraînera un redémarrage de la fibre de walk
.

Comme vous pouvez le voir, en raison du redémarrage, nous envoyons à nouveau «start» et «first done» à la console, mais «first begin» est déjà parti, car il est dans la fibre avec le cache rempli plus tôt, c'est pourquoi son gestionnaire est plus pas appelé. Lorsque le cache de la fibre de walk
est rempli, toutes les fibres intégrées sont détruites, car l'exécution ne les atteindra jamais.
Alors pourquoi a-t-on first begin
imprimer une fois et à en first done
deux? Tout est question d'idempotence. console.log
- opération non idempotente, combien de fois vous l'appelez, tant de fois cela ajoutera une entrée à la console. Mais la fibre qui s'exécute dans une autre fibre est idempotente, elle n'exécute le handle que lors du premier appel, et lors des retours suivants immédiatement le résultat du cache, sans entraîner d'effets secondaires supplémentaires.
$ mol_fiber: idempotence d'abord
Enveloppons console.log
dans une fibre, la rendant ainsi idempotente, et voyons comment le programme se comporte.

Comme vous pouvez le voir, maintenant dans l'arborescence des fibres, nous avons des entrées pour chaque appel à la fonction de log
.
$ mol_fiber: idempotence seconde
Au prochain redémarrage de la fibre de walk
, les appels répétés à la fonction de log
ne conduisent plus à des appels au vrai console.log
, mais dès que nous arrivons à l'exécution des fibres avec un cache vide, les appels à console.log
repris.

Veuillez noter que dans la console, nous n'affichons plus rien de superflu - exactement ce qui serait affiché en code synchrone sans fibre ni quantification.
$ mol_fiber: pause
Comment le calcul s'interrompt-il? Au début du quantum, un délai est fixé. Et avant de démarrer chaque fibre, il est vérifié si nous l'avons atteinte. Et si vous atteignez, Promise
précipite, ce qui est résolu dans le cadre suivant et commence un nouveau quantum.
if( Date.now() > $mol_fiber.deadline ) { throw new Promise( $mol_fiber.schedule ) }
$ mol_fiber: délai
La date limite pour le quantum est facile à fixer. 8 millisecondes sont ajoutées à l'heure actuelle. Pourquoi exactement 8, car il y en a jusqu'à 16 pour préparer le coup? Le fait est que nous ne savons pas à l'avance combien de temps le navigateur devra rendre, nous devons donc laisser un peu de temps pour qu'il fonctionne. Mais il arrive parfois que le navigateur n'ait rien à rendre, puis avec des quanta de 8 ms, nous pouvons insérer un autre quantum dans la même trame, ce qui donnera un emballage dense de quanta avec un temps d'arrêt minimal du processeur.
const now = Date.now() const quant = 8 const elapsed = Math.max( 0 , now - $mol_fiber.deadline ) const resistance = Math.min( elapsed , 1000 ) / 10
Mais si nous lançons une exception toutes les 8 ms, le débogage avec l'arrêt d'exception activé se transformera en une petite branche de l'enfer. Nous avons besoin d'un mécanisme pour détecter ce mode débogueur. Malheureusement, cela ne peut être compris qu'indirectement: une personne prend environ une seconde pour savoir si elle doit continuer son exécution ou non. Et cela signifie que si le contrôle n'est pas revenu au script pendant longtemps, alors soit le débogueur s'est arrêté, soit il y a eu un calcul lourd. Pour nous asseoir sur les deux chaises, nous ajoutons au quantum 10% du temps écoulé, mais pas plus de 100 ms. Cela n'affecte pas beaucoup le FPS, mais il réduit la fréquence d'arrêt du débogueur d'un ordre de grandeur en raison de la quantification.
Déboguer: essayer / attraper
Puisque nous parlons de débogage, que pensez-vous, à quel endroit de ce code le débogueur s'arrête-t-il?
function foo() { throw new Error( 'Something wrong' ) // [1] } try { foo() } catch( error ) { handle( error ) throw error // [2] }
En règle générale, il doit s'arrêter là où l'exception est levée pour la première fois, mais la réalité est qu'il ne s'arrête que là où elle a été lancée la dernière fois, ce qui est généralement très loin de l'endroit où elle s'est produite. Par conséquent, afin de ne pas compliquer le débogage, les exceptions ne doivent jamais être interceptées, via try-catch. Mais même sans exception, la manipulation est impossible.
Débogage: événements non gérés
En règle générale, un runtime fournit un événement global qui se produit pour chaque exception non interceptée.
function foo() { throw new Error( 'Something wrong' ) } window.addEventListener( 'error' , event => handle( event.error ) ) foo()
En plus d'être encombrante, cette solution présente un inconvénient tel que toutes les exceptions se trouvent ici et il est assez difficile de comprendre à partir de quelle fibre et fibre si l'événement s'est produit.
Débogage: promesse
Les promesses sont le meilleur moyen de gérer les exceptions.
function foo() { throw new Error( 'Something wrong' ) } new Promise( ()=> { foo() } ).catch( error => handle( error ) )
La fonction passée à Promise est appelée immédiatement, de manière synchrone, mais l'exception n'est pas interceptée et arrête le débogueur en toute sécurité à l'endroit de son occurrence. Un peu plus tard, de manière asynchrone, il appelle déjà le gestionnaire d'erreurs, dans lequel nous savons exactement quelle fibre a provoqué la panne et quelle panne. C'est précisément le mécanisme utilisé dans $ mol_fiber.
Trace de pile: réagir la fibre
Jetons un coup d'œil à la trace de pile que vous obtenez dans React Fibre.

Comme vous pouvez le voir, nous obtenons beaucoup d'intestin React. De l'utile ici, seuls le point d'exception et les noms des composants sont plus élevés dans la hiérarchie. Pas beaucoup.
Trace de pile: $ mol_fiber
Dans $ mol_fiber, nous obtenons une trace de pile beaucoup plus utile: pas de tripes, seulement des points spécifiques dans le code d'application à travers lesquels il est venu à une exception.

Ceci est réalisé grâce à l'utilisation de la pile native, aux promesses et à l'élimination automatique des intestins. Si vous le souhaitez, vous pouvez étendre l'erreur dans la console, comme dans la capture d'écran, et voir les tripes, mais il n'y a rien d'intéressant.
$ mol_fiber: handle
Donc, pour interrompre un quantum, Promise est lancé.
limit() { if( Date.now() > $mol_fiber.deadline ) { throw new Promise( $mol_fiber.schedule ) }
Mais, comme vous pouvez le deviner, la promesse peut être absolument n'importe quoi - pour une fibre, d'une manière générale, peu importe à quoi s'attendre: la trame suivante, l'achèvement du chargement des données ou autre chose ..
fail( error : Error ) { if( error instanceof Promise ) { const listener = ()=> self.start() return error.then( listener , listener ) }
La fibre s'abonne simplement pour tenir ses promesses et redémarrer. Mais lancer et attraper des promesses manuellement n'est pas nécessaire, car le package comprend plusieurs wrappers utiles.
$ mol_fiber: fonctions
Pour transformer n'importe quelle fonction synchrone en une fibre idempotente, il suffit de l'envelopper dans $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 ) } )
Ici, nous avons rendu console.log
idempotent, et nous avons appris à interrompre en attendant le téléchargement.
$ mol_fiber: gestion des erreurs
Mais comment répondre aux exceptions si nous ne voulons pas utiliser try-catch
? Ensuite, nous pouvons enregistrer le gestionnaire d'erreurs avec $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 nous renvoyons quelque chose de différent de l'erreur, ce sera le résultat de la fibre actuelle. Dans cet exemple, s'il n'est pas possible de télécharger la configuration depuis le serveur, la fonction getConfig
retournera la configuration par défaut.
$ mol_fiber: méthodes
Bien sûr, vous pouvez encapsuler non seulement des fonctions, mais aussi des méthodes à l'aide d'un décorateur.
import { $mol_fiber_method as action } from 'mol_fiber/web' export class Mover { @action move() { sendData( 'ya.ru' , getData( 'goo.gl' ) ) } }
Ici, par exemple, nous avons téléchargé des données de Google et les avons téléchargées sur Yandex.
$ mol_fiber: promesses
Pour télécharger des données depuis le serveur, il suffit de prendre, par exemple, la fonction asynchrone fetch
et d'un simple mouvement de poignet la transformer en synchrone.
import { $mol_fiber_sync as sync } from 'mol_fiber/web' export const getData = sync( fetch )
Cette implémentation est bonne pour tout le monde, mais elle ne prend pas en charge l'annulation d'une demande lorsqu'un arbre de fibres est détruit, nous devons donc utiliser une API
plus confuse.
$ mol_fiber: annuler la demande
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 fonction transmise à l'encapsuleur async
est appelée une seule fois et l'encapsuleur back
est transmis, dans lequel vous devez encapsuler les rappels. Par conséquent, dans ces rappels, vous devez soit renvoyer la valeur, soit lever une exception. Quel que soit le résultat du rappel, il sera également le résultat de la fibre. Veuillez noter qu'au final nous retournons une fonction qui sera appelée en cas de destruction prématurée de la fibre.
$ mol_fiber: annuler la réponse
Côté serveur, il peut également être utile d'annuler le calcul lorsque le client est tombé. midleware
un wrapper sur les midleware
qui créera une fibre dans laquelle midleware
le midleware
origine. , , , .
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 — , . — , . , , . , , , , . , . .
Les liens
Call back

: , , )
: , .
: . , .
: . , . , .
: , . , )
: , .
: - . , , .
: . , , .
: , . 16ms, ? 16 8 , 8, . , . , «».
: — . Je vous remercie!
: . , . !
: , . .
: , , , , , / , .
: , .
: .
: , . 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?
: . . , mol_fiber … , 30fps 60fps — . — .