2 puntos por GN⁺ 2024-04-15 | 1 comentarios | Compartir por WhatsApp
  • WebAssembly logró resultados al llevar a la web grandes programas en C++ como Photoshop, pero en apps centradas en el DOM su adopción fue limitada por tener un modelo de programación distinto al de JavaScript
  • El soporte de Wasm GC y tipos de referencia en los navegadores abre oportunidades para lenguajes con memoria administrada como Python y Scheme, pero en la web el tamaño de transferencia se convierte en una barrera de adopción
  • Un programa Wasm simple en Go puede pesar 2 MB, y al agregar imports puede superar los 10 MB; el REPL de Pyodide descarga unos 20 MB, lo que resulta pesado para una app web común
  • El compilador Hoot Scheme apunta a Wasm con soporte de GC y redujo la unidad mínima de compilación “main” a unos 70 KB; las unidades de compilación auxiliares pueden quedar incluso por debajo de 1 KB
  • Un tree shaking efectivo no se limita a borrar funciones no referenciadas: es un problema de compilador donde se combinan el análisis de flujo y el diseño de la biblioteca estándar

Áreas donde WebAssembly ha tenido éxito en la web

  • WebAssembly no se expandió tanto como se esperaba inicialmente en la web, pero en ciertos ámbitos tuvo un éxito limitado
  • El caso representativo es llevar a la web grandes programas en C++, como Photoshop
  • Figma también se menciona como un caso de Wasm de hace cinco años, pero actualmente no pone tanto énfasis en Wasm
  • Muchas pequeñas bibliotecas de NPM compiladas desde C++ o Rust usan Wasm internamente
  • Blazor podría usarse en algunas apps empresariales internas, aunque también es posible que el marketing lo haya exagerado
  • La demo de FPS 3D de Unreal Engine fue un experimento basado en una versión principal anterior a hace cinco años, y actualmente Unreal 5 no soporta WebAssembly como target

Por qué Wasm se frenó en apps centradas en el DOM

  • WebAssembly tiene logros fuera de la web y podría ganar importancia dentro de la plataforma web, pero en la web puede verse como una tecnología que apenas está saliendo del valle de la desilusión
  • Wasm destaca en tareas que JavaScript no hace bien o cuando se necesita una implementación compartida entre cliente y servidor
  • Wasm no tuvo éxito en apps centradas en el DOM
    • No se habla de reescribir el frontend de wordpress.com en Wasm
    • El modelo de programación principal de la web es JavaScript, con tipos dinámicos y memoria administrada
    • WebAssembly 1.0 fue diseñado alrededor de tipos estáticos y memoria lineal
    • Acceder al DOM desde Wasm era tan engorroso que solo los partidarios más entusiastas de Wasm podían tolerarlo
  • Lenguajes como C# deben distribuir también el recolector de basura, y eso fue un obstáculo para la adopción de Wasm en lenguajes distintos de C/Rust

El problema de tamaño de transferencia que persiste incluso después de Wasm GC

  • Los navegadores ofrecerán soporte para tipos de referencia y recolección de basura en los próximos meses
    • Chrome y Firefox ya ofrecen Wasm GC
    • Safari también parece no estar lejos, gracias al trabajo de Asumu Takikawa
  • Wasm GC es un cambio que hará que más lenguajes actualicen sus toolchains para soportar WebAssembly
  • Para que Wasm en la web tenga éxito, los compiladores deben generar código pequeño
    • Si el toolchain de un lenguaje puede producir archivos Wasm útiles de unos pocos KB para transferencia, tendrá ventaja
    • Si no, tendrá que depender de expectativas infladas o de una base de usuarios cautiva, y podría quedarse en un equilibrio inestable hasta encontrar la siguiente solución
  • En el ecosistema JavaScript ya existe una gran industria de herramientas para reducir el tamaño de entrega y la hinchazón
    • Bundlers como esbuild agrupan varios módulos JS en un solo archivo
    • Intentan incluir solo las funciones y tipos de datos que se usan
    • También aplican estrategias de reducción de tamaño como la minificación de nombres

La trampa del nombre tree shaking

  • Tree shaking tiene una metáfora visual: conservar solo el código necesario para una página y dejar caer el resto
  • En esta metáfora, los módulos se imaginan como ramas y las definiciones como hojas, pero un árbol real no indica qué ramas son necesarias y cuáles no al sacudirlo desde el tronco
  • El nombre en sí lleva a pensar en eliminar código innecesario, pero desde el punto de vista algorítmico es más adecuado encontrar un punto fijo que conserve solo el código necesario
  • Aun así, tree shaking es un nombre impactante, por lo que se sigue usando pese a su imprecisión hortícola y algorítmica

La barrera de tamaño que generan los runtimes pesados

  • En lenguajes con runtimes pesados, el tree shaking máximo no había sido una gran prioridad
  • En el soporte de WebAssembly de Go, incluso el programa más simple pesa 2 MB según la wiki de golang
    • Si se agregan imports, puede superar los 10 MB
  • El ejemplo de REPL de Pyodide, un port de Python a WebAssembly, descarga alrededor de 20 MB de datos
  • Estos tamaños son aceptables para demos tecnológicas o aplicaciones muy ricas, pero hacen difícil que sea una opción común de desarrollo web

Toolchains alternativos e implementaciones adaptadas a la plataforma

  • El soporte Wasm integrado de Go y Pyodide se derivan de toolchains upstream, y en servidores el tamaño del binario puede no ser muy importante
  • Cuando se apunta a dispositivos pequeños, aparecen implementaciones separadas
  • El backend Wasm de TinyGo parece poder bajar de 1 KB
  • Estos toolchains alternativos suelen venir con restricciones o particularidades
  • Un programa Python orientado a Wasm que se ejecuta en un entorno DOM inevitablemente diferirá de un programa Python “nativo”
  • Los autores de toolchains intentan ofrecer el mismo lenguaje, pero la implementación de la biblioteca estándar puede variar
  • Los desarrolladores de ClojureScript probablemente también querrían eliminar, si fuera posible, la documentación de diferencias con Clojure, y esa posibilidad surgiría si Wasm se convierte en un target práctico para ClojureScript

El enfoque de tree shaking de Hoot Scheme

  • Después del soporte de GC, Wasm permite pensar en programación del DOM con lenguajes como Python, pero para el uso masivo se necesitan módulos pequeños
  • El compilador Hoot Scheme apunta a Wasm con GC
    • Actualmente, la unidad mínima de compilación “main” es de unos 70 KB
    • Su objetivo es reducir aún más ese tamaño
    • Las unidades de compilación auxiliares que importan desde el módulo main funciones de runtime como manejadores de excepciones pueden quedar por debajo de 1 KB
  • El compilador Hoot antepone un prelude al código del usuario
  • El tree shaking ocurre en varias etapas
    • La evaluación parcial puede evaluar solo los efectos de bindings no usados y luego eliminarlos
    • fixing letrec realiza una tarea similar
    • CPS recorre el programa con frecuencia y sigue solo funciones, valores y aristas de flujo de control referenciados
    • Un pase explícito de dead-code elimination elimina asignaciones sin uso y sin efectos que pueden aparecer tras otras optimizaciones
    • Las definiciones de la biblioteca estándar, escritas en WebAssembly cercano a raw, se incluyen en el binario resultante solo cuando son necesarias

Eliminaciones fáciles y difíciles

  • Las definiciones de procedimientos, como funciones o closures, son relativamente fáciles de manejar
    • Basta con incluir solo las funciones que el código referencia
    • En lenguajes como Scheme, esto por sí solo ya tiene un efecto considerable
  • Las dificultades inmediatas son tres
  • Modelo de evaluación de letrec*

    • El scope de las definiciones del prelude es recursivo, pero tiene orden
    • El valor de un binding puede llamar o referenciar valores definidos antes, y también capturar valores que se definirán después
    • Si para evaluar el valor de un binding se necesita referenciar un valor que recién se define más adelante, es un error
    • En procedimientos esto generalmente no causa problemas, pero en definiciones que no son procedimientos el compilador puede no poder demostrar la propiedad de que “solo referencia bindings anteriores”
    • En ese caso, el algoritmo fixing letrec reloaded puede conservar bindings modificados con set!, y para eliminarlos se necesita un pase de DCE delicado
  • Vtable de tipos de registro

    • Algunas definiciones que no son procedimientos son tipos de registro
    • Un tipo de registro tiene una vtable con cosas como la forma de imprimir registros o comprobar instancias
    • Los callbacks de la vtable pueden mantener vivo mucho código aunque en realidad no se usen
  • Funciones de salida polimórficas

    • Las funciones polimórficas como display amplían mucho el alcance del código necesario
    • Si se llama a display para imprimir una cadena, arrastra todo el sistema de I/O con buffer
    • Como display puede imprimir cualquier cosa, también puede traer código para muchos casos, como bitvector, pair, etc.
    • Si se llama a write-string, que solo usa cadenas, se puede evitar el código de salida de datos general, pero aun así se incluyen instalaciones generales de I/O con buffer, como ports

El tree shaking óptimo es un problema de análisis de flujo

  • El tree shaking óptimo es, en última instancia, un problema de análisis de flujo
  • Si un programa nunca tiene bitvectors, el código dentro de display que maneja bitvectors puede ser código muerto
  • Para saberlo, hay que conocer con qué tipos de argumentos se llama a display, y para eso se necesita análisis de flujo de alto nivel
  • En Python, el problema se vuelve más difícil
    • El despacho orientado a objetos es programación de orden superior, por lo que qué significa foo.bar depende de qué sea foo
    • La búsqueda en Python es más dinámica que en Scheme, y métodos como __getattr__ pueden usarse por todas partes
    • En la práctica, tal vez el análisis de flujo pueda descartar esas búsquedas dinámicas
    • El objetivo del tree shaking en Python no es un término grande con bindings léxicos, sino un conjunto complejo de módulos
    • Esto se parece a JavaScript, pero Python no cuenta con un ecosistema establecido de bundlers con tree shaking

Condiciones para los toolchains de lenguajes Wasm en la web

  • Wasm GC puede hacer posible programar el DOM con lenguajes que no sean JavaScript
  • Para llegar al uso masivo, los módulos Wasm resultantes deben ser pequeños
  • Cada toolchain de lenguaje requiere una inversión considerable
  • Esa inversión suele aparecer como toolchains alternativos, incluidos algoritmos experimentales de tree shaking
  • Las bibliotecas estándar alternativas deben diseñarse para que el tree shaker funcione mejor

1 comentarios

 
GN⁺ 2024-04-15
Opiniones de Hacker News
  • Mantuve el blob Wasm de openEtG (un motor de juego de cartas) por debajo de 400 KB, mientras trasladaba mucha lógica a Wasm, como la generación de texto de cartas, y lo escribí en Rust.
    Para reducir el tamaño hizo falta gestionarlo usando aritmética de punto fijo en vez de punto flotante, pasando de hash maps a vectores, evitando strings, usando un asignador pequeño como talc y reduciendo dependencias.
    Solo estoy usando rand y fxhash, aunque probablemente también podría eliminar rand; fxhash se usa únicamente como hash del estado del juego para verificar si hay desincronización.
    También reduje la cantidad de tipos de instancias genéricas: como ya existe Vec, evité traer tipos adicionales como Box<[i16]>; eliminar punto flotante y hash maps también ayudó a reducir la diversidad de tipos.
    Los algoritmos también se diseñaron teniendo en cuenta el tamaño; por ejemplo, con una tabla de consulta empaquetada en bits codifiqué la mecánica de adrenaline, en la que las criaturas con menor ataque atacan más veces.
    Comparé el costo de almacenar valores sin comprimir con el costo de la lógica de decodificación, y la evaluación de la IA usa precisión fija de 6 bits, porque en WebAssembly 64 se codifica de forma más eficiente que 128.
    La mecánica de selección de objetivos antes también tenía forma de AST, con cada predicado como un enum y AND/OR como slices de expresiones, pero ahora codifica las expresiones en enteros de 32 bits en notación polaca, con AND/OR en 2 bits y los predicados en 6 bits.
    Aquí la notación polaca resultó mejor que la notación polaca inversa, porque permitía la evaluación con cortocircuito de AND/OR.

    • Es interesante que la aritmética de punto fijo haya ahorrado bastante espacio, aun cuando Wasm tiene tipos de punto flotante de forma nativa.
      En el trabajo estoy considerando si el punto fijo podría ayudar en problemas donde conocemos el requisito de resolución máxima, por ejemplo cuando no se necesita precisión de posición por debajo del milímetro, así que me gustaría escuchar más sobre temas relacionados.
    • Otra cosa que ayudó fue usar wasm-opt de binaryen.
      Parece reducir de forma confiable el tamaño de Wasm en alrededor de 20 a 30%: https://github.com/WebAssembly/binaryen
      Al servir bundles Wasm en el navegador, también conviene usar compresión Brotli y configurar el servidor web para que use archivos comprimidos con Brotli.
      Con nginx se puede hacer con un cambio de una línea, y Brotli reduce el tamaño de los bundles Wasm unas 3 veces, mucho mejor que gzip.
    • Creé RLtools, una biblioteca de aprendizaje por refuerzo profundo en C++, y también hice ejemplos en Wasm (https://rl.tools); aunque no me preocupé en absoluto por el tamaño del binario, todo quedaba en unos 200 a 300 KB.
      Ese tamaño incluye propagación hacia adelante y retropropagación de aprendizaje profundo, algoritmos de aprendizaje por refuerzo y simulación de dinámica.
      Incluso ahora no es una carga, pero tengo curiosidad por cuánto más podría reducirlo, así que quiero intentar achicarlo pronto.
    • Pensaba que JavaScript usaba double para todos los números, así que me pregunto si Wasm es completamente distinto en ese aspecto.
      Quiero entender cómo tiene sentido decir que se ahorra espacio evitando punto flotante y usando aritmética de punto fijo.
    • Me gustaría ver cifras de cuánto espacio se ahorró realmente.
      En particular, no creo que una técnica como la del punto 6, usar Vec como Box, vaya a generar un ahorro grande.
  • El nombre tree shaking me parece un término bastante equivocado
    El compilador de Virgil lo llama “análisis de alcanzabilidad” y lo tiene integrado en el modelo de compilación
    El compilador parsea y verifica tipos del programa y del código de las bibliotecas, y ejecuta el código de inicialización, pero después explora desde el punto de entrada principal y solo analiza e incluye en el binario final el código alcanzable
    También genera bien programas con una única función main y sin sistema de runtime; el sistema de runtime solo es necesario para trazas de pila y recolección de basura, así que se puede omitir si se quiere

    • Treeshaker, como nombre de una herramienta de despliegue de aplicaciones, posiblemente provenga de Lisp
      El primer caso que encontré fue la herramienta Treeshaker de Lucid Common Lisp 4.1, de 1992, que era una implementación comercial de Common Lisp para UNIX
      Lucid CL tenía el concepto de imagen, un volcado de memoria guardado del heap de Lisp en ejecución, y una aplicación consistía en una imagen y un runtime
      Normalmente, la imagen incluía casi todo el código y los datos en memoria, por lo que surgía el deseo de crear imágenes más pequeñas para distribución; Treeshaker eliminaba el código y los datos considerados “no usados” antes de guardar la imagen
      Funcionaba podando conexiones en el grafo de datos y código Lisp alcanzables, y luego el GC o código especial recogía la basura para reducir la memoria antes de volcarla en una imagen más pequeña
      Por lo tanto, Treeshaker no era una herramienta del compilador, sino una herramienta para eliminar código y datos no usados del heap de Lisp
      La imagen básica de Lisp incluía incluso el compilador, el intérprete y la implementación del REPL, así que si se interrumpía un programa en ejecución para entrar al REPL, todavía se podía usar todo el código dentro del heap restaurado desde la imagen
      Por eso tenía sentido eliminar incluso el compilador o el REPL
    • El término generalmente correcto es eliminación de código muerto (dead-code elimination): https://en.wikipedia.org/wiki/Dead-code_elimination
      El análisis de alcanzabilidad suele usarse para decidir qué código se puede eliminar, pero el análisis en sí no elimina el código; la eliminación es una etapa posterior
      “Tree shaking” normalmente sugiere eliminación a nivel de funciones, mientras que la eliminación de código muerto puede operar a un nivel mucho más fino, como eliminar ramas de expresiones condicionales, y puede basarse en diversos análisis estáticos
    • En software hay muchos nombres mal puestos, pero personalmente creo que tree shaking es un nombre excelente
      La primera vez que lo vi entendí de inmediato su significado sin investigar más
      Se trata de sacudir un árbol para que caiga lo que está flojo, y aquí era claro que los paquetes no usados “se sacuden y caen” del árbol
    • Nunca pensé que tree shaking se refiriera a un árbol como planta, sino a un árbol como estructura de datos
      Si uno imagina un diagrama de código fuente como un objeto físico, al sacudirlo se desprende lo que no es alcanzable desde la raíz
      No es muy distinto del análisis de alcanzabilidad; solo que una de las expresiones invita más al razonamiento espacial
    • Hay mucho código, y eso es un árbol
      Parte del código no está conectada al tronco que es el punto de entrada
      Tree shaking elimina las partes no conectadas, es decir, las hojas sueltas y las ramas muertas
  • Si el toolchain del compilador de un lenguaje puede producir Wasm útil de apenas unos KB según lo transferido por red, se abren nuevas posibilidades
    Los binarios muy pequeños abrirán nuevos casos de uso para Wasm, y WasmGC sin duda ayuda
    Java y Kotlin también pueden hacerlo bastante bien hoy, alrededor de 2 a 3 KB: https://developer.chrome.com/blog/wasmgc, https://twitter.com/bashorov/status/1661377260274720770
    Sin embargo, hay que tener cuidado porque, según qué API se use, puede arrastrar mucho código
    Aun así, gracias a WasmGC estos lenguajes ya están mucho mejor que C++ y Rust en tamaño de código, porque no necesitan incluir varios KB de código de gestión de memoria en el bundle

    • Me pregunto si WasmGC también ayudaría a Rust
      Podría ayudar al trabajar con objetos de JavaScript, y quizá también servir como un asignador de memoria alternativo menos eficiente
      Aun así, el asignador predeterminado de Rust en Wasm probablemente sea adecuado en la mayoría de los casos
      Si se empieza a optimizar tamaño, se usa wasm-opt y se comprime con Brotli, se puede meter una cantidad enorme de código en menos de 100 KB descargados
      Es un error comparar directamente el costo de 100 KB de Wasm con 100 KB de JavaScript empaquetado, porque JavaScript es varias veces más lento de parsear e inicializar
      El tiempo de descarga es un costo real, pero para el tiempo hasta la primera pantalla, 100 KB de Wasm es mucho mejor que 100 KB de JavaScript
      Aun así, cuanto más pequeño mejor, y entusiasma que Java, Kotlin, C#, Python, Go, etc. se conviertan en lenguajes prácticos para aplicaciones web
      También me da curiosidad cómo será el tamaño de las aplicaciones reales
      Creo que la mayor diferencia vendrá del diseño de los frameworks, y la comparación de DOM virtual inevitablemente será siempre más compleja y lenta que las bibliotecas de componentes reactivos como Svelte, SolidJS o Leptos de Rust
      Cuando WasmGC tenga soporte en todas partes, parece que el framework web elegido tendrá un impacto mucho mayor en el rendimiento que el lenguaje
  • Dudo que sea realmente cierto decir que “Wasm hace concebible programar el DOM en lenguajes que no sean JavaScript”.
    Según entiendo, para manipular el DOM desde lenguajes como Rust, al final se necesitan bindings que serialicen llamadas que se ejecutarán del lado de JavaScript.
    En la forma actual de Wasm, siento que todavía está atado a JavaScript.

    • Es importante incluir la condición anterior de “con GC”.
      En teoría, ahora se pueden importar funciones del DOM desde el runtime y llamarlas con referencias a objetos del DOM, eludiendo JavaScript y llamando directamente al runtime.
      No sé con certeza si en la práctica ya sea posible, pero GC al menos proporciona el mecanismo previo para llegar a ese punto.
    • Hace poco probé Leptos para Rust, y es un framework muy rápido para crear frontends web con Wasm; honestamente, se ve bastante bien.
      Básicamente se parece a SolidJS.
      #[component] fn App() -> impl IntoView { let (count, set_count) = create_signal(0); view! { "Click me: "{move || count()} } }
      Comparado con JavaScript, hay una sobrecarga en el tamaño de Wasm, pero no es grave.
      Después de wasm-opt y compresión Brotli, el bundle Wasm de esta app de contador pesaba 37 KB, un rango similar al de React, y después de ejecutarse es mucho más rápido.
      No probé la manipulación directa del DOM, pero para componentes comunes se ve bien.
    • Según entiendo, una de las intenciones de WASI es ofrecer una API que vaya directo al DOM.
      Por ejemplo, sería una forma de usar un DOM implementado en Rust desde un módulo Wasm escrito en Rust, sin ejecutar JavaScript.
      Sin embargo, es complicado porque hay partes de la API del DOM especificadas según la semántica de JavaScript, así que parece que primero están avanzando con cosas que tienen menos herencia de JavaScript, como solicitudes HTTP, sockets TCP y acceso al sistema de archivos.
  • El término “eliminación de código muerto” existe desde hace mucho; me pregunto por qué surgió la expresión tree shaking.

    • “Eliminación de código muerto” normalmente se refiere a una optimización de compilador de pequeña escala que descarta fragmentos de código dentro de una función a los que nunca se puede llegar.
      “Tree shaking” se refiere al análisis de programa completo que descarta módulos y funciones enteros que no se llaman.
      Conceptualmente son lo mismo, pero quienes escriben compiladores por lo general tienen que implementarlos por separado, así que ayuda que existan dos nombres.
    • Que una palabra sobreviva y se vuelva dominante no tiene mucho que ver con qué tan antigua sea.
      Tree shaking es fácil de imaginar, más cómodo de decir y más accesible que “eliminación de código muerto”, así que parece haberse vuelto el término más popular.
      Al buscar cuál de los términos apareció primero, encontré el uso más temprano de “dead-code elimination” en un artículo de 1973: https://research-repository.st-andrews.ac.uk/bitstream/handle/10023/22636/NicholasAlexandrakisMScThesis1973_original_C.pdf?sequence=1
      En Google Scholar no encontré usos de “tree shaking” o “tree shaker” en informática; la mayoría eran sobre árboles, como cítricos.
      Lo que parece ser la discusión más antigua fue una publicación en comp.lang.lisp: https://groups.google.com/forum/#!topic/comp.lang.lisp/pspFr1XByZk
    • Si de verdad se originó en el contexto de Lisp, probablemente sea porque ahí también se trata con el mismo peso la eliminación de datos y metadatos innecesarios, no solo de código ejecutable.
    • La expresión “eliminación de código muerto” es mucho mejor.
    • Es una expresión más vívida y suena más coloquial que “eliminación de código muerto”, así que no es difícil entender por qué se difundió.
  • Creo que la metáfora de tree shaking quizá venga de algunos métodos de cosecha de árboles frutales.
    Si sacudes el árbol, caen los frutos maduros.
    Aunque no es una metáfora excelente, porque en la cosecha de fruta quieres lo que cae, mientras que al guardar una imagen serializada descartas lo que cayó.

    • Normalmente lo que se cosecha sacudiendo son frutos secos.
      La fruta es más delicada y, si cae desde muy lejos, se magulla, así que por lo general se recoge a mano.
      El proceso de sacudir es bastante brusco, pero no daña al árbol.
  • Mi filosofía es optimizar con empeño el tamaño y el rendimiento en JavaScript simplificando algoritmos y diseño, para liberar tamaño de bundle y margen de CPU para el código que necesita cálculo bruto.
    En el proyecto ClubCompy estoy usando Wasm para implementar un sistema de archivos FAT sobre almacenamiento local, y resultó ser muy costoso en términos de cómputo.
    Más adelante este año también planeo usar Wasm cuando vuelva a incorporar detección de colisiones de sprites con precisión a nivel de píxel.
    La primera implementación era JavaScript puro, y cuando los 256 sprites en pantalla chocaban entre sí, el framerate caía por debajo de 1 fps.
    Creo que podré hacerlo prácticamente gratis en un worker thread, sin impacto en el rendimiento.

    • La detección de colisiones a nivel de píxel suena bien al principio, pero al jugar puede sentirse desagradable.
      Hay una razón por la que la mayoría de los juegos 2D usan cajas de colisión rectangulares: al jugador le resulta fácil predecir si habrá colisión o no.
      Con colisión a nivel de píxel, un mismo movimiento puede chocar o no según la fase del ciclo de animación.
      Se siente mal cuando un movimiento que antes siempre funcionaba falla porque el timing de la animación coincidió de mala manera.
      Además, el nivel de píxel no siempre es realista: pequeños detalles del sprite pueden representar tela o cabello, elementos que en la práctica no producirían una colisión rígida.
      También es común que los sprites se muevan varios píxeles por frame, así que las colisiones de píxeles individuales aumentan la posibilidad de que atraviesen unos a otros.
      Una detección de colisiones simple y predecible suele ser lo mejor.
    • Si con 256 colisiones estás en 1 fps, el cuello de botella no es el lenguaje de programación sino el algoritmo.
      1 fps es el framerate que deberías ver con unas 20 mil colisiones.
      Si es a nivel de píxel, cuando hay suficiente margen de memoria, una forma sencilla sería renderizar un canvas offscreen de todo el campo de juego, dibujar cada sprite como un stencil de un color distinto y luego inspeccionarlo.
      Es tiempo lineal y no requiere particionado aparte.
      Eso sí, como las medidas anti-fingerprinting pueden reemplazar los bits bajos de los datos del canvas con ruido, quizá haya que usar los bits altos.
  • Este artículo dice algo correcto. Wasm tiene un problema de tamaño de código.
    En el navegador es un problema porque hay que descargar todo el código antes de iniciar el sitio; y en arquitecturas serverless también lo es, porque mientras el cliente espera, el código se carga bajo demanda desde almacenamiento frío a un servidor específico.
    El tree shaking puede ayudar, pero parece que se quedará en una optimización incremental.
    En el fondo, la razón por la que los programas Wasm se vuelven pesados es que cada uno tiene que traer completo el runtime y la biblioteca estándar de su lenguaje.
    En cambio, en JavaScript la implementación y la biblioteca básica las proporciona el navegador.
    Es fácil pensar que el navegador no puede traer precargados todos los runtimes de lenguajes, pero habría que considerar otro enfoque: bibliotecas compartidas y enlazado dinámico.
    WebAssembly soporta enlazado dinámico, y puede cargar varios módulos Wasm al mismo tiempo para que se llamen entre sí.
    Pero muchas toolchains de Wasm no quieren soportar esto, y están diseñadas para enlazar estáticamente el programa completo y el runtime del lenguaje en un único módulo enorme.
    Pyodide (CPython sobre Wasm) es un contraejemplo: actualmente está diseñado pensando en el enlazado dinámico.
    Justamente gracias a eso Cloudflare Workers pudo agregar recientemente soporte de primera clase para Python: https://blog.cloudflare.com/python-workers
    Desde la perspectiva del líder técnico de toda la plataforma Workers, todos los Workers que se ejecutan en la misma máquina comparten un único runtime Pyodide compilado, así que no hace falta cargarlo por separado para cada Worker.
    Si el enlazado dinámico tuviera soporte más amplio, se podría imaginar una arquitectura en la que el navegador precargue runtimes de lenguajes populares, e incluso bibliotecas populares, y todas las páginas web que necesiten ese runtime compartan la misma copia de código de solo lectura.
    Estos runtimes seguirían ejecutándose dentro de un sandbox, así que el navegador no tendría que confiar en ellos; solo tendría que proveerlos.
    Así se podrían crear navegadores con soporte “integrado” para lenguajes distintos de JavaScript sin que quienes mantienen el navegador tengan que verificar por completo o preocuparse por la implementación de cada lenguaje.

    • La idea de que el navegador precargue varios runtimes y bibliotecas populares para que las páginas web los compartan suena parecida a aquella idea de ofrecer frameworks JavaScript comunes desde un CDN compartido: como supuestamente ya estarían en la caché de la mayoría de los navegadores, no haría falta descargarlos y el tamaño del framework no importaría tanto.
      No lo conozco bien, pero entiendo que ese enfoque no funcionó muy bien.
      Había demasiadas versiones de las bibliotecas, así que cada versión individual en realidad no se usaba tan ampliamente; y más tarde, por preocupaciones de privacidad, los navegadores fueron hacia la separación de caché por sitio u origen.
      Quizá no esté pensado con la caché en mente, pero este es un problema difícil, más cercano a un problema social que a uno técnico.
      Si se agregan más runtimes de lenguajes como soporte básico del navegador, sube la barrera de entrada para nuevos navegadores, y tampoco se pueden soportar todas las bibliotecas y runtimes que la gente quiere.
      Si se deja que cada quien los traiga y se confía en la caché, queda pendiente cómo evitar los problemas que ya se vivieron con el caché de bibliotecas JavaScript.
    • Incluso un soporte limitado de enlazado dinámico podría tener un impacto bastante grande.
      Por ejemplo, una biblioteca compartida de Go podría incluir el runtime y las partes centrales de la biblioteca estándar que usan muchos programas.
      Aun sin soporte para bibliotecas dinámicas dentro de la app, se podría reducir el tamaño de todos los programas Go, y el runtime del lenguaje no tendría que optimizar tanto el espacio.
      Como ya está cargado, si algún programa usa aunque sea una función, ese espacio no está desperdiciado.
      Esto cambiaría el modelo de costos para optimizar el tamaño de los programas en ese lenguaje.
      Las funciones de la biblioteca estándar incluidas se vuelven prácticamente gratis en cuanto usas el lenguaje, así que basta con usarlas.
      Pero el problema se repite con bibliotecas y frameworks de uso común.
      Al ejecutarse en Cloudflare, seguramente también se querría compartir la biblioteca estándar de Cloudflare para Go.
      El problema es que los lenguajes no evolucionan a la misma velocidad que sus runtimes.
      O se limita el soporte para varias versiones del lenguaje, o las bibliotecas compartidas se van acumulando con el tiempo y disminuye el efecto de compartir entre apps.
      JavaScript tiene un modelo de versiones de “no hay opción”, que exige una fuerte compatibilidad hacia atrás y, a veces, polyfills.
      Puede ser menos adecuado para otros lenguajes.
      Si el runtime de verdad quiere reducir espacio, puede limitar la variedad de plugins.
      Aunque hubo quejas, el modelo de “hay que usar JavaScript” funcionó bastante bien en los navegadores.
      Quizá no haga falta que los lenguajes basados en WebAssembly sean tan diversos, y la diversidad tiene costos, como en una situación de Torre de Babel.
    • Me pregunto si sería posible usar tree shaking basado en perfiles para crear módulos pequeños con solo el código que necesita la aplicación, y traer por enlazado dinámico las funciones menos usadas cuando hagan falta.
      Si el tree shaking se hace con información real de producción, parece posible recortar mucho código muerto o casi muerto sin implementar algoritmos sofisticados de análisis estático.
    • Una arquitectura en la que el navegador descargue de antemano runtimes populares corre el riesgo de terminar con cientos de versiones de runtimes descargadas en el navegador, donde cada binario lo usa un solo sitio y llena la caché.
    • En general estoy de acuerdo, pero quiero señalar que, incluso en Wasm, para hacer cosas que no sean cómputo puro se termina usando bastante JavaScript integrado en el navegador.
      En particular, en contextos como Hoot, cosas como appendChild son funciones externas que se llaman desde Scheme: https://spritely.institute/news/building-interactive-web-pages-with-guile-hoot.html
      En teoría, en cualquier entorno Wasm se podría usar de esta manera gran parte de la biblioteca estándar de JavaScript.
  • Zig es perfecto para este tipo de uso.
    Personalmente, creo que si un archivo Wasm pesa menos de 100 KB no es un factor importante, y que empieza a importar cuando supera el MB.
    El GC integrado es importante para algunas apps, pero no para todas, y lo mejor es hacer apps web sin GC.
    El factor más importante para que una app que usa Wasm tenga éxito sigue siendo la ventaja de rendimiento.

  • Estoy ejecutando una app de Blazor en Cloudflare Pages y, aunque la descarga es rápida y el rendimiento es bueno, el tiempo de carga es terrible.
    Creo que esto no se puede resolver con .NET, y el problema central parece ser que los lenguajes orientados a objetos, por diseño, hacen que todo quede entrelazado.
    Además, es difícil competir con la escala de inversión que se ha puesto en JavaScript, y en JavaScript copiar/pegar parece una funcionalidad del lenguaje, como un código de trampa.
    En tercer lugar, incluso en Blazor sigues necesitando JavaScript y experiencia en ese ecosistema, y creo que ese es el problema principal.

    • Eso no es un problema de la orientación a objetos.
      El problema es que los lenguajes de esa época dependían de la reflexión en muchos casos de uso.
      Hay gente trabajando mucho para eliminar estos casos de uso en gran parte de .NET y marcarlos como seguros para la eliminación de código no usado.
      Si existe la posibilidad de llamar métodos mediante reflexión, es difícil saber qué se puede eliminar de forma segura.
      Aunque no parezca que nadie llame a Foo.Bar(), ¿qué pasa si alguien hace Reflection.getClass(someClass).runMethod(someVar) y esas variables están configuradas como "Foo" y "Bar"?
      Por ejemplo, Dart no permite reflexión en apps compiladas con anticipación, lo que permite eliminar de forma segura el código no usado: https://docs.flutter.dev/resources/faq#does-flutter-come-with-a-reflection-mirrors-system
      Dart es un lenguaje orientado a objetos, pero evita la generación de código en tiempo de ejecución y la reflexión en tiempo de ejecución, y opta por la generación de código en tiempo de compilación.
      .NET también va en esa dirección, pero no es algo que ocurra de la noche a la mañana.
      Eso sí, como dijeron otros, los lenguajes que no son JavaScript también tienen el problema de tener que enviar por su cuenta las partes de la biblioteca estándar incluidas en el runtime de JavaScript del navegador que usan.
    • Blazor no destaca en este aspecto.
      El modelo de empaquetado actual no aprovecha bien la capacidad de trimming de .NET debido a las limitaciones de empaquetar Wasm sobre Mono.
      Para ver qué tanto puede reducirse realmente, conviene compilar una aplicación común con AOT; eso produce un binario pequeño.
      El soporte experimental para el objetivo NativeAOT-LLVM Wasm en dotnet/runtimelab ofrece tamaños de bundle mucho menores y un rendimiento mucho mejor, pero todavía está bajo dotnet/runtimelab y no en dotnet/runtime, así que no sé cuándo estará disponible.