2 puntos por GN⁺ 2024-07-31 | 1 comentarios | Compartir por WhatsApp
  • Porffor es un proyecto de investigación que compila JavaScript por adelantado, en lugar de en tiempo de ejecución, para convertirlo en WebAssembly y binarios nativos
  • Gracias a que no empaqueta un intérprete, apunta a que la salida sea 10 a 30 veces más pequeña y rápida que en proyectos existentes de JS→Wasm
  • Incluso en compilaciones nativas, no empaqueta un runtime, por lo que el tamaño del binario puede reducirse hasta 1000 veces; el ejemplo mostrado baja de unos 90MB a menos de 100KB
  • Está hecho en JS, no tiene eval y propone una arquitectura con soporte nativo para TypeScript sin una etapa de build separada
  • AOT favorece la optimización mediante análisis estático y la compilación previa a la ejecución, pero dificulta la evaluación dinámica de JS como eval, y todavía está en una fase temprana donde mucho código JS aún no funciona

Cómo ejecuta el código Porffor

  • Porffor es un proyecto de investigación que compila JavaScript Ahead-of-Time a WebAssembly y binarios nativos
  • Un binario de TypeScript compilado con Porffor sirve esta página
  • Fue escrito desde el inicio pensando en AOT, con una estructura que intenta optimizaciones que eran difíciles en las formas tradicionales de ejecutar JS

Diferencias entre la salida WebAssembly y la nativa

  • JS → Wasm

    • La salida WebAssembly de Porffor es 10 a 30 veces más pequeña y rápida que la de proyectos existentes de JS→Wasm
    • La diferencia clave está en que compila JS directamente y no empaqueta un intérprete
    • Ejecutar JS sobre Wasm permite una ejecución aislada en sandbox, pero puede implicar una gran pérdida de rendimiento, y Porffor se enfoca en reducir ese costo
    • Casos de uso posibles:
      • Hosting de JS del lado del servidor: en runtimes de edge, el sandboxing con Wasm puede ofrecer ejecución segura sin necesidad de aislamiento excesivo
      • El bajo overhead del AOT, frente a JIT, abre la posibilidad de ejecutar más clientes sobre el mismo hardware con una pérdida mínima de rendimiento
      • Resistencia a la ingeniería inversa: para JS sensible, el código compilado puede ser más difícil de analizar que el código ofuscado
  • JS → Native

    • Como realmente compila JS y no empaqueta un runtime, el tamaño del binario puede ser hasta 1000 veces menor
    • El tamaño de ejemplo es de aproximadamente 90MB → menos de 100KB
    • Internamente compila JS a C y luego lo compila a nativo, así que donde se pueda usar C, se puede usar JS
    • Casos de uso posibles:
      • Ejecución de JS rápido en sistemas embebidos, consolas de videojuegos, etc.
      • Pequeñas apps CLI en JS compiladas como ejecutables de un solo clic de menos de 1MB

Ventajas y limitaciones del AOT

  • Los intérpretes tradicionales o las múltiples etapas de JIT tienen que equilibrar el tiempo de arranque y el rendimiento de JS
  • AOT compila primero y ejecuta después, así que la velocidad de compilación importa para la experiencia del desarrollador, pero no afecta la experiencia del usuario
  • Este enfoque deja espacio para hacer optimizaciones basadas en análisis estático como en C++ y Rust
  • La principal desventaja es que no existe evaluación dinámica de JS como eval, y que hay que crear un motor JS nuevo
  • Como aún está en una fase temprana, mucho código JS todavía no funciona, pero el trabajo de mejora continúa
  • Para seguir el progreso de compatibilidad con ECMAScript, ejecuta la suite oficial de pruebas Test262 en cada commit

1 comentarios

 
GN⁺ 2024-07-31
Opiniones en Hacker News
  • Oliver, el principal desarrollador de Porffor, anunció que trabajará tiempo completo en Porffor: https://x.com/canadahonk/status/1818347311417938237

  • He pensado en algo parecido, pero creo que es difícil lograr un rendimiento mucho mejor en JavaScript. Probablemente lo mejor sea algo como transpilar JS a llamadas C++ de V8
    Las optimizaciones realmente interesantes aparecen al compilar TypeScript o algo cercano. Si se aprovechan los tipos, se pueden obtener grandes beneficios, y las partes sin tipos básicamente caerían en llamadas JS lentas. Las interfaces podrían reducirse a tablas de funciones virtuales o llamadas directas, y también se podría operar sobre structs en vez de mapas. Se podrían tener tipos Int y Float, degradarlos a Number cuando sea necesario y mantenerlos en registros
    El problema central es que tanto TS como V8 son objetivos no estándar que cambian rápido. Un proyecto así requiere un equipo grande, y mantener la compatibilidad se vuelve un trabajo aparte

    • Sin extensiones adicionales, TypeScript ayuda menos de lo que uno pensaría. Para empezar, no fue diseñado para ese uso
      Como ejemplo simple, TypeScript no distingue entre enteros y punto flotante; trata todo como números. Por eso, cada acceso a un arreglo requiere conversión de tipos. Si TypeScript hubiera sido diseñado para ayudar a la compilación estática, probablemente tendría esa distinción
      El problema mayor es el subtipado estructural de TypeScript. Por esta característica, para el compilador es prácticamente imposible determinar estáticamente la estructura física de los argumentos no primitivos que se pasan a una función. Un JIT puede hacer análisis dinámico de formas, así que en cada acceso a campos podría rendir peor que un JIT
    • Como colaborador de Porffor, no estoy de acuerdo. También hay bastante margen para mejorar JavaScript en tiempo de compilación
      Ha habido mucho trabajo en herramientas de análisis estático de tipos para JS, y también son posibles análisis muy exhaustivos. Un ejemplo que se me viene a la mente, aunque algo antiguo, es TAJS
    • Un proyecto que está algo relacionado con esta idea es AssemblyScript: https://www.assemblyscript.org
    • ECMAScript 4 fue un intento de agregar mejores tipos al lenguaje, pero lamentablemente fracasó hace mucho
      Sería bueno que incluso TypeScript permitiera especificar tipos como integer. Aunque en una compilación TS→JS común const val: int se tratara igual que const val: number, un runtime moderno que entienda TS podría aprovechar esa información adicional
      Me pregunto si una sintaxis como const counter: Number podría llegar a aceptarse
    • Después de decir “lo he pensado, pero es difícil obtener mejor rendimiento”, está proponiendo un enfoque que pide cosas explicadas justo en la parte superior de la página principal del sitio
      No sé si el sitio cambió o si me estoy perdiendo algo
  • En windmill.dev, cuando los usuarios despliegan código, usan Bun build para empaquetar el script y todas sus dependencias en un único archivo JS, que luego cargan para mejorar el cold start y el uso de memoria. Por el tamaño del bundle, el resultado se guarda en S3
    Si se pudiera empaquetar todo de forma nativa, sería un cambio total. Por muy bueno que sea el cold start de Bun, le costaría competir contra ejecutar nativamente un binario pequeño de inmediato

    • Como desarrollador, estoy de acuerdo. Parece un caso de uso interesante en el que Porffor podría ayudar potencialmente. Sería bueno conversarlo algún día
  • Es bueno ver más runtimes de JS acercándose a Wasm. Este proyecto me recuerda a Static Hermes, el motor JS de Facebook para acelerar iOS y Android en proyectos de React Native.
    Ambos apuntan a cumplir con JS test262, y mientras Porffor admite tanto salida nativa como Wasm, Static Hermes por ahora se enfoca principalmente en la salida nativa. Porffor está escrito en JS puro y va en la dirección de poder compilarse a sí mismo, mientras que Static Hermes depende de LLVM. Porffor todavía tenía soporte limitado para async/promise/await, y Static Hermes lo admite con algunas limitaciones. Static Hermes está escrito en C++, y Porffor principalmente en JS. Ambos admiten TypeScript, pero Static Hermes transpila el AST de TS a Flow, mientras que Porffor lo admite de forma nativa. Static Hermes tiene un intérprete alternativo para casos de JS difíciles de compilar, como eval, y Porffor solo admite compilación anticipada.
    En general, me entusiasma ver si este proyecto gana tracción y logra hacer más rápidos los motores de JavaScript en el edge. Lo dejo como Syrus de Wasmer.
    https://github.com/facebook/hermes/discussions/1137
    https://github.com/tc39/test262
    https://wasmer.io

    • Como referencia, Static Hermes admite completamente la función de compilar JS a WASM. Como ya tiene un backend de LLVM, es una función que se obtiene casi gratis. Puedes ver un ejemplo en https://x.com/tmikov/status/1706138872412074204
      Sin embargo, no es nuestro foco; nos concentramos principalmente en React Native. En ese entorno, WASM no tiene mucho sentido.
      La función más importante de Static Hermes es su verificador de tipos, que garantiza la solidez en tiempo de ejecución. Porffor es muy interesante; lo he estado siguiendo desde hace un tiempo y le deseo lo mejor.
    • Como colaborador de Porffor, me parece una buena comparación. Aunque Porffor técnicamente también admite promesas. Solo que funcionan de manera síncrona.
      Es un enfoque similar al de Kiesel: https://kiesel.dev/
    • Hay algunas correcciones menores. Porffor todavía no es completamente autoalojado, pero esperamos que pueda serlo. Dicho eso, algunas funciones integradas como Array.prototype.filter, Math.sin y atob sí se compilan parcialmente a sí mismas.
      Recientemente, Porffor también empezó a admitir async/promise/await básico. Todavía no funciona tan bien.
    • Creo que hice que depender de LLVM sonara como algo malo.
  • JavaScript tiene un subconjunto que se puede compilar fácilmente, y lo difícil es la larga cola que queda fuera. Aun así, es genial que se esté investigando dónde está ese límite y cuánto beneficio se puede obtener de ese subconjunto.

  • Me encanta de verdad que admita String.blink. Que los desarrolladores tengan humor y espíritu juguetón siempre es una buena señal.

    • Si se pretende que un “host de ECMAScript se comporte como un navegador web”, entonces obviamente debe admitirlo. Es parte de la especificación: https://tc39.es/ecma262/multipage/additional-ecmascript-feat...
      La implementación también es trivial, algo como function() { return "" + this + ""; }, así que vale la pena implementarlo incluso si el host de ECMAScript no es un navegador web. En ese caso es opcional. No esperaría que esto tenga que ver con “humor o espíritu juguetón”.
    • String.blink está en test262, así que, si el proyecto quiere cumplir su objetivo, en la práctica tiene que admitirlo.
  • Me pregunto cuál es el matiz que se me escapa. No entiendo por qué “motor JS de compilación anticipada” es una mejor descripción que “compilador de JS a Wasm”. Si es principalmente una estrategia de encuadre, también está bien.

    • Ya existen proyectos que hacen JS-to-WASM empaquetando un intérprete de JS. Así que probablemente sea una expresión para dejar más clara la diferencia con ese enfoque.
  • El esquema de versionado descrito aquí me parece un poco sospechoso.
    Si algún cambio provoca regresiones en algunas pruebas de Test262, el número de versión también podría retroceder. Es decir, Porffor no puede tener al mismo tiempo números de versión que aumenten de forma monótona y la capacidad de que cambios necesarios provoquen regresiones en Test262.
    https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...

    • Probablemente la intención sea que el trabajo que provoque regresiones en Test262 sea temporal y se haga en una rama separada, y que solo se fusione en main cuando ya incluya todas las correcciones necesarias para eliminar esas regresiones. El nuevo número de versión se usaría solo después de esa fusión.
  • En galés significa “morado”.

    • La etimología viene del griego para morado, y la palabra inglesa con la misma raíz más común probablemente sea porphyry, un mineral morado.
  • Es refrescante ver varios motores JS para distintos usos.
    He estado trabajando en ofrecer más API compatibles con Node en quickjs mediante llrt, para embeber plugins en aplicaciones.
    https://github.com/awslabs/llrt