🚧 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.
Anglais | Français | Remarques |
---|---|---|
benchmark | test de performance | - |
call tree | arbre d'appel | - |
flame graph | flame graph | - |
Frames Per Second | images par seconde | - |
garbage collector | ramasse-miettes | - |
indirection | indirection | - |
inlined function | fonction intégrée | - |
log | journal | - |
monomorphization | monomorphisation | - |
profiler | profileur | - |
profiling | profilage | - |
🚧 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 :
- 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.
- 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.
cargo-generate
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
viarustup
, - Compiler nos sources Rust en binaires WebAssembly
.wasm
viacargo
, - 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 :
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
danswasm-jeu-de-la-vie/src/lib.rs
pour prendre en paramètre unnom: &str
qui personnalise le message d'alerte, et passez votre nom à la fonctionsaluer
à l'intérieur dewasm-jeu-de-la-vie/www/index.js
. Recompilez le binaire.wasm
avecwasm-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 :
Toute cellule vivante avec moins de deux voisines vivantes meurt, comme si cela était un effet de sous-population.
Toute cellule vivante avec deux ou trois voisines vivantes survit jusqu'à la prochaine génération.
Toute cellule vivante avec plus de trois voisines vivantes meurt, comme si cela était un effet de surpopulation.
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 :
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 :
Un comportement intéressant et étrange émerge de ces règles simples et déterministes :
Le canon à planeurs de Gosper | Un pulsar | Un 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 :
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 :
- 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.
- 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.
- 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 :
- 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.
- 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 desBox
.
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 :
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 :
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 !
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 JavaScriptMath.random
.Réponse
Premièrement, ajoutez
js-sys
comme dépendance danswasm-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 typeFixedBitSet
pour représenter les cellules au lieu d'utiliserVec<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
deFixedBitSet
:#![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 pluslargeur * hauteur
, maislargeur * 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
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
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.
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éthodeUnivers::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'unShift + 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 :
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 :
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.
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 :
Et nous pouvons confirmer que ce n'est pas une anomalie en analysant l'agrégation de l'arbre d'appels de plusieurs images :
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.
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.
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 !
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 :
perf
va annoter quel temp est passé dans chaque instruction si vous appuyez
sur a
:
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 !
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 lequelUnivers
contient deux vecteurs, qui ne seront jamais libérés, et n'alloue jamais de nouveaux tampons dans la fonctiontick
.
- 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 à chaquetick
?
- 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 ?
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 gabaritrustwasm/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]
dewasm-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 globalestatic 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 avecstatic 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 :
rustc
a maintenant une version assez récente delld
qui supporte le drapeau--gc-sections
pour le WebAssembly. Il est activé automatiquement pour les compilations LTO.- L'outil en ligne de commande
wasm-bindgen
exécute automatiquementwasm-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
- Utiliser
console.log
avec la crateweb-sys
: - Utiliser
console.error
avec la crateweb-sys
: - L'objet
console
sur MDN - Outils de Développement Firefox — Console Web
- Outils de Développement de Microsoft Edge — Console
- Débuter avec la Console de Chrome DevTools
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
- Outils de Développement de Firefox — Débogueur
- Outils de Développement de Microsoft Edge — Débogueur
- Débuter dans le Débogage du JavaScript dans Chrome DevTools
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() } }
- Documentation de la fonction
web_sys::window
- Documentation de la méthode
web_sys::Window::performance
- Documentation de la méthode
web_sys::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.
Ressources sur les profileurs web
- Outils de développement de Firefox — Performance
- Outils de développement de Microsoft Edge — Performance
- Profileur JavaScript de Chrome DevTools
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
:
web_sys::console::time_with_label("une opération")
web_sys::console::time_end_with_label("une opération")
Voici une capture d'écran des journaux de console.time
dans la console du
navigateur :
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 :
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
- Utiliser le profileur
perf
sur Linux - Utiliser le profileur Instruments.app sur macOS
- Utiliser le profileur VTune sur Windows and Linux
Shrinking .wasm
Code Size
💬 Cette page n'a pas encore été traduite.