Ao escrever regras, o problema de desempenho mais comum é percorrer ou copiar dados acumulados de dependências. Quando agregadas em toda a build, essas operações podem facilmente levar tempo ou espaço O(N^2). Para evitar isso, é fundamental entender como usar depsets de maneira eficaz.
Isso pode ser difícil de fazer corretamente. Por isso, o Bazel também oferece 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 ela seja amplamente usada.
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 na regra atual.
Um depset 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 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 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. É possível publicar informações locais da regra usando listas, se quiser, porque 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 de visão geral do depset para mais informações.
Evitar chamar depset.to_list()
É possível forçar um depset para uma lista simples usando
to_list(), mas isso geralmente resulta em um custo O(N^2)
. Se possível, evite qualquer nivelamento de depsets, exceto para fins de depuração
Um equívoco comum é que você pode nivelar 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 de 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. Por 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)
Isso pode ser reduzido usando uma compreensão de 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 depset para a fase de execução.
Além de ser estritamente mais rápido, isso reduz o consumo de memória de suas regras, às vezes em 90% ou mais.
Confira algumas dicas:
Transmita depsets e listas diretamente como argumentos, em vez de nivelá-los. Eles serão expandidos por
ctx.actions.args(). Se você precisar de transformações no conteúdo do depset, consulte ctx.actions.args#add para saber se algo se encaixa.Você está transmitindo
File#pathcomo argumentos? Não é necessário. Qualquer arquivo é convertido automaticamente no caminho dele, 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 usandoctx.actions.args#use_param_file. Isso é feito nos bastidores quando a ação é executada. Se você precisar controlar explicitamente o arquivo de parâmetros, poderá 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(file, format="--foo=%s")
# Bad, makes a giant string of a whole depset
args.add(" ".join(["-I%s" % file.short_path for file in files.to_list()])
# 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 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 depset. 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
...
)
Hanging
Se o Bazel parecer travado, pressione Ctrl-\ ou envie
um sinal SIGQUIT (kill -3 $(bazel info server_pid)) para receber um despejo de thread
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
output_base diretório geralmente é o pai do bazel-<workspace>
link simbólico 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 gastou tempo durante a invocação.
A --experimental_command_profile
flag pode ser usada para capturar perfis do Java Flight Recorder de vários tipos
(tempo de CPU, tempo real, alocações de memória e contenção de bloqueio).
A --starlark_cpu_profile
flag pode ser usada para gravar 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 de memória da sua regra's memory use. Se houver um problema, você poderá despejar o heap para encontrar a linha de código exata que está causando o problema.
Ativar o rastreamento de memória
É necessário transmitir essas duas flags de inicialização para cada invocação do Bazel:
STARTUP_FLAGS=\
--host_jvm_args=-javaagent:<path to java-allocation-instrumenter-3.3.4.jar> \
--host_jvm_args=-DRULE_MEMORY_TRACKER=1
Elas iniciam o servidor no modo de rastreamento de memória. Se você esquecer essas flags para uma invocação do Bazel, o servidor será reiniciado e você terá que começar de novo.
Usar o Memory Tracker
Como exemplo, examine o destino foo e veja o que ele faz. Para executar apenas a
análise e não a fase de execução da build, adicione a
--nobuild flag.
$ 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 para onde a memória está indo, produza um pprof arquivo
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 pprof em https://github.com/google/pprof (em inglês).
Receba um despejo de texto dos sites de chamada 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)