Nesta página, você verá como usar workers permanentes, os benefícios, os requisitos e como os workers afetam o sandbox.
Um worker permanente é um processo de longa duração iniciado pelo servidor do Bazel, que
funciona como um wrapper em torno da ferramenta real (geralmente um compilador) ou
é a própria ferramenta. Para aproveitar os workers persistentes, a ferramenta precisa ser compatível com uma sequência de compilações, e o wrapper precisa fazer a conversão entre a API da ferramenta e o formato de solicitação/resposta descrito abaixo. O mesmo
worker pode ser chamado com e sem a flag --persistent_worker
na
mesma build e é responsável por iniciar e iniciar adequadamente com a
ferramenta, bem como por desligar os workers na saída. Cada instância de worker é atribuída, mas não tem acesso root a um diretório de trabalho separado em <outputBase>/bazel-workers
.
O uso de workers permanentes é uma estratégia de execução que diminui a sobrecarga de inicialização, permite mais compilação JIT e permite o armazenamento em cache, por exemplo, das árvores de sintaxe abstratas na execução da ação. Essa estratégia atinge essas melhorias enviando várias solicitações para um processo de longa execução.
Os workers permanentes são implementados em várias linguagens, incluindo Java, Scala, Kotlin e muito mais.
Os programas que usam ambiente de execução NodeJS podem usar a biblioteca auxiliar @bazel/worker para implementar o protocolo do worker.
Como usar workers permanentes
O Bazel 0.27 e versões mais recentes usa workers persistentes por padrão na execução de builds, embora a execução remota tenha precedência. Para ações que não dão suporte a workers persistentes,
o Bazel volta a iniciar uma instância de ferramenta para cada ação. Para configurar explicitamente sua versão para usar workers permanentes, defina a estratégia worker
para as mnemônicas da ferramenta aplicáveis. Como prática recomendada, este exemplo inclui a especificação de local
como
substituto da estratégia worker
:
bazel build //my:target --strategy=Javac=worker,local
Usar a estratégia de workers em vez da estratégia local pode aumentar significativamente a velocidade de compilação, dependendo da implementação. Para Java, os builds podem ser de 2 a 4 vezes mais rápidos, às vezes mais para compilação incremental. A compilação do Bazel é cerca de 2,5 vezes mais rápida com workers. Para mais detalhes, consulte a seção "Como escolher o número de workers".
Se você também tiver um ambiente de build remoto que corresponda ao ambiente
de build local, será possível usar a
estratégia dinâmica experimental,
que compete entre uma execução remota e uma de worker. Para ativar a estratégia dinâmica, transmita a sinalização --experimental_spawn_scheduler. Essa estratégia ativa workers automaticamente. Portanto, não é necessário especificar a estratégia worker
, mas ainda é possível usar local
ou sandboxed
como substitutos.
Como escolher o número de workers
O número padrão de instâncias de worker por mnemônico é 4, mas pode ser ajustado com a sinalização worker_max_instances
. Há uma compensação entre fazer bom uso das CPUs disponíveis e a
quantidade de compilação JIT e ocorrências em cache que você recebe. Com mais workers, mais destinos pagarão os custos de inicialização da execução de código não ajustado e uso de caches frios. Se você tiver um pequeno número de destinos para criar, um único worker poderá oferecer
a melhor compensação entre velocidade de compilação e uso de recursos. Por exemplo,
consulte o problema 8586.
A sinalização worker_max_instances
define o número máximo de instâncias de worker por
mnemônico e conjunto de sinalizações (veja abaixo). Portanto, em um sistema misto, você acaba usando
muita memória se mantiver o valor padrão. Para builds incrementais, o benefício de várias instâncias de worker é ainda menor.
Este gráfico mostra os tempos de compilação do zero (destino
//src:bazel
) em uma estação de trabalho Linux com hiperthread de 6 núcleos, Intel Xeon 3,5 GHz,
com 64 GB de RAM. Para cada configuração de worker, cinco builds limpos são executadas
e a média das quatro últimas é obtida.
Figura 1. Gráfico de melhorias no desempenho de builds limpos.
Para essa configuração, dois workers oferecem a compilação mais rápida, embora com uma melhoria de apenas 14% em comparação com um worker. Um worker é uma boa opção se você quer usar menos memória.
A compilação incremental costuma ser ainda mais vantajosa. Builds limpos são relativamente raros, mas mudar um único arquivo entre compilações é comum, especialmente no desenvolvimento voltado para testes. O exemplo acima também tem algumas ações de empacotamento não Java que podem ofuscar o tempo de compilação incremental.
A recompilação das origens Java somente
(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
)
após a alteração de uma constante de string interna em
AbstractContainerizingSandboxedSpawn.java
oferece uma aceleração de três vezes (média de 20 builds incrementais com um build de aquecimento
descartado):
Figura 2. Gráfico de melhorias no desempenho de builds incrementais.
A aceleração depende da mudança que está sendo feita. A aceleração de um fator 6 é medida na situação acima, quando uma constante usada com frequência é alterada.
Como modificar workers permanentes
É possível transmitir a sinalização
--worker_extra_flag
para especificar sinalizações de inicialização para workers, codificadas por mnemônicos. Por exemplo,
transmitir --worker_extra_flag=javac=--debug
ativa a depuração apenas para Javac.
Somente uma flag de worker pode ser definida por uso dessa flag e apenas para um mnemônico.
Os workers não são criados apenas separadamente para cada mnemônico, mas também para
variações nas flags de inicialização. Cada combinação de sinalizações mnemônicas e de inicialização é combinada em um WorkerKey
, e para cada WorkerKey
até worker_max_instances
workers podem ser criados. Consulte a próxima seção para saber como a
configuração da ação também pode especificar sinalizações de configuração.
Use a flag
--high_priority_workers
para especificar um mnemônico que será executado em preferência aos mnemônicos de prioridade
normal. Isso pode ajudar a priorizar ações que estão sempre no caminho
crítico. Se houver dois ou mais workers de alta prioridade executando solicitações, todos
os outros workers serão impedidos de serem executados. Essa sinalização pode ser usada várias vezes.
Transmitir a sinalização
--worker_sandboxing
faz com que cada solicitação de worker use um diretório de sandbox separado para todas as
entradas. A configuração do sandbox leva mais tempo,
especialmente no macOS, mas oferece uma garantia de melhor correção.
A sinalização
--worker_quit_after_build
é útil principalmente para depuração e criação de perfis. Essa sinalização força todos os workers
a sair quando uma compilação é concluída. Também é possível transmitir
--worker_verbose
para
ter mais resultados sobre o que os workers estão fazendo. Essa sinalização é refletida no campo
verbosity
em WorkRequest
, permitindo que as implementações do worker também sejam
mais detalhadas.
Os workers armazenam os registros no diretório <outputBase>/bazel-workers
, por
exemplo
/tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
.
O nome do arquivo inclui o ID do worker e o mnemônico. Como pode haver mais
de um WorkerKey
por mnemônico, você pode ver mais de arquivos de registro worker_max_instances
para um determinado mnemônico.
Para builds do Android, consulte detalhes na página de desempenho de build do Android.
Como implementar workers persistentes
Consulte a página Como criar workers permanentes para mais informações sobre como criar um worker.
Este exemplo mostra uma configuração do Starlark para um worker que usa JSON:
args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
ctx.actions.write(
output = args_file,
content = "\n".join(["-g", "-source", "1.5"] + ctx.files.srcs),
)
ctx.actions.run(
mnemonic = "SomeCompiler",
executable = "bin/some_compiler_wrapper",
inputs = inputs,
outputs = outputs,
arguments = [ "-max_mem=4G", "@%s" % args_file.path],
execution_requirements = {
"supports-workers" : "1", "requires-worker-protocol" : "json" }
)
Com essa definição, o primeiro uso dessa ação começa com a execução
da linha de comando /bin/some_compiler -max_mem=4G --persistent_worker
. Uma solicitação
para compilar Foo.java
teria esta aparência:
OBSERVAÇÃO: embora a especificação do buffer de protocolo use "snake case" (request_id
),
o protocolo JSON usa "camel case" (requestId
). Neste documento, usaremos
a concatenação nos exemplos do JSON, mas usaremos o snake case ao falar sobre o campo,
independente do protocolo.
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
O worker recebe isso em stdin
no formato JSON delimitado por nova linha (porque
requires-worker-protocol
está definido como JSON). Em seguida, o worker executa a ação
e envia um WorkResponse
formatado em JSON para o Bazel no stdout. Em seguida,
o Bazel analisa essa resposta e a converte manualmente em um proto WorkResponse
. Para
se comunicar com o worker associado usando protobuf codificado em binário em vez de
JSON, requires-worker-protocol
seria definido como proto
, desta forma:
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
Se você não incluir requires-worker-protocol
nos requisitos de execução,
o Bazel vai padronizar a comunicação do worker para usar o protobuf.
O Bazel deriva o WorkerKey
das flags mnemônicas e compartilhadas. Portanto, se essa
configuração permitir a mudança do parâmetro max_mem
, um worker separado será
gerado para cada valor usado. Isso pode levar ao consumo excessivo de memória se
muitas variações forem usadas.
No momento, cada worker só pode processar uma solicitação por vez. O recurso experimental workers multiplex permite o uso de várias linhas de execução, se a ferramenta subjacente tiver várias linhas de execução e o wrapper estiver configurado para entender isso.
Neste repositório do GitHub (link em inglês), você pode ver exemplos de wrappers de worker escritos em Java e Python. Se você estiver trabalhando em JavaScript ou TypeScript, o pacote@bazel/worker e o exemplo de worker do nodejs (em inglês) podem ser úteis.
Como os workers afetam o sandbox?
O uso da estratégia worker
por padrão não executa a ação em um sandbox, semelhante à estratégia local
. Defina a sinalização --worker_sandboxing
para que todos os workers sejam executados em sandboxes, garantindo que cada execução da ferramenta encontre apenas os arquivos de entrada necessários. A ferramenta
ainda pode vazar informações entre solicitações internamente, por exemplo, por um
cache. O uso da estratégia dynamic
exige que os workers estejam no sandbox.
Para permitir o uso correto de caches do compilador com workers, um resumo é transmitido com cada arquivo de entrada. Assim, o compilador ou o wrapper pode verificar se a entrada ainda é válida sem precisar ler o arquivo.
Mesmo ao usar resumos de entrada para proteger contra armazenamento em cache indesejado, os workers no sandbox oferecem um sandbox menos rigoroso do que um sandbox puro, porque a ferramenta pode manter outro estado interno que foi afetado por solicitações anteriores.
Os workers multiplex só podem ser colocados no sandbox se for compatível com a implementação do worker,
e esse sandbox precisa ser ativado separadamente com a
flag --experimental_worker_multiplex_sandboxing
. Veja mais detalhes no
documento de design.
Leia mais
Para mais informações sobre workers permanentes, consulte:
- Postagem do blog sobre workers permanentes originais (em inglês)
- Descrição da implementação do Haskell
- Postagem do blog de Mike Morearty
- Desenvolvimento de front-end com Bazel: Angular/TypeScript e Persistent Workers com Asana
- Estratégias do Bazel explicadas
- Discussão sobre a estratégia informacional na lista de e-mails do bazel-discuss