Warum wir ChainSolve mit Rust und WebAssembly gebaut haben
Ein technischer Deep Dive in die Gründe für unsere Wahl von Rust kompiliert zu WebAssembly für ChainSolves Berechnungs-Engine und die Performance-Gewinne, die wir erreicht haben.
Als wir uns daran machten, ChainSolve zu bauen, hatten wir eine nicht verhandelbare Anforderung: Jede Berechnung muss im Browser mit nahezu nativer Performance ausgeführt werden. Keine Server-Roundtrips. Keine Cloud-Abhängigkeiten. Keine Daten, die die Maschine des Benutzers verlassen. Diese Anforderung führte uns zu Rust und WebAssembly.
Die Performance-Anforderung
ChainSolve ermöglicht Benutzern, Berechnungsgraphen mit potenziell Tausenden von Blöcken zu erstellen, von denen jeder eine mathematische Operation auf seine Eingaben ausführt und Ergebnisse an nachgelagerte Blöcke weitergibt. Wenn ein Benutzer einen Eingabewert ändert, muss der gesamte nachgelagerte Subgraph in Echtzeit neu berechnet und aktualisiert werden — idealerweise innerhalb eines einzigen Animationsrahmens (16ms bei 60fps).
Eine reine JavaScript-Implementierung der Berechnungs-Engine erreichte eine Grenze bei etwa 200 Blöcken, bevor Frame-Drops erkennbar wurden. Wir benötigten mindestens eine 10x Verbesserung, um unser Ziel von 5.000+ Blöcken pro Graph zu unterstützen.
Warum Rust?
Wir haben mehrere Sprachen evaluiert, die zu WebAssembly kompilieren: C, C++, Go und Rust. Wir haben uns für Rust aus drei Gründen entschieden.
Speichersicherheit ohne Garbage Collection
Rusts Ownership-System garantiert Speichersicherheit zur Compile-Zeit ohne einen Garbage Collector. Dies ist kritisch für WebAssembly, da Wasms lineares Speichermodell bedeutet, dass ein Use-after-Free oder Buffer-Overflow nicht nur Ihren Tab zum Absturz bringt — es beschädigt den gesamten Speicherplatz stillschweigend. Rust eliminiert diese Bugs durch Konstruktion.
Zero-Cost Abstractions
Rusts Abstraktionen (Generics, Iterators, Pattern Matching) kompilieren zu denselben Maschinencodes, den Sie von Hand in C schreiben würden. Das bedeutet, wir können sauberen, ausdrucksstarken Code schreiben, ohne eine Performance-Steuer zu zahlen.
Hervorragende WebAssembly-Toolchain
Das Tool wasm-pack, die wasm-bindgen Crate und das breitere Rust-to-Wasm Ökosystem sind ausgereift und gut dokumentiert. Wir können Rust-Funktionen für JavaScript mit minimalem Boilerplate und ohne manuelle FFI-Glue-Code verfügbar machen.
Architektur-Übersicht
Die ChainSolve-Berechnungs-Engine hat drei Ebenen:
-
Step [counter(steps)] Graphdarstellung
Der Berechnungsgraph wird als Adjacency List mit topologischer Ordnung gespeichert. Wenn sich die Graphstruktur ändert (Blöcke hinzugefügt, entfernt oder neu verdrahtet), berechnen wir die topologische Ordnung mit Kahns Algorithmus neu. Dies bestimmt die Evaluierungssequenz.
-
Step [counter(steps)] Evaluierungs-Engine
Die Engine durchläuft die topologische Ordnung und evaluiert jeden Block. Blöcke werden als Rust-Trait-Objekte mit einer gemeinsamen
evaluate(&self, inputs: &[Value]) -> Result<Value, EvalError>Schnittstelle implementiert. Dies ermöglicht es uns, neue Block-Typen hinzuzufügen, ohne die Engine zu modifizieren. -
Step [counter(steps)] JavaScript-Brücke
Die Brückenschicht verwendet
wasm-bindgen, um eine minimale API für JavaScript verfügbar zu machen:create_graph(),add_block(),connect(),set_input(),evaluate()undget_results(). Daten werden als JSON für komplexe Typen serialisiert und als rohe f64-Arrays für numerische Daten weitergegeben, um den Serialisierungs-Overhead zu minimieren.
Benchmarks
Wir haben die Wasm-Engine gegen eine äquivalente reine JavaScript-Implementierung auf drei Workloads benchmarkt:
| Workload | JS (ms) | Wasm (ms) | Speedup |
|---|---|---|---|
| 100 Blöcke, lineare Kette | 2.1 | 0.18 | 11.7x |
| 1.000 Blöcke, verzweigter Graph | 34.2 | 2.4 | 14.3x |
| 5.000 Blöcke, komplexer DAG | 412.0 | 22.1 | 18.6x |
Der Speedup nimmt mit der Graphkomplexität zu, weil die Wasm-Engine von besserer Cache-Lokalität und dem Fehlen von Javas dynamischen Typ-Checks profitiert.
Web Worker Integration
Die Berechnungs-Engine läuft innerhalb eines Web Worker, um zu verhindern, dass der Hauptthread blockiert wird. Das Kommunikationsmuster ist geradlinig:
- Der Hauptthread sendet Graphmutationen (Block hinzugefügt, Verbindung geändert, Eingabe aktualisiert) als strukturierte Nachrichten an den Worker.
- Der Worker wendet Mutationen auf die Wasm-Engine an und löst eine Neubewertung aus.
- Ergebnisse werden über
postMessagean den Hauptthread zurückgesendet. - Für große Ergebnismengen verwenden wir
SharedArrayBuffermit Atomics, um die Serialisierungskosten vonpostMessagezu vermeiden.
Diese Architektur bedeutet, dass der UI-Thread niemals durch Berechnungen blockiert wird. Selbst wenn ein Graph 500ms zur Evaluierung benötigt, kann der Benutzer weiterhin Blöcke ziehen, die Canvas scrollen und mit der Schnittstelle interagieren, ohne Ruckler.
Gelernte Lektionen
Das Bauen einer produktiven Wasm-Anwendung hat uns mehrere Lektionen gelehrt:
-
Wasm-Größe ist wichtig. Unser anfänglicher Build war 2.4MB groß. Nach Aktivierung von LTO,
wasm-optund Entfernen von Debug-Symbolen reduzierten wir ihn auf 340KB komprimiert. Jedes Kilobyte zählt für die Anfangsladezeit. -
Vermeiden Sie häufige kleine Zuordnungen. Wasms Allocator ist einfacher als ein nativer Allocator. Wir verwenden Arena-Zuordnung für temporäre Daten pro Evaluierung, was den Zuordnungs-Overhead um 60% reduzierte.
-
Profilieren Sie im Browser, nicht in der Kommandozeile. Performance-Charakteristiken unterscheiden sich zwischen nativen Rust und Wasm. Chromes DevTools Performance Panel mit Wasm-Profilierungsunterstützung war unser primäres Optimierungstool.
-
Testen Sie beide Seiten der Brücke. Bugs in der
wasm-bindgenSerialisierung sind subtil. Wir pflegen eine umfassende Test-Suite, die sowohl in nativen Rust (viacargo test) als auch im Browser (viawasm-pack test) läuft.
Fazit
Rust und WebAssembly sind die richtige Wahl für ChainSolves Berechnungs-Engine gewesen. Die Kombination aus Speichersicherheit, Zero-Cost Abstractions und nahezu nativer Browser-Performance ermöglicht es uns, ein Echtzeit-Visual-Computation-Erlebnis zu liefern, das mit JavaScript allein nicht möglich wäre.
Wenn Sie Wasm für Ihre nächste Browser-basierte Anwendung in Betracht ziehen, hoffen wir, dass dieser Beitrag nützlichen Kontext für Ihre Entscheidung bietet. Und wenn Sie die Ergebnisse in Aktion sehen möchten, versuchen Sie, einen Berechnungsgraph unter app.chainsolve.co.uk zu erstellen.