Ao escrever regras, o erro de desempenho mais comum é transferir ou copiar dados acumulados das dependências. Quando agregadas em todo o build, essas operações podem facilmente ocupar O(N^2) tempo ou espaço. Para evitar isso, é fundamental entender como usar os depsets de maneira eficaz.
Isso pode ser difícil de acertar, então o Bazel também oferece um Memory Profiler que ajuda você a encontrar pontos em que você pode ter cometido um erro. Cuidado: o custo de escrever uma regra ineficiente pode não ser evidente até que seja amplamente usado.
Usar depsets
Sempre que você incluir informações de dependências de regras, use depsets. Use apenas listas simples ou dicionários para publicar informações locais para a regra atual.
Um conjunto de dados representa informações como um gráfico aninhado que permite o compartilhamento.
Considere o gráfico a seguir:
C -> B -> A
D ---^
Cada nó publica uma única string. Com depsets, os dados têm a seguinte 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 as listas, você conseguiria 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ó vai piorar.
Este é 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 de regra usando listas, se você quiser, 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 do depset para obter mais informações.
Evitar chamar depset.to_list()
Você pode forçar um conjunto de dados a uma lista simples usando to_list()
, mas isso geralmente resulta no custo O(N^2). Se possível, evite achatamento de depsets, exceto para fins de
depuração.
Um equívoco comum é achar que os depsets podem ser nivelados livremente se você 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 ele ainda é O(N^2) quando
você cria um conjunto de destinos com dependências sobrepostas. Isso acontece ao
criar os testes //foo/tests/...
ou ao importar um projeto do ambiente de desenvolvimento integrado.
Reduzir o número de chamadas para depset
Chamar depset
dentro de um loop costuma ser um erro. Isso pode causar 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 facilmente substituído. 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)
Isso pode ser reduzido com uma 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 quaisquer depsets para a fase de execução.
Além de ser estritamente mais rápido, isso reduzirá o consumo de memória das suas regras, às vezes em 90% ou mais.
Aqui estão algumas dicas:
Transmita exclusões e listas diretamente como argumentos, em vez de nivelá-las. Ela será expandida por
ctx.actions.args()
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 no caso.Você está transmitindo
File#path
como argumentos? Não é necessário. Qualquer arquivo é transformado automaticamente no próprio caminho, adiado para o tempo de expansão.Evite criar strings concatenando-as. O melhor argumento de string é uma constante, já que a memória dele 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 condicional ou incondicionalmente em um arquivo de parâmetro usandoctx.actions.args#use_param_file
. Isso é feito em segundo plano quando a ação é executada. Se você precisa controlar explicitamente o arquivo de parâmetros, faça isso manualmente usandoctx.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 um depset. 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
...
)
Deslocamento
Se o Bazel parecer inativo, pressione Ctrl-\ ou envie
um sinal SIGQUIT
(kill -3 $(bazel info server_pid)
) para o Bazel para receber um despejo
de linhas 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 desativado, 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 flag --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 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
É preciso transmitir estas duas sinalizações de inicialização para todas as invocações 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 em pelo menos uma invocação do Bazel, o servidor será reiniciado e você vai precisar começar de novo.
Uso do rastreador de memória
Por exemplo, observe a foo
de destino e observe o que ela 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 toda a instância do Bazel consome:
$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc
> 2594MB
Divida 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)
Veja para onde a memória está indo ao produzir 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 é criar um gráfico de chama usando pprof -flame $HOME/prof.gz
.
Acesse o pprof
em https://github.com/google/pprof (link em inglês).
Receba um despejo de texto dos melhores sites de chamadas com anotações 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)