Introduction aux Web Workers d’HTML5 : le multithreading version JavaScript

Une application HTML5 s’écrit bien entendu en JavaScript. Or, par rapport aux autres modèles de développements connus (clients lourds avec .NET/C++ ou même avec Silverlight), JavaScript dispose historiquement d’une limitation importante : toute son exécution s’effectue dans un seul et même thread. C’est plutôt ballot à l’heure des processeurs multi-cœurs comme les Core i5/i7 proposant jusqu’à 8 cœurs logiques voire même les derniers processeurs mobiles ARM double voire quadri-cœurs. Heureusement, nous allons voir qu’HTML5 nous propose une solution pour mieux exploiter ces nouvelles petites puces.

Illustration : the Web Workers in action

Exposé du problème

Cette limitation historique de JavaScript implique qu’un traitement important va bloquer la fenêtre principale d’affichage (la page web en cours d’utilisation). On dit souvent dans le jargon des développeurs que l’on bloque ainsi le “UI Thread”, le thread qui est dédié à l’affichage et au rafraichissement de celui-ci. Nous connaissons tous les conséquences d’une telle mauvaise pratique : la page se fige et l’utilisateur ne peut plus interagir avec l’application. Conséquence : l’expérience utilisateur est bien entendue très mauvaise et ce dernier finit naturellement par tuer l’onglet ou l’instance du navigateur en cours. Cela n’était surement pas le résultat que vous souhaitiez obtenir !

Par ailleurs, les navigateurs disposent d'un système de protection pour avertir l’utilisateur qu’un script prends trop de temps à s’exécuter. Malheureusement, ce dernier ne différentie pas les cas où le script est effectivement mal écrit des cas où le script a réellement besoin de temps pour finir son traitement. Sauf que comme il bloque le thread d’affichage, on préfère vous prévenir avec ce genre de messages (issus de Firefox 5 et IE9) :

WarningJSBrowsers 

20110419-hrii-image5

Cependant, jusqu’à présent, rares étaient les cas où cela nous posait vraiment problème pour 2 raisons :

1 – HTML et JavaScript n’étaient pas utilisés de la même manière et dans le même but que les technologies capables d’effectuer des traitements multi-threadés. Les sites Web proposaient une expérience beaucoup moins riches que les applications de bureau.
2 – Il existait des techniques permettant plus ou moins de s’en sortir autrement.

Ces techniques sont connues de tous les développeurs Web qui se débrouillent ainsi pour simuler des traitements parallèles grâce aux fonctions setTimeout() ou setInterval() par exemple. De la même manière, les appels HTTP se font de manières asynchrones via l’objet XMLHttpRequest, ce qui évite ainsi de bloquer l’UI pendant le téléchargement de certaines ressources. Pour finir, l’utilisation des évènements du DOM permet d’écrire des applications donnant l’illusion que plusieurs choses se passent en même temps. Illusion ? Oui, pour le comprendre regardons un exemple de code scolaire et tentons de comprendre ce qu’il se passe dans le navigateur :

 <script type="text/javascript">
    function init(){
        { un code qui prends 5 ms à être exécuté } 
        un évènement de type mouseClickEvent est levé
        { un code qui prends 5 ms à être exécuté }
        setInterval(timerTask,"10");
        { un code qui prends 5 ms à être exécuté }
    }

    function handleMouseClick(){
          un code qui prends 8 ms à être exécuté 
    }

    function timerTask(){
          un code qui prends 2 ms à être exécuté 
    }
</script> 

Prenons ce code et projetons le sur un diagramme chargé de modéliser l’exécution dans le temps :

Diagram illustrating single-threaded JavaScript

On voit bien sur ce diagramme la nature non-parallèle des traitements. En fait, le navigateur se contente d’enfiler dans une queue les différentes demandes d’exécution de code :

- de 0 à 5 ms : la fonction init() démarre en effet à 0 ms et commence par un traitement de 5 ms. Au bout de 5 ms, l’utilisateur déclenche un clic de souris. Cependant, la gestion de cet évènement ne peut être gérée car nous sommes toujours dans le bloc de la fonction init() en train d’accaparer le thread principal. La demande de clic de souris est conservée mais sera gérée plus tard.

- de 5 à 10 ms : la fonction init() continue son traitement pendant 5 ms puis on demande à exécuter la fonction timerTask() dans 10 ms. Cette fonction devrait donc logiquement être exécutée au à 20 ms sur l’échelle du temps.

- de 10 à 15 ms : s’en suivent 5 nouvelles millisecondes de traitement pour terminer init() ce qui constitue alors le 1er bloc jaune ininterrompu de 15 ms. Comme on libère le thread, ce dernier dépile alors les demandes.

- de 15 à 23 ms : il commence par exécuter la fonction handleMouseClick() qui dure 8 ms (le bloc bleu).

- de 23 à 25 ms : du coup, la fonction timerTask() , qui était programmée pour être exécutée sur la fenêtre des 20 ms sur l’échelle du temps, est légèrement décalée de 3 ms pour ensuite reprendre la programmation initiale (30 ms, 40 ms, etc.) puisqu’il n’y a pas de code qui reprend du CPU.

Note : cet exemple et le graphique ci-dessus (en SVG ou PNG par “feature detection”) a été inspiré de l’article suivant : HTML5 Web Workers Multithreading in JavaScript

Donc au final, toutes ces techniques ne répondent pas réellement au problème initial : tout s’exécute malgré tout dans le thread principal.

Par ailleurs, même si JavaScript n’était pas utilisé dans le même but que les technologies à base de langages dit de “haut niveau”, cela est entrain de changer ces derniers temps par les possibilités offertes par HTML5 et ses amis. Il a fallut donc doter JavaScript de nouvelles capacités pour le préparer à la réalisation d’applications plus riches et réellement parallèles. C’est justement le but des Web Workers.

Web Workers ou comment sortir de l’UI Thread

Les APIs des Web Workers définissent justement un moyen d’exécuter des scripts en tâche de fond. Cela va donc vous permettre d’exécuter des traitements sur des threads séparés vivant donc à côté de la page principale et n’ayant pas d’impact sur ses performances d’affichage. Cependant, de la même manière que tous les algorithmes ne sont pas forcément parallèlisables, nous verrons que tout ne se prête pas à l’exécution via des workers. Allez, trêves de blabla. Regardons comment marche tout ça.

Mon 1er Web Worker

Comme les Web Workers vont être exécutés sur des threads séparés, il faut que leur code soit hébergé dans un fichier séparé de la page principale. Ensuite, pour les appeler, on instancie un objet de type Worker :

 var monWorker = new Worker('lesuperworker.js');

Puis, on démarre le worker (et donc un thread sous Windows), en lui envoyant un premier message :

 monWorker.postMessage();

En effet, la manière de communiquer entre le Worker et la page principale se fait via l’utilisation de messages. Ces messages doivent être formés par des chaines de caractères classiques ou via des objets JSON. Pour illustrer le passage de messages, on va commencer par faire simple en envoyant une chaine au worker qui va simplement la concaténer avec autre chose. Pour cela, insérez ce code dans “helloworkers.js” :

 function messageHandler(event) {
    // On récupère le message envoyé par la page principale
    var messageSent = event.data;
    // On prépare le message de retour
    var messageReturned = "Bonjour " + messageSent + " depuis un thread séparé !";
    // On renvoit le tout à la page principale
    this.postMessage(messageReturned);
}

// On définit la fonction à appeler lorsque la page principale nous sollicite
// équivalent à this.onmessage = messageHandler;
this.addEventListener('message', messageHandler, false);

Nous avons donc défini dans “helloworkers.js” un code qui doit s’exécuter dans un autre thread. Il est capable de recevoir un message depuis notre page principale, de le traiter puis d’en renvoyer un autre en retour. Il faut maintenant écrire son pendant dans la page principale justement. Voici la page en question :

 <!DOCTYPE html>
<html>
<head>
    <title>Hello Web Workers</title>
</head>
<body>
    <div id="output"></div>

    <script type="text/javascript">
        // On instantie le Worker
        var monWorker = new Worker('helloworkers.js');
        // On se prépare à traiter le message de retour qui sera
        // renvoyé par le worker
        monWorker.addEventListener("message", function (event) {
            document.getElementById("output").textContent = event.data;
        }, false);

        // On démarre le worker en lui envoyant un 1er message
        monWorker.postMessage("David");

        // On stoppe le worker via la commande terminate()
        monWorker.terminate();
    </script>
</body>
</html>

Le résultat sera bien : “Bonjour David depuis un thread séparé ! ”. Ca vous épate hein ? Clignement d'œil

Le worker va ensuite vivre sa vie tant que vous ne l’aurez pas tué. Par ailleurs, l’instanciation d’un worker a un cout non négligeable en temps de démarrage puis ensuite en consommation mémoire. Ils ne sont pas non plus automatiquement “garbage collectés”, c’est donc à vous de contrôler leurs états. Pour stopper le worker, il y a 2 façons de faire :

1 – depuis la page principale en appelant la commande terminate() .

2 – depuis le worker lui-même en appelant la commande close()

DEMO : Vous pouvez tester ce même exemple légèrement plus étoffé ici -> https://david.blob.core.windows.net/html5/HelloWebWorkers.htm <-

Echanges à travers JSON

Bien entendu, nous allons la plupart du temps envoyer des données à faire traiter par le worker. Les Web Workers peuvent également discuter entre eux à travers les Message channels. L’unique manière pour structurer les échanges consiste alors à utiliser le format JSON. Heureusement, les navigateurs supportant actuellement les Web Workers ont le bon gout également de supporter JSON de manière native.

Reprenons ainsi notre exemple précédent. Nous allons construire un objet de type WorkerMessage qui va nous servir à envoyer des commandes à notre Web Worker accompagnées d’un paramètre. Prenons ainsi la page web suivante simplifiée HelloWebWorkersJSON.htm :

 <!DOCTYPE html>
<html>
<head>
    <title>Hello Web Workers JSON</title>
</head>
<body>
    <input id=inputForWorker /><button id=btnSubmit>Envoyer au worker</button><button id=killWorker>Stopper le worker</button>
    <div id="output"></div>

    <script src="HelloWebWorkersJSON.js" type="text/javascript"></script>
</body>
</html>

On utilise ici la méthode dite d’Unobtrusive JavaScript permettant de séparer notre vue de la logique associée derrière. Voici alors le script HelloWebWorkersJSON.js :

 // HelloWebWorkerJSON.js associé à HelloWebWorkersJSON.htm

// Notre objet WorkerMessage sera automatiquement
// sérialisé puis désérialisé via le parseur JSON natif
function WorkerMessage(cmd, parameter) {
    this.cmd = cmd;
    this.parameter = parameter;
}

// Le div où sera affiché les messages en retour du worker
var _output = document.getElementById("output");

/* Vérifie si les Web Workers sont supportés */
if (window.Worker) {
    // On récupère les instances de nos 3 éléments HTML restant
    var _btnSubmit = document.getElementById("btnSubmit");
    var _inputForWorker = document.getElementById("inputForWorker");
    var _killWorker = document.getElementById("killWorker");

    // On instantie le Worker
    var monWorker = new Worker('helloworkersJSON.js');
    // On se prépare à traiter le message de retour qui sera
    // renvoyé par le worker
    monWorker.addEventListener("message", function (event) {
        _output.textContent = event.data;
    }, false);

    // On démarre le worker en lui envoyant la commande 'init'
    monWorker.postMessage(new WorkerMessage('init', null));

    // On branche l'évènement click sur le bouton Submit
    // pour envoyer le contenu de l'input au worker
    _btnSubmit.addEventListener("click", function (event) {
        // On envoit désormais les messages via la commande 'hello'
        monWorker.postMessage(new WorkerMessage('hello', _inputForWorker.value));
    }, false);

    // On branche l'évènement click sur le bouton Kill
    // pour stopper le worker. Il ne sera plus utilisable après
    _killWorker.addEventListener("click", function (event) {
        // On aurait pu créer une commande 'stop' qui aurait été traitée
        // au sein du worker qui se serait fait hara-kiri via .close()
        monWorker.terminate();
        _output.textContent = "Le worker a été stoppé.";
    }, false);
}
else {
    _output.innerHTML = "Les Web Workers ne sont pas supportés par votre navigateur. 
    Réessayez avec IE10 : <a href=\"https://ie.microsoft.com/testdrive\">téléchargez la dernière Platform Preview d'IE10 </a>";
}

Pour finir voici le nouveau code du worker contenu dans helloworkerJSON.js :

 function messageHandler(event) {
    // On récupère le message envoyé par la page principale
    var messageSent = event.data;

    // On teste la commande envoyée
    switch (messageSent.cmd) {
        case 'init':
            // On peut initialiser certaines parties de nos objets qui serviront
            // dans ce worker (attention au scope cependant !)
            break;
        case 'hello':
            // On prépare le message de retour
            var messageReturned = "Bonjour " + messageSent.parameter + " depuis un thread séparé !";
            // On renvoit le tout à la page principale
            this.postMessage(messageReturned);
            break;
    }
}

// On définit la fonction à appeler lorsque la page principale nous sollicite
this.addEventListener('message', messageHandler, false);

A nouveau cet exemple est extrêmement simple. Mais je pense que vous avez désormais compris le principe. Par exemple, rien ne vous empêche désormais d’envoyer à un worker des éléments d’un jeu qui devront être mis à jour par un moteur d’IA ou physique fonctionnant au sein d’un Web Worker.

DEMO : Vous pouvez tester ce même exemple ici -> https://david.blob.core.windows.net/html5/HelloWebWorkersJSON.htm <-

Support dans les navigateurs browserslogos

Le support des Web Workers vient d’arriver dans la PP2 (Platform Preview) d’IE10 et cela fonctionne également sous Firefox (depuis la 3.6), Safari (depuis la 4.0), Chrome et Opera 11. Attention : cela n’est pas encore supporté par les versions mobiles de ces mêmes navigateurs. Si vous voulez un tableau plus précis, rendez-vous sur CanIUse : https://caniuse.com/#search=worker

Pour savoir dynamiquement dans votre code si cette fonctionnalité est supportée par le navigateur, je vous rappelle que désormais la bonne pratique consiste à faire du “feature detection”. Ne faites plus de tests sur le user-agent, c’est le mal !

Pour cela, 2 techniques. Soit vous utilisez la très célèbre librairie Modernizr (que l’on trouve nativement dans les projets ASP.NET MVC3) via un code similaire à celui-ci :

 <script type="text/javascript">
    var leDivWebWorker = document.getElementById("webWorkers");
    if (Modernizr.webworkers) {
        leDivWebWorker.innerHTML = "Les Web Workers sont supportés";
    }
    else {
        leDivWebWorker.innerHTML = "Les Web Workers ne sont PAS supportés";
    }
</script>

Et le résultat avec votre navigateur actuel est le suivant :

Soit vous testez vous-même la feature en utilisant en testant simplement ce booléen :

 /* Vérifie si les Web Workers sont supportés */
if (window.Worker) {
   // Code utilisant les Web Workers
 }

Cela vous permettra par détection d’offrir une version plus performante aux navigateurs les plus récents tout en conservant l’application fonctionnelle pour ceux ne supportant pas encore cette fonctionnalité.

Les éléments non accessibles depuis un worker

Plutôt de voir à quoi vous n’avez pas accès depuis un worker, regardons plutôt à quoi vous avez uniquement accès :

Méthode Description
void close(); Stop le worker thread.
void importScripts(urls); Une liste de fichiers JavaScript additionnels à importer séparés par des virgules.
void postMessage(data); Envoies un message depuis et vers un worker thread.
Attributs Type Description
location WorkerLocation Représente une URL absolue, incluant les composants protocol, host, port, hostname, pathname, search, and hash.
navigator WorkerNavigator Représente l'identité et l'état onLine du navigateur actuel.
self WorkerGlobalScope La portée totale du worker, ce qui inclus donc les objets WorkerLocation et WorkerNavigator.
Evènement Description
onerror Une erreur s'est produite pendant l'exécution.
onmessage Un message contenant des données a été reçu.
Méthode Description
void clearInterval(handle); Annule un traitement programmé identifié par le handle fournit en paramètre.
void clearTimeout(handle); Annule un traitement programmé identifié par le handle fournit en paramètre.
long setInterval(handler, valeur de timeout, arguments); Programme un traitement devant être exécuté de manière répétitive après le nombre indiqué de millisecondes. Retourne un handle. Vous pouvez alors l'annuler avec clearInterval.
long setTimeout(handler, valeur de timeout, arguments); Programme un traitement devant être exécuté après le nombre indiqué de millisecondes. Retourne un handle. Vous pouvez alors l'annuler avec clearInterval.

 

Vous n’avez ainsi pas du tout accès au DOM. Cela est également résumé à travers ces 2 diagrammes :

20110701-wwiibjmwaf-image2

Ainsi, si vous vous posiez la question, comme l’objet window n’est pas accessible, vous n’avez pas accès au Local Storage depuis un worker. Ce fonctionnement limité peut paraitre parfois trop contraignant pour ceux qui ont l’habitude des opérations multithréadées dans d’autres environnements. Cependant, il a l’immense avantage du coup de ne pas récupérer tous les problèmes inhérents aux partages de ressources : lock, races conditions, etc. Cela rend ainsi les web workers particulièrement accessibles à tous tout en permettant une augmentation des performances dans certains scénarios plus ciblés.

Gérer les erreurs et le debugging

Pour gérer les erreurs levées depuis votre Web Worker, rien de plus simple. Il suffit de s’abonner à l’évènement OnError de la même manière que nous l’avons fait avec OnMessage :

 monWorker.addEventListener("error", function (event) {
    _output.textContent = event.data;
}, false);

Ensuite, à vous de vous débrouiller… Bon, avouons le, cela n’est pas super comme pratique comme moyen de debug non? On est d’accord.

La barre de développement F12 pour le debug

Justement, IE10 vous propose la possibilité de débugger directement le code de vos Web Workers comme avec n’importe lequel de vos scripts.

Pour cela, lancez la barre de développement avec la touche F12 et rendez-vous sur l’onglet “Script”. Au début, vous ne verrez pas votre fichier JS correspondant à votre worker apparaitre. Il suffit alors de presser le bouton “Start debugging” pour le voir apparaitre :

F12DebugWorker001

Ensuite, débuggez votre worker comme vous avez l’habitude de le faire !

F12DebugWorker002

Une solution pour avoir console.log()

Pour finir, il faut savoir que l’objet console n’est pas disponible au sein d’un worker. Ainsi, si vous souhaitez écrire ce qu’il se passe dans votre worker via la méthode .log() , cela ne va pas fonctionner car l’objet console ne sera pas définit. Pour palier à cela, j’ai trouvé cet excellent exemple qui se propose de simuler console.log() en faisant appel aux MessageChannel : console.log() for Web Workers . Cela ne fonctionne donc que dans IE10, Chrome et Opera 11.50. Firefox ne supportant pas encore les MessageChannel.

Note : pour que cela fonctionne bien dans IE10, il faut changer cette ligne de code :

 console.log.apply(console, args); // Pass the args to the real log

Par celle-ci :

 console.log(args); // Pass the args to the real log

Ensuite, voici un exemple de résultat en action :

F12DebugConsole

DEMO : Vous pouvez tester ce même exemple ici -> https://david.blob.core.windows.net/html5/HelloWebWorkersJSONdebug.htm <-

Scénarios d’usages et comment identifier le code candidat

Web Workers pour quels usages ?

Lorsque l’on parcours la toile à la recherche d’exemples pertinents sur l’utilisation des Web Workers, on tombe toujours sur la même chose : des démonstrations de calculs mathématiques réellement intensifs gérés par les web workers. On retrouve ainsi des raytracers, des calculs de fractales, de nombres premiers, de l’âge de chacun des capitaines de chacune des étoiles de l’univers, etc. Bref, de belles démos technologiques mais peu de perspectives concrètes sur les applications dites du “monde réel”.

Il est vrai que les limitations que fixent les Web Workers sur les ressources disponibles amenuisent potentiellement d’autant les scénarios. Cependant, il suffit juste de se creuser un peu la tête pour rapidement voir émerger des choses intéressantes :

- traitement d’image en utilisant les données issues de l’élément <canvas> ou <video> : on peut alors diviser les zones de traitement pour les fournir à différents workers qui bosseront en parallèle. On bénéficie ici des processeurs multi-cœurs. Plus nous aurons de cœurs, plus le traitement sera rapide.

- traitement de gros lot de données ramenées par un appel via XMLHTTPRequest : cela alors évite tout simplement de le faire dans le thread principal et donc permet à l’application d’être réactive au près de l’utilisateur.

- analyse de texte en arrière-plan : le fait d’avoir davantage de CPU disponible permet d’envisager de nouveaux scénarios comme le traitement temps réel de ce que tape l’utilisateur sans pénaliser à nouveau l’interface principale. Imaginez alors une version Word (comme celle issue des Office Web Apps) en ligne pouvant bénéficier de ce scénario : recherche dynamique dans un dictionnaire en arrière-plan pour une aide à la saisie, correction automatique, etc.

- requêtes multiples et concurrentes vers une base de données locale : IndexDB permettra ce que le local storage ne peut nous fournir : un environnement thread-safe pour les web workers.

Par ailleurs, dans le domaine du jeu vidéo, on peut envisager de basculer le moteur d’IA ou le moteur physique vers des web workers. Il y a par exemple cette expérimentation qui a été faite : On Web Workers, GWT, and a New Physics Demo qui propose un basculement du moteur Box2D vers les workers. Dans le cas de l’IA, cela veut aussi dire que le même laps de temps, on peut envisager d’anticiper davantage de choses (des coups dans le cas d’un jeu d’échecs par exemple).

Ensuite certains diront que les seules limites se résument à votre propre imagination ! Clignement d'œil 

Mais d’une manière générale, dès l’instant que le DOM n’est pas touché, tout traitement JavaScript relativement long et pouvant pénaliser la fluidité et l’expérience utilisateur méritera d’être un candidat aux web workers. Il faudra cependant être prudent à 3 choses :

1 – A ce que le temps d’initialisation et de communication vers le worker ne soit pas supérieur au temps de traitement en lui-même

2 – A bien faire attention également à la mémoire prise supplémentaire

3 – A la dépendance des morceaux de code entre eux et donc des besoins de synchro. La parallelisation n’est pas une science simple mes amis !

De notre côté, nous avons publié récemment la démo nommée Web Workers Fontains :

Web Workers Fountains

Cette dernière calcule des effets de particules (les fontaines) et utilise 1 web worker par fontaine pour tenter de calculer le plus rapidement possible les effets de particules. Le résultat de chacun des workers est ensuite agrégé pour être affiché par le canvas. Les Web Workers peuvent ensuite éventuellement communiquer entre eux via les Message Channels pour se synchroniser sur la couleur actuellement retenue pour peintre chacune des fontaines. N’hésitez pas à aller jouer avec sous Internet Explorer 10, c’est rigolo ! Sourire

Comment identifier les points chauds dans son code

Pour détecter les goulots d’étranglement et savoir éventuellement quelles parties de votre code envoyer aux web workers, vous pouvez utiliser le profileur de script intégré à IE9/10 avec la barre de développement accessible via la touche F12. Cela va vous permettre d’identifier les points chauds. Mais cela ne veut pas dire pour autant que les zones identifiées seront de bons candidats. Pour mieux le comprendre, faisons le test à travers 2 exemples.

Exemple 1 : animation dans <canvas> avec Speed Reading

Cette démo est issue de notre centre IE Test Drive qui peut être vue ici :  Speed Reading. Elle s’occupe d’essayer d’afficher le plus rapidement possible des lettres en utilisant <canvas>. Cette démo a pour but de mettre en valeur la qualité de l’accélération matérielle de votre navigateur. Mais est-il possible de gratter encore un peu plus de performance en splittant certains traitements sur des threads? Cela mérite analyse.

Justement, si on la lance dans IE9/10, on peut utiliser le profileur de script pendant quelques secondes. Voici ce que l’on obtient :

F12ProfilingSpeedReading

Si l’on trie les fonctions consommant le plus de temps de manière décroissante, on voit clairement ressortir les fonctions suivantes : DrawLoop() , Draw() et drawImage() . Si on double-clique sur Draw, on saute dans le code de la méthode et on observe alors de nombreux appels de ce type :

 surface.drawImage(imgTile, 0, 0, 70, 100, this.left, this.top, this.width, this.height); 

Sachant que l’objet surface référence un élément de type <canvas>.

Au final, la conclusion de cette analyse rapide est que cette démo passe son temps à faire du dessin via la méthode drawImage() de l’élément Canvas. Or, cet élément n’est pas accessible depuis un web worker donc on ne va pas pouvoir sous-traiter ces opérations à différents threads. On aurait pu imaginer en effet gérer l’élément Canvas de manière concurrente. Cette démo n’apparait donc pas comme un bon candidat à la parrallélisation.

Mais elle illustre bien la démarche à mettre en place. Si après un profiling, vous découvrez que la partie la plus consommatrice de temps CPU utilise des éléments du DOM, les web workers ne pourront pas vous aider à augmenter les performances.

Exemple 2 : Raytracer dans <canvas>

Prenons maintenant l’exemple typique facile à comprendre, un raytracer comme celui-ci : Flog.RayTracer Canvas Demo . Un raytracer met en place des calculs mathématiques consommant énormément de CPU dans le but de simuler le trajet effectué par des rayons dont on simule le lancé. On calcule ainsi les effets de réflexions, diffraction, de matière, etc. 

Lançons le calcul d’une scène tout en lançant en parallèle le profileur de script et observons le résultat :

F12ProfilingRayTracer

A nouveau si l’on trie les fonctions appelées de manière décroissante, on voit clairement 2 choses ressortir : renderScene() et getPixelColor().

getPixelColor() a pour but de calculer la couleur du pixel courant. En effet, par ray-tracing , une scène est dessinée pixel par pixel. Cette méthode appelle ensuite la méthode rayTrace() chargée de calculer les ombres, la lumière ambiante, etc. C’est le cœur même de notre application. Et justement en analysant le code de cette méthode rayTrace() , on s’aperçoit que c’est du JavaScript 100% pur jus. Aucune dépendance vers le DOM n’y est présente. Bref, vous l’aurez compris, c’est un excellent candidat à la parallélisation. D’autant plus que l’on peut facilement répartir le calcul de l’image sur différents threads (et donc potentiellement différents processeurs) car il n’y pas de phénomène de synchronisation entre le calcul de chacun des pixels. Chaque opération est indépendante du résultat du voisin (il n’y pas d’anti-aliasing dans cette démo).

D’ailleurs, ce n’est ainsi pas un hasard si l’on retrouve des exemples de ray-tracers mettant en œuvre les web workers comme ici par exemple : https://nerget.com/rayjs-mt/rayjs.html 

Si on analyse ce dernier exemple au sein d’IE10 avec le profileur, on observe très bien la différence entre l’utilisation d’aucun web worker et de 4 web workers :

RayTracersWebWorkersIE10

On voit ainsi sur le résultat du profiling que dans le 1er cas, la méthode processRenderCommand() sature à elle seul l’ensemble du CPU et l’on obtient alors le résultat en 2,854s.

Avec 4 web workers, on voit très bien la méthode processRenderCommand() s’exécuter en parallèle sur 4 différents threads (dont on peut même voir les Worker Id dans la colonne de droite). On obtient alors le même résultat en 1,473s. Le bénéfice a donc été réel : un traitement 2 fois plus rapide.

Conclusion

Il n’y a pas de magie ou de nouveautés supplémentaires introduites par les web workers dans la démarche à entreprendre pour tenter de paralléliser un code. Il faut isoler un traitement long, indépendant du reste de la vie de notre page pour éviter d’avoir à attendre le résultat pour continuer et surtout n’ayant aucun besoin d’accès au DOM. Si vous tombez dans ce cas, pensez aux web workers, ils pourraient vous aider à augmenter les performances générales de votre application !

Ressources complémentaires

Si vous lisez l’anglais, voici des liens qui m’ont paru intéressants à lire et que je vous conseille donc :

- La spécification officielle du W3C : https://www.w3.org/TR/workers/

- Web Workers in IE10: Background JavaScript Makes Web Apps Faster : https://blogs.msdn.com/b/ie/archive/2011/07/01/web-workers-in-ie10-background-javascript-makes-web-apps-faster.aspx

- The Basics of Web Workers : https://www.html5rocks.com/en/tutorials/workers/basics/

- La documentation issue du MDN : https://developer.mozilla.org/En/Using_web_workers

- An Introduction to HTML 5 Web Workers : https://cggallant.blogspot.com/2010/08/introduction-to-html-5-web-workers.html

- A Deeper Look at HTML 5 Web Workers : https://cggallant.blogspot.com/2010/08/deeper-look-at-html-5-web-workers.html

- Une très belle illustration des Web Workers : https://wearehugh.com/public/2010/08/html5-web-workers/ (j’adore le concept !)

Au boulot travailleurs du web !

David