Le chargement d'un gros module WebAssembly était auparavant séquentiel : le navigateur téléchargeait tout le fichier dans un ArrayBuffer, le confiait au moteur JavaScript, et ne commençait qu'ensuite à compiler. Sur les connexions rapides, le réseau n'était plus le goulot d'étranglement ; c'était le temps CPU de décodage et de compilation, et tout se produisait après le téléchargement, avec le thread principal en pause.

Les correctifs évidents avaient chacun un coût. Un unique compilateur optimisant produisait du code rapide mais démarrait lentement. Les moteurs JavaScript compilaient paresseusement à l'exécution, ce qui évitait du travail mais maintenait la compilation sur le thread principal et attendait la fin de l'analyse. Et il semblait impossible de compiler avant l'arrivée du fichier, car l'ancienne API n'acceptait qu'un ArrayBuffer complet.

Firefox 58 s'attaquait aux deux extrémités. La compilation en streaming envoyait chaque fragment téléchargé directement au moteur via l'API de streaming, et comme la section de code d'un module wasm précède ses données, la compilation pouvait commencer alors que des mégaoctets de données étaient encore en transit. Un nouveau compilateur à niveaux ajoutait un niveau de base qui construisait le code 10 à 15 fois plus vite que le compilateur optimisant, et sa sortie ne s'exécutait qu'environ 2 fois plus lentement, si bien que le code commençait à s'exécuter presque immédiatement ; des threads d'arrière-plan recompilaient ensuite tout le module avec l'optimiseur et le remplaçaient à chaud. Les deux compilateurs répartissaient le travail par fonction sur plusieurs cœurs.

Comme le débit de compilation dépassait désormais le débit du réseau, le code était prêt presque dès l'arrivée du dernier paquet. Mozilla présentait ce changement comme le fait de rendre le chargement de WebAssembly semblable au décodage d'une image plutôt qu'à l'analyse de JavaScript : aucune pause de compilation sur le thread principal et aucune attente de la totalité de la charge utile.

Faire affluer les octets dans un niveau de base rapide à mesure de leur arrivée, exécuter ce code immédiatement, et remplacer à chaud par du code optimisé en arrière-plan

Compiler le premier paquet masque le temps de téléchargement dans le travail CPU au lieu de sérialiser téléchargement puis compilation.

Un niveau de base qui compile 10 à 15 fois plus vite transforme le démarrage en démarrage rapide ; le code environ 2 fois plus lent s'exécute pendant que l'optimisation se poursuit.

Comme les fonctions wasm se compilent indépendamment, le travail se répartit sur plusieurs cœurs au lieu de faire la queue sur un seul thread.

Placer la section de code d'un module avant ses données permet de compiler en streaming la partie coûteuse pendant que les octets de la charge utile arrivent encore.

Chaque changement raccourcit le démarrage sans modifier le code machine finalement exécuté, donc les performances de pointe restent intactes.

Lorsque l'étape goulot d'étranglement attend toute l'entrée, la démarrer sur le premier fragment et livrer d'abord une version peu coûteuse ; l'améliorer en arrière-plan au lieu de bloquer

Firefox 58 a livré le compilateur en streaming et à niveaux en janvier 2018 ; l'article de lancement a suscité un fil Hacker News de 912 points. La page de test de Mozilla permettait aux utilisateurs de mesurer la vitesse de compilation sur leurs propres appareils, et l'équipe indiquait que le terrain était préparé pour mettre en cache le code machine compilé dans le cache HTTP, en étendant le cache de bytecode JavaScript livré dans la même version.

VÉRIFIEZ LES SOURCES

Les sources

  1. Making WebAssembly even faster: Firefox's new streaming and tiering compiler Mozilla Hacks · hacks.mozilla.org ·
  2. Hacker News discussion: Making WebAssembly even faster Hacker News · news.ycombinator.com ·