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 usandoctx.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 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
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 perfil de trace JSON pode ser muito útil para entender rapidamente no que o Bazel gastou durante a invocação.
O --experimental_command_profile
um sinalizador pode ser usado 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).
O --starlark_cpu_profile
O flag pode ser usado para criar um perfil pprof do uso de CPU por todas as linhas de execução Starlark.
Criação de perfil de memória
O Bazel vem com um Memory Profiler integrado que ajuda a verificar as regras 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:<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 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)