🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Rust 🦀 et WebAssembly 🕸

Ce petit livre décrit comment utiliser ensemble Rust et WebAssembly.

Vous êtes actuellement sur la version du livre traduite en Français de la version en Anglais.

A qui est destiné ce livre ?

Ce livre est écrit pour celles et ceux qui s'intéressent à compiler du code en Rust pour WebAssembly afin d'utiliser du code rapide et fiable sur le Web. Vous devez déjà connaître le de Rust, et être famillier·e avec le Javascript, HTML et CSS. Mais vous n'avez pas non plus besoin d'être un·e expert·e avec chacun.

Vous ne connaissez pas encore le Rust ? Commencez d'abord par parcourir le livre Le langage de programmation Rust ou alors sa version Anglaise.

Vous ne connaissez pas le Javascript, le HTMl, ou le CSS ? En savoir plus sur eux sur le MDN.

Comment lire ce livre

Vous devriez commencer par lire les raisons d'utiliser Rust et WebAssembly ensemble, et vous familliariser avec le contexte et les concepts.

Le tutoriel est écrit pour être lu du début à la fin. Vous devez donc suivre sa progression : écrire, compiler et exécuter le code du tutorial vous aussi. Si vous n'avez jamais utilisé Rust et WebAssembly ensemble, suivez ce tutoriel !

Les sections références peuvent toutefois être consultées dans n'importe quel ordre.

💡 Astuce : Vous pouvez rechercher dans ce livre en cliquant sur l'icône 🔍 en haut de la page, ou en appuyant sur la touche s à tout moment.

Contribution à ce livre

Ce livre est open source ! Si vous trouvez une faute, vous pouvez nous envoyer une pull request. Si nous avons oublié quelque chose, envoyez une pull request sur la version Anglaise !

Traduction des termes

Voici les principaux termes techniques qui ont été traduits de l'anglais vers le français.

AnglaisFrançaisRemarques
benchmarktest de performance-
call treearbre d'appel-
flame graphflame graph-
Frames Per Secondimages par seconde-
garbage collectorramasse-miettes-
indirectionindirection-
inlined functionfonction intégrée-
logjournal-
monomorphizationmonomorphisation-
profilerprofileur-
profilingprofilage-

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Pourquoi Rust et le WebAssembly ?

Ils offrent un contrôle de bas-niveau, mais avec une ergonomie de haut-niveau

Les applications web en Javascript ont du mal à atteindre et conserver des performances satisfaisantes. Le système de type dynamique du Javascript et les interruptions pour le ramasse-miette n'aident pas non plus. Des petits changements en apparance mineurs peuvent impliquer de grosses régressions de performances si vous écartez de la trajectoire prédit par le système Just-In-Time.

Rust offre aux développeurs un contrôle du bas-niveau et des performances efficaces. Il n'est pas soumis aux interruptions du ramasse-miettes qui accable le Javascript. Les développeurs ont le contrôle sur l'indirection, la monomorphisation, et l'utilisation de la mémoire.

Ils produisent des .wasm légers

La taille du code est très importante car le .wasm doit être téléchargé à partir du réseau. Rust n'a pas d'environnement d'exécution, ce qui permet d'obtenir des .wasm légers car il n'a pas de charge supplémentaire comme par exemple un ramasse-miettes. Vous ne payez (en terme de taille de code) que pour les fonctions que vous utilisez vraiment.

Il n'est pas nécessaire de tout ré-écrire

La base de code existante n'a pas besoin d'être jetée. Vous pouvez commencer par porter en Rust vos fonctions Javascript les plus critiques pour les performances pour obtenir immédiatement des gains de performances. Et vous pouvez vous y arrêter là si vous le souhaitez.

Ils s'accommodent bien avec les autres

Rust et WebAssembly s'intègrent dans les outils existants en Javascript. Ils supportent les modules ECMAScript et vous pouvez continuer à utiliser vos outils préférés, comme par exemple npm ou Webpack.

Ils offrent des commodités dont vous avez besoin

Rust offre des services que les développeurs attendent implicitement, comme :

  • une gestion de packets efficiente avec cargo,
  • des abstractions explicites (et sans coût),
  • et une communauté chaleureuse ! 😊

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Le contexte et les concepts

Cette section explique le contexte nécessaire pour comprendre le développement en Rust et WebAssembly.

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Qu'est-ce que le WebAssembly ?

Le WebAssembly (wasm) est un modèle de système simple et un format d'exécutable qui est décrit par des spécifications détaillées. Il est conçu pour être portable, compacte, et s'exécuter presque aussi vite que si c'était un programme natif.

En tant que langage de programmation, WebAssembly est constitué de deux formats qui représentent les mêmes structures, bien qu'étant dans des formats différents :

  1. Le format textuel .wat (qui sont les initiales de "WebAssembly Text") qui utilise les expressions symboliques, et ressemble aux langages de la famille Lisp comme Scheme et Clojure.
  1. Le format binaire .wasm qui est plus bas-niveau et est destiné à être utilisé par des machines virtuelles pour le wasm. Il est techniquement similaire à l'ELF et au Mach-O.

Pour illustrer ceci, voici une fonction factorielle au format wat :

(module
  (func $fac (param f64) (result f64)
    get_local 0
    f64.const 1
    f64.lt
    if (result f64)
      f64.const 1
    else
      get_local 0
      get_local 0
      f64.const 1
      f64.sub
      call $fac
      f64.mul
    end)
  (export "fac" (func $fac)))

Si vous vous demandez à quoi ressemble un fichier wasm, vous pouvez utiliser la démonstation wat2wasm avec le code ci-dessus.

La mémoire linéaire

WebAssembly suit un modèle de mémoire très simple. Un module wasm n'a accès qu'à une seule "mémoire linéaire", qui est principalement un tableau uniforme d'octets. Cette mémoire peut être agrandie d'une taille correspondant à un multiple d'une taille de page (64K). Elle ne peut pas être réduite.

Est-ce que le WebAssembly est conçu uniquement pour le web ?

Bien qu'il ait actuellement attiré l'attention des communautés JavaScript et du web en général, wasm ne sait rien sur son environnement d'accueil. Ainsi, il est raisonnable de penser que wasm puisse devenir un format "d'exécutable portable" qui puisse être utilisé à l'avenir dans des contextes variés. Cependant, à l'heure de l'écriture de ces lignes, wasm est majoritairement associé au JavaScript (JS), qui se décline sous plusieurs formats (y compris ceux sur le Web et dans Node.js).

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Tutoriel : le jeu de la vie

Ceci est un tutoriel qui va implémenter le Jeu de la vie, de Conway en Rust et WebAssembly.

A qui s'adresse ce tutoriel ?

Ce tutoriel a été créé pour ceux qui ont déjà une expérience de base avec Rust et le JavaScript, et qui souhaitent en savoir plus sur l'utilisation conjointe de Rust, WebAssembly, et JavaScript.

Vous devez donc être à l'aise pour lire et écrire du code Rust, JavaScript, et HTML. Toutefois, vous n'avez vraiment pas besoin d'en être un expert.

Qu'allons-nous apprendre ?

  • Comment régler une toolchain de Rust pour compiler en WebAssembly.
  • Suivre une procédure pour développer des programmes polyglottes constitués de Rust, WebAssembly, HTML et CSS.
  • Comment concevoir des API pour profiter des avantages à la fois de Rust et de de WebAssembly et aussi des avantages de JavaScript.
  • Comment déboguer les modules en WebAssembly, compilés avec Rust.
  • Comment créer un profil chronologique de programmes en Rust et WebAssembly pour améliorer leurs performances.
  • Comment établir un profil de poids des programmes en Rust et WebAssembly pour réduire la taille des binaires .wasm et ainsi accélérer leur téléchargement via le réseau.

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Réglages

Cette section décrit comment régler la toolchain pour compiler les programmes Rust en WebAssembly et les intégrer dans JavaScript.

La toolchain Rust

Vous allez avoir besoin de la toolchain Rust standard, y compris rustup, rustc, et cargo.

Cliquez ici pour suivre les instructions pour installer la toolchain Rust.

L'expérience entre Rust et WebAssembly suit les trains de publications de Rust stable ! Cela signifie que nous n'avons pas besoin de drapeaux de fonctionnalitées expérimentales. Cependant, nous avons besoin de Rust 1.30 ou plus récent.

wasm-pack

wasm-pack sera votre interlocuteur unique pour compiler, tester, et publier du WebAssembly généré par Rust.

Obtenez wasm-pack ici !

cargo-generate

cargo-generate va vous aider à vous mettre sur pied rapidement avec un nouveau projet Rust en utilisant comme modèle un dépôt git déjà existant.

Vous pouvez installer cargo-generate avec cette commande :

cargo install cargo-generate

npm

npm est un gestionnaire de paquets pour JavaScript. Nous allons l'utiliser pour installer et exécuter un bundler JavaScript et un serveur de développement. A la fin de ce tutoriel, nous allons publier notre .wasm dans le registre npm.

Cliquez ici pour suivre les instructions pour installer npm.

Si vous avez déjà npm d'installé, assurez-vous qu'il est à jour avec cette commande :

npm install npm@latest -g

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Hello, World !

Cette section va vous expliquer comment compiler et exécuter votre premier programme en Rust et WebAssembly : une page web qui affiche une boite de dialogue "Hello, World !".

Assurez-vous que vous avez suivi les réglages avant de commencer.

Cloner le modèle de projet

Le modèle de projet est livré avec des réglages préconfigurés par défaut avec des valeurs stables, afin que vous puissiez compiler, intégrer et créer un paquet pour le Web.

Vous pouvez cloner le modèle du projet avec cette commande :

cargo generate --git https://github.com/Jimskapt/wasm-pack-template-fr

(la version anglaise originale du modèle est aussi disponible à l'adresse https://github.com/rustwasm/wasm-pack-template)

Elle devrait vous demander le nom du nouveau projet. Nous allons y renseigner "wasm-jeu-de-la-vie".

wasm-jeu-de-la-vie

Qu'est-ce qui est livré ?

Entrez dans le dossier wasm-jeu-de-la-vie du nouveau projet ...

cd wasm-jeu-de-la-vie

... et regardez son contenu :

wasm-jeu-de-la-vie/
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
└── src
    ├── lib.rs
    └── utils.rs

Maintenant, analysons en détail le contenu de certains de ces fichiers.

wasm-jeu-de-la-vie/Cargo.toml

Le fichier Cargo.toml renseigne les dépendances et les métadonnées pour cargo, le gestionnaire de paquets et outil de compilation de Rust. Ce fichier est préconfiguré avec une dépendance à wasm-bindgen, quelques dépendances optionnelles que nous verrons plus tard, ainsi que la propriété crate-type bien réglé pour générer des bibliothèques en .wasm.

wasm-jeu-de-la-vie/src/lib.rs

Le fichier src/lib.rs est la racine de la crate Rust que nous compilerons en WebAssembly. Il utilise wasm-bindgen pour s'interfacer avec JavaScript. Il importe la fonction JavaScript window.alert, et exporte la fonction Rust saluer, qui affiche un message de salutation.


#![allow(unused)]
fn main() {
mod utils;

use wasm_bindgen::prelude::*;

// Lorsque la fonctionnalité `wee_alloc` est activée, nous allons utiliser
// `wee_alloc` en tant qu'allocateur global.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn saluer() {
    alert("Salut, wasm-jeu-de-la-vie !");
}
}

wasm-jeu-de-la-vie/src/utils.rs

Le module src/utils.rs fournit quelques outils communs pour faciliter la compilation de Rust en WebAssembly. Nous nous discuterons en détails de ces outils plus tard dans le tutoriel, en particulier lorsque nous demanderons comment déboguer notre code wasm, mais pour l'instant nous pouvons nous contenter d'ignorer ce fichier.

Compiler le projet

Nous allons utiliser wasm-pack pour orchestrer les étapes de compilation suivantes :

  • S'assurer que nous avons Rust 1.30 ou plus récent et la cible wasm32-unknown-unknown via rustup,
  • Compiler nos sources Rust en binaires WebAssembly .wasm via cargo,
  • Utiliser wasm-bindgen pour générer l'API JavaScript pour utiliser notre WebAssembly généré par Rust.

Pour faire tout cela, lancez cette commande dans le dossier du projet :

wasm-pack build

Lorsque la compilation sera achevée, nous pourrons trouver ses artefacts dans le dossier pkg, et il devrait avoir ce contenu :

pkg/
├── package.json
├── README.md
├── wasm_jeu_de_la_vie_bg.wasm
├── wasm_jeu_de_la_vie.d.ts
└── wasm_jeu_de_la_vie.js

Le fichier README.md est copié à partir de la racine du projet, mais les autres sont complètement nouveaux.

wasm-jeu-de-la-vie/pkg/wasm_jeu_de_la_vie_bg.wasm

Le fichier .wasm est le binaire WebAssembly qui est généré par le compilateur Rust à partir de nos sources Rust. Il contient les formes compilées en wasm de toutes nos fonctions et nos données. Par exemple, il a une fonction exportée saluer.

wasm-jeu-de-la-vie/pkg/wasm-jeu-de-la-vie.js

Le fichier .js est généré par wasm-bindgen et contient la glu en JavaScript pour importer le DOM et les fonctions JavaScript dans Rust et exposer une API conviviale aux fonctions en WebAssembly à destination du JavaScript. Par exemple, il existe une fonction JavaScript saluer qui englobe la fonction saluer exportée du module en WebAssembly. Pour le moment, cette glu ne fait pas grand-chose, mais lorsque nous commencerons à y envoyer des valeurs plus intéressantes qui vont et viennent entre wasm et JavaScript, cela facilitera le passage de l'un à l'autre côté.

import * as wasm from './wasm_jeu_de_la_vie_bg';

// ...

export function saluer() {
    return wasm.saluer();
}

wasm-jeu-de-la-vie/pkg/wasm_jeu_de_la_vie.d.ts

Le fichier .d.ts contient des déclarations de type TypeScript pour la glu JavaScript. Si vous utilisez TypeScript, vous pourrez faire en sorte que les appels aux fonctions en WebAssembly vérifient les types, et votre IDE pourra vous proposer de l'autocompletion et des suggestions ! Si vous n'utilisez pas le TypeScript, vous pouvez ignorer ce fichier sans problème.

export function saluer(): void;

wasm-jeu-de-la-vie/pkg/package.json

Le fichier package.json contient des métadonnées sur le paquet généré en JavaScript et en WebAssembly. Il est utilisé par npm et les packageurs JavaScript pour décrire les dépendances entre les paquets, les noms de ces paquets, leurs versions, et un tas d'autres choses. Il nous aide à nous intégrer avec les outils JavaScript et nous permet de publier notre paquet sur npm.

{
  "name": "wasm-jeu-de-la-vie",
  "collaborators": [
    "Votre nom <votre.email@exemple.com>"
  ],
  "description": null,
  "version": "0.1.0",
  "license": null,
  "repository": null,
  "files": [
    "wasm_jeu_de_la_vie_bg.wasm",
    "wasm_jeu_de_la_vie.d.ts"
  ],
  "main": "wasm_jeu_de_la_vie.js",
  "types": "wasm_jeu_de_la_vie.d.ts"
}

Tout intégrer dans une page web

Pour intégrer notre paquet wasm-jeu-de-la-vie dans une page Web et l'utiliser, nous avons avoir recours au modèle de projet JavaScript create-wasm-app-fr.

Lancez ensuite la commande suivante dans le dossier wasm-jeu-de-la-vie :

npm init wasm-app-fr www

ou, pour sa version anglaise :

npm init wasm-app www

Maintenant, notre nouveau sous-dossier wasm-jeu-de-la-vie/www contient :

wasm-jeu-de-la-vie/www/
├── bootstrap.js
├── index.html
├── index.js
├── LICENSE-APACHE
├── LICENSE-MIT
├── package.json
├── README.md
└── webpack.config.js

A nouveau, regardons certains de ces fichiers.

wasm-jeu-de-la-vie/www/package.json

Ce package.json est préconfiguré avec les dépendances webpack et webpack-dev-server, ainsi qu'une dépendance à salut-wasm-pack, qui est une version du paquet initial wasm-pack-template qui a été publié sur npm.

wasm-jeu-de-la-vie/www/webpack.config.js

Ce fichier configure webpack et son serveur de développement local. Il est préconfiguré pour que vous n'ayez pas à y toucher pour que webpack et son serveur de développement local fonctionnent correctement.

wasm-jeu-de-la-vie/www/index.html

C'est le fichier HTML racine de notre page Web. Elle ne fait pas grand-chose d'autre que de charger bootstrap.js, qui est une petite enveloppe autour de index.js.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Salut, wasm-pack !</title>
  </head>
  <body>
    <noscript>Cette page utilise du webassembly et du javascript, veuillez activer le javascript dans votre navigateur.</noscript>
    <script src="./bootstrap.js"></script>
  </body>
</html>

wasm-jeu-de-la-vie/www/index.js

Le fichier index.js est le point d'entrée central du JavaScript de notre page Web. Il importe le paquet npm salut-wasm-pack, qui contient la glu WebAssembly et JavaScript précompilée de wasm-pack-template, et qui ensuite appelle la fonction saluer de salut-wasm-pack.

import * as wasm from "salut-wasm-pack";

wasm.saluer();

Installer les dépendances

D'abord, il va falloir s'assurer que le serveur de développement local et ses dépendances sont installées en lançant npm install dans le sous-dossier wasm-jeu-de-la-vie/www :

npm install

Cette commande n'a besoin d'être exécutée une seule fois, et va installer le packageur JavaScript webpack et son serveur de développement.

Notez que webpack n'est pas nécessaire pour travailler Rust et WebAssembly, c'est juste le packageur et le serveur de développement que nous avons choisi par confort ici. Parcel et Rollup devraient aussi implémenter l'import de WebAssembly en tant que module ECMAScript. Vous pouvez aussi utiliser Rust et WebAssembly sans packageur si vous le préférez !

Utiliser notre paquet local wasm-jeu-de-la-vie dans www

Plutôt que d'utiliser le paquet hello-wasm-pack provenant de npm, nous voulons utiliser notre paquet local wasm-jeu-de-la-vie. Cela va nous permettre de développer de manière incrémentale notre programme de "Jeu de la vie".

Ouvrez wasm-jeu-de-la-vie/www/package.json et à côté de "devDependencies", ajoutez le champ "dependencies", et ajoutez-lui l'entrée "wasm-jeu-de-la-vie": "file:../pkg" :

{
  // ...
  "dependencies": {                     // Ajoutez ce bloc de trois lignes !
    "wasm-jeu-de-la-vie": "file:../pkg"
  },
  "devDependencies": {
    //...
  }
}

Ensuite, modifiez wasm-jeu-de-la-vie/www/index.js pour importer wasm-jeu-de-la-vie à la place du paquet salut-wasm-pack :

import * as wasm from "wasm-jeu-de-la-vie";

wasm.saluer();

Comme nous avons déclaré une nouvelle dépendance, nous devons l'installer :

npm install

Notre page web est maintenant prête à être servie localement !

Servir localement

Maintenant, ouvrez un nouveau terminal pour le serveur de développement. Exécuter le serveur dans un nouveau terminal nous permet de l'exécuter en arrière-plan, et ainsi ne nous empêche pas de lancer d'autres commandes en même temps. Dans le nouveau terminal, lancez cette commande dans le dossier wasm-jeu-de-la-vie/www :

npm run start

Rendez-vous à l'adresse http://localhost:8080/ avec votre navigateur web et vous devriez être accueilli par un message d'avertissement :

Capture d'écran de l'alerte "Salut, wasm-jeu-de-la-vie !" sur la page web

A chaque fois que vous allez faire des changements dans le code Rust et que vous souhaitez les intégrer dans http://localhost:8080/, relancez simplement la commande wasm-pack build dans le dossier wasm-jeu-de-la-vie.

Exercice

  • Essayez de modifier la fonction saluer dans wasm-jeu-de-la-vie/src/lib.rs pour prendre en paramètre un nom: &str qui personnalise le message d'alerte, et passez votre nom à la fonction saluer à l'intérieur de wasm-jeu-de-la-vie/www/index.js. Recompilez le binaire .wasm avec wasm-pack build, et ensuite rafraîchissez http://localhost:8080/ dans votre navigateur web, et vous devrez voir une salutation personnalisée !
Réponse

Nouvelle version de la fonction saluer dans wasm-jeu-de-la-vie/src/lib.rs:


#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub fn saluer(nom: &str) {
    alert(&format!("Salut, {} !", nom));
}
}

Nouvelle utilisation de saluer dans wasm-jeu-de-la-vie/www/index.js:

wasm.saluer("votre nom");

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Les règles du jeu de la vie de Conway

Note : si vous connaissez bien les règles du jeu de la vie de Conway, vous pouvez sauter à la section suivante !

Wikipedia décrit bien les règles du jeu de la vie de Conway :

L'univers du jeu de la vie est une grille orthogonale infinie de cellules carrées sur deux dimensions, qui ont deux états possibles, soit vivante soit morte. Chaque cellule interagit avec ses 8 cellules voisines, celles qui sont alignées verticalement, horizontalement, et en diagonale. A chaque étape, les évolutions suivantes se produisent :

  1. Toute cellule vivante avec moins de deux voisines vivantes meurt, comme si cela était un effet de sous-population.

  2. Toute cellule vivante avec deux ou trois voisines vivantes survit jusqu'à la prochaine génération.

  3. Toute cellule vivante avec plus de trois voisines vivantes meurt, comme si cela était un effet de surpopulation.

  4. Toute cellule morte avec exactement trois voisines vivantes devient une cellule vivante, comme si cela était un effet de reproduction.

Le dessin initial représente la graine du système. La première génération est créée en appliquant simultanément les règles précédentes sur chaque cellule de la graine, ainsi les naissances et les morts se produisent simultanément, et le moment précis où cela se produit est parfois appelé un tick (autrement dit, chaque génération dépend purement de la précédente). Les règles continuent à s'appliquer en boucle pour engendrer les générations suivantes.

Imaginons l'univers initial suivant :

Univers initial

Nous pouvons calculer la prochaine génération en analysant chaque cellule. La cellule d'en haut à gauche est morte. La règle (4) est la seule règle d'évolution qui s'applique aux cellules mortes. Cependant, comme la cellule d'en haut à gauche n'a pas exactement trois voisines vivantes, la règle d'évolution ne peut pas s'appliquer, et elle reste morte à la prochaine génération. C'est aussi ce qui se passe pour les autres cellules dans la première ligne.

Les choses deviennent intéressantes lorsque nous analysons la première cellule vivante en haut, dans la deuxième ligne et troisième colonne. Pour les cellules vivantes, les trois premières règles peuvent potentiellement s'appliquer. Dans le cas de cette cellule, elle n'a qu'une seule voisine vivante, ce qui fait que la règle (1) s'applique : cette cellule va mourir à la prochaine génération. Le même destin va se produire pour la cellule vivante d'en bas.

La cellule du milieu a deux voisines vivantes : les cellules du haut et du bas. Cela signifie que la règle (2) s'applique, et donc elle reste vivante à la prochaine génération.

Les derniers cas intéressants concernent les cellules mortes à la gauche et la droite de la cellule vivante du milieu. Les trois cellules vivantes sont toutes des voisines de ces deux cellules, ce qui signifie que la règle (4) s'applique, et que ces cellules vont naître et être vivantes à la prochaine génération.

Une fois toutes ces conditions assemblées, nous obtenons l'univers suivant après le prochain tick :

L'univers suivant

Un comportement intéressant et étrange émerge de ces règles simples et déterministes :

Le canon à planeurs de GosperUn pulsarUn vaisseau spatial
Le canon à planeurs de GosperUn pulsarUn petit vaisseau spatial

Exercices

  • Calculez à la main la prochaine tick de notre univers d'exemple. Est-ce que vous ne remarquez pas quelque chose ?
Réponse

Vous devriez retrouver l'état initial de l'univers de l'exemple :

Univers initial

Ce schéma est périodique : il retourne à son état initial tous les deux ticks.

  • Pouvez-vous trouver un univers initial qui est stable ? Cela désigne un univers dont chaque génération est toujours la même.
Réponse

Il y a un nombre infini d'univers stables ! L'univers stable le plus trivial est l'univers qui est vide. Un carré de deux cellules de large et de deux cellules de haut est aussi un univers stable.

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Implémenter le jeu de la vie de Conway

Conception

Avant de nous plonger dans le sujet, nous devons prendre en considération quelques choix de conception.

Un univers infini

Le jeu de la vie se déroule dans un univers infini, mais nous n'avons pas une mémoire et une puissance de calcul infinie. Pour contourner cette limitation plutôt ennuyeuse, il a généralement trois possibilités :

  1. Identifier dans quel sous-ensemble de l'univers il se passe des choses intéressantes, et agrandir cette zone si nécessaire. Dans le pire des cas, cette expansion se fera sans limites et donc la simulation deviendra de plus en plus lent et arrivera à cours de mémoire.
  1. Créer un univers à taille fixe, dans lequel les cellules sur ses bords auront moins de voisines que les cellules au centre. Le désavantage de cette approche est que les schémas infinis, comme les planeurs, qui atteignent probablement la fin de l'univers, seront éliminés.
  1. Créer un univers à taille fixe, mais en boucle, où les cellules sur les bords seront directement voisines de celles qui sont de l'autre côté de l'univers. Comme les voisines de recoupent d'un bout à l'autre de l'univers, les planeurs pourront continuer à vivre à l'infini.

Nous allons implémenter la troisième option.

Interfacer Rust et le JavaScript

⚡ C'est l'un des concepts les plus importants à comprendre et à retenir de ce tutoriel !

Le tas du JavaScript qui est géré par le ramasse-miettes — dans lequel sont stockés les objets Object, les tableaux Array, et les noeuds du DOM — se distingue de l'espace mémoire linéaire du WebAssembly, dans lequel vivent nos valeurs Rust. WebAssembly n'a actuellement pas d'accès direct au tas géré par le ramasse-miettes (du moins en avril 2018, cela peut changer à l'avenir avec la proposition des "Interface Types"). JavaScript, de l'autre côté, peut lire et écrire sur l'espace mémoire linéaire de WebAssembly, mais seulement via un ArrayBuffer de valeurs scalaires (comme le u8, i32, f64, etc ...). Les fonctions WebAssembly prennent elles aussi des valeurs scalaires et en retourne. Ce sont les éléments de base sur lesquels repose la communication entre WebAssembly et JavaScript.

wasm_bindgen définit une vision partagée pour travailler avec des structures composées pour passer outre ces limites. Cette crate passe une structure Rust dans une std::boxed::Box et enveloppe ce pointeur dans une classe JavaScript pour faciliter son utilisation, ou utilise des indices dans une table d'objets dans Rust qui représentent des objets JavaScript. wasm_bindgen est très utile, mais nous devons toujours garder en tête comment les données sont modélisées, et quelles sont les valeurs et les structures qui passent entre ces deux domaines. Considérez-la plutôt comme un outil permettant de choisir votre moyen pour s'interfacer.

Lorsqu'on conçoit une interface entre WebAssembly et JavaScript, nous voulons optimiser les propriétés suivantes :

  1. Réduire au maximum les copies de données sur et à partir de la mémoire linéaire de WebAssembly. Les copies inutiles provoquent des surcharges inutiles.
  1. Minimiser les sérialisations et les déserialisations. Pour la même raison que pour les copies, les sérialisations et la déserialisations provoquent des surcharges, et impose parfois aussi des copies, en plus. Si nous pouvons utiliser des manipulateurs opaques pour une structure de données, plutôt que d'avoir à la sérialiser d'un côté, de la copier dans un endroit connu dans la mémoire linéaire de WebAssembly, et la déserialiser de l'autre côté, alors très souvent on économise beaucoup de ressources. wasm_bindgen nous aide à définir et travailler avec des manipulateurs opaques d'objets JavaScript ou de structures Rust intégrées dans des Box.

En règle générale, une bonne conception d'interface JavaScript↔WebAssembly nécessite souvent que les grosses structures de données à durée de vie longue soient implémentées comme étant des types Rust qui vivent dans la mémoire linéaire de WebAssembly, et soient utilisées en JavaScript via des manipulateurs opaques. Le JavaScript appelle les fonctions WebAssembly exportées qui prennent en argument ces manipulateurs opaques, transforment leurs données, procèdent à des calculs lourds, consultent les données, et retournent finalement un petit résultat copiable. En retournant uniquement un petit résultat de l'opération, nous évitons de copier et/ou de tout sérialiser tout ce qui transite entre le tas géré par le ramasse-miettes de JavaScript et la mémoire linéaire de WebAssembly.

Interfacer Rust et JavaScript dans notre jeu de la vie

Commençons par évoquer les pièges à éviter. Nous ne devons pas copier tout l'univers à l'intérieur et à partir de la mémoire linéaire de WebAssembly à chaque tick. Nous ne devons pas allouer des objets pour chaque cellule dans l'univers, ni faire des appels transversaux entre les deux domaines pour lire et écrire chaque cellule.

Qu'est-ce que tout cela implique ? Que nous pouvons représenter l'univers comme un tableau à une dimension qui vit dans la mémoire linéaire de WebAssembly, et qui a un octet pour chaque cellule. 0 modélisera une cellule morte, et 1 sera une cellule vivante.

Voici à quoi ressemble un univers de 4 par 4 dans la mémoire :

Capture d'écran d'un univers 4 par 4

Pour trouver l'indice d'une cellule dans le tableau à partir d'une ligne et d'une colonne, nous pouvons utiliser cette formule :

indice(ligne, colonne, univers) = ligne * largeur(univers) + colonne

Nous pouvons exposer les cellules de l'univers au JavaScript de différentes manières. Pour commencer, nous allons implémenter std::fmt::Display sur Univers, qui nous permettra de générer une String en Rust des cellules qui représentera les cellules avec des caractères. Cette chaîne de caractères Rust est ensuite copiée à partir de la mémoire linéaire de WebAssembly dans une chaîne de caractères en JavaScript, stockée dans le tas géré par le ramasse-miettes de JavaScript, et est ensuite affichée dans l'élément HTML contenuTextuel. Plus tard dans ce chapitre, nous allons faire évoluer cette implémentation pour éviter de copier les cellules de l'univers entre les tas et les intégrer dans un <canvas>.

Une autre conception alternative acceptable serait que Rust retourne une liste de toutes les cellules qui changent d'état après chaque tick, au lieu de donner l'intégralité de l'univers au JavaScript. Ainsi, JavaScript n'aurait pas besoin d'itérer sur tout l'univers lorsqu'il s'occupe du rendu, mais uniquement sur le sous-ensemble concerné. Le désavantage est que cette conception basée sur les différences et un peu plus difficile à implémenter.

Implémentation de Rust

Dans le dernier chapitre, nous avons cloné un modèle initial de projet. Nous allons maintenant modifier ce projet.

Commençons par enlever l'import de alert et la fonction saluer dans wasm-jeu-de-la-vie/src/lib.rs, et remplacons-les par une définition d'un type pour les cellules :


#![allow(unused)]
fn main() {
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cellule {
    Morte = 0,
    Vivante = 1,
}
}

Il est important d'avoir #[repr(u8)] pour que chaque cellule soit représentée par un seul octet. Il est aussi important que la variante Morte soit 0 et que la variante Vivante vaut 1, afin que nous puissions facilement compter les voisines vivantes d'une cellule en les additionnant.

Ensuite, définissons l'univers. L'univers a une largeur et une hauteur, et a un vecteur de cellules qui a une taille de largeur * hauteur.


#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub struct Univers {
    largeur: u32,
    hauteur: u32,
    cellules: Vec<Cellule>,
}
}

Pour accéder à la cellule à une ligne et colonne donnée, nous calculons l'emplacement dans le vecteur de cellules avec la ligne et la colonne comme nous l'avons décrit précédemment :


#![allow(unused)]
fn main() {
impl Univers {
    fn calculer_indice(&self, ligne: u32, colonne: u32) -> usize {
        (ligne * self.largeur + colonne) as usize
    }

    // ...
}
}

Pour calculer le prochain état d'une cellule, nous devons compter combien de cellules sont vivantes dans son voisinage. Ecrivons donc une méthode compter_voisines_vivantes pour cela !


#![allow(unused)]
fn main() {
impl Universe {
    // ...

    fn compter_voisines_vivantes(&self, ligne: u32, colonne: u32) -> u8 {
        let mut compteur = 0;
        for delta_ligne in [self.hauteur - 1, 0, 1].iter().cloned() {
            for delta_colonne in [self.largeur - 1, 0, 1].iter().cloned() {
                if delta_ligne == 0 && delta_colonne == 0 {
                    continue;
                }

                let ligne_voisine = (ligne + delta_ligne) % self.hauteur;
                let colonne_voisine = (colonne + delta_colonne) % self.largeur;
                let indice = self.calculer_indice(ligne_voisine, colonne_voisine);
                compteur += self.cellules[indice] as u8;
            }
        }
        compteur
    }
}
}

La méthode compter_voisines_vivantes utilise les deltas et les modulos pour éviter de traiter les cas particuliers des bords de l'univers avec le if. Lorsqu'on applique un delta de -1, nous ajoutons self.hauteur - 1 et nous laissons le modulo faire son travail, plutôt que d'essayer d'enlever 1. ligne ou colonne peut valoir 0, et si nous essayons de leur soustraire 1, nous serons alors en dehors des valeurs acceptées par les entiers non-signés.

Maintenant, nous avons tout ce dont nous avons besoin pour calculer la prochaine génération ! Chaque règle du jeu suit des transformations simples suivant des conditions qui peuvent tenir dans une expression match. De plus, comme nous souhaitons que le JavaScript contrôle lorsque les ticks se produisent, nous allons intégrer cette méthode dans un bloc #[wasm_bindgen], pour qu'il soit exposé au JavaScript.


#![allow(unused)]
fn main() {
/// Méthodes publiques, exportées en JavaScript.
#[wasm_bindgen]
impl Univers {
    pub fn tick(&mut self) {
        let mut generation_suivante = self.cellules.clone();

        for ligne in 0..self.hauteur {
            for colonne in 0..self.largeur {
                let indice = self.calculer_indice(ligne, colonne);
                let cellule = self.cellules[indice];
                let voisines_vivantes = self.compter_voisines_vivantes(ligne, colonne);

                let prochain_etat = match (cellule, voisines_vivantes) {
                    // Règle 1 : toute cellule vivante avec moins de deux
                    // voisines vivantes meurt, comme si cela était un effet de
                    //  sous-population.
                    (Cellule::Vivante, x) if x < 2 => Cellule::Morte,
                    // Règle 2 : toute cellule vivante avec deux ou trois
                    // voisines vivantes survit jusqu'à la prochaine génération.
                    (Cellule::Vivante, 2) | (Cellule::Vivante, 3) => Cellule::Vivante,
                    // Règle 3 : toute cellule vivante avec plus de trois
                    // voisines vivantes meurt, comme si cela était un effet de
                    // surpopulation.
                    (Cellule::Vivante, x) if x > 3 => Cellule::Morte,
                    // Règle 4 : toute cellule morte avec exactement trois
                    // voisines vivantes devient une cellule vivante, comme si
                    // cela était un effet de reproduction.
                    (Cellule::Morte, 3) => Cellule::Vivante,
                    // Les cellules qui ne répondent à aucune de ces conditions
                    // restent dans le même état.
                    (statut, _) => statut,
                };

                generation_suivante[idx] = prochain_etat;
            }
        }

        self.cellules = generation_suivante;
    }

    // ...
}
}

Pour l'instant, l'état de l'univers est modélisé par un vecteur de cellules. Pour rendre cela lisible pour un humain, implémentons un rendu textuel basique. L'idée est d'écrire l'univers ligne par ligne textuellement, ainsi nous allons écrire le caractère Unicode (le "carré moyen noir") pour chaque cellule vivante. Et pour les cellules mortes, nous allons écrire (le "carré moyen blanc").

En implémentant le trait Display de la bibliothèque standard de Rust, nous pouvons ajouter un moyen de formater la structure de manière à ce qu'elle soit adaptée pour l'utilisateur. Cela va aussi nous fournir automatiquement une méthode to_string.


#![allow(unused)]
fn main() {
use std::fmt;

impl fmt::Display for Univers {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for ligne in self.cellules.as_slice().chunks(self.largeur as usize) {
            for &cellules in ligne {
                let symbole = if cellule == Cellule::Morte { '◻' } else { '◼' };
                write!(f, "{}", symbole)?;
            }
            write!(f, "\n")?;
        }

        Ok(())
    }
}
}

Enfin, nous définissons un constructeur qui initialise l'univers avec un schéma intéressant avec des cellules vivantes et mortes, ainsi qu'une méthode rendu :


#![allow(unused)]
fn main() {
/// Méthodes publiques, exportées en JavaScript.
#[wasm_bindgen]
impl Univers {
    // ...

    pub fn new() -> Univers {
        let largeur = 64;
        let hauteur = 64;

        let cellules = (0..largeur * hauteur)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cellule::Vivante
                } else {
                    Cellule::Morte
                }
            })
            .collect();

        Univers {
            largeur,
            hauteur,
            cellules,
        }
    }

    pub fn rendu(&self) -> String {
        self.to_string()
    }
}
}

Avec tout cela, la partie Rust de notre jeu de la vie est complète !

Recompilez-la en WebAssembly en lançant wasm-pack build dans le dossier wasm-jeu-de-la-vie.

Le rendu avec JavaScript

Pour commencer, ajoutons une balise <pre> à wasm-jeu-de-la-vie/www/index.html, dans lequel afficher l'univers, juste avant la balise <script> :

<body>
  <pre id="canvas-jeu-de-la-vie"></pre>
  <script src="./bootstrap.js"></script>
</body>

De plus, nous voulons que le <pre> soit centré au milieu de la page Web. Nous pouvons utiliser les boites flex pour faire cela. Ajoutez la balise <style> suivante dans le <head> de wasm-jeu-de-la-vie/www/index.html :

<style>
  body {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
</style>

En haut de wasm-jeu-de-la-vie/www/index.js, corrigeons notre import pour importer le Univers plutôt que la vieille fonction saluer :

import { Univers } from "wasm-jeu-de-la-vie";

Ensuite, obtenez la balise <pre> que nous venons juste d'ajouter et instancier un nouvel univers :

const pre = document.getElementById("canvas-jeu-de-la-vie");
const univers = Univers.new();

Le JavaScript exécute dans une boucle requestAnimationFrame. A chaque itération, il écrit l'univers courant dans le <pre>, et fait ensuite appel à Univers::tick.

const boucleDeRendu = () => {
  pre.textContent = univers.rendu();
  univers.tick();

  requestAnimationFrame(boucleDeRendu);
};

Pour initier le processus de rendu, tout ce que nous avons à faire est de faire le premier appel à la première itération de la boucle de rendu :

requestAnimationFrame(boucleDeRendu);

Assurez-vous que votre serveur de développement continue de s'exécuter (lancez npm run start dans wasm-jeu-de-la-vie/www) et voici ce à quoi http://localhost:8080/ devrait ressembler :

Capture d'écran de l'implémentation du jeu de la vie avec le rendu textuel

Afficher dans un canvas directement à partir de la mémoire

Générer (et allouer) un String en Rust et le convertir en String JavaScript valide par wasm-bindgen génère des copies inutiles des cellules de l'univers. Comme le code JavaScript connait déjà la largeur et la hauteur de l'univers, et peux lire la mémoire linéaire de WebAssembly qui contient les cellules, nous allons modifier la méthode rendu pour retourner un pointeur vers le début du tableau des cellules.

De plus, au lieu d'afficher du texte Unicode, nous allons utiliser l'API de canvas. Nous utiliserons alors cette conception dans la suite du tutoriel.

Dans wasm-jeu-de-la-vie/www/index.html, remplaçons le <pre> que nous avons ajouté précédemment par un <canvas> dans lequel nous allons faire notre rendu (il devrait toujours se trouver dans la <body>, avant le <script> qui charge notre JavaScript) :

<body>
  <canvas id="canvas-jeu-de-la-vie"></canvas>
  <script src='./bootstrap.js'></script>
</body>

Pour obtenir les informations de l'implémentation Rust nécessaires, nous avons besoin d'ajouter plus d'accesseurs pour obtenir la largeur, la hauteur de l'univers, et le pointeur à son tableau de cellules. Ils seront eux aussi exposés au JavaScript. Faites ces ajouts à wasm-jeu-de-la-vie/src/lib.rs :


#![allow(unused)]
fn main() {
/// Méthodes publiques, exportées en JavaScript.
#[wasm_bindgen]
impl Univers {
    // ...

    pub fn largeur(&self) -> u32 {
        self.largeur
    }

    pub fn hauteur(&self) -> u32 {
        self.hauteur
    }

    pub fn cellules(&self) -> *const Cellule {
        self.cellules.as_ptr()
    }
}
}

Ensuite, dans wasm-jeu-de-la-vie/www/index.js, ajoutons aussi l'import de Cellule de wasm-jeu-de-la-vie, et définissons quelques constantes que nous utiliserons lorsque nous ferons le rendu dans le canvas :

import { Univers, Cellule } from "wasm-jeu-de-la-vie";

const TAILLE_CELLULE = 5; // px
const COULEUR_GRILLE = "#CCCCCC";
const COULEUR_MORTE = "#FFFFFF";
const COULEUR_VIVANTE = "#000000";

Maintenant, ré-écrivons le reste du code JavaScript pour ne plus avoir à écrire avec textContent dans le <pre> mais dessiner dans <canvas> à la place :

// Construit l'univers, et obtient sa largeur et son hauteur
const univers = Univers.new();
const largeur = univers.largeur();
const hauteur = univers.hauteur();

// Applique une taille au canvas pour accueillir toutes nos cellules et une
// bordure de 1px autour d'elles.

const canvas = document.getElementById("canvas-jeu-de-la-vie");
canvas.height = (TAILLE_CELLULE + 1) * largeur + 1;
canvas.width = (TAILLE_CELLULE + 1) * hauteur + 1;

const ctx = canvas.getContext('2d');

const boucleDeRendu = () => {
  univers.tick();

  dessinerGrille();
  dessinerCellules();

  requestAnimationFrame(boucleDeRendu);
};

Pour dessiner la grille entre les cellules, nous dessinons un jeu de lignes espacées régulièrement horizontalement, et un jeu de lignes espacées régulièrement verticalement. Ces lignes s'entrecroisent pour former la grille.

const dessinerGrille = () => {
  ctx.beginPath();
  ctx.strokeStyle = COULEUR_GRILLE;

  // Lignes verticales.
  for (let i = 0; i <= largeur; i++) {
    ctx.moveTo(i * (TAILLE_CELLULE + 1) + 1, 0);
    ctx.lineTo(i * (TAILLE_CELLULE + 1) + 1, (TAILLE_CELLULE + 1) * hauteur + 1);
  }

  // Lignes horizontales.
  for (let j = 0; j <= hauteur; j++) {
    ctx.moveTo(0,                                  j * (TAILLE_CELLULE + 1) + 1);
    ctx.lineTo((TAILLE_CELLULE + 1) * largeur + 1, j * (TAILLE_CELLULE + 1) + 1);
  }

  ctx.stroke();
};

Nous pouvons accéder directement à la mémoire linéaire de WebAssembly via memory, qui est défini dans le module brut wasm_jeu_de_la_vie_bg. Pour dessiner les cellules, nous obtenons le pointeur vers les cellules de l'univers, construisons un Uint8Array qui sert de surcouche tampon pour les cellules, itère sur chaque cellule, et dessine un rectangle blanc ou noir, respectivement si la cellule est morte ou vivante. En travaillant avec des pointeurs et des surcouches, nous évitons de copier les cellules entre les deux domaines à chaque tick.

// Importe la mémoire de WebAssembly au début du fichier.
import { memory } from "wasm-jeu-de-la-vie/wasm_jeu_de_la_vie_bg";

// ...

const calculerIndice = (ligne, colonne) => {
  return ligne * largeur + colonne;
};

const dessinerCellules = () => {
  const pointeurCellules = univers.cellules();
  const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur);

  ctx.beginPath();

  for (let ligne = 0; ligne < hauteur; ligne++) {
    for (let colonne = 0; colonne < largeur; colonne++) {
      const indice = calculerIndice(ligne, colonne);

      ctx.fillStyle = cellules[indice] === Cellule.Morte
        ? COULEUR_MORTE
        : COULEUR_VIVANTE;

      ctx.fillRect(
        colonne * (TAILLE_CELLULE + 1) + 1,
        ligne * (TAILLE_CELLULE + 1) + 1,
        TAILLE_CELLULE,
        TAILLE_CELLULE
      );
    }
  }

  ctx.stroke();
};

Pour démarrer le processus de rendu, nous allons utiliser le même code que ci-dessus pour démarrer la première itération de la boucle de rendu :

dessinerGrille();
dessinerCellules();
requestAnimationFrame(boucleDeRendu);

Notez que nous faisons appel à dessinerGrille() et à dessinerCellules() ici avant de faire appel à requestAnimationFrame(). La raison à cela est que l'état initial de l'univers est dessiné avant que nous procédions à nos modifications. Si nous avions simplement appelé requestAnimationFrame(boucleDeRendu) à la place, nous nous serions retrouvé dans une situation dans laquelle la première séquence serait dessinée après le premier appel à univers.tick(), qui est le second "tick" dans la vie de ces cellules.

Cela fonctionne !

Recompilez le WebAssembly et la glue de liaison en lançant cette commande dans le dossier racine wasm-jeu-de-la-vie :

wasm-pack build

Assurez-vous que votre serveur de développement fonctionne toujours. Si ce n'est plus le cas, relancez-le dans le dossier wasm-jeu-de-la-vie/www :

npm run start

Si vous rafraîchissez http://localhost:8080/, vous devriez être accueilli par une simulation de la vie captivante !

Capture d'écran de l'implémentation du jeu de la vie

Ceci dit, il existe aussi un algorithme très intéressant pour implémenter le jeu de la vie qui s'appelle hashlive. Il utilise une méthode de gestion de la mémoire poussée et peut devenir exponentiellement plus rapide pour calculer les prochaines générations au fur et à mesure qu'il s'exécute ! Sachant cela, vous vous demandez peut-être pourquoi nous n'avons pas implémenté hashlife dans ce tutoriel. Ce n'est pas le but de ce document, car nous nous concentrons sur l'intégration de Rust en WebAssembly, mais nous vous encourageons vivement d'en apprendre plus sur hashlife par vous-même !

Exercices

  • Initialiser l'univers avec un simple vaisseau spatial.
  • Au lieu de coder en dur l'univers initial, générez-en un aléatoire, dans lequel chaque cellule a cinquante pour cent de chance d'être vivante ou morte.

    Astuce : utilisez la crate js-sys pour importer la fonction JavaScript Math.random.

    Réponse

    Premièrement, ajoutez js-sys comme dépendance dans wasm-jeu-de-la-vie/Cargo.toml :

    # ...
    [dependencies]
    js-sys = "0.3"
    # ...
    

    Ensuite, utilisez la fonction js_sys::Math::random pour générer un nombre aléatoire :

    
    #![allow(unused)]
    fn main() {
    extern crate js_sys; // (facultatif à partir de Rust 2018)
    
    // ...
    
    if js_sys::Math::random() < 0.5 {
        // Vivante ...
    } else {
        // Morte ...
    }
    }
    
  • Représenter chaque cellule avec un octet facilite l'itération sur les cellules, mais cela gaspille de la mémoire. Chaque octet a huit bits, mais nous n'avons besoin d'un seul bit pour représenter si chaque cellule est vivante ou morte. Remaniez la représentation des données pour que chaque cellule utilise uniquement un seul bit en mémoire.

    Réponse

    En Rust, vous pouvez utiliser la crate fixedbitset et son type FixedBitSet pour représenter les cellules au lieu d'utiliser Vec<Cell> :

    
    #![allow(unused)]
    fn main() {
    // Assurez-vous d'avoir aussi ajouté la dépendance dans Cargo.toml !
    extern crate fixedbitset; // (facultatif en Rust 2018)
    use fixedbitset::FixedBitSet;
    
    // ...
    
    #[wasm_bindgen]
    pub struct Univers {
        largeur: u32,
        hauteur: u32,
        cellules: FixedBitSet,
    }
    }
    

    Le constructeur de l'Univers peut être corrigé comme ceci :

    
    #![allow(unused)]
    fn main() {
    pub fn new() -> Univers {
        let largeur = 64;
        let hauteur = 64;
    
        let taille = (largeur * hauteur) as usize;
        let mut cellules = FixedBitSet::with_capacity(taille);
    
        for i in 0..taille {
            cellules.set(i, i % 2 == 0 || i % 7 == 0);
        }
    
        Univers {
            largeur,
            hauteur,
            cellules,
        }
    }
    }
    

    Pour modifier une cellule à la prochaine tick de l'univers, nous utilisons la méthode set de FixedBitSet :

    
    #![allow(unused)]
    fn main() {
    generation_suivante.set(indice, match (cellule, voisines_vivantes) {
        (true, x) if x < 2 => false,
        (true, 2) | (true, 3) => true,
        (true, x) if x > 3 => false,
        (false, 3) => true,
        (statut, _) => statut
    });
    }
    

    Pour passer un pointeur vers le départ des bits en JavaScript, vous pouvez convertir le FixedBitSet en une slice et ensuite convertir la slice en pointeur :

    
    #![allow(unused)]
    fn main() {
    #[wasm_bindgen]
    impl Univers {
        // ...
    
        pub fn cellules(&self) -> *const u32 {
            self.cellules.as_slice().as_ptr()
        }
    }
    }
    

    En JavaScript, la construction d'un Uint8Array à partir de la mémoire de WebAssembly est la même que précédemment, excepté que la longueur du tableau n'est plus largeur * hauteur, mais largeur * hauteur / 8 puisque nous avons un bit par cellule au lieu d'un octet :

    const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur / 8);
    

    Pour un indice et un Uint8Array donné, vous pouvez obtenir le nième bit avec la fonction suivante :

    const bitVautTrue = (n, tableau) => {
      const octet = Math.floor(n / 8);
      const masque = 1 << (n % 8);
      return (tableau[octet] & masque) === masque;
    };
    

    En ayant tout cela, la nouvelle version de dessinerCellules ressemble à ceci :

    const dessinerCellules = () => {
      const pointeurCellules = univers.cellules();
    
      // On a modifié cela !
      const cellules = new Uint8Array(memory.buffer, pointeurCellules, largeur * hauteur / 8);
    
      ctx.beginPath();
    
      for (let ligne = 0; ligne < hauteur; ligne++) {
        for (let colonne = 0; colonne < largeur; colonne++) {
          const indice = calculerIndice(ligne, colonne);
    
          // On a modifié cela !
          ctx.fillStyle = bitVautTrue(indice, cellules)
            ? COULEUR_VIVANTE
            : COULEUR_MORTE;
    
          ctx.fillRect(
            colonne * (TAILLE_CELLULE + 1) + 1,
            ligne * (TAILLE_CELLULE + 1) + 1,
            TAILLE_CELLULE,
            TAILLE_CELLULE
          );
        }
      }
    
      ctx.stroke();
    };
    

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Tester le jeu de la vie de Conway

Maintenant que nous avons notre implémentation en Rust du jeu de la vie qui s'exécute dans le navigateur web avec JavaScript, nous pouvons voir comment tester nos fonctions WebAssembly générées par Rust.

Nous allons tester notre fonction tick pour s'assurer qu'elle nous donne bien le résultat que nous souhaitons.

Ensuite, nous allons créer des mutateurs et des accesseurs dans notre bloc impl Univers dans le ficher wasm-jeu-de-la-vie/src/lib.rs. Nous allons créer une fonction set_largeur et set_hauteur pour que nous puissions créer des Univers de différentes tailles.


#![allow(unused)]
fn main() {
#[wasm_bindgen]
impl Univers {
    // ...

    /// Définit la largeur de l'univers.
    ///
    /// Cela va tuer toutes les cellules.
    pub fn set_largeur(&mut self, largeur: u32) {
        self.largeur = largeur;
        self.cellules = (0..largeur * self.hauteur).map(|_i| Cellule::Morte).collect();
    }

    /// Définit la hauteur de l'univers.
    ///
    /// Cela va tuer toutes les cellules.
    pub fn set_hauteur(&mut self, hauteur: u32) {
        self.hauteur = hauteur;
        self.cellules = (0..self.largeur * hauteur).map(|_i| Cellule::Morte).collect();
    }

}
}

Nous allons créer un autre bloc impl Univers dans notre fichier wasm-jeu-de-la-vie sans l'attribut #[wasm_bindgen]. Il y a quelques fonctions que nous avons besoin pour tester que nous ne souhaitons pas exposer au JavaScript. Les fonctions WebAssembly générées par Rust ne peut pas retourner des références empruntées. Essayez de compiler le WebAssembly généré par Rust avec l'attribut et constatez les erreurs que vous obtenez.

Nous allons écrire l'implémentation de get_cellules pour obtenir le contenu de cellules d'un Univers. Nous allons aussi écrire une fonction set_cellules pour que nous puissions donner vie à des cellules d'un Univers.


#![allow(unused)]
fn main() {
impl Univers {
    /// Donne toutes les cellules mortes et vivantes de l'univers.
    pub fn get_cellules(&self) -> &[Cellule] {
        &self.cellules
    }

    /// Définit les cellules vivantes de l'univers en lui fournissant la ligne
    /// et la colonne de chacune des cellules dans un tableau.
    pub fn set_cellules(&mut self, cellules: &[(u32, u32)]) {
        for (ligne, colonne) in cellules.iter().cloned() {
            let indice = self.get_index(ligne, colonne);
            self.cellules[indice] = Cellule::Vivante;
        }
    }

}
}

Maintenant nous pouvons créer notre test dans le fichier wasm-jeu-de-la-vie/tests/web.rs.

Avant de nous lancer, nous constatons qu'il existe déjà un test qui fonctionne dans ce fichier. Vous pouvez confirmer que le test du WebAssembly généré par Rust fonctionne en exécutant wasm-pack test --chrome --headless dans le dossier wasm-jeu-de-la-vie. Vous pouvez utiliser les options --firefox, --safari, et --node pour tester votre code dans ces navigateurs.

Dans le fichier wasm-jeu-de-la-vie/tests/web.rs, nous devons importer notre crate wasm_jeu_de_la_vie et le type Univers.


#![allow(unused)]
fn main() {
extern crate wasm_jeu_de_la_vie; // (facultatif en Rust 2018)
use wasm_jeu_de_la_vie::Univers;
}

Dans le fichier wasm-jeu-de-la-vie/tests/web.rs, nous allons ajouter quelques fonctions qui créent des créateurs de vaisseaux spatiaux.

Nous allons en ajouter une pour créer notre vaisseau spatial initial lorsque nous appellerons la fonction tick et nous voulons qu'il soit toujours là après une tick. Nous avons choisi les cellules que nous voulons donner vie pour créer notre vaisseau spatial dans la fonction vaisseau_spatial_initial. La position du vaisseau spatial dans la fonction vaisseau_spatial_attendu dans la tick suivant vaisseau_spatial_initial a été calculée manuellement. Vous pouvez vérifier par vous-même que les cellules du vaisseau spatial initial sont les mêmes que celles attendues après une tick.


#![allow(unused)]
fn main() {
#[cfg(test)]
pub fn vaisseau_spatial_initial() -> Univers {
    let mut univers = Univers::new();
    univers.set_largeur(6);
    univers.set_hauteur(6);
    univers.set_cellules(&[(1,2), (2,3), (3,1), (3,2), (3,3)]);
    univers
}

#[cfg(test)]
pub fn vaisseau_spatial_attendu() -> Univers {
    let mut univers = Univers::new();
    univers.set_largeur(6);
    univers.set_hauteur(6);
    univers.set_cellules(&[(2,1), (2,3), (3,2), (3,3), (4,2)]);
    univers
}
}

Maintenant nous allons écrire l'implémentation de notre fonction test_tick. Pour commencer, nous allons créer une instance de notre vaisseau_spatial_initial() et de notre vaisseau_spatial_attendu(). Ensuite, nous appellerons tick sur univers_initial. Enfin, nous utiliserons la macro assert_eq! pour faire appel à get_cellules() pour s'assurer que univers_initial et univers_attendu ont la même valeur pour leur tableau de Cellules. Nous avons ajouté l'attribut #[wasm_bindgen_test] à notre bloc de code pour que nous puissions tester notre code WebAssembly généré par Rust et utiliser wasm-pack test pour tester le code WebAssembly.


#![allow(unused)]
fn main() {
#[wasm_bindgen_test]
pub fn test_tick() {
    // On crée un petit univers avec un petit vaisseau spatial, pour tester !
    let mut univers_initial = vaisseau_spatial_initial();

    // C'est ce à quoi doit ressembler notre vaisseau spatial après une tick
    // dans notre univers.
    let univers_attendu = vaisseau_spatial_attendu();

    // Appelons `tick` et voyons ensuite si les cellules dans les `Univers` sont
    // les mêmes.
    univers_initial.tick();
    assert_eq!(&univers_initial.get_cells(), &univers_attendu.get_cells());
}
}

Exécutez les tests dans le dossier wasm-jeu-de-la-vie en exécutant wasm-pack test --firefox --headless.

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Débogage

Avant d'écrire plus de code, nous devons nous équiper d'outils de débogage pour les moments où cela se passe mal. Prenez donc une minute pour consulter la page de référence qui liste les outils et les approches pour déboguer le WebAssembly généré par Rust.

Activer les journaux pour les paniques

Si notre code panique, nous souhaitons avoir un message d'erreur qui nous en informe dans la console de développement.

Notre wasm-pack-template-fr applique une dépendance optionnelle et activée par défaut envers la crate console_error_panic_hook qui est configurée dans wasm-jeu-de-la-vie/src/utils.rs. Tout ce que nous avons besoin de faire est d'installer le système dans une fonction d'initialisation ou dans un code standard qui s'exécutera. Nous pouvons faire appel à cela dans le constructeur Univers::new dans wasm-jeu-de-la-vie/src/lib.rs :


#![allow(unused)]
fn main() {
pub fn new() -> Univers {
    utils::set_panic_hook();

    // ...
}
}

Ajouter des journaux dans notre jeu de la vie

Utilisons la fonction console.log via la crate web-sys pour ajouter quelques journaux sur le traitement de chaque cellule dans notre fonction Univers::tick.

Pour commencer, ajoutez web-sys comme dépendance et ajoutez sa fonctionnalité "console" dans wasm-jeu-de-la-vie/Cargo.toml :

[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]

Pour que ce soit plus pratique, nous allons intégrer la fonction console.log dans une macro du même style que println! :


#![allow(unused)]
fn main() {
extern crate web_sys;

// Une macro dans le même style que `println!(..)` pour la journalisation avec
// `console.log`.
macro_rules! log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}
}

Maintenant nous pouvons commencer à ajouter des journaux dans la console en insérant des appels à log dans le code Rust. Par exemple, pour afficher l'état de chaque cellule, le compteur de ses voisines vivantes, et le prochain état, nous pouvons modifier wasm-jeu-de-la-vie/src/lib.rs comme ceci :

diff --git a/src/lib.rs b/src/lib.rs
index f757641..a30e107 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -123,6 +122,14 @@ impl Univers {
                 let cellule = self.cellules[indice];
                 let voisines_vivantes = self.compter_voisines_vivantes(ligne, colonne);

+                log!(
+                    "La cellule [{}, {}] est initialement {:?} et a {} voisines vivantes",
+                    ligne,
+                    colonne,
+                    cellule,
+                    voisines_vivantes
+                );
+
                 let prochain_etat = match (cellule, voisines_vivantes) {
                     // Règle 1 : toute cellule vivante avec moins de deux
                     // voisines vivantes meurt, comme si cela était un effet de
                     // sous-population.
@@ -140,6 +147,8 @@ impl Universe {
                     (statut, _) => statut,
                 };

+                log!("    et elle devient {:?}", prochain_etat);
+
                 generation_suivante[idx] = prochain_etat;
             }
         }

Utiliser un débogueur pour faire une pause entre chaque tick

Les débogueurs par étape des navigateurs sont utiles pour inspecter le JavaScript avec lequel interagit notre WebAssembly généré par Rust.

Par exemple, nous pouvons utiliser le débogueur pour faire une pause sur chaque itération de notre fonction boucleDeRendu en plaçant une instruction JavaScript debugger; juste avant l'appel à univers.tick().

const boucleDeRendu = () => {
  debugger;
  univers.tick();

  dessinerGrille();
  dessinerCellules();

  requestAnimationFrame(boucleDeRendu);
};

Cela nous fournit un point de passage efficace pour inspecter les journaux, et pour comparer le rendu actuel avec le précédent.

Capture d'écran du débogage du jeu de la vie

Exercices

  • Ajoutez des journaux à la fonction tick qui affiche la ligne et la colonne de chaque cellule qui chaque d'état, de vivante à morte et vice-versa.
  • Ajoutez un panic!() dans la méthode Univers::new. Inspectez alors la trace de pile dans le débogueur JavaScript de votre navigateur Web. Puis, désactivez les symboles de débogage, recompilez sans la dépendance optionnelle à console_error_panic_hook, et inspectez à nouveau la trace de pile. Pratique, n'est-ce pas ?

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Ajouter de l'interactivité

Nous allons continuer à explorer l'interfaçage entre le JavaScript et le WebAssembly en ajouter des fonctionnalités d'interaction avec notre implémentation du jeu de la vie. Nous allons permettre aux utilisateurs de changer le statut vivant ou mort d'une cellule en cliquant dessus, et aussi leur permettre de mettre en pause le jeu, ce qui va faciliter le dessin de certains schémas.

Mettre en pause et reprendre le jeu

Ajoutons un bouton pour changer si le jeu est en cours de lecture ou en pause. Dans wasm-jeu-de-la-vie/www/index.html, ajoutez le bouton juste au-dessus le <canvas> :

<button id="lecture-pause"></button>

Dans le JavaScript wasm-game-of-life/www/index.js, nous allons faire les changements suivants :

  • Conserver l'identifiant retourné par le dernier appel à requestAnimationFrame, pour que nous puissions annuler l'animation en faisant appel à cancelAnimationFrame avec cet identifiant.
  • Lorsque le bouton lecture/pause sera actionné, on va regarder si nous avons un identifiant pour un calcul d'une génération de l'animation. Si c'est le cas, alors le jeu est actuellement en cours de lecture, et cela veut donc dire que nous souhaitons annuler le calcul pour éviter que boucleDeRendu soit appelé à nouveau, ce qui aura pour effet de mettre en pause le jeu. Si nous n'avons pas d'identifiant pour un calcul d'une génération de l'animation, alors cela veut dire que nous sommes actuellement en pause, et nous devons faire appel à requestAnimationFrame pour reprendre le jeu.

Comme le JavaScript pilote le Rust via le WebAssembly, c'est tout ce que nous avons besoin de faire, et nous n'avons pas besoin de changer le code source Rust.

Nous ajoutons la variable animationId pour conserver l'identifiant retourné par requestAnimationFrame. Lorsqu'il n'y a pas de calcul d'une génération de l'animation, nous donnons assignons la valeur null à cette variable.

let animationId = null;

// Cette fonction est la même qu'avant, sauf que le résultat de
// `requestAnimationFrame` est assigné à `animationId`.
const boucleDeRendu = () => {
  dessinerGrille();
  dessinerCellules();

  univers.tick();

  animationId = requestAnimationFrame(boucleDeRendu);
};

A n'importe quel moment, nous pouvons savoir si le jeu est en pause ou non en vérifiant la valeur de animationId :

const estEnPause = () => {
  return animationId === null;
};

Maintenant lorsque le bouton lecture/pause est cliqué, nous vérifions si le jeu est en pause ou en lecture, et respectivement nous reprenons l'animation boucleDeRendu ou nous arrêtons le calcul de la prochaine génération de l'animation. De plus, nous allons changer le texte du bouton pour refléter l'action que le bouton va faire lors du prochain clic.

const boutonLecturePause = document.getElementById("lecture-pause");

const lecture = () => {
  boutonLecturePause.textContent = "⏸";
  boucleDeRendu();
};

const pause = () => {
  boutonLecturePause.textContent = "▶";
  cancelAnimationFrame(animationId);
  animationId = null;
};

boutonLecturePause.addEventListener("click", event => {
  if (estEnPause()) {
    lecture();
  } else {
    pause();
  }
});

Enfin, jusqu'ici nous avons démarré le jeu et son animation en faisant directement appel à requestAnimationFrame(boucleDeRendu), mais nous voulons remplacer cela par l'appel à lecture pour que le bouton ait la bonne icone initiale.

// On utilise cela à la place de `requestAnimationFrame(boucleDeRendu)`.
lecture();

Rafraîchissez http://localhost:8080/ et vous devriez maintenant mettre en pause et reprendre le jeu en cliquant sur le bouton !

Changer l'état d'une cellule avec un évènement "click"

Maintenant que nous pouvons mettre le jeu en pause, nous pouvons ajouter la possibilité de muter les cellules en cliquant sur elles.

Pour basculer le statut d'une cellule de vivante à morte, ou de morte à vivante. Ajoutez une méthode basculer à Cellule dans wasm-jeu-de-la-vie/src/lib.rs :


#![allow(unused)]
fn main() {
impl Cellule {
    fn basculer(&mut self) {
        *self = match *self {
            Cellule::Morte => Cellule::Vivante,
            Cellule::Vivante => Cellule::Morte,
        };
    }
}
}

Pour basculer l'état d'une cellule à une ligne et colonne donnée, nous traduisons le couple ligne et colonne en indice du vecteur de toutes les cellules et nous faisons appel à la méthode basculer sur la cellule à cet indice :


#![allow(unused)]
fn main() {
/// Méthodes publiques, exportées en JavaScript.
#[wasm_bindgen]
impl Univers {
    // ...

    pub fn basculer_cellule(&mut self, ligne: u32, colonne: u32) {
        let indice = self.calculer_indice(ligne, colonne);
        self.cellules[indice].basculer();
    }
}
}

Cette méthode est définie dans le bloc impl qui est annoté avec #[wasm_bingen] afin qu'il puisse être appelé en JavaScript.

Dans wasm-jeu-de-la-vie/www/index.js, nous écoutons les évènements de clics sur le noeud <canvas>, puis nous traduisons les coordonnées du clic dans le contexte de la page en coordonnées dans le canvas, et ensuite ces coordonnées en ligne et colonne, puis nous évoquons la méthode basculer_cellule, et enfin nous redessinons la scène.

canvas.addEventListener("click", evenement => {
  const zoneRectangulaire = canvas.getBoundingClientRect();

  const echelleX = canvas.width / zoneRectangulaire.width;
  const echelleY = canvas.height / zoneRectangulaire.height;

  const distanceGaucheDuCanvas = (evenement.clientX - zoneRectangulaire.left) * echelleX;
  const distanceHautDuCanvas = (evenement.clientY - zoneRectangulaire.top) * echelleY;

  const ligne = Math.min(Math.floor(distanceHautDuCanvas / (CELL_SIZE + 1)), hauteur - 1);
  const colonne = Math.min(Math.floor(distanceGaucheDuCanvas / (CELL_SIZE + 1)), largeur - 1);

  univers.basculer_cellule(ligne, colonne);

  dessinerGrille();
  dessinerCellules();
});

Recompilez avec wasm-pack build dans le dossier wasm-jeu-de-la-vie, ensuite rafraîchissez à nouveau la page http://localhost:8080/ et vous devriez pouvoir dessiner vos propres schémas en cliquant sur les cellules pour pouvoir changer leur état.

Exercices

  • Ajouter un composant <input type="range"> pour pouvoir régler combien de ticks se produisent à chaque séquence de l'animation.
  • Ajouter un bouton qui réinitialise l'univers dans un état initial aléatoire lorsqu'on clique dessus. Et un autre bouton qui réinitialise l'univers avec uniquement des cellules mortes.
  • Lors d'un Ctrl + Clic, insérez un planeur centré sur la cellule cible. Lors d'un Shift + Clic, insérez un pulsar.

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

🚧 Les captures d'écran de cette page n'ont pas encore été traduites !

Le profilage temporel

Dans ce chapitre, nous allons améliorer la performance de l'implémentation de notre jeu de la vie. Nous allons utiliser le profilage temporel pour orienter notre travail.

Avant de continuer, familiarisez-vous avec [les outils disponibles pour le profilage temporel pour le code Rust et WebAssembly].

Créer un compteur d'images par seconde avec window.performance.now

Ce compteur d'images par seconde sera un indicateur utile lors de nos recherches d'améliorations de performances du rendu de notre jeu de la vie.

Nous allons commencer par rajouter un objet ips (pour Images Par Seconde) à wasm-jeu-de-la-vie/www/index.js :

const ips = new class {
  constructor() {
    this.ips = document.getElementById("ips");
    this.images = [];
    this.timeStampDeLaDerniereImage = performance.now();
  }

  afficher() {
    // Convertit la différence de temps entre la dernière image en
    // images par seconde.
    const maintenant = performance.now();
    const difference = maintenant - this.timeStampDeLaDerniereImage;
    this.timeStampDeLaDerniereImage = maintenant;
    const ips = 1 / difference * 1000;

    // Ne conserve que les 100 dernières images.
    this.images.push(ips);
    if (this.images.length > 100) {
      this.images.shift();
    }

    // Trouve la valeur minimale, maximale, et la moyenne des
    // 100 dernières images.
    let min = Infinity;
    let max = -Infinity;
    let somme = 0;
    for (let i = 0; i < this.images.length; i++) {
      somme += this.images[i];
      min = Math.min(this.images[i], min);
      max = Math.max(this.images[i], max);
    }
    let moyenne = somme / this.images.length;

    // Affiche les statistiques.
    this.ips.textContent = `
Images Par Seconde :
                           actuel = ${Math.round(ips)}
 moyenne des 100 dernières images = ${Math.round(moyenne)}
mininima des 100 dernières images = ${Math.round(min)}
  maxima des 100 dernières images = ${Math.round(max)}
`.trim();
  }
};

Ensuite, nous devons appeler la fonction afficher à chaque itération de boucleDeRendu :

const boucleDeRendu = () => {
    ips.afficher(); //new

    univers.tick();
    dessinerGrille();
    dessinerCellules();

    animationId = requestAnimationFrame(boucleDeRendu);
};

Enfin, n'oubliez pas d'ajouter le noeud #ips à wasm-jeu-de-la-vie/www/index.html, juste au-dessus du <canvas> :

<div id="ips"></div>

Ainsi qu'un peu de style CSS pour améliorer son rendu :

#ips {
  white-space: pre;
  font-family: monospace;
}

Et voilà ! Rafraîchissez la page http://localhost:8080 et vous avez maintenant un compteur d'images par seconde !

Chronomètrer chaque Univers::tick avec console.time et console.timeEnd

Pour mesurer la durée que prends chaque appel à Univers::tick, nous pouvons utiliser console.time et console.timeEnd avec la crate web-sys.

Pour commencer, ajoutez la dépendance web-sys dans wasm-jeu-de-la-vie/Cargo.toml :

[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]

Comme nous aurons un appel à console.timeEnd correspondant à chaque appel à console.time, il nous est plus pratique de les intégrer dans un type qui implémente le concept de RAII :


#![allow(unused)]
fn main() {
extern crate web_sys;
use web_sys::console;

pub struct Chronometre<'a> {
    nom: &'a str,
}

impl<'a> Chronometre<'a> {
    pub fn new(nom: &'a str) -> Chronometre<'a> {
        console::time_with_label(nom);
        Chronometre { nom }
    }
}

impl<'a> Drop for Chronometre<'a> {
    fn drop(&mut self) {
        console::time_end_with_label(self.nom);
    }
}
}

Maintenant, nous pouvons chronométrer le temps que prend Univers::tick en ajoutant ceci en haut de la méthode :


#![allow(unused)]
fn main() {
let _chronometre = Chronometre::new("Univers::tick");
}

La durée de chaque appel à Univers::tick est maintenant affichée dans la console :

Capture d'écran des journaux de console.time

De plus, les coupes de console.time et de console.timeEnd vont être mis en évidence dans la vue "timeline" ou "chronologie" du profileur du navigateur :

Capture d'écran des journaux de console.time

Agrandir l'univers de notre jeu de la vie

⚠️ Cette section utilise des captures d'écran de Firefox comme exemples. Bien que tous les navigateurs ont des outils qui se ressemblent, il peut y avoir quelques petites différences lorsque vous travaillez avec des outils de développements différents. Les informations du profilage que vous allez récupérer sera généralement le même, mais sa représentation peut changer en fonction des vues des différents outils que vous aurez et leur façon de nommer les choses.

Que va-t-il se passer si nous agrandissons l'univers de notre jeu de la vie ? Remplacer l'univers de 64 par 64 par un univers de 128 par 128 (en modifiant Univers::new dans wasm-jeu-de-la-vie/src/lib.rs) va réduire drastiquement les images par seconde de 60 ips fluide à un 40 ips trouble sur certaines machines.

Si nous générons un profilage et regardons la vue "chronologie", nous pouvons constater que chaque image de l'animation prend plus de 20 millisecondes de calcul. Retenez que 60 images par seconde signifie qu'il se passe environ 16 millisecondes entre chaque image. Cela ne s'applique pas uniquement aux calculs du JavaScript et du WebAssembly, mais aussi à tout ce que le navigateur fait d'autre pendant ce temps, comme le rendu et l'affichage à l'écran.

Capture d'écran de la vue chronologie du rendu d'une image

Si nous analysons ce qui se passe à chaque image de l'animation, on peut voir que le mutateur CanvasRenderingContext2D.fillStyle prend beaucoup de temps !

⚠️ Dans Firefox, si vous voyez une ligne qui dit simplement "DOM" au lieu du canvasRenderingContext2D.filleStyle que nous avons mentionné précédemment, vous devriez activer l'option "Afficher les données de la plate-forme Gecko" dans les options de vos outils de développement :

Activation de l'option afficher les données de la plate-forme Gecko

Capture d'écran d'une vue flamegraph du rendu d'une image

Et nous pouvons confirmer que ce n'est pas une anomalie en analysant l'agrégation de l'arbre d'appels de plusieurs images :

Capture d'écran d'une vue flamegraph du rendu d'une image

On passe presque 40% du temps dans ce mutateur !

⚡ Nous pourrions nous attendre à ce que la méthode tick explique la perte de performances, mais ce n'est pas le cas. Ayez toujours le réflexe d'utiliser le profileur pour orienter vos efforts, autrement vous risquez de perdre votre temps à optimiser des parties qui sont négligeables en terme de performance.

Dans la fonction dessinerCellules de wasm-jeu-de-la-vie/www/index.js, la propriété fillStyle est définie pour chaque cellule de l'univers, à chaque image de l'animation.

for (let ligne = 0; ligne < hauteur; ligne++) {
  for (let colonne = 0; colonne < largeur; colonne++) {
    const indice = calculerIndice(ligne, colonne);

    ctx.fillStyle = cellules[indice] === Cellule.Morte
      ? COULEUR_MORTE
      : COULEUR_VIVANTE;

    ctx.fillRect(
      colonne * (TAILLE_CELLULE + 1) + 1,
      ligne * (TAILLE_CELLULE + 1) + 1,
      TAILLE_CELLULE,
      TAILLE_CELLULE
    );
  }
}

Maintenant que nous avons découvert qu'utiliser fillStyle est très chronophage, qu'est-ce que nous pouvons faire pour éviter de l'utiliser aussi souvent ? Nous devons changer fillStyle si une cellule est vivante ou morte. Si nous faisons en sorte que fillStyle = COULEUR_VIVANTE et que nous dessinons ensuite toutes les cellules vivantes en une seule fois sur une première passe, et que nous faisons ensuite en sorte que fillStyle = COULEUR_MORTE puis que nous dessinons toutes les cellules mortes en une deuxième passe, alors nous utilisons fillStyle seulement deux fois, plutôt qu'une fois sur chaque cellule.

// Cellules vivantes.
ctx.fillStyle = COULEUR_VIVANTE;
for (let ligne = 0; ligne < hauteur; ligne++) {
  for (let colonne = 0; colonne < largeur; colonne++) {
    const indice = calculerIndice(ligne, colonne);
    if (cellules[indice] !== Cellule.Vivante) {
      continue;
    }

    ctx.fillRect(
      colonne * (TAILLE_CELLULE + 1) + 1,
      ligne * (TAILLE_CELLULE + 1) + 1,
      TAILLE_CELLULE,
      TAILLE_CELLULE
    );
  }
}

// Cellules mortes.
ctx.fillStyle = COULEUR_MORTE;
for (let ligne = 0; ligne < hauteur; ligne++) {
  for (let colonne = 0; colonne < largeur; colonne++) {
    const indice = calculerIndice(ligne, colonne);
    if (cellules[indice] !== Cellule.Morte) {
      continue;
    }

    ctx.fillRect(
      colonne * (TAILLE_CELLULE + 1) + 1,
      ligne * (TAILLE_CELLULE + 1) + 1,
      TAILLE_CELLULE,
      TAILLE_CELLULE
    );
  }
}

Après avoir sauvegardé ces changements et rafraîchi http://localhost:8080/, le rendu est à nouveau fluide à 60 images par secondes.

Si nous générons un nouveau profilage, nous pouvons constater que maintenant seulement 10 millisecondes se passent entre chaque image.

Capture d'écran de la vue chronologie du rendu d'une image après les modifications sur dessinerCellules

En décomposant image par image, on constate que cette utilisation de fillStyle n'a plus de impact lourd, et que la plupart du temps de calcul de notre image se passe dans fillRect, qui dessine le rectangle de chaque cellule.

Capture d'écran d'une vue flamegraph du rendu après les changements apportés à dessinerCellules

Faire en sorte que le temps s'accélère

Certaines personnes n'aiment pas attendre, et préfèrent qu'au lieu d'un seul tick de l'univers se déroule à chaque image de l'animation, il se passe neuf ticks. Nous pouvons modifier la fonction boucleDeRendu dans wasm-jeu-de-la-vie/www/index.js pour implémenter cela facilement :

for (let i = 0; i < 9; i++) {
  univers.tick();
}

Sur certaines machines, cela nous ralentit à seulement 35 images par secondes. Ce n'est pas bon, nous voulons en avoir 60 !

Nous savons maintenant que le calcul de Univers::tick prend plus de temps, donc nous allons ajouter quelques chronomètres pour couvrir certaines de ses parties avec les appels à console.time et console.timeEnd pour voir ce que cela peut nous apprendre. Notre hypothèse est que l'allocation d'un nouveau vecteur de cellules et le nettoyage de l'ancien vecteur à chaque tick est coûteux, et nous impute d'une partie importante de notre enveloppe de temps.


#![allow(unused)]
fn main() {
pub fn tick(&mut self) {
    let _chronometre = Chronometre::new("Univers::tick");

    let mut generation_suivante = {
        let _chronometre = Chronometre::new("création cellules prochaine génération");
        self.cellules.clone()
    };

    {
        let _chronometre = Chronometre::new("calcul nouvelle génération");
        for ligne in 0..self.hauteur {
            for colonne in 0..self.largeur {
                let indice = self.calculer_indice(ligne, colonne);
                let cellule = self.cellules[indice];
                let voisines_vivantes = self.compter_voisines_vivantes(ligne, colonne);

                let prochain_etat = match (cellule, voisines_vivantes) {
                    // Règle 1 : toute cellule vivante avec moins de deux
                    // voisines vivantes meurt, comme si cela était un effet de
                    //  sous-population.
                    (Cellule::Vivante, x) if x < 2 => Cellule::Morte,
                    // Règle 2 : toute cellule vivante avec deux ou trois
                    // voisines vivantes survit jusqu'à la prochaine génération.
                    (Cellule::Vivante, 2) | (Cellule::Vivante, 3) => Cellule::Vivante,
                    // Règle 3 : toute cellule vivante avec plus de trois
                    // voisines vivantes meurt, comme si cela était un effet de
                    // surpopulation.
                    (Cellule::Vivante, x) if x > 3 => Cellule::Morte,
                    // Règle 4 : toute cellule morte avec exactement trois
                    // voisines vivantes devient une cellule vivante, comme si
                    // cela était un effet de reproduction.
                    (Cellule::Morte, 3) => Cellule::Vivante,
                    // Les cellules qui ne répondent à aucune de ces conditions
                    // restent dans le même état.
                    (statut, _) => statut,
                };

                generation_suivante[indice] = prochain_etat;
            }
        }
    }

    let _chronometre = Chronometre::new("nettoyage des anciennes cellules");
    self.cellules = generation_suivante;
}
}

Vu les résultats des chronomètres, il est clair que notre hypothèse est incorrecte : l'écrasante majorité du temps est consacré à calculer la prochaine génération des cellules. L'allocation et le nettoyage d'un vecteur à chaque tick est un coût négligeable, contre toute attente. Voilà une bonne raison de toujours orienter vos efforts avec le profilage !

Capture d'écran des résultats du chronomètre Univers::tick

Ecrivons un code natif avec #[bench] qui fait les mêmes choses que notre WebAssembly, mais où nous pouvons utiliser des outils de profilage plus mâtures. Voici le nouveau fichier wasm-jeu-de-la-vie/benches/bench.rs :


#![allow(unused)]
#![feature(test)]

fn main() {
extern crate test;
extern crate wasm_jeu_de_la_vie;

#[bench]
fn ticks_univers(b: &mut test::Bencher) {
    let mut univers = wasm_jeu_de_la_vie::Univers::new();

    b.iter(|| {
        univers.tick();
    });
}
}

Nous devons également commenter toutes les annotations #[wasm_bindgen], et les parties "cdylib" dans le Cargo.toml, car sinon la compilation du code natif va échouer et aura des erreurs de liaisons avec des bibliothèques.

Une fois ceci en place, nous pouvons exécuter cargo bench | tee avant.txt pour compiler et exécuter notre comparatif ! La partie | tee avant.txt va récupérer la sortie de cargo bench et l'écrire dans le fichier avant.txt.

$ cargo bench | tee avant.txt
    Finished release [optimized + debuginfo] target(s) in 0.0 secs
     Running target/release/deps/wasm_jeu_de_la_vie-91574dfbe2b5a124

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/release/deps/bench-8474091a05cfa2d9

running 1 test
test ticks_univers ... bench:     664,421 ns/iter (+/- 51,926)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out

Cela va aussi nous indiquer où est le binaire, et nous pourrons à nouveau exécuter le comparatif, mais cette fois avec le profileur de notre système d'exploitation. Dans notre cas, nous l'exécutons sous Linux, donc nous allons utiliser perf :

$ perf record -g target/release/deps/bench-8474091a05cfa2d9 --bench
running 1 test
test ticks_univers ... bench:     635,061 ns/iter (+/- 38,764)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out

[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.178 MB perf.data (2349 samples) ]

L'ouverture du profil avec perf report nous montre que tout le temps est passé dans Univers::tick, comme nous nous y attendions :

Capture d'écran de perf report

perf va annoter quel temp est passé dans chaque instruction si vous appuyez sur a :

Capture d'écran de l'annotation d'instruction de perf

On apprend donc que 26.67% du temps est passé au comptage des cellules voisines, que 23.41% du temps est consacré à calculer l'indice de la colonne correspondante à la cellule voisine, et que 15.42% du temps en plus est passé à obtenir l'indice de la ligne de la voisine. Parmi ces trois instructions coûteuses, la seconde et la troisième sont des instructions div (pour division) assez coûteuses. Ces divisions sont implémentées dans la logique d'indexation faisant appel aux modulos, dans Univers::compter_voisines_vivantes.

Souvenez-vous de la définition de compter_voisines_vivantes dans wasm-jeu-de-la-vie/src/lib.rs :


#![allow(unused)]
fn main() {
fn compter_voisines_vivantes(&self, ligne: u32, colonne: u32) -> u8 {
    let mut compteur = 0;
    for delta_ligne in [self.hauteur - 1, 0, 1].iter().cloned() {
        for delta_colonne in [self.largeur - 1, 0, 1].iter().cloned() {
            if delta_ligne == 0 && delta_colonne == 0 {
                continue;
            }

            let ligne_voisine = (ligne + delta_ligne) % self.hauteur;
            let colonne_voisine = (colonne + delta_colonne) % self.largeur;
            let indice = self.calculer_indice(ligne_voisine, colonne_voisine);
            compteur += self.cellules[indice] as u8;
        }
    }
    compteur
}
}

La raison pour laquelle nous avions utilisé le modulo était d'éviter d'encombrer le code avec des branches if pour gérer les cas des bords des premières lignes et colonnes. Mais nous payons le prix de l'utilisation des instructions de division même pour les cas les plus courants, c'est-à-dire lorsque ni ligne, ni la colonne ne se trouvent pas sur les bords de l'univers et n'ont pas besoin du traitement du rebouclage avec le modulo. Mais si à la place nous utilisons des if pour détecter les cas des bordures et dérouler cette boucle, les branches devraient être bien appréhendées par le prédicteur de branches du CPU.

Ré-écrivons compter_voisines_vivantes de cette manière :


#![allow(unused)]
fn main() {
fn compter_voisines_vivantes(&self, ligne: u32, colonne: u32) -> u8 {
    let mut compteur = 0;

    let nord = if ligne == 0 {
        self.hauteur - 1
    } else {
        ligne - 1
    };

    let sud = if ligne == self.hauteur - 1 {
        0
    } else {
        ligne + 1
    };

    let ouest = if colonne == 0 {
        self.largeur - 1
    } else {
        colonne - 1
    };

    let est = if colonne == self.largeur - 1 {
        0
    } else {
        colonne + 1
    };

    let no = self.calculer_indice(nord, ouest);
    compteur += self.cellules[no] as u8;

    let n = self.calculer_indice(nord, colonne);
    compteur += self.cellules[n] as u8;

    let ne = self.calculer_indice(nord, est);
    compteur += self.cellules[ne] as u8;

    let o = self.calculer_indice(ligne, ouest);
    compteur += self.cellules[o] as u8;

    let e = self.calculer_indice(ligne, est);
    compteur += self.cellules[e] as u8;

    let so = self.calculer_indice(sud, ouest);
    compteur += self.cellules[so] as u8;

    let s = self.calculer_indice(sud, colonne);
    compteur += self.cellules[s] as u8;

    let se = self.calculer_indice(sud, est);
    compteur += self.cellules[se] as u8;

    compteur
}
}

Lançons à nouveau les comparatifs ! Et cette fois nous allons l'enregistrer dans apres.txt.

$ cargo bench | tee apres.txt
   Compiling wasm_jeu_de_la_vie v0.1.0 (file:///home/fitzgen/wasm_jeu_de_la_vie)
    Finished release [optimized + debuginfo] target(s) in 0.82 secs
     Running target/release/deps/wasm_jeu_de_la_vie-91574dfbe2b5a124

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/release/deps/bench-8474091a05cfa2d9

running 1 test
test ticks_univers ... bench:      87,258 ns/iter (+/- 14,632)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out

On dirait que cela a fait du bien ! Nous pouvons constater les améliorations grâce à l'outil benchcmp et des deux fichiers texte que nous avons créé précédemment :

$ cargo benchcmp avant.txt apres.txt
 name            before.txt ns/iter  after.txt ns/iter  diff ns/iter   diff %  speedup
 universe_ticks  664,421             87,258                 -577,163  -86.87%   x 7.61

Ouah ! C'est 7.61 fois plus rapide !

Le WebAssembly aspire à ressembler au plus aux architectures matérielles les plus courantes, mais nous devons nous assurer que ces améliorations des performances s'appliquent aussi au WebAssembly.

Recompilons le .wasm avec wasm-pack build et rafraîchissons http://localhost:8080/. Sur certaines machines, cette page s'exécute à nouveau à 60 images par secondes, et l'enregistrement d'un nouveau profil avec le profileur du navigateur dans ce cas nous apprendra que chaque image de l'animation se calcule en environ dix millisecondes.

Victoire !

Capture d'écran de la vue waterfall du rendu d'une image après avoir remplacé les modulos par des branches

Exercices

  • A ce stade, l'optimisation la plus accessible pour Univers::tick est d'enlever l'allocation et la libération en mémoire. Pour cela, implémentez le double buffering des cellules, avec lequel Univers contient deux vecteurs, qui ne seront jamais libérés, et n'alloue jamais de nouveaux tampons dans la fonction tick.
  • Essayez d'implémenter une alternative, un concept du chapitre "Implémenter le jeu de la vie de Conway", basé sur les différences, dans lequel le code Rust retourne la liste des cellules qui ont changé d'état au JavaScript. Est-ce que cela améliore la rapidité du rendu du <canvas> ? Pouvez-vous implémenter ce concept sans allouer une nouvelle liste de différences à chaque tick ?
  • Comme le profilage nous l'as appris, le rendu 2D du <canvas> n'est pas particulièrement rapide. Essayez de remplacer le rendu 2D du canvas par un rendu avec WebGL. Quels sont les gains de performance ? A partir de quelle taille de l'univers le rendu en WebGL améliore les performances ?

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Réduire la taille des .wasm

Pour les binaires .wasm que nous distribuons aux clients à travers le réseau, comme notre application du Jeu de la vie, nous voulons veiller à la taille du code. Plus notre .wasm sera petit, plus vite notre page se chargera rapidement, et plus nos utilisateurs seront satisfaits.

A quel point nous pouvons diminuer la taille des binaires .wasm avec la configuration de la compilation ?

Prenez un moment pour consulter les options de configuration de compilation que nous pouvons activer pour obtenir du code .wasm plus petit

Avec la configuration de compilation par défaut pour la publication (sans les symboles de débogage), notre binaire de WebAssembly fait 29 410 octets :

$ wc -c pkg/wasm_jeu_de_la_vie_bg.wasm
29410 pkg/wasm_jeu_de_la_vie_bg.wasm

Après avoir activé le LTO, avoir réglé opt-level = "z", et lancé wasm-opt -Oz, le binaire .wasm qui en résulte est réduit à seulement 17 317 octets :

$ wc -c pkg/wasm_jeu_de_la_vie_bg.wasm
17317 pkg/wasm_jeu_de_la_vie_bg.wasm

Si nous le compressons avec gzip (ce que fait presque tout serveur HTTP), nous arrivons à une taille minuscule de 9 045 octets !

$ gzip -9 < pkg/wasm_jeu_de_la_vie_bg.wasm | wc -c
9045

Exercices

  • Utilisez l'outil wasm-snip pour enlever les fonctions de l'infrastructure de panique sur le binaire .wasm de notre jeu de la vie. Combien d'octets cela économise ?

  • Compilez notre crate du Jeu de la vie avec et sans l'allocateur global wee_alloc. Le gabarit rustwasm/wasm-pack-template, que nous avons cloné pour démarrer ce projet, avait une fonctionnalité "wee_alloc" que vous pouvez activer en l'ajoutant à la clé default dans la section [features] de wasm-jeu-de-la-vie/Cargo.toml :

    [features]
    default = ["wee_alloc"]
    

    Quelle taille we_alloc économise sur le binaire .wasm ?

  • Nous n'avons instancié qu'un seul Univers, donc au lieu de fournir un constructeur, nous pouvons exporter des opérations qui travaillent sur une instance globale static mut. Si cette instance globale utilise aussi la technique de double buffering que nous avions évoqué dans les chapitres précédent, nous pouvons aussi rendre globaux ces tampons avec static mut. Cela enleve toute la partie d'allocation dynamique de notre implémentation du Jeu de la vie, et nous pouvons la transformer en crate #![no_std] qui ne contient pas d'allocateur. Quelle taille a été économisé sur le .wasm en enlevant complètement cette dépendance à l'allocateur ?

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Publier sur npm

Maintenant que nous avons un paquet jeu-de-la-vie qui fonctionne, qui est rapide et de petite taille, nous pouvons le publier sur npm pour que les autres développeurs Javascript puissent le réutiliser, si ils ont besoin d'une implémentation du Jeu de la vie.

Prérequis

Tout d'abord, faites en sorte d'avoir un compte npm.

Ensuite, assurez-vous que vous êtes connectés à ce compte en local, en lançant cette commande :

wasm-pack login

Publier

Assurez-vous que la compilation wasm-jeu-de-la-vie/pkg est à jour en exécutant wasm-pack dans le dossier wasmè-jeu-de-la-vie :

wasm-pack build

Prenez ensuite un petit moment pour vérifier le contenu de wasm-jeu-de-la-vie/pkg, c'est ce que nous allons publier sur npm à la prochaine phase !

Lorsque vous serez prêt, lancez wasm-pack publish pour téléverser le paquet sur npm :

wasm-pack publish

C'est tout ce qu'il faut faire pour publier sur npm !

... sauf que d'autres compères ont déjà suivi ce tutoriel, et donc le nom wasm-jeu-de-la-vie est déjà pris sur npm, et donc cette dernière commande va échouer.

Ouvrez wasm-jeu-de-la-vie/Cargo.toml et ajoutez votre nom d'utilisateur à la fin du nom pour distinguer votre paquet de manière unique :

[package]
name = "wasm-jeu-de-la-vie-mon-nom-utilisateur"

Et ensuite, recompilez et publiez-le à nouveau :

wasm-pack build
wasm-pack publish

Et cette fois-ci, cela devrait fonctionner !

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Référence

Cette section contient du contenu de référence pour le développement avec Rust et le WebAssembly. Ce n'est pas sensé être un récit et être lu dans l'ordre du début à la fin. Chaque sous-section est au contraire indépendante des autres.

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Les crates que vous devriez connaître

Ceci est une compilation de crates très intéressantes que vous devriez connaître pour développer en Rust et WebAssembly.

Vous pouvez aussi parcourir les crates publiées sur crates.io dans la catégorie WebAssembly.

Interagir avec le JavaScript et le D.O.M.

wasm-bindgen | crates.io | dépôt

wasm-bindgen facilite les interactions de haut-niveau entre Rust et JavaScript. Il permet d'importer des choses en JavaScript dans du Rust et exporter des choses en Rust dans du JavaScript.

wasm-bindgen-futures | crates.io | dépôt

wasm-bindgen-futures est un pont entre les Promises du JavaScript et les Futures de Rust. Il peut faire la conversion dans les deux directions et s'avère utile lorsque nous travaillons avec des tâches asynchrones en Rust, et permet d'interagir avec les évènements du D.O.M. et aux opérations d'entrée/sortie.

js-sys | crates.io | dépôt

Ce sont des imports bruts de wasm-bindgen pour tous les types et méthodes globales en JavaScript, comme Object, Function, eval et compagnie. Ces API sont portables sur tous environnements ECMAScript standard, et pas uniquement que le Web, comme par exemple Node.js.

web-sys | crates.io | dépôt

Ce sont des imports bruts de wasm-bindgen pour toutes les API web, comme la manipulation du D.O.M., de l'utilisation de setTimeout, du Web GL, Web Audio, et autres.

Communication d'erreurs et journalisation

console_error_panic_hook | crates.io | dépôt

Cette crate vous permet de déboguer les paniques en wasm32-unknown-unknown, en fournissant ce déclencheur à la panique, qui renvoie les messages de panique dans console.error.

console_log | crates.io | dépôt

Cette crate fournit une infrastructure dorsale pour la crate log qui renvoie les journaux dans la console de développement.

Allocation dynamique

wee_alloc | crates.io | dépôt

wwe_alloc signifie Wasm-Enabled, Elfin Allocator, ce qui signifie grossièrement en français Allocateur de Elfin conçu pour le Wasm. C'est une implémentation d'allocateur de petite taille (un .wasm d'environ 1Ko non compressé) pour l'utiliser lorsque le poids du code est plus important que la performance d'allocation.

Exploration et génération de binaires .wasm

parity-wasm | crates.io | dépôt

C'est une bibliothèque de bas-niveau en WebAssembly pour sérialiser, désérialiser, et compiler des binaires .wasm. Elle a un bon support des sections personnalisées les plus connues, comme les sections "names" et "reloc.WHATEVER".

wasmparser | crates.io | dépôt

Une bibliothèque simple, pilotée par les évènements pour explorer les fichiers binaires de WebAssembly. Elle fournit les correspondances d'octets pour chaque élément exploré, ce qui s'avère utile pour interpréter les relocs, par exemple.

Interpréter et compiler du WebAssembly

wasmi | crates.io | dépôt

L'interpréteur WebAssembly de Parity, qui peut être embarqué dans un projet.

cranelift-wasm | crates.io | dépôt

Compile le WebAssembly dans le code machine natif de l'hôte. Elle fait partie du projet de générateur de code Cranelift (initialement nommé Cretonne).

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Les outils que vous devriez connaître

Ceci est une compilation d'outils très intéressants que vous devriez connaître lorsque vous développez en Rust et WebAssembly.

Développement, compilation, et organisation des flux de travail

wasm-pack | dépôt

wasm-pack aspire à être un outil générique pour travailler et compiler le WebAssembly avec Rust, qui interagira avec le JavaScript, sur le Web ou avec Node.js. wasm-pack vous aide à compiler et publier du WebAssembly généré par Rust dans le registre de npm pour être utilisé avec n'importe quel autre paquet JavaScript dans les systèmes que vous utilisez déjà.

Optimiser et manipuler des binaires .wasm

wasm-opt | dépôt

L'outil wasm-opt lit en entrée du WebAssembly, procède à des transformations, des optimisations, et/ou fait des passes d'outillages dessus, et retourne ensuite le WebAssembly en sortie. L'exécution sur des binaires .wasm produits par LLVM avec rustc devrait généralement créer des binaires .wasm qui sont à la fois léger et qui devraient s'exécuter plus rapidement. Cet outil fait partie du projet binaryen.

wasm2js | dépôt

L'outil wasm2js compile du WebAssembly en "presque du asm.js". C'est intéressant pour prendre en charge des navigateurs qui n'implémentent pas le WebAssembly, comme Internet Explorer 11. Cet outil fait partie du projet binaryen.

wasm-gc | dépôt

C'est un petit outil pour exécuter un ramasse-miettes sur un module WebAssembly et enlève les exports, imports, fonctions et autres qui ne sont pas utiles. C'est comme le drapeau de liaison --gc-sections pour WebAssembly.

Vous ne devriez pas normalement avoir besoin de cet outil pour deux raisons :

  1. rustc a maintenant une version assez récente de lld qui supporte le drapeau --gc-sections pour le WebAssembly. Il est activé automatiquement pour les compilations LTO.
  2. L'outil en ligne de commande wasm-bindgen exécute automatiquement wasm-gc pour vous.

wasm-snip | dépôt

wasm-snip remplace le corps d'une fonction WebAssembly par une instruction unreachable.

Peut-être que vous connaissez des fonctions que ne seront jamais utilisées à l'exécution, mais que le compilateur ne peut pas le détecter à la compilation ? Découpez-les ! Ensuite exécutez à nouveau wasm-gc et toutes les fonctions qu'elles seules utilisaient (qui ne devraient donc pas être appelées à l'exécution) devraient aussi être enlevées.

C'est utile pour enlever de force les infrastructures de panique pour les compilations destinées à l'environnement de production sans débogage.

Inspecter des binaires .wasm

twiggy | dépôt

twiggy est un profileur de la taille de code pour les binaires .wasm. Il analyse l'arbre des appels pour répondre aux questions comme celles-ci :

  • Pourquoi cette fonction est présente dans le binaire ? Par exemple, pour comprendre quelles sont les fonctions exportées qui les appellent indirectement ?
  • Quelle est la taille totale de cette fonction ? Par exemple, quelle taille pourrait être économisée si je l'enlève et quelles fonctions deviendront inutilisables après ce nettoyage ?

Utilisez twiggy pour alléger vos binaires !

wasm-objdump | dépôt

Affiche des informations de bas-niveau sur un binaire .wasm et sur chacune de ses sections. Elle permet aussi de désassembler au format texte WAT. C'est l'équivalent objdump mais pour le WebAssembly. Il fait partie du projet WABT.

wasm-nm | dépôt

Liste les symboles de fonctions importées, exportées, et privées qui sont définies dans le binaire .wasm. C'est comme nm mais pour le WebAssembly.

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Les gabarits de projet

Le groupe de travail de Rust et WebAssembly compile et maintient une variété de gabarits de projets pour vous aider à démarrer des nouveaux projets afin de se mettre rapidement au travail.

wasm-pack-template

Ce gabarit permet de démarrer un projet en Rust et WebAssembly à utiliser avec wasm-pack.

Utilisez cargo generate pour cloner ce gabarit de projet :

cargo install cargo-generate
cargo generate --git https://github.com/rustwasm/wasm-pack-template.git

create-wasm-app

Ce gabarit s'utilise pour des projets en JavaScript qui utilisent des paquets de npm qui ont été créés à partir de Rust avec wasm-pack.

Ce gabarit a été traduit en français : create-wasm-app-fr

Utilisez-le avec npm init :

mkdir mon-projet
cd mon-projet/
npm init wasm-app
# ou pour la version française :
npm init wasm-app-fr

Ce gabarit est parfois utilisé avec wasm-pack-template, où les projets wasm-pack-template sont installés en local avec npm link, et utilisés comme dépendance dans les projets create-wasm-app.

rust-webpack-template

Ce gabarit est livré pré-configuré avec tout l'outillage pour compiler Rust en WebAssembly et de l'intégrer dans un cycle de compilation de Webpack avec le rust-loader de Webpack.

Utilisez-le avec npm init :

mkdir my-project
cd my-project/
npm init rust-webpack

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Débogage du WebAssembly généré par Rust

Cette section présente des conseils pour le débogage du WebAssembly généré par Rust.

Compiler avec des symboles de débogage

⚡ Lorsque déboguez, assurez-vous que vous compilez avec les symboles de débogage !

Si n'activez pas les symboles de débogage, la section personnalisée "name" ne sera pas présente dans le binaire compilé en .wasm, et les traces de pile auront des noms de fonctions comme wasm-function[42] plutôt que le nom de la fonction en Rust, comme wasm_jeu_de_la_vie::Univers::compter_voisines_vivantes.

Les symboles de débogage sont activés par défaut lorsqu'un utilise une compilation en mode "debug" (comme wasm-pack build --debug ou cargo build).

Avec une compilation en mode "release", les symboles de débogage ne sont pas activés par défaut. Pour activer les symboles de débogage, assurez-vous que debug = true soit bien présent dans la section [profile.release] de votre Cargo.toml :

[profile.release]
debug = true

Journaliser avec les API de console

La journalisation est un des outils les plus efficients que nous avons à notre disposition pour prouver et réfuter des hypothèses sur le comportement déficient de nos programmes. Sur le Web, la fonction console.log est une manière de journaliser les messages dans la console d'outils de développement du navigateur.

Nous pouvons utiliser la crate web-sys pour accéder aux fonctions de journalisation de console :


#![allow(unused)]
fn main() {
extern crate web_sys; // (faculatif en Rust 2018)

web_sys::console::log_1(&"Hello, world!".into());
}

Sachez aussi que la fonction console.error a la même signature que console.log, mais les outils de développement ont tendance à aussi récupérer et afficher les traces de pile à côté du message de journal lorsqu'on utilise console.error.

Documentations sur console

Journaliser les paniques

La crate console_error_panic_hook journalise les paniques involontaires dans la console avec console.error. Plutôt que d'avoir des messages d'erreur abstraits, difficiles à déboguer comme le RuntimeError: unreachable executed, il vous fournit à la place un message de panique formaté par Rust.

Tout ce que vous avez besoin de faire est d'installer ce système en faisant appel à console_error_panic_hook::set_once() dans une fonction d'initialisation ou dans un code standard qui s'exécutera :


#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub fn installer_systeme_journalisation_panique() {
    console_error_panic_hook::set_once();
}
}

Utiliser un débogueur

Malheureusement, le débogage pour le WebAssembly reste embryonnaire. Dans la plupart des systèmes Unix, DWARF est utilisé pour encoder les informations qu'un débogueur a besoin d'avoir pour procéder à l'inspection de la source d'un programme en cours d'exécution. Il existe aussi un format alternatif qui encode des informations similaires sous Windows. Pour l'instant, il n'y a pas d'équivalent pour WebAssembly. C'est pourquoi les débogueurs sont des outils limités pour le moment, et nous finissons par passer par des instructions WebAssembly brutes émises par le compilateur, plutôt que par le code source Rust que nous avons rédigé.

Il existe une sous-commission du groupe W3C dédiée au débogage, donc attendez-vous à ce que les choses s'améliorent à l'avenir !

Toutefois, les débogueurs restent utiles pour inspecter le JavaScript qui interagit avec notre WebAssembly, et inspecter l'état brut du wasm.

Documentations sur le débogueur

Eviter d'avoir besoin de déboguer le WebAssembly dans un premier temps

Si le bogue concerne des interactions avec le JavaScript ou des API Web, alors écrivez des tests avec wasm-bindgen-test.

Si le bogue n'implique pas d'interactions avec JavaScript ou des API Web, alors essayez de le reproduire dans une fonction #[test] Rust traditionnelle, dans laquelle vous pouvez profiter des outils natifs de votre système d'exploitation lorsque vous déboguez. Utilisez des crates pour les tests comme quickcheck et ses cas de test qui réduisent automatiquement les cas à tester. Finalement, il vous sera plus facile de trouver et de corriger des bogues si vous pouvez les isoler dans des cas de test plus petits et qui ne nécessitent pas d'interaction avec le JavaScript.

Notez toutefois que pour pouvoir exécuter des #[test] natifs sans erreurs de compilateur et de liaison, vous aurez besoin de vous assurer que "rlib" soit bien présent dans le tableau [lib.crate-type] dans votre fichier Cargo.toml.

[lib]
crate-type = ["cdylib", "rlib"]

🚧 Attention, peinture fraîche !

Cette page a été traduite par une seule personne et n'a pas été relue et vérifiée par quelqu'un d'autre ! Les informations peuvent par exemple être erronées, être formulées maladroitement, ou contenir d'autres types de fautes.

Le profilage temporel

Cette section décrit comment profiler des pages web qui utilise Rust et WebAssembly dans le but d'améliorer le débit ou de réduire la latence.

⚡ Assurez-vous que vous utilisez toujours une compilation optimisée lorsque vous faites un profilage ! wasm-pack build devrait compiler avec les optimisations par défaut.

Les outils disponibles

Le chronomètre window.performance.now()

La fonction performance.now() retourne un chronomètre en millisecondes qui commence depuis que la page web a été chargée.

L'appel à performance.now n'a que très peu d'impact sur les performances, donc nous pouvons créer des mesures simples et granulaires grâce à elle sans impacter le reste du système et sans introduire de biais à ces mesures.

Nous pouvons l'utiliser pour chronométrer différentes opérations, et nous pouvons accéder à window.performance.now() via la crate web-sys :


#![allow(unused)]
fn main() {
extern crate web_sys;

fn now() -> f64 {
    web_sys::window()
        .expect("nous n'avons pas accès à l'objet `window`")
        .performance()
        .expect("nous n'avons pas accès à l'objet `window.performance`")
        .now()
}
}

Les profileurs des outils de développement

Tous les outils de développement intégrés dans les navigateurs web embarquent un profileur. Ces profileurs mettent en évidence les fonctions qui prennent le plus de temps à s'exécuter avec les outils de visualisation habituels comme les arbres d'appels et les flame graphs.

Si vous compilez avec les symboles de déboguage afin que la section personnalisée "name" soit ajoutée dans le binaire wasm, alors ces profileurs devraient afficher les noms des fonctions Rust plutôt qu'un nom obscur comme wasm-function[123].

Sachez toutefois que ces profileurs ne vont pas montrer les fonctions intégrées, et comme Rust et LLVM utilisent beaucoup de fonction intégrées, les résultats deviendraient compliqués.

Capture d'écran d'un profileur avec les symboles Rust

Ressources sur les profileurs web

Les fonctions console.time et console.timeEnd

Les fonctions console.time et console.timeEnd vous permettent de journaliser la chronologie des opérations demandées dans la console d'outils de développements du navigateur. Vous pouvez appeler console.time("le nom de l'opération") lorsque l'opération commence, et appeler console.timeEnd("le nom de l'opération") lorsqu'elle se termine. Le nom de l'opération est optionnel.

Vous pouvez utiliser directement ces fonctions avec la crate web-sys :

Voici une capture d'écran des journaux de console.time dans la console du navigateur :

Capture d'écran des journaux de console.time

De plus, les journaux de console.time et de console.timeEnd vont s'afficher dans les vues "timeline" ou "chronologie" du profileur de votre navigateur :

Capture d'écran des journaux de console.time

Utiliser #[bench] sur du code natif

De la même manière que nous pouvons tirer avantage des outils de déboguage de code de notre système d'exploitation en créant des fonctions #[test] plutôt que de déboguer sur le web, nous pouvons tirer avantage des outils de profilage de code de notre système d'exploitation en créant des fonctions #[bench].

Ecrivez vos tests de performance dans le sous-dossier benches de votre crate. Assurez-vous que votre crate-type inclut bien "rlib" ou sinon les binaires ne seront pas capables de se relier à votre bibliothèque principale.

Cependant, attention ! Assurez-vous d'abord que le problème de performance se trouve bien dans le WebAssembly avant d'investir votre temps dans le profilage du code natif ! Utilisez le profileur de votre navigateur pour vérifier cela, ou sinon vous risques de perdre votre temps à optimiser du code qui est potentiellement négligeable en termes de performance.

Ressources sur les profileurs natifs

Shrinking .wasm Code Size

💬 Cette page n'a pas encore été traduite.

📚 Visiter ce livre en Anglais