Pourquoi nous avons choisi Rust et WebAssembly pour les outils de calcul d'ingénierie
Un regard technique sur les raisons pour lesquelles le moteur de calcul de ChainSolve est écrit en Rust et compilé en WebAssembly, et ce que cela signifie pour la performance, la fiabilité et la portabilité.
Les exigences
Quand nous avons commencé à développer le moteur de calcul de ChainSolve, nous avions un ensemble clair d’exigences :
-
Vitesse de calcul quasi native. Les calculs d’ingénierie peuvent impliquer de grandes matrices, des solveurs itératifs et des graphes de dépendances complexes comptant des centaines de nœuds. Le moteur doit les évaluer en millisecondes, non en secondes.
-
Exécution dans le navigateur. L’outil doit fonctionner entièrement dans le navigateur sans aller-retour serveur pour les calculs. Les ingénieurs travaillant sur des projets sensibles ne peuvent pas envoyer des données de calcul propriétaires à des serveurs externes.
-
Capacité hors ligne. Les ingénieurs dans les installations de test, sur les chaînes de production et sur les sites clients ont souvent une connectivité Internet limitée ou inexistante. L’outil doit fonctionner hors ligne une fois chargé.
-
Sécurité mémoire. Un outil de calcul qui s’arrête ou produit silencieusement des résultats erronés en raison de corruption mémoire est pire que inutile. L’exactitude est non négociable.
-
Portabilité. Le même moteur doit s’exécuter dans les navigateurs, dans Node.js pour l’intégration CI/CD, et potentiellement en tant qu’outil CLI natif.
Ces exigences ont considérablement réduit le champ des possibilités.
Pourquoi pas JavaScript ?
JavaScript est le choix évident pour les applications basées sur navigateur. C’est le langage natif du web, il dispose d’excellents outils et d’un écosystème massif. Pour le code UI, nous utilisons JavaScript (spécifiquement TypeScript avec React). Mais pour le moteur de calcul, JavaScript a des limitations fondamentales.
JavaScript est un langage à typage dynamique avec ramasse-miettes. Pour le rendu UI et la gestion des événements, c’est acceptable. Pour le calcul numérique, cela introduit deux problèmes :
Plafond de performance. Les moteurs JavaScript modernes comme V8 sont remarquablement rapides pour un langage dynamique, mais ils ne peuvent pas égaler la performance du code compilé en avance pour les travaux numériquement intensifs. La compilation JIT ajoute des pauses imprévisibles. Le ramasse-miettes ajoute des pics de latence. C’est acceptable dans une couche UI mais problématique dans un moteur de calcul qui a besoin d’une évaluation cohérente et rapide.
Précision numérique. JavaScript a un type numérique unique : le nombre à virgule flottante double précision IEEE 754. C’est adéquat pour la plupart des calculs d’ingénierie, mais JavaScript ne permet pas de contrôler le comportement à virgule flottante, n’a pas d’intrinsèques SIMD pour le calcul vectorisé, et n’a pas de types entiers pour les cas où l’arithmétique entière exacte est requise (comme les algorithmes de graphe sur le graphe de dépendances).
Un moteur de calcul JavaScript fonctionnerait. Il serait adéquat pour les petits et moyens calculs. Mais il atteindrait un plafond de performance avec des chaînes complexes, et nécessiterait des contournements prudents pour les cas limites numériques.
Pourquoi Rust ?
Rust satisfait à chacune de nos exigences de calcul :
Performance. Rust compile en code machine (ou WebAssembly) sans ramasse-miettes et sans surcharge d’exécution. Le code numérique en Rust a des performances comparables à C et C++. Pour le moteur de calcul de ChainSolve, cela signifie que l’évaluation des chaînes est constamment rapide, sans pauses GC ou d’échauffement JIT.
Sécurité mémoire sans ramasse-miettes. Le système de propriété de Rust garantit la sécurité mémoire au moment de la compilation. Il n’y a pas de déréférencement de pointeur nul, pas de bugs use-after-free, pas de courses aux données. Pour un outil de calcul où l’exactitude est primordiale, c’est un avantage significatif par rapport à C et C++.
Cible WebAssembly. Rust a un support de première classe pour compiler en WebAssembly via wasm-pack et wasm-bindgen. Le module WASM résultant s’exécute dans n’importe quel navigateur moderne à une vitesse quasi native. Cela nous donne l’exécution du navigateur et la capacité hors ligne sans sacrifier les performances.
Système de types fort. Le système de types de Rust nous permet d’encoder les contraintes d’ingénierie dans le système de types lui-même. Un UnitValue<Millimeters> ne peut pas être accidentellement ajouté à un UnitValue<Inches>, le compilateur le rejette. Cela évite des catégories entières d’erreurs d’ingénierie au moment de la compilation.
Portabilité. Le même code Rust compile en WASM pour l’exécution du navigateur, en binaires natifs pour les outils CLI, et en bibliothèques partagées pour l’intégration avec d’autres outils. Un code source, plusieurs cibles.
L’architecture
L’architecture de ChainSolve sépare le calcul de la présentation :
+------------------------------------------+
| Navigateur (TypeScript + React) |
| - Composants UI |
| - Éditeur de chaîne |
| - Visualisation |
+------------------------------------------+
| interface wasm-bindgen |
+------------------------------------------+
| Module WASM (Rust) |
| - Solveur de graphe de dépendances |
| - Moteur d'évaluation de bloc |
| - Système de conversion d'unités |
| - Analyseur d'expressions |
| - Bibliothèque de méthodes numériques |
+------------------------------------------+
Le module Rust/WASM expose une API propre à la couche TypeScript via wasm-bindgen. La couche TypeScript gère toutes les préoccupations UI, le rendu, l’interaction utilisateur, la disposition. La couche Rust gère tout le calcul, la traversée de graphe, l’évaluation de bloc, la conversion d’unités, l’analyse d’expressions.
Cette séparation signifie que l’UI ne se bloque jamais sur le calcul. L’évaluation des chaînes se fait dans le module WASM, retourne les résultats à TypeScript, et l’UI se met à jour. Pour les très grandes chaînes, nous exécutons le module WASM dans un Web Worker pour garder le thread UI complètement réactif.
Résultats de performance
Quelques repères représentatifs de nos versions de développement (mesurés sur un ordinateur portable de milieu de gamme, MacBook Air M2) :
| Opération | JavaScript (baseline) | Rust/WASM |
|---|---|---|
| Évaluer une chaîne de 100 blocs | 12 ms | 0,8 ms |
| Évaluer une chaîne de 1 000 blocs | 340 ms | 8 ms |
| Tri topologique (1 000 nœuds) | 5 ms | 0,3 ms |
| Conversion d’unités (10 000 valeurs) | 18 ms | 0,6 ms |
| Analyse + évaluation d’expression | 0,4 ms | 0,02 ms |
L’implémentation Rust/WASM est constamment 15 à 40 fois plus rapide que l’équivalent JavaScript. Pour les petites chaînes, les deux sont assez rapides. Pour les grandes chaînes (que les utilisateurs en production créeront inévitablement), la différence est entre une rétroaction instantanée et un délai perceptible.
Défis et compromis
Rust et WebAssembly ne sont pas sans compromis :
Courbe d’apprentissage. Rust est un langage plus complexe que JavaScript ou Python. Le système de propriété, bien que puissant, nécessite un modèle mental qui prend du temps à développer. Pour un fondateur seul, c’était un investissement dans la productivité à long terme au prix d’une vélocité à court terme.
Taille du binaire WASM. Le module WASM compilé fait actuellement environ 800 KB compressé. C’est un ajout significatif au chargement initial de la page. Nous l’atténuons avec le chargement paresseux, le module WASM est chargé de manière asynchrone après le rendu du shell UI.
Débogage. Le débogage du code Rust compilé en WASM s’améliore mais reste moins pratique que le débogage de JavaScript dans les outils de développement du navigateur. Nous nous appuyons fortement sur la suite de tests de Rust (exécutée nativement) et utilisons la journalisation spécifique à WASM pour le débogage du navigateur.
Écosystème. L’écosystème Rust pour le calcul numérique se développe mais est moins mature que l’écosystème Python NumPy/SciPy. Nous avons implémenté certaines méthodes numériques à partir de zéro là où les caisses Rust existantes étaient insuffisantes.
Malgré ces compromis, la décision d’utiliser Rust et WebAssembly a été validée par les caractéristiques de performance et de fiabilité du moteur résultant. Pour un outil où les ingénieurs s’appuieront sur les résultats pour des décisions critiques pour la sécurité, les garanties au moment de la compilation que Rust fournit valent la complexité de développement supplémentaire.
Conclusion
Choisir Rust et WebAssembly pour le moteur de calcul de ChainSolve était une décision architecturale délibérée motivée par les exigences d’ingénierie, non par la mode technologique. Performance quasi native dans le navigateur, sécurité mémoire sans ramasse-miettes, et un système de types fort qui évite les erreurs d’unités au moment de la compilation, ce ne sont pas des « c’est bien d’avoir ». Ce sont des exigences.
Si vous êtes intéressé par les détails techniques de notre architecture Rust/WASM, ou si vous évaluez des choix technologiques similaires pour vos propres outils d’ingénierie, nous accueillons la conversation. Contactez-nous via notre page de contact.