Como otimizar o desempenho

Informar um problema Mostrar fonte Por noite · 7,3 · 7,2 · 7,1 · 7,0 · 6,5

Ao escrever regras, o problema de desempenho mais comum é percorrer ou copiar dados que são acumulados das dependências. Quando agregados em todo o ser criadas, essas operações podem ocupar facilmente O(N^2) tempo ou espaço. Para evitar isso, é crucial para entender como usar dependências de forma eficaz.

Isso pode ser difícil de acertar, então o Bazel também tem um Memory Profiler que ajuda a encontrar pontos em que você pode ter cometido um erro. Atenção: O custo de escrever uma regra ineficiente pode não ser evidente até que esteja para um amplo uso.

Usar depssets

Sempre que você adicionar informações de dependências de regras, deverá usar depsets. Usar apenas listas simples ou dicts para publicar informações local à regra atual.

Uma dependência representa as informações como um gráfico aninhado que permite o compartilhamento.

Considere o seguinte gráfico:

C -> B -> A
D ---^

Cada nó publica uma única string. Com desativações, os dados ficam assim:

a = depset(direct=['a'])
b = depset(direct=['b'], transitive=[a])
c = depset(direct=['c'], transitive=[b])
d = depset(direct=['d'], transitive=[b])

Cada item é mencionado apenas uma vez. Com listas, você teria isto:

a = ['a']
b = ['b', 'a']
c = ['c', 'b', 'a']
d = ['d', 'b', 'a']

Nesse caso, 'a' é mencionado quatro vezes. Com gráficos maiores, isso o problema ficará cada vez mais grave.

Este é um exemplo de implementação de regra que usa depsets corretamente para publicar informações transitivas. Não há problema em publicar regras locais informações usando listas, se desejar, pois este não é O(N^2).

MyProvider = provider()

def _impl(ctx):
  my_things = ctx.attr.things
  all_things = depset(
      direct=my_things,
      transitive=[dep[MyProvider].all_things for dep in ctx.attr.deps]
  )
  ...
  return [MyProvider(
    my_things=my_things,  # OK, a flat list of rule-local things only
    all_things=all_things,  # OK, a depset containing dependencies
  )]

Consulte a página de visão geral do depósito para mais informações.

Evitar chamar depset.to_list()

Você pode forçar um conjunto a uma lista simples usando to_list(), mas isso geralmente resulta em O(N^2) custo. Se possível, evitar o achatamento de depssets, exceto para depuração propósitos.

Um equívoco comum é achar que é possível nivelar os dependências livremente se você só fizer isso em metas de nível superior, como uma regra <xx>_binary, porque o custo não será acumulado em cada nível do gráfico de build. Mas isso ainda é O(N^2) quando você cria um conjunto de destinos com dependências sobrepostas. Isso acontece quando crie seus testes //foo/tests/... ou ao importar um projeto de ambiente de desenvolvimento integrado.

Reduza o número de chamadas para depset

Chamar depset dentro de uma repetição costuma ser um erro. Isso pode levar a desacelerações aninhamento muito profundo, que tem desempenho ruim. Exemplo:

x = depset()
for i in inputs:
    # Do not do that.
    x = depset(transitive = [x, i.deps])

Esse código pode ser substituído facilmente. Primeiro, colete as dependências transitivas e mescle todas elas de uma só vez:

transitive = []

for i in inputs:
    transitive.append(i.deps)

x = depset(transitive = transitive)

Isso pode ser reduzido com a compreensão da lista:

x = depset(transitive = [i.deps for i in inputs])

Usar ctx.actions.args() para linhas de comando

Ao criar linhas de comando, use ctx.actions.args(). Isso adia a expansão de qualquer dependência para a fase de execução.

Além de ser estritamente mais rápido, isso reduz o consumo de memória às suas regras, às vezes em 90% ou mais.

Aqui estão alguns truques:

  • Transmitir depsets e listas diretamente como argumentos, em vez de os nivelar você mesmo. Eles vão aparecer ctx.actions.args() para você. Se você precisar de transformações no conteúdo de configuração, consulte ctx.actions.args#add para verificar se algo é adequado.

  • Você está transmitindo File#path como argumentos? Não é necessário. Qualquer um Arquivo é automaticamente transformado nos path, adiado para o tempo de expansão.

  • As concatenações evitam construir strings. O melhor argumento de string é uma constante, já que a memória é compartilhada entre todas as instâncias da regra.

  • Se os argumentos forem muito longos para a linha de comando, um objeto ctx.actions.args() pode ser gravado condicionalmente ou incondicionalmente em um arquivo de parâmetro usando ctx.actions.args#use_param_file. Isso é é feita em segundo plano quando a ação é executada. Se você precisar explicitamente controlar o arquivo de parâmetros. Você pode gravá-lo manualmente usando ctx.actions.write

Exemplo:

def _impl(ctx):
  ...
  args = ctx.actions.args()
  file = ctx.declare_file(...)
  files = depset(...)

  # Bad, constructs a full string "--foo=<file path>" for each rule instance
  args.add("--foo=" + file.path)

  # Good, shares "--foo" among all rule instances, and defers file.path to later
  # It will however pass ["--foo", <file path>] to the action command line,
  # instead of ["--foo=<file_path>"]
  args.add("--foo", file)

  # Use format if you prefer ["--foo=<file path>"] to ["--foo", <file path>]
  args.add(format="--foo=%s", value=file)

  # Bad, makes a giant string of a whole depset
  args.add(" ".join(["-I%s" % file.short_path for file in files])

  # Good, only stores a reference to the depset
  args.add_all(files, format_each="-I%s", map_each=_to_short_path)

# Function passed to map_each above
def _to_short_path(f):
  return f.short_path

As entradas de ação transitiva precisam ser depsets

Ao criar uma ação usando ctx.actions.run, não esqueça que o campo inputs aceita uma dependência. Use esta opção sempre que houver entradas e coletados de dependências de forma transitiva.

inputs = depset(...)
ctx.actions.run(
  inputs = inputs,  # Do *not* turn inputs into a list
  ...
)

Pendurado

Se o Bazel estiver travado, pressione Ctrl-\ ou envie O Bazel é um sinal SIGQUIT (kill -3 $(bazel info server_pid)) para receber uma linha de execução. despejar no arquivo $(bazel info output_base)/server/jvm.out.

Como talvez não seja possível executar o bazel info se o Bazel estiver pendurado, o O diretório output_base geralmente é o pai do bazel-<workspace> no diretório do espaço de trabalho.

Criação de perfis de desempenho

O Bazel grava um perfil JSON em command.profile.gz na base de saída ao padrão. Você pode configurar o local com o a flag --profile, por exemplo --profile=/tmp/profile.gz. Os locais que terminam com .gz são compactados com pelo GZIP.

Para acessar os resultados, abra o chrome://tracing em uma guia do navegador Chrome, clique em "Carregar" e escolha o arquivo de perfil (possivelmente compactado). Para mais detalhes resultados, clique nas caixas no canto inferior esquerdo.

Use estes controles de teclado para navegar:

  • Pressione 1 para "selecionar" modo Neste modo, você pode selecionar caixas específicas para inspecionar os detalhes do evento (consulte o canto inferior esquerdo). Selecione vários eventos para ver um resumo e as estatísticas agregadas.
  • Pressione 2 para "pan" modo Em seguida, arraste o mouse para mover a visualização. Você também pode usar a/d para mover para a esquerda/direita.
  • Pressione 3 para "zoom" modo Em seguida, arraste o mouse para aplicar o zoom. Você pode use w/s para aumentar/diminuir o zoom.
  • Pressione 4 para "tempo" em que é possível medir a distância entre dois eventos.
  • Pressione ? para saber mais sobre todos os controles.

Informações do perfil

Exemplo de perfil:

Exemplo de perfil

Figura 1. Exemplo de perfil.

Há algumas linhas especiais:

  • action counters: mostra quantas ações simultâneas estão em andamento. Clique em para conferir o valor real. Deve subir até o valor de --jobs em builds limpos.
  • cpu counters: para cada segundo do build, exibe a quantidade de CPU usada pelo Bazel (um valor de 1 significa que um núcleo é 100% ocupado).
  • Critical Path: mostra um bloco para cada ação no caminho crítico.
  • grpc-command-1: linha de execução principal do Bazel. Útil para ter uma visão geral da o que o Bazel está fazendo, por exemplo, "Launch Bazel", "evaluateTargetPatterns", e "runAnalysisFase".
  • Service Thread: exibe pausas menores e maiores da coleta de lixo (GC, na sigla em inglês).

As outras linhas representam linhas de execução do Bazel e mostram todos os eventos delas.

Problemas comuns de desempenho

Ao analisar os perfis de performance, procure:

  • Fase de análise mais lenta do que o esperado (runAnalysisPhase), especialmente em builds incrementais. Isso pode ser um sinal de má implementação da regra, um exemplo que nivela dependências. O carregamento do pacote pode demorar quantidade excessiva de alvos, macros complexas ou globs recursivos.
  • Ações lentas individuais, especialmente as que estão no caminho crítico. Pode ser possível dividir grandes ações em várias ações menores ou reduzir a de dependências (transitivas) para acelerá-las. Verifique também se há um erro incomum alto diferente de PROCESS_TIME (como REMOTE_SETUP ou FETCH).
  • Gargalos, ou seja, um pequeno número de tópicos ocupados, enquanto todos os outros estão inativo / aguardando o resultado (veja cerca de 15 a 30 segundos na captura de tela acima). Para otimizar essa opção, provavelmente você vai precisar tocar nas implementações das regras ou o próprio Bazel para incluir mais paralelismo. Isso também pode acontecer quando uma quantidade incomum de GC.

Formato do arquivo de perfil

O objeto de nível superior contém metadados (otherData) e os dados de rastreamento reais. (traceEvents). Os metadados contêm informações extras, como o ID de invocação e a data da invocação do Bazel.

Exemplo:

{
  "otherData": {
    "build_id": "101bff9a-7243-4c1a-8503-9dc6ae4c3b05",
    "date": "Tue Jun 16 08:30:21 CEST 2020",
    "profile_finish_ts": "1677666095162000",
    "output_base": "/usr/local/google/_bazel_johndoe/573d4be77eaa72b91a3dfaa497bf8cd0"
  },
  "traceEvents": [
    {"name":"thread_name","ph":"M","pid":1,"tid":0,"args":{"name":"Critical Path"}},
    {"cat":"build phase marker","name":"Launch Bazel","ph":"X","ts":-1824000,"dur":1824000,"pid":1,"tid":60},
    ...
    {"cat":"general information","name":"NoSpawnCacheModule.beforeCommand","ph":"X","ts":116461,"dur":419,"pid":1,"tid":60},
    ...
    {"cat":"package creation","name":"src","ph":"X","ts":279844,"dur":15479,"pid":1,"tid":838},
    ...
    {"name":"thread_name","ph":"M","pid":1,"tid":11,"args":{"name":"Service Thread"}},
    {"cat":"gc notification","name":"minor GC","ph":"X","ts":334626,"dur":13000,"pid":1,"tid":11},

    ...
    {"cat":"action processing","name":"Compiling third_party/grpc/src/core/lib/transport/status_conversion.cc","ph":"X","ts":12630845,"dur":136644,"pid":1,"tid":1546}
 ]
}

Os carimbos de data/hora (ts) e as durações (dur) nos eventos de rastreamento são fornecidos em microssegundos. A categoria (cat) é um dos valores de tipo enumerado de ProfilerTask. Observe que alguns eventos serão mesclados se forem muito curtos e próximos de uns aos outros; transmita --noslim_json_profile se quiser evitar a mesclagem de eventos.

Consulte também Especificação de formato de eventos de rastreamento do Chrome.

analyze-profile

Esse método de criação de perfil consiste em duas etapas. Primeiro, você precisa executar seu build/teste com a flag --profile, por exemplo

$ bazel build --profile=/tmp/prof //path/to:target

O arquivo gerado (neste caso, /tmp/prof) é um arquivo binário, que pode ser pós-processados e analisados pelo comando analyze-profile:

$ bazel analyze-profile /tmp/prof

Por padrão, ele mostra um resumo das informações de análise do perfil especificado. arquivo de dados. Isso inclui estatísticas cumulativas para diferentes tipos de tarefa para cada fase de criação e uma análise do caminho crítico.

A primeira seção da saída padrão é uma visão geral do tempo gasto nas diferentes fases de build:

INFO: Profile created on Tue Jun 16 08:59:40 CEST 2020, build ID: 0589419c-738b-4676-a374-18f7bbc7ac23, output base: /home/johndoe/.cache/bazel/_bazel_johndoe/d8eb7a85967b22409442664d380222c0

=== PHASE SUMMARY INFORMATION ===

Total launch phase time         1.070 s   12.95%
Total init phase time           0.299 s    3.62%
Total loading phase time        0.878 s   10.64%
Total analysis phase time       1.319 s   15.98%
Total preparation phase time    0.047 s    0.57%
Total execution phase time      4.629 s   56.05%
Total finish phase time         0.014 s    0.18%
------------------------------------------------
Total run time                  8.260 s  100.00%

Critical path (4.245 s):
       Time Percentage   Description
    8.85 ms    0.21%   _Ccompiler_Udeps for @local_config_cc// compiler_deps
    3.839 s   90.44%   action 'Compiling external/com_google_protobuf/src/google/protobuf/compiler/php/php_generator.cc [for host]'
     270 ms    6.36%   action 'Linking external/com_google_protobuf/protoc [for host]'
    0.25 ms    0.01%   runfiles for @com_google_protobuf// protoc
     126 ms    2.97%   action 'ProtoCompile external/com_google_protobuf/python/google/protobuf/compiler/plugin_pb2.py'
    0.96 ms    0.02%   runfiles for //tools/aquery_differ aquery_differ

Criação de perfil de memória

O Bazel vem com um Memory Profiler integrado para ajudar você a verificar os erros e uso de memória. Se houver um problema, você pode despejar a heap para encontrar os linha de código exata que está causando o problema.

Como ativar o rastreamento de memória

Transmita estas duas sinalizações de inicialização para todas as chamadas do Bazel:

  STARTUP_FLAGS=\
  --host_jvm_args=-javaagent:$(BAZEL)/third_party/allocation_instrumenter/java-allocation-instrumenter-3.3.0.jar \
  --host_jvm_args=-DRULE_MEMORY_TRACKER=1

Eles iniciam o servidor no modo de rastreamento de memória. Se você esquecer isso por uma invocação do Bazel que o servidor vai reiniciar e você terá que recomeçar.

Como usar o rastreador de memória

Por exemplo, observe a foo de destino e veja o que ela faz. Apenas para executar a análise e não executar a fase de execução do build, adicione o sinalização --nobuild.

$ bazel $(STARTUP_FLAGS) build --nobuild //foo:foo

Em seguida, confira quanta memória a instância inteira do Bazel consome:

$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc
> 2594MB

Separe por classe de regra usando bazel dump --rules:

$ bazel $(STARTUP_FLAGS) dump --rules
>

RULE                                 COUNT     ACTIONS          BYTES         EACH
genrule                             33,762      33,801    291,538,824        8,635
config_setting                      25,374           0     24,897,336          981
filegroup                           25,369      25,369     97,496,272        3,843
cc_library                           5,372      73,235    182,214,456       33,919
proto_library                        4,140     110,409    186,776,864       45,115
android_library                      2,621      36,921    218,504,848       83,366
java_library                         2,371      12,459     38,841,000       16,381
_gen_source                            719       2,157      9,195,312       12,789
_check_proto_library_deps              719         668      1,835,288        2,552
... (more output)

Confira para onde a memória está indo produzindo um arquivo pprof. usando bazel dump --skylark_memory:

$ bazel $(STARTUP_FLAGS) dump --skylark_memory=$HOME/prof.gz
> Dumping Starlark heap to: /usr/local/google/home/$USER/prof.gz

Use a ferramenta pprof para investigar o heap. Um bom ponto de partida é receber um gráfico de chama usando pprof -flame $HOME/prof.gz.

Acesse o pprof em https://github.com/google/pprof.

Receba um despejo de texto dos melhores sites de chamadas anotados com as linhas:

$ pprof -text -lines $HOME/prof.gz
>
      flat  flat%   sum%        cum   cum%
  146.11MB 19.64% 19.64%   146.11MB 19.64%  android_library <native>:-1
  113.02MB 15.19% 34.83%   113.02MB 15.19%  genrule <native>:-1
   74.11MB  9.96% 44.80%    74.11MB  9.96%  glob <native>:-1
   55.98MB  7.53% 52.32%    55.98MB  7.53%  filegroup <native>:-1
   53.44MB  7.18% 59.51%    53.44MB  7.18%  sh_test <native>:-1
   26.55MB  3.57% 63.07%    26.55MB  3.57%  _generate_foo_files /foo/tc/tc.bzl:491
   26.01MB  3.50% 66.57%    26.01MB  3.50%  _build_foo_impl /foo/build_test.bzl:78
   22.01MB  2.96% 69.53%    22.01MB  2.96%  _build_foo_impl /foo/build_test.bzl:73
   ... (more output)