2 pontos por GN⁺ 2024-11-30 | 1 comentários | Compartilhar no WhatsApp
  • O Ninja, lançado em 2011, começou como um sistema de build parecido com o Make e se tornou um caso representativo de sucesso em open source, espalhando-se do Chrome e partes do Android até projetos baseados em Meson e CMake
  • O principal fator que definiu seu sucesso e fracasso não foi o código em si, mas a arquitetura e, mais ainda, questões sociais como manutenção e expectativas dos usuários
  • O Ninja lê os comandos, entradas e saídas descritos em ninja.build, verifica os horários de modificação dos arquivos e executa em paralelo apenas o trabalho necessário, com foco em tornar builds incrementais rápidas em projetos grandes
  • A maior decisão de design foi não incorporar diretamente recursos de build de alto nível, permanecendo como um executor de grafo de ações (action graph), enquanto decisões complexas ficam separadas em geradores (generators)
  • A integração com o CMake, o suporte ao Windows e o padrão de execução paralela ampliaram sua adoção, mas exigências e contribuições que conflitavam com os objetivos do design continuaram sendo um grande peso para a manutenção em open source

O contexto em que o Ninja passou a ser amplamente usado

  • O Ninja é um sistema de build parecido com o Make, lançado cerca de 9 anos antes, e começou como um projeto paralelo compartilhado com certa vergonha, mas depois passou a ser amplamente usado
  • Os principais casos de uso incluem:
    • O Chrome acabou removendo todos os builds que não usavam Ninja
    • O Android usa o Ninja em partes importantes do sistema
    • O projeto Meson usa Ninja e parece ser um sistema de build cada vez mais usado no mundo do software livre
    • Muitos projetos usam Ninja com CMake, e até o guia de build do Swift exige a instalação do Ninja
  • O Ninja foi lançado em 2011, a propriedade do projeto mudou em 2014 e depois passou para um terceiro mantenedor
  • A experiência central revelada por esta retrospectiva é que a arquitetura importa mais que o código, e as questões sociais importam ainda mais que a arquitetura

Funcionamento básico para builds rápidas

  • O que o Ninja faz é relativamente simples:
    • O usuário escreve no arquivo ninja.build os comandos a executar, os arquivos consumidos por cada comando e os arquivos gerados
    • O Ninja lê esse arquivo e verifica os horários de modificação de vários arquivos
    • Ele executa em paralelo os comandos necessários para deixar tudo atualizado
  • Em comparação com o Make, a linguagem de entrada tem menos recursos e a estrutura é focada em executar esses poucos recursos com muita rapidez
  • O fluxo de execução se divide em três etapas:
    • parsing e interpretação do arquivo de build
    • verificação dos horários de modificação dos arquivos de entrada
    • execução dos comandos necessários
  • O objetivo é chegar à terceira etapa o mais rápido possível, mesmo em projetos grandes com mais de 100 mil arquivos de entrada
  • Como exemplo de otimização, o Ninja mapeia os caminhos dos arquivos de entrada o mais cedo possível para objetos únicos em memória e depois usa comparação de ponteiros para verificar a identidade dos caminhos
  • Mais detalhes de desempenho de baixo nível são discutidos no capítulo sobre o Ninja em The Performance of Open Source Software
  • Ao longo dos anos também surgiram reimplementações do Ninja:
    • llbuild e Shake aceitam arquivos do Ninja como entrada
    • samurai é um projeto praticamente reimplementado arquivo por arquivo, com menos código, mas também menos recursos e sem testes
  • Fazer a implementação “divertida” de 20% é fácil, mas os 80% restantes têm muitos detalhes, e não se conhece uma implementação mais rápida

Escolhas importantes no design

  • Representação do grafo

    • O Make não lida bem com casos em que uma única regra de build gera vários arquivos
    • O Ninja usa um grafo bipartido entre arquivos e comandos, em vez de um grafo apenas entre arquivos
    • Nós de arquivo são arestas que entram em nós de comando, e nós de comando geram arestas que saem para os arquivos de saída
    • Essa estrutura captura melhor a forma real de um build
    • Se uma das entradas muda, o comando fica desatualizado, e quando é executado atualiza todas as saídas
    • Um arquivo específico pode ter no máximo uma aresta de entrada
    • A própria linha de comando também pode ser vista como entrada do nó de comando, então mudanças em flags de linha de comando deixam o comando e suas saídas desatualizados
  • deps log e dependências de headers em C

    • Para tratar corretamente dependências de headers em C, é preciso consumir dados extras de dependência gerados pelo compilador C
    • Entre introduzir um banco de dados e preservar a simplicidade, foi criado o formato de representação do deps log
    • Esse formato é bastante compacto, mas ainda está errado de maneiras importantes
  • Design end-to-end / crash-only

    • O Ninja não é um processo daemon persistente; ele recomeça tudo do zero a cada execução
    • Essa escolha foi influenciada pelo princípio end-to-end e por crash-only software
    • Se o caminho que começa do zero for rápido, não há necessidade de criar um caminho de código “online” separado
    • Projetos que podem ficar residentes em memória tendem, no fim, a negligenciar o desempenho de inicialização
  • Verificação do estado dos arquivos

    • Um dos motivos para esperar que uma ferramenta de build fique residente em memória é fazer cache do estado dos arquivos em disco
    • Na prática, o kernel já armazena essas informações em memória, e repetir esse cache no espaço do usuário não economiza muito
    • No Linux, obter o estado de um arquivo é muito rápido, e o Ninja faz isso em uma única thread
    • Mesmo em máquinas “rápidas” de 10 anos atrás, era possível executar stat em 30 mil arquivos em algumas dezenas de milissegundos
  • Compromissos entre escala e especificação

    • Como regra prática, um aumento de 2x na escala pode ser resolvido com otimização, mas um aumento de 10x exige rearquitetura
    • O Ninja foi projetado em torno do build do Chrome, que na época tinha cerca de 30 mil etapas de build
    • Hoje ele também é usado em ambientes menores, mas nesses casos a vantagem de velocidade pode nem ser necessária
    • Em ambientes maiores, como o build do Android, já está batendo em limites de escala, e talvez seja necessária outra abordagem
    • O Ninja exige que o usuário forneça informações suficientes para execução paralela, mas não obriga que todo o build seja completamente conhecido
    • Há trade-offs entre correção, conveniência e desempenho, e em todo o ecossistema uma ferramenta que sacrifica um pouco de correção em troca de conveniência pode gerar resultados mais corretos do que uma ferramenta mais correta, porém incômoda

A metáfora do “assembler” e a separação que ela criou

  • Sistemas de build oferecem muitos recursos de alto nível, e as formas como cada ferramenta se descreve são tão amplas que mal permitem comparação direta
  • A principal percepção do Ninja é que, independentemente dos recursos de alto nível oferecidos, um sistema de build acaba precisando montar um grafo de ações para manter os arquivos atualizados
  • O Ninja executa apenas esse grafo de ações e deixa para o usuário decidir que gerador colocar por cima
  • Essa separação entre dois programas foi criada porque se encaixava bem no projeto Chrome, mas depois se tornou a contribuição mais importante do Ninja
  • Há duas vantagens nisso:
    • o próprio Ninja permanece simples e rápido
    • trabalhos caros, como fazer glob em *.c, são empurrados para o lado do gerador
  • Ao contrário de outros sistemas de build que fazem tudo de uma vez, a estrutura do Ninja faz com que o grafo de ações calculado seja salvo em disco como um snapshot
  • Na prática, isso vira uma forma de cache do grafo de ações entre builds
  • O gerador pode ser tão de alto nível quanto se quiser:
    • por exemplo, pode fazer glob em toda a árvore de código-fonte e encontrar arquivos de teste pelo nome contendo test
  • O desenvolvedor precisa decidir diretamente por quais custos quer pagar
    • se o gerador fizer glob no disco inteiro, isso é possível, mas também fica mais claro por que o build é lento
  • A separação entre gerador e grafo de ações não é realmente simples, e o Ninja também tem muitos detalhes sobre qual camada deve fazer o quê
  • Em teoria, sistemas de build do Xcode ou do Visual Studio poderiam fazer a mesma pré-computação e salvar o resultado em snapshot, mas isso tende a não funcionar bem por causa da tentação de misturar camadas
  • O Make tentou incluir recursos voltados para programadores, como globbing, expansão de variáveis, substring e funções; acabou sendo uma linguagem fraca demais para expressar tudo de que se precisa, mas forte o suficiente para incentivar o uso de Makefiles lentos
  • O Ninja tentou evitar essa direção

O padrão domina a percepção de desempenho

  • O Ninja executa os comandos desejados em paralelo por padrão
  • O Make também pode executar em paralelo com a flag -j, mas o padrão é a execução serial
  • Makefiles costumam ser escritos sem declarar dependências suficientes para que a execução paralela seja segura
  • Como o Ninja sempre executa em paralelo, mesmo em sistemas de um único núcleo, ele expõe esse tipo de erro mais cedo
  • Programas que se adaptam bem ao Ninja normalmente também passam a ser seguros para build paralela
  • O Ninja não tem um sistema avançado para detectar dependências erradas; a estrutura apenas faz com que builds incorretos apareçam com mais frequência, revelando o problema
  • Como muitos usuários esquecem ou nem conhecem a flag de paralelismo do Make, só esse padrão já pode fazer o Ninja parecer “mais de duas vezes mais rápido” no uso real
  • Uma otimização que o usuário nunca de fato experimenta tem pouco valor

Métricas que importavam para a velocidade

  • Desempenho de sistemas de build pode significar várias coisas:
    • quanto tempo leva um build completo do zero
    • quanto tempo leva para recompilar depois de modificar um único arquivo em algo já construído
  • O Ninja foi focado em acelerar o loop de edição-compilação em builds incrementais de codebases grandes
  • Quando o Ninja foi criado, havia a lembrança de que o blaze, isto é, o bazel, era muito rápido, e a meta era alcançar essa velocidade
  • Mais tarde ficou claro que, na métrica que realmente importava, o blaze não era especialmente rápido e, por ser um programa em Java, até exibir a ajuda era lento
  • A obsessão com builds incrementais vinha da ideia de que o tempo de iteração afeta fortemente a satisfação dos programadores
  • O Ninja é usado no loop de edição-compilação, em que a diferença entre 1 segundo e 4 segundos importa
  • O significado de “rápido” é difícil de transmitir aos usuários, e o manual do Ninja avisa que, em projetos pequenos, o ganho de velocidade pode quase não ser perceptível
  • Ainda assim, “rápido” vende bem, então até usuários de apps pequenos frustrados com a falta de recursos tentam usar o Ninja
  • Embora o foco fosse rebuilds incrementais, alguns usuários relataram melhoras também no desempenho de builds completos:
    • como o Ninja quase não faz nada, ele consome pouca CPU durante o build e disputa menos CPU com o trabalho real de compilação
  • A saída também é extremamente concisa:
    • builds bem-sucedidos normalmente imprimem só uma linha
    • outros sistemas de build podem imprimir muitas etapas e informações de tempo, parecendo mais pesados
  • Essa característica de falar pouco faz o Ninja parecer mais “invisível”

Integração com CMake e suporte ao Windows impulsionaram a adoção

  • CMake

    • O Ninja foi criado originalmente para ser usado com o sistema de build específico do Chrome
    • Peter Collingbourne fez o trabalho de conectar o Ninja ao sistema de build CMake, muito mais difundido
    • Essa integração começou por causa do trabalho no LLVM e exigiu adicionar nova semântica tanto ao CMake quanto ao Ninja
    • Peter é apontado como a pessoa com maior mérito no sucesso real do Ninja no mundo
    • Depois, os autores do CMake assumiram a integração, mas não conseguiram responder suficientemente bem às solicitações e preocupações
    • O autor nunca chegou a usar CMake diretamente até hoje
  • Windows

    • Como o Chrome também tinha o Windows como alvo, o Ninja acabou funcionando no Windows
    • Boa parte do suporte ao Windows foi escrita por um colaborador
    • Tecnicamente, o suporte ao Windows é uma grande dor de cabeça
    • Execução de processos e captura de saída são diferenças do tipo que obrigam a aprender APIs específicas de cada plataforma
    • O design do Ninja depende da propriedade de conseguir obter rapidamente, do kernel, o horário da última modificação dos arquivos em cache, e isso não vale no Windows
    • Mesmo assim, o Windows é uma plataforma com uma enorme base de desenvolvedores
    • Existe um forte impulso de compartilhar ferramentas bacanas no Linux, mas no Windows há mais impulso para vendê-las, então há relativamente menos ferramentas disponíveis livremente
    • Ter muitos usuários de Windows entre os primeiros usuários do Ninja foi surpreendente na época, mas faz sentido, porque há muitos desenvolvedores em Windows e basta uma pequena fração se interessar para isso aparecer em números absolutos

Entender o trabalho relacionado

  • Como o Ninja começou quase como um hack de fim de semana, o autor lamenta não ter pesquisado mais o trabalho relacionado antes de criá-lo
  • Ao construir algo, é importante realmente entender o espaço de design
  • O termo “action graph” não era uma expressão usada ao criar o Ninja; foi tomado do sistema de build do Google, blaze/bazel
  • O bazel explica que um grafo de alvos de alto nível, como “library” e “binary”, gera um action graph, que é um grafo de comandos
  • A ideia de tratar o texto da linha de comando como uma entrada, assim como arquivos, é um exemplo de computação incremental
  • Computação incremental se conecta não só a sistemas de build, mas também à incrementalidade de interfaces de usuário
  • O blog da Jane Street tem um texto de introdução ao Incremental que também se conecta a formas de construir UI como o React
  • "Build Systems à la Carte" é um artigo que discute computação incremental no contexto de sistemas de build, e é citado como algo que o autor gostaria que existisse antes de escrever o Ninja

O peso da manutenção em open source

  • A experiência como mantenedor de open source não foi particularmente agradável
  • O sentimento em relação ao projeto mistura orgulho quando alguém faz um elogio com uma decepção ainda maior
  • Como o código foi publicado de graça, às vezes apareciam pessoas pedindo algo com gentileza, mas com mais frequência surgiam usuários irritados fazendo exigências, e agradecimentos eram raros
  • Também havia repetidamente pessoas ameaçando fazer fork se suas demandas não fossem aceitas
  • Contribuições razoáveis que entravam em conflito com os objetivos do design eram outra dificuldade
    • ao recusar a contribuição de alguém gentil e inteligente, havia vontade de retribuir com uma explicação suficiente, e o esforço de dar essa explicação era exaustivo
  • O autor começou a programar graças ao software livre e escreveu código querendo devolver o que recebeu, mas hoje o software livre parece menos um compartilhamento entre iguais e mais uma dinâmica em que usuários se veem como clientes e tratam o autor como alguém de quem podem reclamar ao gerente
  • A motivação atual está mais próxima de impressionar um pequeno grupo de hackers respeitados ou corresponder às expectativas deles do que de ter sucesso amplo
  • O sucesso do Ninja trouxe muito aprendizado, mas o autor acredita que um sucesso menor talvez tivesse ensinado quase as mesmas lições

Mantenedores e contribuidores

  • Nico Weber foi um colaborador cuidadoso e atuou como mantenedor do Ninja por vários anos
  • Jan Niklas Hasse assumiu o projeto depois de Nico e parece estar fazendo um bom trabalho
  • Além deles, muitos outros contribuidores do Ninja participaram do projeto

1 comentários

 
GN⁺ 2024-11-30
Comentários do Hacker News
  • A frase “fala-se sobre programação como se fosse escrever código, mas na prática a arquitetura é mais importante do que o código, e as questões sociais se tornam mais importantes do que a arquitetura” expressa com precisão algo que eu já pensava havia muito tempo

    • Concordo fortemente. Peopleware 1987 [1] diz a mesma coisa
      O capítulo 1 do livro afirma que “os principais problemas do nosso trabalho são mais sociológicos do que técnicos” e trata de questões sociais e políticas como química e coesão de equipe, “tempo de concentração”, ambiente de trabalho silencioso e o custo da rotatividade
      [1] https://en.wikipedia.org/wiki/Peopleware:_Productive_Project...
    • Programação e suas ferramentas são para pessoas, não para computadores, e, exceto por tarefas muito triviais, é preciso várias pessoas para criar algo que rode em um computador
      Por isso, vejo a engenharia de software como algo muito mais próximo das ciências sociais do que muita gente admite ou considera confortável. Como as ciências sociais são mais fluidas e menos previsíveis do que as naturais, parece haver uma tendência a evitar a parte social ou tratá-la de forma muito primitiva. Há obsessão com os detalhes atômicos da tecnologia, mas, no fim, o produto feito pela equipe frequentemente acaba sendo ruim demais
    • https://en.wikipedia.org/wiki/Conway's_law
      “Organizações que projetam sistemas inevitavelmente produzem projetos que copiam a estrutura de comunicação dessas organizações.” — Melvin E. Conway, How Do Committees Invent?
    • Pela minha experiência, cerca de 80% dos problemas técnicos surgem porque duas pessoas ou duas equipes simplesmente relutam em sentar e conversar
    • Isso não quer dizer que seja uma dicotomia. Uma boa arquitetura atende ao que as pessoas exigem do sistema e, ao mesmo tempo, o mantém compreensível para seres humanos
  • Isso já tinha sido discutido na época:
    The Success and Failure of Ninja - https://news.ycombinator.com/item?id=23157783 - maio de 2020, 38 comentários
    Repostar depois de cerca de 1 ano é ok, e o link para a thread antiga é para leitores que quiserem se aprofundar mais

  • A parte “o Android usa Ninja em algum grande componente do sistema, embora eu nunca tenha entendido exatamente onde” é engraçada
    Ninja de fato tem um papel enorme no AOSP. O sistema de build começou usando Makefile, mas ficou rapidamente complexo por causa do soong, um sistema de build declarativo customizado, e da migração fracassada e interrompida para Bazel. O Google criou o kati (https://github.com/google/kati) para converter Makefile em arquivos de build do Ninja, e o resultado é realmente gigantesco:
    λ wc -l out/build-qssi.ninja
    3035442 out/build-qssi.ninja
    A passagem de Makefile/soong para Ninja é dolorosa e leva alguns minutos mesmo em máquinas modernas, mas, depois que o Ninja assume, ele roda incrivelmente rápido

    • Nunca usei Ninja, então fiquei curioso sobre quais seriam as vantagens em comparação com Makefile
      Também me pergunto se vale a pena introduzir mais uma ferramenta só para traduzir de uma ferramenta para outra. Ainda mais se os arquivos Ninja forem tão grandes e difíceis de ler por humanos
  • Na parte “acredito que programadores percebem latência, e isso afeta seu humor mesmo quando eles não notam conscientemente. O Google pesquisou isso recentemente e de certa forma confirmou minha crença”, fiquei curioso se esse estudo sobre latência foi de fato publicado

  • Ninja é bastante popular entre desenvolvedores de jogos
    Achei divertida a frase “Windows continua sendo uma plataforma enorme do ponto de vista de desenvolvedores, e esses desenvolvedores estão famintos por ferramentas”. Como alguém que desenvolve principalmente no Windows, para mim os desenvolvedores de Linux é que parecem famintos por ferramentas, vivendo sem um bom depurador como o Visual Studio ou um profiler como o Superluminal. Ultimamente, a diferença entre as duas plataformas parece estar diminuindo, e também gosto dos utilitários em Rust que em geral funcionam de forma multiplataforma

    • Toda vez que alguém coloca Visual Studio e “bom” na mesma frase, tenho a sensação estranha de que o universo é uma simulação e as outras pessoas vieram de outro universo
      Não consigo pensar em nada nas minhas interações com o Visual Studio que eu chamaria de “bom”. Consigo pensar em algumas coisas “mais ou menos aceitáveis”, mas depuração não entra nisso. Ainda assim, no 2022 o depurador não é mais cheio de bugs, então talvez seja disso que estão falando
    • Não sei em que mundo o depurador do Visual Studio é considerado bom. A menos que finalmente tenham consertado, da última vez que usei era insuportavelmente lento, a ponto de levar vários segundos para avançar uma única linha
    • Há uma comunidade que vê UNIX como o conjunto completo das ferramentas de desenvolvimento, e acho que nisso acabam vendo a floresta e perdendo as árvores
      Conheço UNIX bem, a ponto de ter usado várias variantes desde que tive contato com Xenix em 1993, mas quase nunca uso WSL, exceto para rodar contêineres Docker Linux
  • As poucas coisas para as quais eu ainda usava Ninja eu troquei por samurai, e melhorou em todos os aspectos possíveis
    Ainda assim, acho que esse tipo de sistema de build seguiu a direção errada. O que eu quero de um sistema de build é fazer hash do conteúdo de todas as entradas transitivas e verificar se o resultado está no registro

    • Sim. Sistemas de build com suporte a cache distribuído basicamente usam digest em vez de timestamp para detectar mudanças: Bazel, Pants, Buck e outros
      Mas todos são extremamente complexos. Considerando apenas builds locais, pelo que sei SCons e Waf também usam hash para detectar mudanças
    • Talvez você se interesse pelo n2, feito pelo autor do Ninja
    • Acho que essa era a ideia do NetKernel
      Fiz algo parecido, a biblioteca “TDAR” para Deno[1], e funciona bem, mas dá um certo trabalho embrulhar ferramentas de linha de comando que assumem operar sobre um sistema de arquivos mutável e fingir que são chamadas de função pura
      [1] Ainda não separei isso do projeto principal[2], mas falei sobre isso neste vídeo no YouTube: https://youtu.be/sty29o8sUKI
      [2] Se alguém se interessar por esse tipo de coisa, pode me cutucar para eu abrir o código-fonte. togos zero zero at gee mail dot comb
    • Fiquei curioso sobre o que o Samurai faz melhor. Eu achava que era um subconjunto compatível do Ninja
      E “não é o que eu queria” não significa necessariamente “está errado”. Existem pessoas no mundo com gostos diferentes
    • Não sei se o Samurai ainda está ativo. Enviei um pull request para melhorar o tratamento de sinais, e ele está ignorado há mais de meio ano
  • Como é preciso Ninja para usar módulos C++20 no CMake, o Ninja provavelmente vai continuar por bastante tempo

  • A parte mais interessante foi a ideia de que “muitas vezes é preciso fazer concessões entre correção e conveniência ou desempenho, e você deve estar consciente de onde escolhe ficar nesse espectro”
    Alguns programadores enxergam essa dinâmica de forma rígida demais e acham que um dos valores obviamente deve ter prioridade. Mas, na prática, a interação é bem sutil. Por exemplo, uma ferramenta que sacrifica um pouco de correção em favor da conveniência pode acabar produzindo resultados mais corretos no ecossistema como um todo do que uma alternativa mais correta, porém menos conveniente. Isso acontece se os programadores acabam evitando a segunda

    • Isso é realmente uma mina de ouro de insight. É por isso que Python, Go, TypeScript/JavaScript são muito mais populares que Haskell/OCaml
  • “Perdoem o nome constrangedor”, mas o nome é ótimo
    P.S.: também poderia ficar mais rápido se isto fosse implementado: https://github.com/ninja-build/ninja/issues/2157 embora, como o texto explica, essa ferramenta seja deliberadamente sem estado, sem incluir nem mesmo a menor pista de execuções anteriores