Revisão de Engenharia: Edição 2025

Everett Pompeii

Everett Pompeii


Quando se desenvolve uma nova tecnologia, como o Bencher, há uma tensão fundamental entre querer escolher tecnologia entediante e superar as médias. No momento, pode ser difícil dizer exatamente onde se está neste cabo de guerra. A cada três anos, a linguagem de programação Rust lança uma nova edição do Rust. Acho que esta é uma boa cadência. É tempo suficiente para um progresso real ser feito. Ainda assim, curto o bastante para não se afastar muito. Com o Bencher completando 3 anos nesta primavera, achei que seria um ótimo momento para parar e refletir sobre todas as decisões de engenharia que nos trouxeram até aqui.

Neste post, vou olhar para trás e ver onde o Bencher gastou seus “tokens de inovação” nos últimos três anos. O Bencher é uma suíte open source de ferramentas de benchmarking contínuo. Começarei pelo frontend da arquitetura do Bencher e irei até o final da pilha. Em cada parada ao longo do caminho, discutirei como chegamos aqui e darei um veredicto binário sobre como cada decisão de engenharia se saiu.

Frontend

Biblioteca de Frontend

Como um desenvolvedor C++ em recuperação, sou um grande fã do Rust. Se eu pudesse escolher, teria escrito Bencher em Rust full-stack. Mergulhe nas profundezas do repositório do Bencher, você me verá tentando fazer exatamente isso. Experimentei com Yew, Seed e Sycamore. Embora possam funcionar bem para alguns projetos, havia um ponto crítico que eu simplesmente não consegui superar: Interoperabilidade com JavaScript.

Embora a interoperação com JS seja possível a partir do WASM via Rust, não seria fácil. Eu sabia que queria que o Bencher tivesse gráficos altamente interativos. Isso significava usar uma biblioteca como D3, o que implicava em interoperação com JS.

Então, se eu tivesse que usar JavaScript, qual biblioteca deveria escolher?

Voltando aos pacotes do Rust que experimentei, Yew é o análogo do Rust ao React Hooks. Eu já havia construído e implantado um frontend usando React Hooks no passado, então eu conhecia bem esse framework. No entanto, achei o ciclo de vida do React Hooks muito complicado e cheio de armadilhas e casos extremos estranhos.

Gostei muito dos princípios básicos da programação funcional reativa (FRP). Isso me levou a experimentar tanto o Elm quanto seu análogo no Rust, Seed. Infelizmente, usar Elm sofre dos mesmos problemas que usar Rust. Elm requer sua própria Interoperabilidade com JavaScript. Eu também achei A Arquitetura Elm um pouco restritiva demais para o meu gosto.

De todos os frameworks Rust que experimentei, gostei mais do Sycamore. Sycamore foi inspirado por Solid. Quanto mais eu aprendia sobre Solid, mais eu gostava. Ao contrário do React, Solid não usa um DOM virtual. Em vez disso, ele compila para o bom e velho JavaScript. Isso o torna muito mais rápido, menor e mais fácil de trabalhar. Solid é composto de apenas alguns primitivos poderosos que permitem uma reatividade fina. Quando algo na interface do usuário é atualizado, apenas o código que depende disso será executado novamente. Nos últimos três anos, descobri que Solid é um prazer de se trabalhar.

TecnologiaVeredicto
Yew
Seed
Sycamore
Elm
SolidJS

Framework de Frontend

Solid em si é apenas uma biblioteca. Para construir um frontend moderno, eu precisaria usar um framework de aplicativo web completo. Querendo manter as coisas simples, coloquei todas as minhas fichas no Solid, e inicialmente usei o SolidStart. Na época, o SolidStart suportava apenas aplicativos de página única (SPAs).

Uma SPA era adequada para começar. No entanto, eventualmente, eu precisei começar a me preocupar com coisas como SEO. Eu estava começando a escrever muito mais documentação do Bencher. Eu também estava planejando a seção Aprender do site. Isso significava que eu precisava tanto de renderização no lado do cliente (CSR) quanto de geração de site estático (SSG). O SolidStart era muito novo, e eu não consegui fazer com que ele atendesse todas as minhas necessidades.

Depois de aprender sobre o Astro e experimentá-lo, decidi portar todo o frontend do Bencher do SolidStart para o Astro. Isso teve algumas desvantagens. A mais óbvia foi o esforço envolvido. Honestamente, não foi tão ruim. O Astro tem sua arquitetura de ilhas e uma integração Solid de primeira classe. Eu também fui capaz de pegar muita da lógica que eu preciso do Solid Router, e ela simplesmente funcionou.

O grande compromisso que ainda está presente hoje é que o Bencher passou de um aplicativo de página única para um aplicativo de várias páginas. A maioria dos lugares em que você clica no console causa uma nova renderização de página completa. O Astro prometia transições de visualização quando fiz a mudança. Eu as experimentei, mas estavam com problemas. Ainda preciso retornar a isso.

Enquanto isso, parece que o SolidStart avançou um pouco. Eles agora suportam tanto CSR quanto SSG. Embora eu não tenha verificado se ambos funcionam no mesmo site como eu preciso. Água passada.

TecnologiaVeredicto
SolidStart
Astro

Linguagem Frontend

Astro tem suporte embutido para TypeScript. Na transição de SolidStart para Astro, também comecei a transição de JavaScript para TypeScript. A configuração de TypeScript do Bencher está definida na configuração mais restritiva do Astro. No entanto, o Astro não realiza verificação de tipos durante as builds. No momento da escrita, o Bencher ainda possui 604 erros de tipo. Esses erros de tipo são usados mais como dicas ao editar o código, mas eles não bloqueiam a build (ainda não github issue 557).

Também adicionei Typeshare para sincronizar os tipos de dados Rust do Bencher com o frontend TypeScript. Isso tem sido incrivelmente útil para desenvolver o Bencher Console. Além disso, todos os validadores de campos para coisas como nomes de usuário, e-mails, etc. são compartilhados entre o código Rust e o frontend TypeScript via WASM. Tem sido um pouco complicado fazer o WASM funcionar tanto no SolidStart quanto no Astro. A maior classe de erros que vi no frontend são lugares onde uma função WASM é chamada mas o módulo WASM ainda não foi carregado. Descobri como consertar isso, mas ainda às vezes esqueço e o problema ressurge.

Ter tanto os tipos quanto os validadores compartilhados gerados automaticamente do código Rust tornou a interface com o frontend muito mais fácil. Ambos são verificados em CI, então nunca ficam fora de sincronização. Tudo que preciso garantir é que as requisições HTTP estejam bem formadas, e tudo funciona. Isso faz com que não poder usar Rust full-stack doa um pouco menos.

TecnologiaVeredicto
Rust
JavaScript
TypeScript
Typeshare
WASM

Hospedagem de Frontend

Minha decisão inicial de apostar tudo no Solid foi bastante influenciada pelo Netlify contratar o criador do Solid para trabalhar em tempo integral nele. Veja, o maior concorrente do Netlify é o Vercel. A Vercel criou e mantém o Next.js. E imaginei que o Netlify queria que o Solid fosse o Next.js deles. Portanto, pensei que não haveria lugar melhor para hospedar um site SolidStart do que o Netlify.

Por padrão, o Netlify tenta fazer você usar seu sistema de build. Usar o sistema de build do Netlify torna muito difícil fazer deploys atômicos. O Netlify ainda publicaria o frontend mesmo se o pipeline do backend falhasse. Muito ruim! Isso me levou a mudar para construir o frontend no mesmo ambiente de CI/CD que o backend e então apenas carregar a última versão para o Netlify com seu CLI. Quando fiz a transição do SolidStart para o Astro, fui capaz de manter a mesma configuração de CI/CD. O Astro possui uma integração de Netlify oficial.

O Bencher conseguiu permanecer dentro do nível gratuito do Netlify por um bom tempo. No entanto, com a crescente popularidade do Bencher, começamos a exceder alguns dos limites do nível gratuito. Considerei mover o site Astro para sst na AWS. No entanto, as economias de custo não pareceram valer o esforço neste momento.

TecnologiaVeredito
Netlify Builds
Netlify Deploys

Backend

Linguagem de Backend

Rust.

TecnologiaConclusão
Rust

Framework de Servidor HTTP

Uma das minhas principais considerações ao selecionar um framework de servidor HTTP em Rust foi o suporte embutido para especificação OpenAPI. Pelas mesmas razões que investi na configuração de Typeshare e WASM no frontend, eu queria a capacidade de autogerar tanto a documentação da API quanto os clientes a partir dessa especificação. Era importante para mim que essa funcionalidade fosse integrada e não um complemento de terceiros. Para que a automação realmente valesse a pena, ela precisa funcionar bem próximo de 100% do tempo. Isso significa que o ônus da manutenção e da compatibilidade precisa estar nos próprios engenheiros do framework principal. Caso contrário, você inevitavelmente se encontrará em um inferno de casos de borda.

Outra consideração importante foi o risco de abandono. Existem vários frameworks HTTP em Rust que prometiam muito, mas agora estão praticamente abandonados. O único framework que encontrei com suporte embutido para especificação OpenAPI e no qual estava disposto a apostar foi o Dropshot. Dropshot foi criado e ainda é mantido pela Oxide Computer.

Até agora, tive apenas um grande problema com o Dropshot. Quando um erro é gerado pelo servidor de API, ele causa uma falha de CORS no frontend devido a cabeçalhos de resposta ausentes. Isso significa que o frontend web não pode exibir mensagens de erro muito úteis para os usuários. Em vez de trabalhar em uma correção upstream, concentrei meus esforços em tornar o Bencher mais fácil e intuitivo de usar. Mas, ao que parece, a solução era menos de 100 linhas de código. Ironia do destino!

Como observação, o framework axum ainda não havia sido lançado quando comecei a trabalhar no Bencher. Se ele existisse na época, eu poderia ter tentado combiná-lo com um dos muitos complementos OpenAPI de terceiros, apesar do meu melhor julgamento. Sorte minha que o axum ainda não estava lá para me tentar. Dropshot tem sido uma ótima escolha. Veja a seção Cliente da API para mais sobre este ponto.

TecnologiaVeredicto
Dropshot

Banco de Dados

Tentei manter o Bencher o mais simples possível. A primeira versão do Bencher capturava tudo, incluindo os resultados dos benchmarks, através dos parâmetros de consulta na URL. Rapidamente aprendi que todos os navegadores têm um limite para o comprimento da URL. Faz sentido.

Em seguida, considerei armazenar os resultados dos benchmarks no git e apenas gerar um arquivo HTML estático com os gráficos e resultados. No entanto, essa abordagem tem duas grandes desvantagens. Primeiro, os tempos de git clone eventualmente se tornariam inviáveis para usuários pesados. Segundo, todos os dados históricos teriam que estar presentes no arquivo HTML, levando a tempos de carregamento inicial muito longos para usuários pesados. Uma ferramenta de desenvolvimento deve amar seus usuários pesados, não puni-los.

Acontece que há uma solução para o meu problema. É chamada de banco de dados.

Então, por que não simplesmente integrar o Postgres e encerrar o dia? Bem, eu realmente queria que as pessoas pudessem autohospedar o Bencher. Quanto mais simples eu pudesse fazer a arquitetura, mais fácil (e barato) seria para outros autohospedarem. Eu já estava planejando exigir dois contêineres devido ao frontend e backend separados. Poderia evitar um terceiro? Sim!

Antes do Bencher, eu só tinha usado o SQLite como um banco de dados de teste. A experiência do desenvolvedor foi fantástica, mas nunca considerei executá-lo em produção. Então, me deparei com o Litestream. O Litestream é uma ferramenta de recuperação de desastres para o SQLite. Ele roda em segundo plano e replica continuamente as alterações para o S3 ou qualquer outro datastore de sua escolha. Isso o torna tanto fácil de usar quanto incrivelmente econômico para executar, já que o S3 não cobra por gravações. Pense em centavos por dia para uma pequena instância.

Quando me deparei com o Litestream pela primeira vez, também havia a promessa de réplicas de leitura ao vivo em breve. No entanto, isso nunca se concretizou. A alternativa sugerida foi um projeto sucessor pelo mesmo desenvolvedor chamado LiteFS. No entanto, há grandes desvantagens no LiteFS. Não oferece recuperação de desastres integrada, se todas as réplicas falharem. Para ter várias réplicas, você precisa infectar sua lógica de aplicação com o conceito de se são leitores ou escritores. E o obstáculo absoluto foi que ele requer uma instância do Consul rodando o tempo todo para gerenciar as réplicas. O objetivo inteiro de usar o SQLite era evitar mais um serviço. Felizmente, também não tentei usar o LiteFS com o Bencher Cloud, pois o LiteFS Cloud foi descontinuado um ano após o lançamento, e o LiteFS agora está praticamente morto.

Atualmente, o pequeno tempo de inatividade entre as implantações é gerenciado pelo Bencher CLI. No futuro, planejo mover para implantações sem tempo de inatividade usando o Kamal. Com Rails 8.0 padronizando para Kamal e SQLite, sinto-me bastante confiante de que Kamal e Litestream devem funcionar bem juntos.

TecnologiaVeredicto
Parâmetros de URL
git + HTML
SQLite
Litestream
LiteFS

Driver de Banco de Dados

Quanto mais próximo do banco de dados eu chego, mais quero que as coisas sejam fortemente tipadas. Está tudo bem ser um pouco mais flexível no frontend. Se eu cometer um erro, tudo ficará certo na próxima publicação em produção. No entanto, se eu corromper o banco de dados, é muito mais difícil corrigir o problema. Com isso em mente, eu escolhi usar o Diesel.

Diesel é um mapeador objeto-relacional (ORM) fortemente tipado e construtor de consultas para Rust. Ele verifica todas as interações com o banco de dados em tempo de compilação, prevenindo erros em tempo de execução. Essa verificação em tempo de compilação também faz do Diesel uma abstração de custo zero sobre SQL. Exceto por um pequeno bug do meu lado ao tornar as coisas 1200x mais rápidas com ajustes de desempenho, eu não tive erros de SQL em tempo de execução ao trabalhar com Diesel.

🐰 Curiosidade: Diesel usa Bencher para benchmarking contínuo!

TecnologiaVeredicto
Diesel

Hospedagem de Backend

Da mesma forma que escolhi o Netlify para a hospedagem do meu frontend porque estava usando Solid, escolhi o Fly.io para a hospedagem do meu backend porque estava usando Litestream. Fly.io tinha acabado de contratar o criador de Litestream para trabalhar nele em tempo integral. Como mencionado acima, esse trabalho no Litestream foi eventualmente canibalizado pelo LiteFS, e o LiteFS agora está morto. Então, isso não saiu exatamente como eu esperava.

No futuro, quando eu mudar para o Kamal, também sairei do Fly.io. Fly.io teve alguns grandes problemas de interrupções que derrubaram o Bencher por meio dia cada vez. Mas o maior problema é a incompatibilidade que vem do uso do Litestream.

Toda vez que faço login no painel do Fly.io, vejo este aviso:

ℹ Seu aplicativo está rodando em uma única máquina

Escale e execute seu aplicativo em mais Máquinas para garantir alta disponibilidade com um comando:

fly scale count 2

Confira a documentação para mais detalhes sobre escalonamento.

Mas com o Litestream, você ainda não pode ter mais de uma máquina! Vocês nunca entregaram a replicação de leitura, como prometeram!

Então sim, isso é um pouco irônico e frustrante. Em um ponto, eu procurei o libSQL e o Turso. No entanto, o libSQL requer um servidor backend especial para replicação, o que faz não funcionar com o Diesel. De qualquer forma, parece que me livrei de outro encerramento de fim de vida lá também. Estou muito interessado em ver o que o Turso faz com o Limbo, sua reescrita do SQLite em Rust. Mas não farei essa mudança em breve. O próximo passo é uma VM agradável, entediante e estável executando Kamal.

O backend AWS S3 para a replicação do Litestream tem funcionado perfeitamente. Mesmo com o puxão de tapete em torno do Litestream e Fly.io, ainda acho que fiz a escolha certa ao usar o Litestream com o Bencher. Estou começando a enfrentar alguns problemas de escalonamento com o Bencher Cloud, mas isso é um bom problema para se ter.

TecnologiaVeredicto
Fly.io
AWS S3

CLI

Biblioteca CLI

Ao construir uma CLI em Rust, Clap é um tipo de padrão de fato. Então imagine meu choque quando eu fiz a primeira demonstração pública do Bencher e o próprio criador, Ed Page, estava lá! 🤩

Com o tempo, continuo descobrindo mais e mais coisas úteis que o Clap pode fazer. É um pouco constrangedor, mas acabei de descobrir a opção default_value. Todas essas capacidades realmente ajudam a reduzir a quantidade de código que tenho que manter no CLI bencher.

🐰 Curiosidade: Clap usa Bencher para monitorar o tamanho do binário!

TecnologiaVeredicto
Clap

Cliente de API

Um fator importante na escolha do Dropshot como o framework de servidor HTTP do Bencher foi sua capacidade integrada de gerar uma especificação OpenAPI. Eu estava esperançoso de que um dia poderia gerar automaticamente um cliente de API a partir dessa especificação. Um ano ou mais depois, os criadores do Dropshot entregaram: Progenitor.

O Progenitor é o yin para o yang do Dropshot. Usando a especificação OpenAPI do Dropshot, o Progenitor pode gerar um cliente de API Rust em um padrão posicional:

client.instance_create("bencher", "api", None)

ou em um padrão de construtor:

client.instance_create().organization("bencher").project("api").send()

Pessoalmente, eu prefiro o último, então é o que o Bencher usa. Progenitor também pode gerar um CLI Clap completo para interagir com a API. No entanto, eu não usei isso. Eu precisava ter um controle mais rígido sobre as coisas, especialmente para comandos como bencher run.

A única desvantagem notável que encontrei com os tipos gerados é que, devido a limitações no JSON Schema, você não pode simplesmente usar um Option<Option<Item>> quando precisa ser capaz de desambiguar entre uma chave item ausente e uma chave item com o valor definido como null. Isso é possível com algo como double_option, mas tudo parece igual no nível do JSON Schema. Usar um enum de struct interna achatada ou não-tagged não funciona bem com Dropshot. A única solução que encontrei foi usar um enum de nível superior, não-tagged. Existem apenas dois campos desse tipo em toda a API até o momento, então não é um grande problema.

TecnologiaConclusão
Progenitor

Desenvolvimento

Ambiente de Desenvolvimento

Quando comecei a trabalhar no Bencher, o pessoal estava pedindo o fim do localhost. Eu já estava bem além da necessidade de um novo laptop de desenvolvimento, então decidi experimentar um ambiente de desenvolvimento em nuvem. Na época, o GitHub Workspaces não estava disponível (GA) para meu caso de uso, então optei pelo Gitpod.

Este experimento durou cerca de seis meses. Minha conclusão: ambientes de desenvolvimento em nuvem não funcionam bem para projetos paralelos. Você quer começar e fazer cinco minutos de trabalho? Não! Você vai sentar lá e esperar que seu ambiente de desenvolvimento se reinicialize pela milésima vez. Ah, você tem uma tarde inteira no fim de semana para realmente fazer muito trabalho? Não! Seu ambiente de desenvolvimento vai apenas parar de funcionar aleatoriamente enquanto você o estiver usando. De novo e de novo e de novo.

Enfrentei esses problemas como usuário pago. A $25/mês, eu poderia obter um novo MacBook Pro M1 com especificações muito melhores a cada cinco anos. Quando o Gitpod anunciou que estava mudando seu modelo de precificação de tarifa fixa para baseado em uso, eu simplesmente deixei que eles cancelassem meu plano e fui para apple.com.

Talvez isso tenha sido um problema com a decisão agora abandonada do Gitpod de usar Kubernetes. Mas eu não tenho pressa para experimentar outro ambiente de desenvolvimento em nuvem com o Bencher novamente. Eventualmente, portei a configuração do Gitpod para um dev container para facilitar para os colaboradores começar. Para mim, contudo, vou continuar com o localhost.

TecnologiaVeredicto
Gitpod
M1 MacBook Pro

Integração Contínua

Bencher é open source. Como um projeto open source moderno, você meio que tem que estar no GitHub. Isso significa que o caminho de menor resistência para integração contínua (CI) é o GitHub Actions. Ao longo dos anos, comecei a odiar as DSLs de CI baseadas em YAML. Cada uma tem suas próprias peculiaridades, e quando se trata de uma empresa tão grande quanto o GitHub, conseguir um ícone ⚠️ em vez de um ícone ❌ pode demorar anos.

Isso me motivou a experimentar o Dagger. Na época, você podia usar Dagger apenas através dessa linguagem esotérica chamada CUE. Eu tentei. Eu realmente tentei. Por praticamente um final de semana inteiro. Talvez se o ChatGPT existisse na época, eu pudesse ter conseguido. Mas não era só eu. O Dagger eventualmente abandonou o CUE completamente por SDKs mais racionais. Mas a essa altura, já era tarde demais para mim.

Derrotado pelo Dagger, aceitei meu destino com a DSL de CI baseada em YAML, e agora o Bencher usa GitHub Actions. Poxa, eu até construí uma Ação do GitHub Bencher CLI. Seja o agente de mudança problema que você deseja ver no mundo.

TecnologiaVeredito
Dagger
GitHub Actions⚠️

Conclusão

Construir o Bencher me ensinou muito sobre os compromissos que acompanham cada decisão de engenharia. Existem algumas escolhas que eu faria de maneira diferente agora, mas isso é uma coisa boa. Isso significa que aprendi uma ou duas coisas ao longo do caminho. No geral, estou muito satisfeito com onde o Bencher está hoje. O Bencher evoluiu de um esboço no meu caderno para um produto completo com uma base de usuários crescente, uma comunidade vibrante e clientes pagantes. Estou animado para ver onde os próximos três anos nos levarão!

PilhaComponenteTecnologiaVeredicto
FrontendBiblioteca FrontendYew
Seed
Sycamore
Elm
SolidJS
Linguagem FrontendRust
JavaScript
TypeScript
Typeshare
WASM
Hospedagem FrontendNetlify Builds
Netlify Deploys
BackendLinguagem BackendRust
Framework Servidor HTTPDropshot
Banco de DadosURL Query Params
git + HTML
SQLite
Litestream
LiteFS
Driver de Banco de DadosDiesel
Hospedagem BackendFly.io
AWS S3
CLIBiblioteca CLIClap
Cliente APIProgenitor
DesenvolvimentoAmbiente de DesenvolvimentoGitpod
M1 MacBook Pro
Integração ContínuaDagger
GitHub Actions⚠️

Bencher: Benchmarking Contínuo

🐰 Bencher

Bencher é um conjunto de ferramentas de benchmarking contínuas. Já teve algum impacto de regressão de desempenho nos seus usuários? Bencher poderia ter prevenido isso. Bencher permite que você detecte e previna regressões de desempenho antes que cheguem à produção.

  • Execute: Execute seus benchmarks localmente ou no CI usando suas ferramentas de benchmarking favoritas. O CLI bencher simplesmente envolve seu harness de benchmark existente e armazena seus resultados.
  • Rastreie: Acompanhe os resultados de seus benchmarks ao longo do tempo. Monitore, consulte e faça gráficos dos resultados usando o console web do Bencher baseado na branch de origem, testbed e medida.
  • Capture: Capture regressões de desempenho no CI. Bencher usa análises personalizáveis e de última geração para detectar regressões de desempenho antes que elas cheguem à produção.

Pelos mesmos motivos que os testes de unidade são executados no CI para prevenir regressões de funcionalidades, benchmarks deveriam ser executados no CI com o Bencher para prevenir regressões de desempenho. Bugs de desempenho são bugs!

Comece a capturar regressões de desempenho no CI — experimente o Bencher Cloud gratuitamente.

🤖 Este documento foi gerado automaticamente pelo OpenAI GPT-4. Pode não ser preciso e pode conter erros. Se você encontrar algum erro, abra um problema no GitHub.