Saltar al contenido

Por qué elegimos Rust y WebAssembly para herramientas de cálculo de ingeniería

Un análisis técnico de por qué el motor de computación de ChainSolve está escrito en Rust y compilado a WebAssembly, y qué significa esto para el rendimiento, la confiabilidad y la portabilidad.

BG ben godfrey · · 6 min read
RUST

Los requisitos

Cuando nos propusimos construir el motor de cálculo de ChainSolve, teníamos un conjunto claro de requisitos:

  1. Velocidad de computación cercana al nativo. Los cálculos de ingeniería pueden implicar grandes matrices, solucionadores iterativos y gráficos de dependencias complejos con cientos de nodos. El motor debe evaluar estos en milisegundos, no en segundos.

  2. Ejecución en el navegador. La herramienta debe funcionar completamente en el navegador sin viajes de ida y vuelta al servidor para computación. Los ingenieros que trabajan en proyectos sensibles no pueden enviar datos de cálculo propietarios a servidores externos.

  3. Capacidad sin conexión. Los ingenieros en instalaciones de prueba, en pisos de fábrica y en sitios de clientes a menudo tienen conectividad limitada o nula. La herramienta debe funcionar sin conexión una vez cargada.

  4. Seguridad de memoria. Una herramienta de cálculo que falla o produce resultados silenciosamente incorrectos debido a corrupción de memoria es peor que inútil. La corrección es innegociable.

  5. Portabilidad. El mismo motor debe ejecutarse en navegadores, en Node.js para integración CI/CD, y potencialmente como una herramienta CLI nativa.

Estos requisitos redujeron considerablemente el campo.

¿Por qué no JavaScript?

JavaScript es la opción obvia para aplicaciones basadas en navegador. Es el lenguaje nativo de la web, tiene herramientas excelentes y un ecosistema masivo. Para código de UI, usamos JavaScript (específicamente TypeScript con React). Pero para el motor de computación, JavaScript tiene limitaciones fundamentales.

JavaScript es un lenguaje de tipado dinámico y recolección de basura. Para renderizado de UI y manejo de eventos, esto está bien. Para computación numérica, introduce dos problemas:

Techo de rendimiento. Los motores modernos de JavaScript como V8 son notablemente rápidos para un lenguaje dinámico, pero no pueden igualar el rendimiento del código compilado ahead-of-time para trabajo intensivo numéricamente. La compilación JIT añade pausas impredecibles. La recolección de basura añade picos de latencia. Estos son aceptables en una capa de UI pero problemáticos en un motor de computación que necesita evaluación consistente y rápida.

Precisión numérica. JavaScript tiene un único tipo numérico: punto flotante de doble precisión IEEE 754. Esto es adecuado para la mayoría de cálculos de ingeniería, pero JavaScript no proporciona forma de controlar el comportamiento del punto flotante, no tiene intrínsecos SIMD para computación vectorizada, y no tiene tipos enteros para casos donde la aritmética entera exacta es requerida (como algoritmos de grafos en el gráfico de dependencias).

Un motor de cálculo en JavaScript funcionaría. Sería adecuado para cálculos pequeños a medianos. Pero alcanzaría un techo de rendimiento con cadenas complejas, y requeriría soluciones alternativas cuidadosas para casos límite numéricos.

¿Por qué Rust?

Rust satisface cada uno de nuestros requisitos de computación:

Rendimiento. Rust compila a código de máquina (o WebAssembly) sin recolector de basura y sin sobrecarga de tiempo de ejecución. El código numérico en Rust se comporta de manera comparable a C y C++. Para el motor de cálculo de ChainSolve, esto significa que la evaluación de cadenas es consistentemente rápida, sin pausas de GC o warmup de JIT.

Seguridad de memoria sin recolección de basura. El sistema de propiedad de Rust garantiza seguridad de memoria en tiempo de compilación. No hay desreferencias de puntero nulo, sin bugs use-after-free, sin carreras de datos. Para una herramienta de cálculo donde la corrección es primordial, esto es una ventaja significativa sobre C y C++.

Destino WebAssembly. Rust tiene soporte de primera clase para compilar a WebAssembly a través de wasm-pack y wasm-bindgen. El módulo WASM resultante se ejecuta en cualquier navegador moderno a velocidad cercana al nativo. Esto nos da ejecución en navegador y capacidad sin conexión sin sacrificar rendimiento.

Sistema de tipos fuerte. El sistema de tipos de Rust nos permite codificar restricciones de ingeniería en el propio sistema de tipos. Un UnitValue<Millimeters> no puede ser accidentalmente sumado a un UnitValue<Inches>, el compilador lo rechaza. Esto captura categorías enteras de errores de ingeniería en tiempo de compilación.

Portabilidad. El mismo código Rust se compila a WASM para ejecución en navegador, a binarios nativos para herramientas CLI, y a bibliotecas compartidas para integración con otras herramientas. Un codebase, múltiples destinos.

La arquitectura

La arquitectura de ChainSolve separa computación de presentación:

+------------------------------------------+
|  Navegador (TypeScript + React)          |
|  - Componentes de UI                     |
|  - Editor de cadenas                     |
|  - Visualización                         |
+------------------------------------------+
       |  interfaz wasm-bindgen  |
+------------------------------------------+
|  Módulo WASM (Rust)                      |
|  - Solucionador de gráfo de dependencias |
|  - Motor de evaluación de bloques        |
|  - Sistema de conversión de unidades     |
|  - Parser de expresiones                 |
|  - Biblioteca de métodos numéricos       |
+------------------------------------------+

El módulo Rust/WASM expone una API limpia a la capa TypeScript a través de wasm-bindgen. La capa TypeScript maneja todas las preocupaciones de UI, renderizado, interacción del usuario, layout. La capa Rust maneja toda la computación, traversal de grafos, evaluación de bloques, conversión de unidades, parsing de expresiones.

Esta separación significa que la UI nunca se bloquea en computación. La evaluación de cadenas ocurre en el módulo WASM, devuelve resultados a TypeScript, y la UI se actualiza. Para cadenas muy grandes, ejecutamos el módulo WASM en un Web Worker para mantener el hilo de UI completamente responsivo.

Resultados de rendimiento

Algunos benchmarks representativos de nuestras compilaciones de desarrollo (medidos en un portátil de rango medio, M2 MacBook Air):

OperaciónJavaScript (línea base)Rust/WASM
Evaluar cadena de 100 bloques12 ms0.8 ms
Evaluar cadena de 1.000 bloques340 ms8 ms
Ordenamiento topológico (1.000 nodos)5 ms0.3 ms
Conversión de unidades (10.000 valores)18 ms0.6 ms
Parse de expresión + evaluación0.4 ms0.02 ms

La implementación Rust/WASM es consistentemente 15-40x más rápida que el equivalente en JavaScript. Para cadenas pequeñas, ambas son lo suficientemente rápidas. Para cadenas grandes (que los usuarios de producción inevitablemente crearán), la diferencia está entre retroalimentación instantánea y retraso perceptible.

Desafíos y compensaciones

Rust y WebAssembly no están sin compensaciones:

Curva de aprendizaje. Rust es un lenguaje más complejo que JavaScript o Python. El sistema de propiedad, aunque poderoso, requiere un modelo mental que toma tiempo desarrollar. Para un fundador en solitario, esto fue una inversión en productividad a largo plazo al costo de velocidad a corto plazo.

Tamaño del binario WASM. El módulo WASM compilado es actualmente aproximadamente 800 KB comprimido. Esto es una adición significativa a la carga de página inicial. Mitigamos esto con carga lazy, el módulo WASM se carga de forma asincrónica después de que la capa de UI se renderiza.

Depuración. Depurar código Rust compilado a WASM está mejorando pero sigue siendo menos conveniente que depurar JavaScript en las DevTools del navegador. Dependemos fuertemente de la suite de pruebas de Rust (ejecutada nativamente) y usamos logging específico de WASM para depuración en navegador.

Ecosistema. El ecosistema de Rust para computación numérica está creciendo pero es menos maduro que el ecosistema NumPy/SciPy de Python. Hemos implementado algunos métodos numéricos desde cero donde los crates de Rust existentes eran insuficientes.

A pesar de estas compensaciones, la decisión de usar Rust y WebAssembly ha sido validada por las características de rendimiento y confiabilidad del motor resultante. Para una herramienta donde los ingenieros confiarán en los resultados para decisiones críticas de seguridad, las garantías en tiempo de compilación que proporciona Rust valen la complejidad adicional del desarrollo.

Conclusión

Elegir Rust y WebAssembly para el motor de cálculo de ChainSolve fue una decisión arquitectónica deliberada impulsada por requisitos de ingeniería, no por moda de tecnología. Rendimiento cercano al nativo en el navegador, seguridad de memoria sin recolección de basura, y un sistema de tipos fuerte que captura errores de unidad en tiempo de compilación, estos no son nice-to-haves para una herramienta de cálculo de ingeniería. Son requisitos.

Si estás interesado en los detalles técnicos de nuestra arquitectura Rust/WASM, o si estás evaluando decisiones de tecnología similares para tus propias herramientas de ingeniería, te damos la bienvenida a la conversación. Comunícate a través de nuestra página de contacto.

Written by
BG
ben godfrey
Ingeniero en Godfrey Engineering Ltd.