Como otimizar o desempenho

<ph-0-0>

Ao escrever regras, o problema de desempenho mais comum é transferir ou copiar dados acumulados das dependências. Quando agregadas em toda a versão, essas operações podem facilmente ocupar espaço ou tempo O(N^2). Para evitar isso, é fundamental entender como usar os recursos de forma eficaz.

Isso pode ser difícil de acertar, então o Bazel também oferece um Memory Profiler que ajuda você a encontrar possíveis erros. Cuidado: o custo de criar uma regra ineficiente pode não ser evidente até que esteja em uso generalizado.

Usar depssets

Sempre que você estiver adicionando informações de dependências de regras, use depsets. Use apenas listas simples ou dicts para publicar informações localmente à 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, esse problema só piorará.

Confira um exemplo de implementação de regra que usa depssets corretamente para publicar informações transitivas. Observe que não há problema em publicar informações de regra local usando listas, se você quiser, já que 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()

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

Um equívoco comum é achar que é possível nivelar os descartes livremente se você fizer isso apenas em destinos de nível superior, como uma regra <xx>_binary, já que o custo não será acumulado em cada nível do grafo de build. Mas ele 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 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 detecções com aninhamento muito profundo, que têm 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 de uma 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, você precisa usar ctx.actions.args(). Isso adia a expansão de quaisquer dependências para a fase de execução.

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

Aqui estão alguns truques:

  • Transmita depsets e listas diretamente como argumentos, em vez de nivelá-los por conta própria. 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 ver se alguma delas é adequada.

  • Você está transmitindo File#path como argumentos? Não é necessário. Qualquer arquivo é automaticamente transformado no caminho dele, 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 será compartilhada entre todas as instâncias da regra.

  • Se os argumentos forem muito longos para a linha de comando, um objeto ctx.actions.args() poderá ser gravado condicionalmente ou incondicionalmente em um arquivo de parâmetro usando ctx.actions.args#use_param_file. Isso é feito nos bastidores, quando a ação é executada. Caso seja necessário controlar explicitamente o arquivo de parâmetros, grave-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
  # 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 se esqueça de que o campo inputs aceita uma dependência. Use-o sempre que as entradas forem coletadas 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 um sinal SIGQUIT (kill -3 $(bazel info server_pid)) para o Bazel para receber um despejo de linha de execução no arquivo $(bazel info output_base)/server/jvm.out.

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

Criação de perfis de desempenho

O perfil de rastreamento JSON pode ser muito útil para entender rapidamente em que o Bazel passou tempo durante a invocação.

A flag --experimental_command_profile pode ser usada para capturar perfis do Java Flight Recorder de vários tipos (tempo de CPU, tempo decorrido, alocações de memória e contenção de bloqueio).

A sinalização --starlark_cpu_profile pode ser usada para criar um perfil pprof do uso da CPU por todas as linhas de execução do Starlark.

Criação de perfil de memória

O Bazel vem com um Memory Profiler integrado que pode ajudar você a verificar o uso da memória da sua regra. Se houver um problema, você poderá despejar o heap para encontrar a 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:<path to 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 esses dados até mesmo uma 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, observe a foo de destino e veja o que ela faz. Para executar apenas a análise e não a fase de execução do build, adicione a 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 é gerar 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)