2 pontos por GN⁺ 2024-04-27 | 1 comentários | Compartilhar no WhatsApp
  • O Bun v1.1.5 adiciona o crash reporter bun.report, que envia informações de stack em Zig/C++ usando apenas uma URL de cerca de 150 bytes, sem dados pessoais, mesmo quando ocorre um crash ou pânico
  • Os crash reporters nativos do sistema operacional e os core dumps têm custos altos em símbolos de depuração, desempenho, privacidade e tamanho do executável, o que dificulta seu uso em uma ferramenta CLI como o Bun
  • A nova abordagem converte endereços que perdem significado por causa do ASLR em endereços relativos ao módulo, e o servidor restaura os nomes de função usando símbolos de depuração compatíveis com o commit SHA e a plataforma
  • A URL inclui plataforma, subcomando, commit SHA, feature flags, endereços da stack, tipo de crash e mensagem, e os endereços da stack são codificados de forma curta com base64 VLQ
  • Nenhum código-fonte JavaScript/TypeScript nem variáveis de ambiente é enviado; a equipe do Bun recebe apenas as informações de stack em Zig/C++ e alguns metadados necessários para diagnóstico

Por que o Bun criou seu próprio crash reporter

  • No momento da escrita, o Bun tem mais de 2.600 issues abertas no GitHub, e algumas delas são especialmente difíceis de reproduzir e depurar
  • Serviços de crash reporting como o Sentry funcionam bem para apps e produtos SaaS, mas, em uma ferramenta CLI como o Bun, fazer upload de core dumps amplia muito os problemas de privacidade, desempenho e tamanho do executável
  • O Bun v1.1.5 introduz um novo formato pequeno para relatórios de crash em Zig e C++
    • O relatório de crash cabe em uma URL de cerca de 150 bytes
    • Não inclui dados pessoais

Limitações de usar apenas o crash reporter do sistema operacional

  • Alguns sistemas operacionais, como o macOS, têm crash reporters embutidos, mas, para usá-los de forma adequada, normalmente é preciso distribuir símbolos de depuração junto com a aplicação
  • Os símbolos de depuração aumentam bastante o tamanho da distribuição do Bun
    • Símbolos de depuração no Linux: cerca de 30MB
    • Símbolos de depuração no macOS: cerca de 9MB
    • Arquivos .pdb no Windows: mais de 250MB
  • Um exemplo do executável do Bun cai de 60M para 51M antes e depois de llvm-strip
  • Sem símbolos de depuração, quando ocorre um crash, o stack trace fica apenas com ??? e endereços, o que tem pouca utilidade
  • Por causa do ASLR (Address space layout randomization), os endereços de função incluem offsets aleatórios, e assim não é possível restaurar os nomes de função diretamente

Como o bun.report funciona

  • Quando ocorre um crash ou pânico no Bun v1.1.5, o Bun imprime um link bun.report junto com versão, plataforma, argumentos de execução, uso de memória e mensagem de crash
  • Quando o usuário abre o link, ele é redirecionado para um formulário de issue do GitHub já preenchido
  • Dentro da URL, há um stack trace remapeado e codificado
  • Com base nas informações contidas na URL, o servidor reconstrói os endereços da stack e os converte em um relatório de crash legível pela equipe do Bun

Processo para transformar endereços em um stack trace legível

  • Os endereços de função são ponteiros para a posição em que o código da aplicação foi carregado na memória, e incluem um offset aleatório por motivos de segurança
  • A ideia básica é subtrair o endereço-base (base address) do binário do endereço bruto para obter um endereço relativo
  • A implementação real é mais complexa por causa das diferenças de API entre plataformas
    • No Windows, usa GetModuleHandleExW com a flag GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, e usa o ponteiro do módulo como endereço-base
    • No Linux, usa dl_iterate_phdr para percorrer os módulos carregados e usa dl_phdr_info.dlpi_addr do módulo que contém o endereço como endereço-base
    • No macOS, percorre os módulos com _dyld_image_count e _dyld_get_image_header, e obtém o slide de ASLR com _dyld_get_image_vmaddr_slide
      • O endereço resultante no macOS ainda contém o offset da imagem, que no caso do Bun é 0x100000000
      • Para encurtar a URL, esse offset é removido, mas precisa ser somado novamente antes do remapeamento com llvm-symbolizer
  • No Linux e no macOS, o primeiro módulo aponta para o binário principal da aplicação
  • No Windows, é possível determinar se é o binário principal comparando o nome do módulo com peb.ProcessParameters.ImagePathName
  • O Bun não baixa nem analisa símbolos de depuração localmente; ele delega o demangling ao servidor
    • O servidor pode armazenar os símbolos de depuração em cache
    • Pode fazer o demangling do stack trace em poucos segundos
    • E, ao mesmo tempo, funciona como link para abrir uma nova issue no GitHub

Estrutura da URL do bun.report

  • A URL do bun.report codifica as seguintes informações
    • Platform: uma letra que representa a plataforma. Por exemplo, w é Windows x86_64 e M é macOS aarch64
    • Subcommand: uma letra que representa subcomandos como bun test, bun install e bun run
    • Commit SHA: o commit SHA da versão atual do Bun, usado depois para buscar símbolos de depuração
    • Feature Flags: marcas que indicam quais APIs e recursos estavam em uso antes do crash
    • Stack Trace Addresses: os endereços calculados na etapa anterior
    • Crash Type: uma letra que representa o tipo de crash
    • Crash Message: uma mensagem cujo formato varia conforme o tipo de crash
  • O número de versão presente na URL serve mais para leitura humana do que para o processamento real
  • Só com essas informações já dá para identificar manualmente algumas características do crash
    • Ao ver o identificador w, é possível saber rapidamente que foi um crash no Windows
    • Ao ver A2 no fim da string, é possível identificar uma falha de segmentação

Codificação VLQ para URLs curtas

  • Os endereços do stack trace são codificados como números base64 Variable Length Quantity (VLQ) para manter a URL curta
  • O VLQ permite representar números pequenos com menos caracteres, sem deixar de codificar números grandes
  • A mesma técnica também é usada para armazenar números de linha em source maps de JavaScript
  • O servidor decodifica os valores VLQ de volta para endereços relativos, baixa os símbolos de depuração usando o hash do commit e a plataforma, e depois faz o demangling dos nomes de função com llvm-symbolizer
  • No crash de exemplo, isso revela que uma assertion falhou em dirInfoCachedMaybeLog, parte do código do resolvedor de módulos no Windows

Codificação de feature flags

  • A URL também codifica um inteiro de 64 bits, em que cada bit corresponde ao uso de um recurso específico do Bun
  • Essas flags dão pistas sobre quais APIs e sistemas podem ter influenciado o crash
    • Se um arquivo .env foi carregado automaticamente, a feature dotenv é marcada
    • Se fetch() foi usado, a feature fetch é marcada
  • O Bun rastreia o uso de recursos por meio de um contêiner de variáveis globais e marca o uso incrementando o número correspondente dentro de cada API
  • Usando metaprogramação em tempo de compilação no Zig, ele percorre a lista de features e gera dinamicamente uma packed struct que usa 1 bit para cada recurso
  • Com inline for, é possível percorrer a lista de features em tempo de compilação, enquanto a marcação real dos bits acontece em tempo de execução
  • Ao adicionar uma nova feature à struct Features existente, o crash reporter também passa a tratá-la sem necessidade de código repetido
  • O mesmo tipo de abordagem seria possível com macros em C ou Rust, mas, na implementação do Bun, o comptime do Zig foi usado de forma mais simples e legível

Diferença em relação aos core dumps

  • Core dumps contêm muito mais informação, mas são grandes, só são realmente úteis com símbolos de depuração e podem incluir muitas informações sensíveis ou confidenciais
  • O novo formato de relatório do Bun evita situações em que código-fonte JavaScript/TypeScript, variáveis de ambiente e outras informações sensíveis seriam enviados
  • Em vez de mandar tudo por padrão, ele envia apenas o stack trace em Zig/C++ e alguns detalhes com maior chance de serem úteis para diagnosticar o problema
  • Se forem necessárias informações adicionais, elas podem ser solicitadas separadamente ao usuário
  • Isso torna mais fácil para a equipe do Bun diagnosticar crashes do que no cenário anterior, em que restavam apenas endereços sem mapeamento

Demo

  • Um pequeno webapp para testar o crash reporter está disponível em bun.report
  • Ao adicionar /view ao final de qualquer URL de relatório de crash, é possível abrir a tela desse webapp

1 comentários

 
GN⁺ 2024-04-27
Opiniões no Hacker News
  • Se o motivo para usar essa abordagem em vez de um stack trace comum é evitar distribuir vários MB de símbolos de depuração, parece que ignoraram uma opção melhor: colocar apenas os nomes das funções nas tabelas de depuração.
    É um método muito melhor do que ter que usar um serviço web para ver o stack trace, e isso não é só teoria; já está implementado no LLVM: https://clang.llvm.org/docs/UsersManual.html#cmdoption-gline...

    • O principal motivo para usar essa abordagem em vez de um stack trace comum não é o tamanho dos símbolos de depuração, mas o fato de que pouquíssimas pessoas têm paciência para postar relatórios de crash em issues do GitHub.
      Se uma única URL preencher quase automaticamente o conteúdo necessário, fica fácil o bastante, e é assim que desenvolvedores de fato enviam relatórios de crash. O tamanho também importa, para não criar desvantagens para o usuário, mas o ponto central é tornar todo o processo muito fácil
    • Dizer “ignoraram uma opção melhor” e “obviamente é melhor” soa um pouco categórico demais. Provavelmente eles sabiam dessa possibilidade.
      Neste cenário de uso, precisar ver o stack trace por meio de um serviço web não é uma grande desvantagem. É quase igual a ofuscar/minificar bundles JavaScript de frontend, enviar source maps para o Sentry e depois usar o Sentry para restaurar stack traces vindos do navegador do usuário. O usuário, de qualquer forma, não vai olhar aquele stack trace, e para mim não é inconveniente vê-lo via Sentry. Se não fosse assim, eu nem conseguiria vê-lo
    • Criticar dizendo que “obviamente” ou “basta” fazer outra coisa, sem conhecer o contexto da discussão nem quais trade-offs existiam, soa um pouco arrogante.
      Há várias formas de propor alternativas
    • No macOS/iOS, também há a opção de distribuir binários Mach-O contendo apenas a seção LC_FUNCTION_STARTS.
      Nessas plataformas, é assim que a simbolização consegue descobrir nomes de funções de bibliotecas do sistema mesmo sem os símbolos de depuração completos
    • Ainda assim, isso pode ficar bem grande. Pessoalmente acho que vale a pena e normalmente sempre incluo, mas a maioria dos softwares não faz isso
  • Excelente e muito criativo. Muitos projetos fariam bem em seguir essa abordagem. O ponto principal é registrar stack traces usando o program counter relativo ao executável/objeto compartilhado.
    Pelo que sei, o Bun é linkado estaticamente; em um sistema com linking dinâmico, seria preciso prefixar cada program counter normalizado com um ID de objeto compartilhado em forma de número pequeno

    • Não é uma abordagem totalmente nova. É comum em ambientes como jogos, em que não é possível distribuir símbolos, quando ocorre um crash no PC do jogador.
      Por exemplo, o crash reporter da Unreal Engine já conseguia enviar esse formato simples há anos e restaurar números de função/linha com bastante precisão em cada frame da stack. Normalmente, porém, prefere-se um minidump, porque ter até as variáveis da stack pode dar pistas adicionais sobre o que aconteceu
  • A Microsoft é realmente muito boa nisso. No SQL Server, usavam minidumps com informações pessoais removidas, que eram muito pequenos e extremamente úteis.
    Mesmo naquela época, há 15 anos, dumps completos de SQL Server em produção eram arquivos enormes, difíceis de transferir

    • Fiquei curioso: isso era para serviços internos da Microsoft ou para ambientes distribuídos a clientes? Se era o segundo caso, como sabiam o que era informação pessoal identificável?
  • Venho acompanhando o Bun há alguns anos, desde que vi o primeiro tweet relacionado ao Zig, e comecei a usá-lo recentemente; simplesmente funciona bem, sem muita complicação

  • O Bun é bem atraente. Usei em alguns pequenos projetos de exemplo, é rápido e gosto do fato de combinar gerenciamento de pacotes com runtime JavaScript.
    Porém, uso Dependabot na maioria dos projetos sérios. Pelo que sei, o suporte do Dependabot ao Bun está em andamento, ou pelo menos em discussão em algumas issues de repositórios, então estou adiando o uso até esse suporte ser lançado

    • Também hesitamos antes de migrar por causa da falta de suporte do Dependabot, mas descobrimos que o Renovate funciona com Bun e é uma alternativa suficiente por enquanto.
      Não me arrependo nem um pouco. As economias acumuladas pelas partes que ficaram mais rápidas e a grande melhoria na experiência do desenvolvedor valem o quanto esperávamos
  • Pouca gente vai perceber quanto cuidado foi colocado nisso. É bom ver o quanto a equipe do Bun se importa com seu craft

  • O Bun é impressionante, mas recentemente tentei criar um servidor HTTP/2 com Fastify e não funcionou.
    Recebi o erro node:http2 createServer is not yet implemented in Bun, e a issue indicada pela mensagem na verdade é sobre suporte a cliente HTTP/2. O suporte a cliente já foi lançado na v1.0.13: https://bun.sh/blog/bun-v1.0.13#http2-client-support
    A mensagem NotImplementedError deveria ser alterada para apontar para a issue do lado servidor: https://github.com/oven-sh/bun/issues/8823
    O suporte a servidor HTTP/2 está entre as solicitações de recurso mais populares: https://github.com/oven-sh/bun/issues?q=is%3Aissue+is%3Aopen...
    Quando esse recurso sair, acho que muito mais gente poderá migrar para o Bun

    • Esse é o estado atual do Bun. Você espera pela implementação; quando ela termina, descobre que precisa de outra implementação de API; espera de novo e ela aparece, mas dá crash em vários casos de borda; aí espera de novo, e o ciclo se repete.
      O Bun ainda está em uma fase muito inicial do ciclo de vida. Mesmo assim, tenho grandes expectativas para o projeto
  • Fico curioso se há pessoas usando Bun de verdade. É tão bom quanto se espera?

    • Ainda não usei em produção, mas foi ótimo para scripts pontuais e projetos paralelos.
      Configurar ts-node, ts-jest, suporte a ESM, top-level await etc. em um ambiente TypeScript Node dá mais trabalho do que deveria. Lançamentos recentes do Node reduziram parte desse incômodo, mas ainda não é tão simples quanto bun init. Também tenho gostado de usar a API de shell do bun: https://bun.sh/blog/the-bun-shell
    • É ok se você não precisar de REPL nem planejar usar módulos nativos. Dizem que há um REPL, mas a cada atualização ele sempre fica travado por mais de 6 segundos, o que é muito inconveniente.
      As mensagens de erro também são bem piores que as do Node. Usei por um tempo, mas hoje em dia o Node com —loader tsx faz tudo o que eu quero e sem as desvantagens. Para um servidor simples, por exemplo usando WebSocket e se você tiver certeza de que não precisa de módulos nativos, vale considerar o Bun. De fato, mantenho alguns serviços assim em operação
    • Comecei a usar assim que a versão 1.0 saiu e não voltei atrás. Agora estou adotando em todos os projetos
    • Uso como executor de desenvolvimento e testes em um projeto de linguagem de programação com cerca de 15 mil linhas, e até agora não tive nenhum problema específico do Bun.
      A velocidade de inicialização instantânea ainda me surpreende
    • Tenho usado recentemente e é muito bom. Melhorias de qualidade de vida, como não ter que se preocupar com compilação de TypeScript, são realmente convenientes, e ele também é rápido.
      Ainda há coisas faltando, mas para mim já é melhor que o Node
  • Este texto também parece um excelente estudo de caso de Zig. Interessante

  • O Bun precisa baixar 37 pacotes antes de usar o REPL. Sem internet, nem o REPL funciona.
    Ao executar bun repl, aparece um erro dizendo que falhou ao baixar o manifesto do pacote bun-repl. Não é um grande problema, mas eu esperava — e estava bem animado com a ideia — que bastasse colocar um único executável no PATH para ele funcionar imediatamente, sem instalação

    • O REPL ainda não foi priorizado na implementação. O REPL atual é o pacote npm bun-repl implementado pela comunidade.
      Internamente, bun repl faz o mesmo que bunx bun-repl