Ao escrever regras, o problema de desempenho mais comum é percorrer ou copiar dados acumulados das dependências. Quando agregadas em todo o build, essas operações podem facilmente levar tempo O(N^2) ou espaço. Para evitar isso, é crucial entender como usar os depsets de forma eficaz.
Pode ser difícil acertar. Por isso, o Bazel também oferece um Memory Profiler que ajuda você a encontrar pontos em que pode ter cometido um erro. Cuidado: o custo de escrever uma regra ineficiente pode não ser evidente até que esteja em uso generalizado.
Usar depsets
Sempre que você agrupar informações de dependências de regras, use depsets. Use apenas listas simples ou dicts para publicar informações locais para a regra atual.
Um depset 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 as desativações, 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ê consegue o seguinte:
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.
Veja 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 de visão geral do desligamento para mais informações.
Evitar chamar depset.to_list()
É possível forçar um conjunto de dados a uma lista simples usando to_list()
, mas isso geralmente resulta em custo O(N^2). Se possível, evite nivelar os depsets, exceto para fins de
depuração.
Um equívoco comum é que você pode nivelar livremente os conjuntos de dados 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 criação. 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 importar um projeto do ambiente de desenvolvimento integrado.
Reduzir o número de chamadas para depset
Chamar depset
dentro de uma repetição geralmente é um erro. Isso pode levar a degradações com
aninhamento muito profundo, que têm baixo desempenho. 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, reúna as dependências transitivas e mescle todas 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 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 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 depsets e listas diretamente como argumentos, em vez de nivelá-las. Eles serão expandidos pela
ctx.actions.args()
para você. Se você precisar de alguma transformação no conteúdo do depset, confira ctx.actions.args#add para ver se algo se encaixa na conta.Você está transmitindo
File#path
como argumentos? Não é necessário. Qualquer arquivo é automaticamente transformado no próprio 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 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 condicionalmente 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, é possível gravá-lo manualmente usandoctx.actions.write
.
Exemplos
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 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
...
)
De pendurar
Se o Bazel parecer travado, pressione Ctrl-\ ou envie
ao Bazel um sinal SIGQUIT
(kill -3 $(bazel info server_pid)
) 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 Bazel grava um perfil JSON em command.profile.gz
na base de saída por
padrão. É possível configurar o local com a sinalização --profile
, por exemplo, --profile=/tmp/profile.gz
. Os locais que terminam em .gz
são compactados com GZIP.
Para conferir os resultados, abra chrome://tracing
em uma guia do navegador Chrome, clique em
"Carregar" e escolha o arquivo de perfil (possivelmente compactado). Para resultados mais detalhados, clique nas caixas no canto inferior esquerdo.
Use os controles do teclado para navegar:
- Pressione
1
para usar o modo "Selecionar". Nesse modo, você pode selecionar caixas específicas para inspecionar os detalhes do evento (consulte o canto inferior esquerdo). Selecione vários eventos para receber um resumo e estatísticas agregadas. - Pressione
2
para usar o modo "movimentar". Em seguida, arraste o mouse para mover a visualização. Também é possível usara
/d
para mover para a esquerda/direita. - Pressione
3
para usar o modo "zoom". Em seguida, arraste o mouse para aplicar zoom. Também é possível usarw
/s
para aumentar/diminuir o zoom. - Pressione
4
para usar o modo "tempo" em que é possível medir a distância entre dois eventos. - Pressione
?
para saber mais sobre todos os controles.
Informações do 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. Precisa 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 secundárias e importantes da coleta de lixo (GC, na sigla em inglês).
Outras linhas representam linhas de execução do Bazel e mostram todos os eventos delas.
Problemas comuns de desempenho
Ao analisar os perfis de performance, procure:
- Fase de análise mais lenta que o esperado (
runAnalysisPhase
), especialmente em builds incrementais. Isso pode ser um sinal de uma implementação ruim da regra, por exemplo, uma que nivela os depsets. O carregamento de pacotes pode ficar lento por uma quantidade excessiva de destinos, macros complexas ou globs recursivos. - Ações lentas individuais, especialmente aquelas no caminho crítico. Talvez seja
possível dividir ações grandes em várias menores ou reduzir o
conjunto de dependências (transitivas) para acelerá-las. Verifique também se há um
alto não
PROCESS_TIME
incomum (comoREMOTE_SETUP
ouFETCH
). - Os gargalos, que são um pequeno número de linhas de execução, estão ocupados enquanto todas as outras estão inativas / aguardando o resultado (veja cerca de 15s a 30 segundos na captura de tela acima). Para otimizar isso provavelmente vai 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 do 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, como o ID
e a data da invocação do Bazel.
Exemplos
{
"otherData": {
"build_id": "101bff9a-7243-4c1a-8503-9dc6ae4c3b05",
"date": "Tue Jun 16 08:30:21 CEST 2020",
"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 vão ser mesclados se forem muito curtos e próximos
entre si. Transmita --noslim_json_profile
se quiser
evitar a mesclagem de eventos.
Consulte também a Especificação de formato do evento de rastreamento do Chrome.
analisar-perfil
Esse método de criação de perfil consiste em duas etapas. Primeiro, é necessário executar seu
build/teste com a sinalização --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 resumidas de análise para o arquivo de dados do 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 perfis 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. Se houver um problema, você poderá despejar o heap para encontrar a linha exata de código que está causando o problema.
Como ativar o rastreamento de memória
Transmita estas duas sinalizações de inicialização para cada invocação 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
Elas iniciam o servidor no modo de rastreamento de memória. Se você esquecer esses caracteres em pelo menos uma invocação do Bazel, o servidor será reiniciado e você vai precisar começar de novo.
Uso do tracker 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 da versão, adicione a
flag --nobuild
.
$ bazel $(STARTUP_FLAGS) build --nobuild //foo:foo
Em seguida, veja a quantidade de memória que 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)
Confira para onde a memória está 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 pprof
em https://github.com/google/pprof.
Crie um despejo de texto dos melhores sites de ligações 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)