Como otimizar o desempenho

Reportar um problema Ver a fonte Nightly · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Ao escrever regras, o problema de desempenho mais comum é percorrer ou copiar dados acumulados de dependências. Quando agregadas em todo o build, essas operações podem facilmente levar tempo ou espaço O(N^2). Para evitar isso, é crucial entender como usar os depsets de forma eficaz.

Isso pode ser difícil de fazer corretamente, então o Bazel também oferece um criador de perfil de memória 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 ficar evidente até que ela seja usada amplamente.

Usar depsets

Sempre que você estiver acumulando informações de dependências de regras, use depsets. Use apenas listas ou dicionários simples para publicar informações locais da regra atual.

Um conjunto de dependências representa 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 depsets, os dados têm esta aparência:

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 isso:

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

Nesse caso, 'a' é mencionado quatro vezes. Com gráficos maiores, esse problema só vai piorar.

Confira um exemplo de implementação de regra que usa depsets corretamente para publicar informações transitivas. Não há problema em publicar informações locais da regra usando listas, já que isso 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 Visão geral de depsets para mais informações.

Evite ligar para depset.to_list()

É possível converter um depset em uma lista simples usando to_list(), mas isso geralmente resulta em um custo O(N^2). Se possível, evite o achatamento de conjuntos de dependências, exceto para fins de depuração.

Um equívoco comum é que você pode achatar depsets livremente se fizer isso apenas em destinos de nível superior, como uma regra <xx>_binary, já que o custo não é 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 ao criar seus testes //foo/tests/... ou ao importar um projeto do IDE.

Reduzir o número de chamadas para depset

Chamar depset dentro de um loop geralmente é um erro. Isso pode levar a depsets com aninhamento muito profundo, que têm um 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 os depsets transitivos e mescle todos de uma só vez:

transitive = []

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

x = depset(transitive = transitive)

Às vezes, isso pode ser reduzido usando uma compreensão de lista:

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

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

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

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

Confira algumas dicas:

  • Transmita depsets e listas diretamente como argumentos, em vez de achatar você mesmo. O ctx.actions.args() vai expandir essas informações para você. Se você precisar de transformações no conteúdo do conjunto de dados, consulte ctx.actions.args#add para ver se algo se encaixa.

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

  • Evite construir strings concatenando-as. O melhor argumento de string é uma constante, já que a memória dela será compartilhada entre todas as instâncias da sua regra.

  • Se os argumentos forem muito longos para a linha de comando, um objeto ctx.actions.args() poderá ser gravado condicional ou incondicionalmente em um arquivo de parâmetros usando ctx.actions.args#use_param_file. Isso é feito nos bastidores quando a ação é executada. Se você precisar controlar explicitamente o arquivo de parâmetros, escreva-o 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
  # I<t will ho>wever pass ["--foo", file path] to the acti<on comman>d line,
  # instead of ["--foo=file_path"]
  args.add(&qu<ot;--foo&>quot;, file)

  <# Use for>mat 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ções transitivas precisam ser depsets

Ao criar uma ação usando ctx.actions.run, não se esqueça de que o campo inputs aceita um conjunto de dependências. Use isso sempre que as entradas forem coletadas de dependências de maneira transitiva.

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

Pendurado

Se o Bazel parecer travado, pressione Ctrl-\ ou envie um sinal SIGQUIT para o Bazel (kill -3 $(bazel info server_pid)) para receber um despejo de encadeamento no arquivo $(bazel info output_base)/server/jvm.out.

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

Criação de perfis de desempenho

Por padrão, o Bazel grava um perfil JSON em command.profile.gz na base de saída. É possível configurar o local com a flag --profile, por exemplo --profile=/tmp/profile.gz. Os locais que terminam com .gz são compactados com GZIP.

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

Use estes controles do teclado para navegar:

  • Pressione 1 para o modo "selecionar". Nesse modo, é possível selecionar caixas específicas para inspecionar os detalhes do evento (veja no canto inferior esquerdo). Selecione vários eventos para receber um resumo e estatísticas agregadas.
  • Pressione 2 para o modo "panorâmica". Em seguida, arraste o mouse para mover a visualização. Também é possível usar a/d para mover para a esquerda/direita.
  • Pressione 3 para o modo "zoom". Em seguida, arraste o mouse para aplicar zoom. Também é possível usar w/s para aumentar/diminuir o zoom.
  • Pressione 4 para o modo "tempo", em que é possível medir a distância entre dois eventos.
  • Pressione ? para conhecer 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 nele para ver o valor real. Deve chegar ao valor de --jobs em builds limpos.
  • cpu counters: para cada segundo do build, mostra a quantidade de CPU usada pelo Bazel. Um valor de 1 equivale a um núcleo 100% ocupado.
  • Critical Path: mostra um bloco para cada ação no caminho crítico.
  • grpc-command-1: a linha de execução principal do Bazel. Útil para ter uma visão geral do que o Bazel está fazendo, por exemplo, "Launch Bazel", "evaluateTargetPatterns" e "runAnalysisPhase".
  • Service Thread: mostra pausas de coleta de lixo (GC) secundárias e principais.

Outras linhas representam linhas de execução do Bazel e mostram todos os eventos nessa linha.

Problemas comuns de desempenho

Ao analisar perfis de desempenho, procure:

  • Fase de análise mais lenta do que o esperado (runAnalysisPhase), principalmente em builds incrementais. Isso pode ser um sinal de uma implementação ruim da regra, por exemplo, uma que achata depsets. O carregamento de pacotes pode ser lento devido a uma quantidade excessiva de destinos, macros complexas ou globs recursivos.
  • Ações lentas individuais, principalmente aquelas no caminho crítico. É possível dividir ações grandes em várias menores ou reduzir o conjunto de dependências (transitivas) para acelerar o processo. Verifique também se há um número alto e incomum de não PROCESS_TIME (como REMOTE_SETUP ou FETCH).
  • Gargalos, ou seja, um pequeno número de linhas de execução está ocupado enquanto todas as outras estão ociosas / aguardando o resultado (veja por volta de 15 a 30 segundos na captura de tela acima). Para otimizar isso, provavelmente será necessário alterar as implementações de regras ou o próprio Bazel para introduzir mais paralelismo. Isso também pode acontecer quando há 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, por exemplo, 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 enumeração de ProfilerTask. Alguns eventos são mesclados se forem muito curtos e próximos uns dos outros. Transmita --noslim_json_profile se quiser evitar a mesclagem.

Consulte também a especificação do formato de evento de rastreamento do Chrome.

analyze-profile

Esse método de criação de perfil consiste em duas etapas. Primeiro, execute 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-processado e analisado pelo comando analyze-profile:

$ bazel analyze-profile /tmp/prof

Por padrão, ele imprime informações de análise resumida para o arquivo de dados de perfil especificado. Isso inclui estatísticas cumulativas para diferentes tipos de tarefas em cada fase de build 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 criador de perfil de memória integrado que pode ajudar você a verificar o uso de memória da sua regra. Se houver um problema, você poderá despejar o heap para encontrar a linha exata de código que está causando o problema.

Ativar o rastreamento de memória

Você precisa transmitir estas duas flags de inicialização para todas as invocações 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 em uma única invocação do Bazel, o servidor será reiniciado e você terá que começar de novo.

Como usar o rastreador de memória

Por exemplo, confira o destino foo e veja o que ele faz. Para executar apenas a análise e não a fase de execução do build, adicione a flag --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

Para detalhar por classe de regra, use 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)

Para saber onde a memória está indo, produza 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 é gerar um gráfico de chamas usando pprof -flame $HOME/prof.gz.

Baixe pprof em https://github.com/google/pprof.

Receba um despejo de texto dos sites de chamadas mais frequentes anotados com 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)