Revisión de Ingeniería: Edición 2025
Everett Pompeii
Al desarrollar una nueva tecnología, como Bencher, existe una tensión fundamental entre querer elegir tecnología aburrida y superar los promedios. En el momento, puede ser difícil saber exactamente dónde se está en este tira y afloja. Cada tres años, el lenguaje de programación Rust lanza una nueva edición de Rust. Creo que este es un buen ritmo. Es lo suficientemente largo para que se realicen avances reales. Sin embargo, lo suficientemente corto para no desviarse demasiado. Con Bencher cumpliendo 3 años esta primavera, pensé que sería un buen momento para detenerse y reflexionar sobre todas las decisiones de ingeniería que nos llevaron hasta aquí.
En esta publicación, voy a mirar hacia atrás en dónde Bencher ha gastado sus “fichas de innovación” durante los últimos tres años. Bencher es una suite de código abierto de herramientas de evaluación continua. Comenzaré en el frontend de la arquitectura de Bencher y avanzaré hasta el final de la pila. En cada parada a lo largo del camino, discutiré cómo llegamos aquí y daré un veredicto binario sobre cómo resultó cada decisión de ingeniería.
Frontend
Biblioteca Frontend
Como desarrollador en recuperación de C++, soy un gran admirador de Rust. Si pudiera haberlo hecho a mi manera, habría podido escribir Bencher completamente en Rust full-stack. Si vuelves a los recovecos más profundos del repositorio de Bencher, verás que estaba intentando hacer exactamente eso. Experimenté con Yew, Seed y Sycamore. Aunque pueden funcionar muy bien para algunos proyectos, había un problema importante que simplemente no podía superar: Interoperabilidad con JavaScript.
Aunque la interoperabilidad con JS es posible desde WASM a través de Rust, no iba a ser fácil. Sabía que quería que Bencher tuviera gráficos altamente interactivos. Esto significaba usar una biblioteca como D3, lo que implicaba interoperabilidad con JS.
Entonces, si iba a tener que usar JavaScript, ¿qué biblioteca debería elegir?
Volviendo a esos crates de Rust con los que experimenté, Yew es el análogo de Rust a React Hooks. Había construido y desplegado un frontend utilizando React Hooks en el pasado, así que sabía más sobre este framework. Sin embargo, encontré que el ciclo de vida de React Hooks era muy complicado y lleno de casos especiales y complicaciones.
Realmente me gustaron los principios básicos de la programación funcional reactiva (FRP). Esto me llevó a probar tanto Elm como su análogo en Rust, Seed. Desafortunadamente, usar Elm sufre de los mismos problemas que usar Rust. Elm requiere su propia Interoperabilidad con JavaScript. También encontré que La Arquitectura Elm era un poco demasiado restrictiva para mi gusto.
De todos los frameworks de Rust que probé, Sycamore fue el que más me gustó. Sycamore se inspiró en Solid. Cuanto más aprendía sobre Solid, más me gustaba. A diferencia de React, Solid no usa un DOM virtual. En cambio, se compila al buen JavaScript de siempre. Esto lo hace mucho más rápido, más pequeño y fácil de trabajar. Solid se compone de solo unos pocos primitivos poderosos que permiten una reactividad de grano fino. Cuando algo en la interfaz de usuario se actualiza, solo se volverá a ejecutar el código que depende de ello. Durante los últimos tres años, he encontrado que trabajar con Solid ha sido un placer.
Tecnología Veredicto Yew ❌ Seed ❌ Sycamore ❌ Elm ❌ SolidJS ✅
Marco de trabajo Frontend
Solid en sí mismo es solo una biblioteca. Para construir un frontend moderno, necesitaba usar un marco de aplicación web completo. Deseando mantener las cosas simples, decidí apostar completamente por Solid, y al principio usé SolidStart. En ese momento, SolidStart solo soportaba aplicaciones de una sola página (SPA).
Una SPA estaba bien para comenzar. Sin embargo, eventualmente necesitaba preocuparme por cosas como el SEO. Estaba comenzando a escribir mucha más documentación de Bencher. También estaba planeando la sección Aprende del sitio. Esto significaba que necesitaba tanto renderizado del lado del cliente (CSR) como generación de sitios estáticos (SSG). SolidStart era muy joven y no podía satisfacer todas mis necesidades.
Después de conocer Astro y probarlo, decidí portar todo el frontend de Bencher de SolidStart a Astro. Esto tuvo un par de inconvenientes. El más obvio fue el esfuerzo involucrado. Honestamente, no fue tan malo. Astro tiene su arquitectura de islas y una integración de Solid de primera clase. También pude tomar gran parte de la lógica que necesitaba del Router de Solid, y simplemente funcionó.
El gran compromiso que aún está presente hoy es que Bencher pasó de ser una aplicación de una sola página a una de varias páginas. La mayoría de los lugares donde haces clic en la consola causan un rerenderizado de página completa. Astro tenía la promesa de transiciones de vista, cuando hice el cambio por primera vez. Las probé, pero eran defectuosas. Aún necesito volver.
Mientras tanto, parece que SolidStart ha avanzado un poco. Ahora soportan tanto CSR como SSG. Aunque no he verificado si ambos funcionan en el mismo sitio como necesito. Agua bajo el puente.
Tecnología Veredicto SolidStart ❌ Astro ✅
Lenguaje Frontend
Astro tiene soporte integrado de TypeScript.
En la transición de SolidStart a Astro, también comencé la transición de JavaScript a TypeScript.
La configuración de TypeScript de Bencher está establecida en la configuración más estricta
de Astro.
Sin embargo, Astro no realiza comprobación de tipos durante las compilaciones.
Al momento de escribir, Bencher todavía tiene 604
errores de tipo.
Estos errores de tipo se utilizan más como sugerencias al editar código,
pero no bloquean la compilación (aún).
También agregué Typeshare para sincronizar los tipos de datos Rust de Bencher con el frontend de TypeScript. Esto ha sido increíblemente útil para desarrollar la Consola de Bencher. Además, todos los validadors de campos para cosas como nombres de usuario, correos electrónicos, etc., se comparten entre el código Rust y el frontend de TypeScript a través de WASM. Ha sido un poco complicado hacer que WASM funcione tanto en SolidStart como en Astro. La clase más grande de error que he visto en el frontend ha sido lugares donde se llama a una función WASM, pero el módulo WASM aún no se ha cargado. He descubierto cómo solucionarlo, pero a veces lo olvido y vuelve a aparecer.
Tener tanto los tipos compartidos como los validadores autogenerados desde el código Rust ha facilitado mucho la interfaz con el frontend. Ambos se verifican en CI, por lo que nunca están desincronizados. Todo lo que tengo que hacer es asegurarme de que las solicitudes HTTP estén bien formadas, y todo funciona. Esto hace que no poder usar Rust en toda la pila duela un poco menos.
Tecnología Veredicto Rust ❌ JavaScript ❌ TypeScript ✅ Typeshare ✅ WASM ✅
Alojamiento Frontend
Mi decisión inicial de apostar completamente por Solid fue bastante influenciada por Netlify contratando al creador de Solid para trabajar en ello a tiempo completo. Verás, el mayor competidor de Netlify es Vercel. Vercel creó y mantiene Next.js. Y pensé que Netlify quería que Solid fuera su Next.js. Por lo tanto, pensé que no habría mejor lugar para alojar un sitio SolidStart que Netlify.
Por defecto, Netlify intenta que uses su sistema de compilación. Usar el sistema de compilación de Netlify hace que sea muy difícil realizar despliegues atómicos. Netlify seguiría publicando el frontend incluso si la tubería de backend fallara. ¡Muy malo! Esto me llevó a pasar a construir el frontend en el mismo entorno CI/CD que el backend y luego simplemente subir la última versión a Netlify con su CLI. Cuando hice la transición de SolidStart a Astro, pude mantener la misma configuración de CI/CD. Astro tiene una integración de Netlify de primera mano.
Bencher logró mantenerse dentro del nivel gratuito de Netlify durante bastante tiempo.
Sin embargo, con la creciente popularidad de Bencher,
hemos empezado a exceder algunos de los límites del nivel gratuito.
He considerado mover el sitio de Astro a sst
en AWS.
Sin embargo, los ahorros de costos no han parecido valer el esfuerzo en este punto.
Tecnología Veredicto Compilaciones de Netlify ❌ Despliegues de Netlify ✅
Backend
Lenguaje de Backend
Rust.
Tecnología Veredicto Rust ✅
Marco de trabajo del servidor HTTP
Una de mis principales consideraciones al seleccionar un marco de trabajo de servidor HTTP en Rust fue el soporte incorporado para la especificación OpenAPI. Por las mismas razones por las que invertí en configurar Typeshare y WASM en el frontend, quería la capacidad de autogenerar tanto la documentación de la API como los clientes a partir de esa especificación. Era importante para mí que esta funcionalidad estuviera integrada y no fuera un complemento de terceros. Para que la automatización realmente valga la pena, tiene que funcionar muy cerca del 100% del tiempo. Esto significa que la carga de mantenimiento y compatibilidad debe recaer en los ingenieros del marco de trabajo principal. De lo contrario, inevitablemente te encontrarás en un infierno de casos límite.
Otra consideración clave fue el riesgo de abandono. Hay varios marcos HTTP de Rust que alguna vez fueron prometedores y que ahora están casi abandonados. El único marco que encontré que tenía soporte incorporado para la especificación OpenAPI en el que estaba dispuesto a apostar fue Dropshot. Dropshot fue creado y todavía es mantenido por Oxide Computer.
Hasta ahora solo he tenido un problema importante con Dropshot. Cuando el servidor API genera un error, causa una falla de CORS en el frontend debido a la falta de encabezados de respuesta. Esto significa que el frontend web no puede mostrar mensajes de error muy útiles para los usuarios. En lugar de trabajar en upstreaming una solución, puse mis esfuerzos en hacer que Bencher fuera más fácil e intuitivo de usar. Pero resulta que la solución era menos de 100 líneas de código. ¡La broma fue para mí!
Como un aparte, el marco axum
aún no había sido lanzado cuando comencé a trabajar en Bencher. Si hubiera estado disponible en ese momento, podría haber intentado combinarlo con uno de los muchos complementos de OpenAPI de terceros, a pesar de mi mejor juicio. Afortunadamente para mí, axum
aún no estaba allí para tentarme. Dropshot ha sido una gran elección. Consulte la sección Cliente de API para más sobre este punto.
Tecnología Veredicto Dropshot ✅
Base de Datos
He intentado mantener Bencher tan simple como sea posible. La primera versión de Bencher tomaba todo, incluyendo los resultados de los benchmarks, a través de parámetros de consulta en la URL. Rápidamente aprendí que todos los navegadores tienen un límite en la longitud de las URLs. Tiene sentido.
Luego, consideré almacenar los resultados de los benchmarks en git
y simplemente generar un archivo HTML estático con las gráficas y resultados. Sin embargo, este enfoque tiene dos grandes desventajas. Primero, los tiempos para git clone
eventualmente se volverían insostenibles para usuarios avanzados. Segundo, todos los datos históricos tendrían que estar presentes en el archivo HTML, llevando a tiempos de carga iniciales muy largos para usuarios avanzados. Una herramienta de desarrollo debe amar a sus usuarios avanzados, no castigarlos.
Resulta que hay una solución para mi problema. Se llama base de datos.
Entonces, ¿por qué no simplemente usar Postgres y terminar el día? Bueno, realmente quería que las personas pudieran autohospedar Bencher. Cuanto más simple pudiera hacer la arquitectura, más fácil (y económico) sería para otros autohospedarse. Ya iba a requerir dos contenedores debido al frontend y backend separados. ¿Podría evitar un tercero? ¡Sí!
Antes de Bencher, solo había utilizado SQLite como una base de datos de prueba. La experiencia del desarrollador fue fantástica, pero nunca consideré ejecutarlo en producción. Luego me encontré con Litestream. Litestream es una herramienta de recuperación ante desastres para SQLite. Se ejecuta en segundo plano y replica continuamente los cambios a S3 u otro almacenamiento de datos de tu elección. Esto lo hace fácil de usar e increíblemente eficiente en costos, ya que S3 no cobra por escrituras. Piensa en centavos por día para una instancia pequeña.
Cuando me encontré por primera vez con Litestream, también había la promesa de réplicas de lectura en vivo próximamente. Sin embargo, esto nunca se materializó. La alternativa sugerida fue un proyecto sucesor por el mismo desarrollador llamado LiteFS. Sin embargo, hay grandes desventajas con LiteFS. No ofrece recuperación ante desastres incorporada, si todas las réplicas se caen. Para tener múltiples réplicas, tienes que infectar la lógica de tu aplicación con el concepto de si son lectores o escritores. Y la barrera absoluta era que requiere una instancia de Consul para estar ejecutándose todo el tiempo para gestionar las réplicas. El punto de usar SQLite era evitar otro servicio más. Afortunadamente, tampoco intenté usar LiteFS con Bencher Cloud, ya que LiteFS Cloud fue descontinuado un año después de su lanzamiento, y LiteFS ahora está casi muerto.
Actualmente, el pequeño tiempo de inactividad entre implementaciones es manejado por el Bencher CLI. En el futuro, planeo moverme a implementaciones sin tiempo de inactividad usando Kamal. Con Rails 8.0 estableciendo como predeterminado a Kamal y SQLite, me siento bastante confiado en que Kamal y Litestream deberían funcionar bien juntos.
Tecnología Veredicto Parámetros en URL ❌ git + HTML ❌ SQLite ✅ Litestream ✅ LiteFS ❌
Controlador de Base de Datos
Cuanto más me acerco a la base de datos, más fuertemente tipificado quiero que sean las cosas. Está bien ser un poco flexible en el frontend. Si cometo un error, todo estará bien con el próximo despliegue a producción. Sin embargo, si corrompo la base de datos, es mucho más complicado arreglarlo. Con eso en mente, elegí usar Diesel.
Diesel es un mapeador objeto-relacional (ORM) y generador de consultas fuertemente tipificado para Rust. Verifica todas las interacciones con la base de datos en tiempo de compilación, previniendo errores en tiempo de ejecución. Esta verificación en tiempo de compilación también hace de Diesel una abstracción sin costo sobre SQL. Aparte de un pequeño error por mi parte al hacer las cosas 1200x más rápidas con ajustes de rendimiento, no he tenido errores de SQL en tiempo de ejecución al trabajar con Diesel.
🐰 Dato Curioso: ¡Diesel usa Bencher para benchmarking continuo!
Tecnología Veredicto Diesel ✅
Alojamiento de Backend
De la misma manera que elegí Netlify para el alojamiento de mi frontend porque estaba usando Solid, elegí Fly.io para el alojamiento de mi backend porque estaba usando Litestream. Fly.io acaba de contratar al creador de Litestream para trabajar en ello a tiempo completo. Como se mencionó anteriormente, este trabajo en Litestream fue eventualmente canibalizado por LiteFS, y LiteFS ahora está muerto. Así que realmente eso no resultó como esperaba.
En el futuro, cuando cambie a Kamal, también me mudaré de Fly.io. Fly.io ha tenido un par de interrupciones importantes que sacaron a Bencher de funcionamiento por medio día cada vez. Pero el mayor problema es la diferencia de impedancia que surge al usar Litestream.
Cada vez que inicio sesión en el panel de Fly.io, veo esta advertencia:
ℹ Tu aplicación se está ejecutando en una sola máquina
Escala y ejecuta tu aplicación en más Máquinas para asegurar alta disponibilidad con un solo comando:
Consulta la documentación para más detalles sobre cómo escalar.
¡Pero con Litestream, aún no puedes tener más de una máquina! ¡Nunca entregaron la replicación de lectura, como prometieron!
Así que sí, todo eso es un poco irónico y frustrante. En un momento, investigué libSQL y Turso. Sin embargo, libSQL requiere un servidor backend especial para la replicación, lo que hace que no funcione con Diesel. De cualquier manera, parece que también esquivé otro cierre de fin de vida allí. Estoy muy interesado en ver lo que Turso hace con Limbo, su reescritura de SQLite en Rust. Pero no haré ese cambio pronto. La próxima parada es una máquina virtual agradable, aburrida y estable que ejecute Kamal.
El backend de AWS S3 para la replicación de Litestream ha funcionado perfectamente. Incluso con el percance alrededor de Litestream y Fly.io, aún creo que tomé la decisión correcta al usar Litestream con Bencher. Estoy comenzando a enfrentar algunos problemas de escalamiento con Bencher Cloud, pero esto es un buen problema para tener.
Tecnología Veredicto Fly.io ❌ AWS S3 ✅
CLI
Biblioteca CLI
Al construir una CLI en Rust, Clap es un poco el estándar de facto. Así que imagina mi sorpresa cuando mostré públicamente Bencher por primera vez ¡y el propio creador, Ed Page, estaba allí! 🤩
Con el tiempo, sigo encontrando más y más cosas útiles que Clap puede hacer. Es un poco vergonzoso, pero acabo de descubrir recientemente la opción default_value
. Todas estas capacidades realmente ayudan a reducir la cantidad de código que tengo que mantener en la CLI de bencher
.
🐰 Dato Curioso: ¡Clap usa Bencher para contar el tamaño del binario!
Tecnología Veredicto Clap ✅
Cliente API
Un factor importante al elegir Dropshot como el framework de servidor HTTP de Bencher fue su capacidad incorporada para generar una especificación OpenAPI. Tenía la esperanza de que algún día pudiera autogenerar un cliente API a partir de esa especificación. Un año más o menos después, los creadores de Dropshot entregaron: Progenitor.
Progenitor es el yin al yang de Dropshot. Usando la especificación OpenAPI de Dropshot, Progenitor puede generar un cliente API de Rust en un patrón posicional:
o en un patrón de constructor:
Personalmente, prefiero el último,
así que eso es lo que usa Bencher.
Progenitor también puede generar un CLI de Clap completo para interactuar con la API.
Sin embargo, no lo he usado.
Necesitaba tener un control más estricto sobre las cosas,
especialmente para comandos como bencher run
.
El único inconveniente notable que he encontrado con los tipos generados es que
debido a limitaciones en JSON Schema, no puedes simplemente usar un Option<Option<Item>>
cuando necesitas poder desambiguar entre una clave item
faltante y una clave item
con el valor establecido en null
.
Esto es posible con algo como double_option
,
pero todo se ve igual al nivel del JSON Schema.
Usar un enum de estructura interna aplanada o sin etiquetar
no juega bien con Dropshot.
La única solución que encontré fue usar un enum sin etiquetar de nivel superior.
Solo hay dos campos de este tipo en toda la API en este momento,
así que no es un gran problema.
Tecnología Veredicto Progenitor ✅
Desarrollo
Entorno de Desarrollo
Cuando comencé a trabajar en Bencher, la gente pedía el fin de localhost. Ya había pasado mucho tiempo desde que necesitaba un nuevo portátil de desarrollo, así que decidí probar un entorno de desarrollo en la nube. En ese momento, GitHub Workspaces no estaba generalmente disponible (GA) para mi caso de uso, por lo que opté por Gitpod.
Este experimento duró unos seis meses. Mi conclusión: los entornos de desarrollo en la nube no funcionan bien para proyectos secundarios. ¿Quieres conectarte y hacer cinco minutos de trabajo? ¡No! Vas a sentarte y esperar a que tu entorno de desarrollo se reinicialice por la milésima vez. Oh, ¿tienes toda una tarde el fin de semana para realmente avanzar en el trabajo? ¡No! Tu entorno de desarrollo simplemente va a dejar de funcionar aleatoriamente mientras lo usas. Una y otra vez.
Me encontré con estos problemas siendo un usuario de pago. Por $25/mes, podría obtener un nuevo MacBook Pro M1 con mucho mejores especificaciones cada cinco años. Cuando Gitpod anunció que estaban cambiando su precio de una tarifa fija a ser basado en el uso, simplemente dejé que cancelaran mi plan y me dirigí a apple.com.
Tal vez todo esto fue un problema con la decisión ahora abandonada de Gitpod de usar Kubernetes.
Pero no tengo prisa por probar otro entorno de desarrollo en la nube con Bencher nuevamente.
Finalmente hice la transición de la configuración de Gitpod a un contenedor de desarrollo
para facilitar el inicio a los contribuyentes.
Para mí, sin embargo, me quedo con localhost
.
Tecnología Veredicto Gitpod ❌ M1 MacBook Pro ✅
Integración Continua
Bencher es de código abierto. Como un proyecto moderno de código abierto, prácticamente tienes que estar en GitHub. Esto significa que el camino de menor resistencia para la integración continua (CI) es GitHub Actions. A lo largo de los años, he comenzado a aborrecer los DSL de CI basados en YAML. Cada uno tiene sus propias peculiaridades, y cuando se trata de una empresa tan grande como GitHub, conseguir un icono ⚠️ en lugar de un icono ❌ puede prolongarse durante años.
Esto me motivó a probar Dagger. En ese momento, solo podías usar Dagger a través de este lenguaje esotérico llamado CUE. Lo intenté. De verdad lo hice. Durante todo un fin de semana. Tal vez si ChatGPT hubiera existido en ese entonces, podría haberlo logrado. Pero no era solo yo. Dagger eventualmente abandonó CUE por SDKs más razonables. Sin embargo, para ese entonces, ya era demasiado tarde para mí.
Derrotado por Dagger, acepté mi destino de DSL de CI en YAML,
y ahora Bencher utiliza GitHub Actions.
De hecho, incluso construí una acción de GitHub para Bencher CLI.
Sé el cambio problema que deseas ver en el mundo.
Tecnología Veredicto Dagger ❌ GitHub Actions ⚠️
Conclusión
Construir Bencher me ha enseñado mucho sobre los compromisos que acompañan cada decisión de ingeniería. Hay algunas elecciones que haría de manera diferente ahora, pero eso es algo bueno. Significa que he aprendido una o dos cosas en el camino. En general, estoy muy contento con donde está Bencher hoy. Bencher ha pasado de ser un boceto en mi cuaderno a un producto completamente desarrollado con una base de usuarios en crecimiento, una comunidad vibrante y clientes que pagan. ¡Estoy emocionado de ver a dónde nos llevarán los próximos tres años!
Stack Componente Tecnología Veredicto Frontend Biblioteca Frontend Yew ❌ Seed ❌ Sycamore ❌ Elm ❌ SolidJS ✅ Lenguaje Frontend Rust ❌ JavaScript ❌ TypeScript ✅ Typeshare ✅ WASM ✅ Alojamiento Frontend Netlify Builds ❌ Netlify Deploys ✅ Backend Lenguaje Backend Rust ✅ Framework Servidor HTTP Dropshot ✅ Base de Datos Parámetros de Consulta URL ❌ git + HTML ❌ SQLite ✅ Litestream ✅ LiteFS ❌ Driver de Base de Datos Diesel ✅ Alojamiento Backend Fly.io ❌ AWS S3 ✅ CLI Biblioteca CLI Clap ✅ Cliente API Progenitor ✅ Desarrollo Entorno de Desarrollo Gitpod ❌ M1 MacBook Pro ✅ Integración Continua Dagger ❌ GitHub Actions ⚠️
Bencher: Benchmarking continuo
Bencher es un conjunto de herramientas de benchmarking continuo. ¿Alguna vez has tenido un impacto de regresión de rendimiento en tus usuarios? Bencher podría haber evitado que eso sucediera. Bencher te permite detectar y prevenir las regresiones de rendimiento antes de que lleguen a producción.
- Ejecutar: Ejecute sus benchmarks localmente o en CI usando sus herramientas de benchmarking favoritas. La CLI
bencher
simplemente envuelve su arnés de benchmarks existente y almacena sus resultados. - Seguir: Sigue los resultados de tus benchmarks con el tiempo. Monitoriza, realiza consultas y representa gráficamente los resultados utilizando la consola web de Bencher basándose en la rama de origen, el banco de pruebas y la medida.
- Capturar: Captura las regresiones de rendimiento en CI. Bencher utiliza analíticas de vanguardia y personalizables para detectar regresiones de rendimiento antes de que lleguen a producción.
Por las mismas razones que las pruebas unitarias se ejecutan en CI para prevenir regresiones funcionales, los benchmarks deberían ejecutarse en CI con Bencher para prevenir regresiones de rendimiento. ¡Los errores de rendimiento son errores!
Empiece a capturar regresiones de rendimiento en CI — prueba Bencher Cloud gratis.