Detalhamento do desempenho do build

O Bazel é complexo e faz muitas coisas diferentes durante um build, algumas das quais podem ter um impacto no desempenho do build. Esta página tenta mapear alguns desses conceitos do Bazel para as implicações deles no desempenho do build. Embora não seja extensa, incluímos alguns exemplos de como detectar problemas de desempenho do build extraindo métricas e o 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 alguns trabalhos já concluídos.

Sugerimos analisar 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). Eles também representam duas experiências de usuário diferentes. Em comparação com o início de um build limpo do zero (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 mais rápido, já que o cache geralmente 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 amplamente como um build incremental. O usuário pode ter mudado para flags ou destinos diferentes, causando um build efetivamente limpo. Qualquer definição mais rigorosa de incrementalidade provavelmente terá que vir na forma de uma heurística, por exemplo, observando o número de pacotes carregados (PackageMetrics.packages_loaded).

Métricas de build determinísticas como um proxy para o desempenho do build

Medir o desempenho do build pode ser difícil devido à natureza não determinística de determinadas métricas (por exemplo, o Tempo de CPU do Bazel ou os tempos de fila em um cluster remoto). Assim, pode ser útil usar métricas determinísticas como um proxy para a quantidade de trabalho feito pelo Bazel, o que, por sua vez, afeta o desempenho dele.

O tamanho de uma solicitação de build pode ter uma implicação significativa no desempenho do build. Um build maior pode representar mais trabalho na análise e construção dos gráficos de build. O crescimento orgânico de builds ocorre naturalmente com o desenvolvimento, à medida que mais dependências são adicionadas/criadas e, portanto, crescem em complexidade e ficam mais caras para serem criadas.

Podemos dividir esse problema nas várias fases de build e usar as seguintes métricas como métricas de proxy para o trabalho realizado em cada fase:

  1. PackageMetrics.packages_loaded: o número de pacotes carregados com sucesso. Uma regressão aqui representa mais trabalho que precisa ser feito para ler e analisar cada arquivo BUILD adicional 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 encontrar 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 na travessia do gráfico de destino configurado.

    • Isso geralmente ocorre devido à adição de dependências e à necessidade de construir o grafo do fechamento transitivo delas.
    • Use cquery para encontrar 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 construção do gráfico de ações. Observe que 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 --experimental_record_metrics_for_all_mnemonics para coletar esses dados para todos os tipos de ação que foram executados.
    • Isso ajuda você a descobrir que tipo de ações foram executadas (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, é necessário garantir que os builds dos quais você extrai essas métricas sejam builds limpos.

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

Uso de recursos locais

O Bazel consome vários recursos na sua máquina local (tanto para analisar o gráfico de build e impulsionar a execução quanto para executar ações locais). Isso pode afetar o desempenho / disponibilidade da sua máquina na execução do build e também de outras tarefas.

Tempo gasto

Talvez as métricas mais suscetíveis a ruídos (e que podem variar muito de build para build) sejam o tempo, em particular, o tempo decorrido, o tempo de CPU e o tempo do sistema. Você pode usar bazel-bench para receber um comparativo de mercado dessas métricas e, com um número suficiente de --runs, aumentar a significância estatística da sua medição.

  • Tempo decorrido é o tempo real decorrido.

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

    • Se o tempo de CPU regredir em dois commits de projeto, sugerimos coletar um perfil de CPU do Starlark. Você também deve usar --nobuild para restringir o build à fase de análise, já que é nela 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 regredir, ele será mais correlacionado 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 --experimental_collect_load_average_in_profiler flag introduzida no Bazel 6.0, o criador de perfil de rastreamento JSON coleta a média de 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 o Bazel programa muitas ações locais em paralelo para sua máquina. Talvez seja necessário ajustar --local_cpu_resources e --local_ram_resources, especialmente em ambientes de contêiner (pelo menos até que #16512 seja mesclado).

Monitorar o uso da memória do Bazel

Há duas fontes principais para receber o uso da memória do Bazel: 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, há peak-heap-size, max-heap-size, used-heap-size e committed-heap-size (consulte a documentação), mas são menos relevantes.
  • BEP's MemoryMetrics.peak_post_gc_heap_size: Tamanho do pico do heap da JVM em bytes após o GC (requer a configuração de --memory_profile que tenta forçar um GC completo).

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 são devidas à 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 criador de perfil de memória integrado para regras.

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

Embora os workers persistentes possam ajudar a acelerar os builds significativamente (especialmente para linguagens interpretadas), a ocupação de memória deles pode ser problemática. O Bazel coleta métricas nos workers. Em particular, o campo WorkerMetrics.WorkerStats.worker_memory_in_kb informa quanta memória os workers usam (por mnemônico).

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

Perfil que inclui o uso da memória pelos workers

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

Diminuir o valor de --worker_max_instances (padrão 4) pode ajudar a reduzir a quantidade de memória usada por workers persistentes. Estamos trabalhando ativamente para tornar o gerenciador de recursos e o programador do Bazel mais inteligentes, para que esse ajuste fino seja necessário com menos frequência no futuro.

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. Assim, 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 NetworkMetrics.SystemNetworkStats proto do BEP (requer a transmissão de --experimental_collect_system_network_usage).

Além disso, os perfis de rastreamento JSON permitem visualizar o uso de rede em todo o sistema durante o build transmitindo a flag --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 de rede em todo o sistema.

Um uso de rede alto, mas bastante plano, ao usar a execução remota pode indicar que a rede é o gargalo no seu build. Se você ainda não estiver usando, considere ativar o build sem os bytes transmitindo --remote_download_minimal. Isso vai acelerar seus builds, evitando o download de artefatos intermediários desnecessários.

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