Perché abbiamo sviluppato ChainSolve con Rust e WebAssembly
Un'analisi tecnica approfondita del motivo per cui abbiamo scelto Rust compilato a WebAssembly per il motore di calcolo di ChainSolve e i miglioramenti di prestazioni ottenuti.
Quando abbiamo iniziato a sviluppare ChainSolve, avevamo un requisito non negoziabile: ogni calcolo deve essere eseguito nel browser con prestazioni quasi native. Niente round-trip server. Niente dipendenze cloud. Nessun dato lascia il computer dell’utente. Questo requisito ci ha portato a Rust e WebAssembly.
Il requisito di prestazione
ChainSolve consente agli utenti di costruire grafici di calcolo con potenzialmente migliaia di blocchi, ciascuno che esegue un’operazione matematica sui suoi input e passa i risultati ai blocchi a valle. Quando un utente cambia un valore di input, l’intero sottografo a valle deve ricalcolare e aggiornarsi in tempo reale — idealmente entro un singolo fotogramma di animazione (16ms a 60fps).
Un’implementazione JavaScript pura del motore di calcolo ha raggiunto un limite intorno a 200 blocchi prima che i cali di fotogrammi diventassero evidenti. Avevamo bisogno di almeno un miglioramento di 10 volte per supportare il nostro obiettivo di 5.000+ blocchi per grafico.
Perché Rust?
Abbiamo valutato diversi linguaggi che compilano a WebAssembly: C, C++, Go e Rust. Abbiamo scelto Rust per tre motivi.
Sicurezza della memoria senza garbage collection
Il sistema di ownership di Rust garantisce la sicurezza della memoria in fase di compilazione senza un garbage collector. Questo è critico per WebAssembly perché il modello di memoria lineare di Wasm significa che un use-after-free o un buffer overflow non solo blocca la tua scheda — corrompe silenziosamente l’intero spazio di memoria. Rust elimina questi bug per costruzione.
Astrazioni a costo zero
Le astrazioni di Rust (generici, iteratori, pattern matching) compilano nello stesso codice macchina che scriveresti manualmente in C. Questo significa che possiamo scrivere codice pulito ed espressivo senza pagare una penalità di prestazioni.
Eccellente toolchain WebAssembly
Lo strumento wasm-pack, il crate wasm-bindgen e l’ecosistema Rust-to-Wasm più ampio sono maturi e ben documentati. Possiamo esporre funzioni Rust a JavaScript con boilerplate minimo e nessun codice FFI manuale.
Panoramica dell’architettura
Il motore di calcolo ChainSolve ha tre livelli:
-
Step [counter(steps)] Rappresentazione del grafico
Il grafico di calcolo è memorizzato come una lista di adiacenza con ordinamento topologico. Quando la struttura del grafico cambia (blocchi aggiunti, rimossi o ricablati), ricalcoliamo l’ordine topologico utilizzando l’algoritmo di Kahn. Questo determina la sequenza di valutazione.
-
Step [counter(steps)] Motore di valutazione
Il motore percorre l’ordine topologico e valuta ogni blocco. I blocchi sono implementati come oggetti trait Rust con un’interfaccia
evaluate(&self, inputs: &[Value]) -> Result<Value, EvalError>comune. Questo ci consente di aggiungere nuovi tipi di blocchi senza modificare il motore. -
Step [counter(steps)] Bridge JavaScript
Il livello bridge utilizza
wasm-bindgenper esporre un’API minima a JavaScript:create_graph(),add_block(),connect(),set_input(),evaluate()eget_results(). I dati vengono serializzati come JSON per i tipi complessi e passati come array f64 grezzi per i dati numerici per ridurre al minimo il sovraccarico di serializzazione.
Benchmark
Abbiamo sottoposto a benchmark il motore Wasm rispetto a un’implementazione JavaScript pura equivalente su tre carichi di lavoro:
| Carico di lavoro | JS (ms) | Wasm (ms) | Accelerazione |
|---|---|---|---|
| 100 blocchi, catena lineare | 2,1 | 0,18 | 11,7x |
| 1.000 blocchi, grafico ramificato | 34,2 | 2,4 | 14,3x |
| 5.000 blocchi, DAG complesso | 412,0 | 22,1 | 18,6x |
L’accelerazione aumenta con la complessità del grafico perché il motore Wasm beneficia di una migliore località cache e dell’assenza dei controlli di tipo dinamici di JavaScript.
Integrazione Web Worker
Il motore di calcolo viene eseguito all’interno di un Web Worker per evitare di bloccare il thread principale. Il modello di comunicazione è diretto:
- Il thread principale invia mutazioni del grafico (blocco aggiunto, connessione modificata, input aggiornato) come messaggi strutturati al Worker.
- Il Worker applica mutazioni al motore Wasm e attiva la rivalutazione.
- I risultati vengono rimandati al thread principale tramite
postMessage. - Per set di risultati di grandi dimensioni, utilizziamo
SharedArrayBuffercon Atomics per evitare il costo di serializzazione dipostMessage.
Questa architettura significa che il thread dell’interfaccia utente non viene mai bloccato dal calcolo. Anche se un grafico impiega 500ms per valutare, l’utente può continuare a trascinare blocchi, scorrere il canvas e interagire con l’interfaccia senza alcun jank.
Lezioni apprese
Costruire un’applicazione Wasm di produzione ci ha insegnato diverse lezioni:
-
La dimensione del binario Wasm conta. La nostra build iniziale era 2,4MB. Dopo l’abilitazione di LTO,
wasm-opte la rimozione dei simboli di debug, l’abbiamo ridotta a 340KB compresso. Ogni kilobyte conta per il tempo di caricamento iniziale. -
Evita allocazioni piccole frequenti. L’allocatore di Wasm è più semplice di un allocatore nativo. Utilizziamo l’allocazione arena per i dati temporanei per valutazione, il che ha ridotto il sovraccarico di allocazione del 60%.
-
Profila nel browser, non sulla riga di comando. Le caratteristiche di prestazione differiscono tra Rust nativo e Wasm. Il pannello Prestazioni di Chrome DevTools con supporto di profilazione Wasm era il nostro strumento di ottimizzazione principale.
-
Prova entrambi i lati del bridge. I bug nella serializzazione
wasm-bindgensono sottili. Manteniamo una suite di test completa che viene eseguita sia in Rust nativo (tramitecargo test) che nel browser (tramitewasm-pack test).
Conclusione
Rust e WebAssembly sono stati la scelta giusta per il motore di calcolo di ChainSolve. La combinazione di sicurezza della memoria, astrazioni a costo zero e prestazioni browser quasi native ci consente di fornire un’esperienza di calcolo visivo in tempo reale che non sarebbe possibile con JavaScript da solo.
Se stai considerando Wasm per la tua prossima applicazione basata su browser, speriamo che questo articolo fornisca contesto utile per la tua decisione. E se vuoi vedere i risultati in azione, prova a costruire un grafico di calcolo su app.chainsolve.co.uk.