Multi-threading côté client

Web workers

Lionel Médini

M1IF13 - Programmation Web avancée et mobile

Université Claude Bernard Lyon 1

cc-by-nc-sa

Position du problème

Faire faire plusieurs “trucs” à un navigateur “en même temps”

  • répondre aux actions utilisateur
  • envoyer une requête et traiter la réponse
  • faire un calcul compliqué et afficher les résultats au fur et à mesure

Rappels

Fonctionnement de JavaScript

  • Single-threaded
  • Event loop

Single thread

Moteur JS interne à un navigateur

→ Modèle d'exécution assez simple

Event loop

Principes de base d’un moteur JS (non optimisé)

Composants

  • Heap (tas) : région mémoire non structurée où les objets sont alloués
  • Stack (pile) : gère une pile de frames (appels de fonctions à traiter)
    • fonction
    • arguments
    • variables locales
  • Message queue (file) : liste de messages à traiter
    • Chaque message correspond à un appel de fonction (callback)

Event loop

Principes de base d’un moteur JS (non optimisé)

Fonctionnement de la queue

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

Traitement d'un message

  • déclenché quand la pile est vide
  • dans l'ordre d'arrivée des messages
  • création d'un frame dans la pile
  • “Run-to-completion”

Ajout de message

  • déclenché par un event listener
    • rappel : un événement sans listener est perdu (ex : click)

Event loop

Exemple 1 :

function timeoutLog() {
  console.log("Fin timeout");
}

function exemple1() {
  setTimeout(timeoutLog, 0);
  console.log("Fin test"); 
}

Exécution

Remarque : c'est la raison pour laquelle le deuxième argument de setTimeout (et setInterval) est une durée minimale et pas garantie…

Event loop

Exemple 2 :

monBouton.addEventListener('click', function () { console.log('Cliqué'); });

function exemple2() {
  let res = 0;
  for(let i=0; i < 1000000000; i++) {
    //calcul quelconque qui prend du temps mais n'ajoute rien à la pile
    res += 1 / (i * i);
  }
  console.log("Fini de calculer"); 
}

Exécution

Concurrence en single-thread

Plusieurs approches

  • Programmer en asynchrone
    • callbacks
    • promesses
  • Transformer du code synchrone en asynchrone
    • setTimeout, setInterval
    • async, defer

Dans tous les cas

  • L'ordre d'exécution est l'ordre d'arrivée des events dans la queue
  • On ne récupère les résultats qu'à la fin de l'exécution du script

Web workers

Objectifs

  • Permettre l'exécution de scripts
    • en tâche de fond dans un navigateur
    • indépendamment de l'interface utilisateur

Intérêt

  • L'utilisation de Web workers est indiquée si :
    • Un script
      • est consommateur de ressources et pourrait bloquer l'interface utilisateur
      • se déroule pendant une longue partie de l'utilisation de la page
    • Plusieurs scripts doivent fonctionner en parallèle

Aspects techniques

Les Web workers

  • possèdent leurs propres message queue, stack & heap
  • n'ont Pas d'accès au DOM de la page
  • ne sont pas interruptibles par des événements de l'interface utilisateur
  • communiquent avec la page principale à l'aide de messages

Limitations

  • L'utilisation de workers est coûteuse en ressources
    • Mémoire par instance
    • Puissance de calcul à la création
  • L'utilisation de Web workers n'est pas indiquée si :
    • Vos scripts doivent accéder à de nombreux éléments de la page
    • Vous devez faire tourner beaucoup de scripts en parallèle
    • Vos scripts ont une durée de vie courte

"For example, it would be inappropriate to launch one worker for each pixel of a four megapixel image."

Deux spécifications

  • Dedicated workers
    • Liés au script qui les a créés
    • Communiquent avec leur script “père” en utilisant un message port
  • Shared workers
    • Peuvent être partagés par tous les scripts de la même origine
    • Peuvent être créés avec un nom
    • N'importe quel autre script de la même origine peut obtenir une référence à un shared worker à l'aide de son nom ou de son URL
    • Communiquent avec d'autres scripts en utilisant des message ports

Communication avec un Web worker

  • Un MessageChannel est implicitement créé lors de l'instanciation / la connexion à un worker
  • Un MessagePort permet de communiquer de chaque côté
  • L'envoi de messages à travers ce channel est standardisée par l'interface Message

Exemple :

worker.onmessage = function (event) { ... };

worker.postMessage({localObject //to be cloned}, [buffer //to be transfered]);

Voir cours Web messaging pour plus de détails

Dedicated workers

Exemple

<body>
  <p>The highest prime number discovered so far is:
    <output id="result"></output>
  </p>
  <script>
    var worker = new Worker('worker.js');
    worker.onmessage = function (event) {
      document.getElementById('result').textContent = event.data;
    };
  </script>
</body>

Exemple

    • Le worker calcule les nombres premiers et envoie un message à l'interface à chaque résultat trouvé
let n = 1;
search: while (true) {
  n += 1;
  for (var i = 2; i <= Math.sqrt(n); i += 1)
    if (n % i == 0)
     continue search;
  // found a prime!
  postMessage(n);
}

Spécification

L'interface WebIDL Worker (dedicated worker)

  • Hérite de EventTarget (interface DOM de gestion d'événements)
  • Implémente AbstractWorker (commune aux deux types de workers mais ne contient qu'une méthode onerror)
  • Définit l' “extérieur” d'un dedicated worker (la façon dont il est créé par un autre script)
  • Constructeur
    • Worker(DOMString scriptURL [, WorkerOptions options]) : crée un dedicated worker si l'URL correspond à un script valide
      • Les options permettent de spécifier un type d'importation de dépendances, un mode de gestion des credentials ou un nom
  • Attributs
    • onmessage : permet d'associer un handler d'événements “simple” au worker
  • Méthodes
    • terminate() : supprime les tâches de la file, arrête le script et vide la file de messages
    • postMessage() : envoie des données (chaînes de caractères, objets…) au worker

Cycle de vie

  • Création
    • Utilisation du constructeur de l'objet JavaScript
    • Paramètre
      • URL relative du fichier JS contenant le script
        nécessite un serveur Web pour vérifier qu'il provient bien de la même origine
var worker = new Worker('task.js');

ou

var worker = new Worker('task.js', { type: "module" });
  • Destruction
  • Depuis le script “père” : worker.terminate()
  • Depuis le worker : self.close() (cf. interface DedicatedWorkerGlobalScope)

Communication

Avec un worker depuis le script “père”

  • Récupération de données

avec onmessage

worker.onmessage = function (event) {
  ... //Récupération des données avec event.data 
};

avec addEventListener()

worker.addEventListener('message', function(event) { ... }, false);
  • Envoi de données
worker.postMessage('Hello World');

Communication

Avec le script “père” depuis un worker

  • Récupération de données

avec onmessage

onmessage = function (event) {
  ... //Récupération des données avec event.data 
};

avec addEventListener()

addEventListener('message', function(event) { ... }, false);
  • Envoi de données
postMessage('Hello World');

Communication

Avec le script “père” depuis un worker

  • Remarques
    • l'utilisation de onmessage et postMessage() sous-entend la référence à l'objet this (propriétés de l'objet Worker)
    • On peut aussi y accéder par le scope du worker en utilisant l'objet self : self.onmessage et self.postMessage()

Shared workers

Exemple

<pre id="log">Log:</pre>
<script>
  const worker = new SharedWorker('test.js');
  const log = document.getElementById('log');
  worker.port.addEventListener('message', function(e) {
    log.textContent += '\n' + e.data;
  }, false);
  worker.port.start();
  worker.port.postMessage('ping');
</script>
<iframe src="inner.html"></iframe>

Exemple

<pre id=log>Inner log:</pre>
<script>
  const worker = new SharedWorker('test.js');
  const log = document.getElementById('log');
  worker.port.onmessage = function(e) {
    log.textContent += '\n' + e.data;
  };
</script>

Exemple

let count = 0;
onconnect = function(e) {
  count += 1;
  var port = e.ports[0];
  port.postMessage('Hello World! You are connection #' + count);
  port.onmessage = function(e) {
    port.postMessage('pong');
  }
}

Spécification

L'interface WebIDL SharedWorker

  • Hérite de EventTarget (interface DOM de gestion d'événements)
  • Implémente AbstractWorker (commune aux deux types de workers mais ne contient qu'une méthode onerror)
  • Définit l' “extérieur” d'un shared worker (la façon dont il est créé par un autre script)
  • Constructeur
    • SharedWorker(DOMString scriptURL, optional DOMString name) : crée un shared worker correspondant au nom name si l'URL correspond à un script valide
  • Attributs
    • port : renvoie le MessagePort associé au worker ; toutes les opérations de gestion (start() et close()) et de communication (onmessage() et postMessage()) sont faites à partir de cet objet

Cycle de vie

Création d'un shared worker

  • Utilisation du constructeur de l'objet JavaScript
  • Paramètres
    • URL relative du fichier JS contenant le script
      • nécessite un serveur Web pour vérifier qu'il provient bien de la même origine
    • String (optionnel) donnant un nom au worker
var worker = new Worker('task.js', 'worker1');

Cycle de vie

Création d'un shared worker

  • Remarques
    • Cas de plusieurs appels au même constructeur depuis des scripts différents
      1. Le premier appel crée le worker et le MessagePort associé
      2. Les suivants pointeront sur la même instance du worker et créent un nouveau MessagePort
    • Démarrage du MessagePort depuis le script appelant
      • Nécessaire uniquement par connexion avec addEventListener
worker.port.start();

Cycle de vie

Fermeture des ports

  • Utilisation de la méthode close() du MessagePort
  • Depuis le script appelant
worker.port.close();
  • Depuis le worker
var port = e.ports[0];
port.close();

Échange de messages

  • Côté script appelant
worker.port.onmessage;
... 
worker.port.postMessage()

Même fonctionnement que pour les dedicated workers

Échange de messages

  • Côté worker
    • Réception d'un événement connect par le worker
    • Accès au MessagePort à travers l'événement connect :

Premier (et seul) élément du tableau ports

let port;
onconnect = function(e) {
  port = e.ports[0];
  ...
}
...
port.onmessage = function(e) {
  port.postMessage(...); // not e.ports[0].postMessage!
  // e.target.postMessage('pong'); would work also
}

Notion de scope

The global scope is the "inside" of a worker.

Spécification

L'interface WebIDL WorkerGlobalScope

  • Hérite de EventTarget (interface DOM de gestion d'événements)
  • Attributs
    • WorkerGlobalScope self : renvoie le scope du worker (permet d'y associer des handlers d'événements)
    • WorkerLocation location : renvoie l'URL absolue du script utilisé pour initialiser le worker (origine)
  • Méthode
    • close() : arrête les tâches courantes et met l'objet WorkerGlobalScope à null
  • Remarque :
    • des interfaces plus spécifiques sont définies pour chaque type de worker : DedicatedWorkerGlobalScope et SharedWorkerGlobalScope

Spécification

L'interface WebIDL DedicatedWorkerGlobalScope

  • Hérite de WorkerGlobalScope
  • Spécifique aux dedicated workers
  • Méthode
    • postMessage() : …

Spécification

L'interface WebIDL SharedWorkerGlobalScope

  • Hérite de WorkerGlobalScope
  • Spécifique aux shared workers
  • Attributs
    • name : nom donné à la création du worker
    • applicationCache : gestion de l'application offline par le navigateur
    • onconnect : callback de l'événement connect;
      • appelé à chaque première connexion d'un autre script;
      • permet de gérer les connexions multiples au même worker

Autres APIs

Spécification

L'interface WebIDL WorkerUtils

  • Permet l'accès à des fonctionnalités (APIs) supplémentaires depuis les workers
  • Spécifique aux shared workers
  • Attribut
    • WorkerNavigator navigator : permet d'obtenir des informations sur le navigateur (user-agent id, online)
  • Méthode
    • importScripts(DOMString... urls) : permet de télécharger des bibliothèques de code (avec restriction d'origine)
importScripts('script1.js', 'script2.js');

Remarque : on peut aussi utiliser la directive JS import si on a déclaré un worker de type “module”

TIP

Synchronisation de données avec un shared worker

  • La spec recommande d'utiliser l'API IndexedDB pour partager des données entre un SW et ses clients

Service Workers

Principes

  • Un service worker est un type de worker particulier (hérite de EventTarget)
  • Il possède les mêmes caractéristiques de base que les dedicated & shared worker (URL, type, nom, message port…)
  • Il joue un rôle de proxy pour des “clients” (browsing contexts d'une origine particulière)
  • Il est capable de cacher des ressources et de les mettre à jour de façon asynchronequand l'appareil est connecté

Spécification

L'interface WebIDL ServiceWorker

  • Attributs
    • scriptURL : cf. slides précédents
    • ServiceWorkerState state : état de fonctionnement du worker (“installing”, “activated”…)
  • Méthode
    • postMessage() : …
  • Evénement
    • onStateChange : changement d'état

Références