Detalhamento do desempenho do build

Informar um problema Acessar fonte

Ele é complexo e faz muitas coisas diferentes durante um build, e algumas delas podem afetar o desempenho do build. Esta página tenta mapear alguns desses conceitos do Bazel para as implicações no desempenho do build. Embora não sejam extensos, incluímos alguns exemplos de como detectar problemas de desempenho do build por meio da extração de métricas e do que você pode fazer para corrigi-los. Com isso, esperamos que você possa aplicar esses conceitos ao investigar regressões de desempenho do build.

Builds limpos x incrementais

Um build limpo é aquele que cria tudo do zero, enquanto um build incremental reutiliza algum trabalho já concluído.

Sugerimos que você analise builds limpos e incrementais separadamente, especialmente quando você estiver coletando / agregando métricas que dependem do estado dos caches do Bazel (por exemplo, métricas de tamanho da solicitação de build ). Elas também representam duas experiências de usuário diferentes. Em comparação com iniciar um build limpo do zero (o que leva mais tempo devido a um cache frio), os builds incrementais acontecem com muito mais frequência à medida que os desenvolvedores iteram no código. Normalmente, o cache é mais rápido, porque o cache já está quente.

Você pode usar o campo CumulativeMetrics.num_analyses no BEP para classificar builds. Se num_analyses <= 1, é um build limpo. Caso contrário, podemos categorizá-lo de modo amplo como provavelmente um build incremental. O usuário poderia ter mudado para diferentes flags ou diferentes destinos, resultando em um build eficientemente limpo. Qualquer definição mais rigorosa de incrementabilidade provavelmente terá que vir na forma de uma heurística, por exemplo, analisando o número de pacotes carregados (PackageMetrics.packages_loaded).

Métricas deterministas de build como substitutos do desempenho do build

Medir o desempenho do build pode ser difícil devido à natureza não determinista de determinadas métricas. Por exemplo, o tempo de CPU do Bazel ou os tempos de fila em um cluster remoto. Dessa forma, pode ser útil usar métricas determinísticas como substitutos da quantidade de trabalho feito pelo Bazel, que, por sua vez, afeta o desempenho dele.

O tamanho de uma solicitação de build pode ter uma implicação significativa no desempenho da versão. Um build maior pode representar mais trabalho na análise e criação dos gráficos de build. O crescimento orgânico das versões ocorre naturalmente com o desenvolvimento, à medida que mais dependências são adicionadas/criadas e, portanto, tornam-se mais complexas e ficam mais caras.

Podemos dividir esse problema em várias fases de criação e usar as seguintes métricas como métricas de proxy para o trabalho feito em cada fase:

  1. PackageMetrics.packages_loaded: o número de pacotes carregados. Uma regressão aqui representa mais trabalho a ser feito para ler e analisar cada arquivo BUILD extra na fase de carregamento.

    • Isso geralmente ocorre devido à adição de dependências e à necessidade de carregar o fechamento transitivo delas.
    • Use query / cquery para descobrir onde novas dependências podem ter sido adicionadas.
  2. TargetMetrics.targets_configured: representa o número de destinos e aspectos configurados no build. Uma regressão representa mais trabalho na construção e navegação do gráfico de destino configurado.

    • Isso geralmente ocorre devido à adição de dependências e à necessidade de construir o gráfico do fechamento transitivo delas.
    • Use o cquery para descobrir onde novas dependências podem ter sido adicionadas.
  3. ActionSummary.actions_created: representa as ações criadas no build, e uma regressão representa mais trabalho na criação do gráfico de ações. Isso também inclui ações não usadas que podem não ter sido executadas.

  4. ActionSummary.actions_executed: o número de ações executadas. Uma regressão representa diretamente mais trabalho na execução dessas ações.

    • O BEP grava as estatísticas de ação ActionData que mostram os tipos de ação mais executados. Por padrão, ele coleta os 20 principais tipos de ação, mas você pode transmitir o --experimental_record_metrics_for_all_mnemonics para coletar esses dados para todos os tipos de ação executados.
    • Isso vai ajudar você a descobrir que tipos de ação foram executados (além disso).
  5. BuildGraphSummary.outputArtifactCount: o número de artefatos criados pelas ações executadas.

    • Se o número de ações executadas não aumentou, é provável que uma implementação de regra tenha sido alterada.

Todas essas métricas são afetadas pelo estado do cache local. Portanto, garanta que as versões das quais você extrai essas métricas sejam versões limpas.

Observamos que uma regressão em qualquer uma dessas métricas pode ser acompanhada por regressões no tempo decorrido, no tempo de CPU e no uso de memória.

Uso de recursos locais

Ele consome vários recursos na máquina local, seja para analisar o gráfico de build e orientar a execução, seja para executar ações locais. Isso pode afetar o desempenho / disponibilidade da máquina na execução do build, além de outras tarefas.

Tempo gasto

Talvez as métricas mais suscetíveis a ruídos (e possam variar muito de uma criação para a outra) sejam o tempo. Especificamente, o tempo decorrido, da CPU e do sistema. Você pode usar bazel-bench para ter um comparativo de mercado para essas métricas e, com um número suficiente de --runs, é possível aumentar a significância estatística da sua medição.

  • Tempo decorrido é o tempo decorrido real.

    • Se apenas o tempo decorrido regressar, sugerimos coletar um perfil de rastreamento JSON e procurar diferenças. Caso contrário, provavelmente seria mais eficiente investigar outras métricas regressadas, já que elas podem ter afetado o tempo decorrido.
  • Tempo de CPU é o tempo gasto pela CPU para executar o código do usuário.

    • Se o tempo de CPU regressar em duas confirmações do projeto, sugerimos coletar um perfil de CPU do Starlark. Você provavelmente também precisa usar --nobuild para restringir o build à fase de análise, já que é nele que a maior parte do trabalho pesado da CPU é feita.
  • O tempo do sistema é o tempo gasto pela CPU no kernel.

    • Se o tempo do sistema regressar, isso é correlacionado principalmente com a E/S quando o Bazel lê arquivos do seu sistema de arquivos.

Criação de perfil de carga em todo o sistema

Usando a flag --experimental_collect_load_average_in_profiler introduzida no Bazel 6.0, o criador de perfil de trace JSON coleta a média da carga do sistema durante a invocação.

Perfil que inclui a média de carga do sistema

Figura 1. Perfil que inclui a média de carga do sistema.

Uma carga alta durante uma invocação do Bazel pode ser uma indicação de que ele programa muitas ações locais em paralelo para sua máquina. Convém ajustar --local_cpu_resources e --local_ram_resources, especialmente em ambientes de contêiner (pelo menos até o #16512 ser mesclado).

Como monitorar o uso de memória do Bazel

Há duas fontes principais para acessar o uso de memória do Bazel, info e o BEP.

  • bazel info used-heap-size-after-gc: a quantidade de memória usada em bytes após uma chamada para System.gc().

    • O Bazel bench também fornece comparativos de mercado para essa métrica.
    • Além disso, existem peak-heap-size, max-heap-size, used-heap-size e committed-heap-size (consulte a documentação), mas são menos relevantes.
  • MemoryMetrics.peak_post_gc_heap_size do BEP: tamanho do tamanho de heap da JVM de pico em bytes após a GC (requer a configuração de --memory_profile, que tenta forçar uma GC completa).

Uma regressão no uso da memória geralmente é resultado de uma regressão nas métricas de tamanho da solicitação de build, que geralmente se deve à adição de dependências ou a uma mudança na implementação da regra.

Para analisar o consumo de memória do Bazel em um nível mais granular, recomendamos usar o Memory Profiler integrado para regras.

Criação de perfil de memória de workers persistentes

Os workers permanentes podem ajudar a acelerar significativamente as versões (especialmente para linguagens interpretadas), mas o consumo de memória deles pode ser problemático. O Bazel coleta métricas sobre os workers. Em particular, o campo WorkerMetrics.WorkerStats.worker_memory_in_kb informa a quantidade de memória usada pelos workers (por mnemônico).

O criador de perfil de trace JSON também coleta o uso de memória do worker persistente durante a invocação transmitindo a sinalização --experimental_collect_system_network_usage (nova no Bazel 6.0).

Perfil que inclui o uso de memória dos workers

Figura 2. Perfil que inclui o uso de memória dos workers.

Reduzir o valor de --worker_max_instances (padrão 4) pode ajudar a reduzir a quantidade de memória usada por workers permanentes. Estamos trabalhando ativamente para tornar o gerenciador de recursos e o programador do Bazel mais inteligentes, para que esses ajustes sejam necessários com menos frequência no futuro.

Como monitorar o tráfego de rede para builds remotos

Na execução remota, o Bazel faz o download de artefatos criados como resultado da execução de ações. Dessa forma, a largura de banda da rede pode afetar o desempenho do build.

Se você estiver usando a execução remota para seus builds, considere monitorar o tráfego de rede durante a invocação usando o .proto NetworkMetrics.SystemNetworkStats do BEP (exige a transmissão de --experimental_collect_system_network_usage).

Além disso, os perfis de rastreamento JSON permitem visualizar o uso da rede em todo o sistema durante a compilação transmitindo a sinalização --experimental_collect_system_network_usage (nova no Bazel 6.0).

Perfil que inclui o uso da rede em todo o sistema

Figura 3. Perfil que inclui o uso da rede em todo o sistema.

Um uso de rede alto, mas simples, ao usar a execução remota pode indicar que a rede é o gargalo do build. Se você ainda não estiver usando a rede, ative o build sem os bytes transmitindo --remote_download_minimal. Isso acelera seus builds, evitando o download de artefatos intermediários desnecessários.

Outra opção é configurar um cache de disco local para economizar a largura de banda durante o download.