Nesta página, explicamos 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 ao redor da ferramenta (normalmente um compilador) ou como a própria ferramenta. Para se beneficiar de 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 sinalização --persistent_worker
no
mesmo build e é responsável por iniciar e falar corretamente com a
ferramenta, além de desligar workers na saída. Cada instância de worker é atribuída
a um diretório de trabalho separado em
<outputBase>/bazel-workers
, mas não tem acesso a eles.
O uso de workers persistentes é uma estratégia de execução que diminui a sobrecarga de inicialização, permite mais compilação do JIT e permite o armazenamento em cache de, por exemplo, as árvores de sintaxe abstratas na execução da ação. Essa estratégia alcança essas melhorias enviando várias solicitações para um processo de longa duração.
Os workers permanentes são implementados em várias linguagens, como Java, Scala, Kotlin e muito mais.
Os programas que usam um ambiente de execução do NodeJS podem usar a biblioteca auxiliar @bazel/worker para implementar o protocolo de worker.
Como usar workers permanentes
O Bazel versão 0.27 e posterior usa workers permanentes por padrão ao executar versões, embora a execução remota tenha precedência. Para as ações que não são compatíveis com workers permanentes,
o Bazel volta a iniciar uma instância de ferramenta para cada ação. Você pode definir explicitamente
seu build para usar workers persistentes definindo a
estratégia worker
para a mneonomia
de ferramentas aplicável. 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, as versões podem ser de duas a quatro vezes mais rápidas, às vezes mais para compilação incremental. A compilação do Bazel é cerca de 2,5 vezes mais rápida com os 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 corresponde ao seu ambiente de compilação
local, use a estratégia dinâmica
experimental, que executa uma execução remota e uma execução de worker. Para ativar a estratégia dinâmica, transmita a sinalização --experimental_spawn_scheduler. Essa estratégia ativa automaticamente os workers. 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ônica é 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 hits de cache que você recebe. Com mais workers, mais
destinos pagarão os custos de inicialização da execução de código não Jitado e do armazenamento em cache
frio. Se você tiver um pequeno número de destinos a serem criados, um único worker poderá ter
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
conjunto mnemônico e de sinalização (veja abaixo). Assim, em um sistema misto, você pode acabar 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 Bazel (//src:bazel
de destino) em uma estação de trabalho Linux Intel Xeon 3.5 GHz com 6 núcleos com 64 GB de RAM. Para cada configuração de worker, cinco builds limpos são executados e
a média dos últimos quatro é usada.
Figura 1. Gráfico de melhorias de desempenho de builds limpos.
Para essa configuração, dois workers fornecem a compilação mais rápida, embora apenas 14% de melhoria em comparação com um worker. Um worker é uma boa opção se você quiser usar menos memória.
A compilação incremental costuma ser ainda mais benéfica. Os builds limpos são relativamente raros, mas alterar 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 somente de fontes Java
(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
)
após a mudança de uma constante de string interna em
AbstractContainerizingSandboxedSpawn.java
oferece uma velocidade três vezes maior (média de 20 builds incrementais com um build de aquecimento
descartado):
Figura 2. Gráfico de melhorias de desempenho de builds incrementais.
A aceleração depende da mudança que está sendo feita. Uma 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ônica. Por exemplo,
transmitir --worker_extra_flag=javac=--debug
ativa a depuração apenas para Javac.
Apenas uma sinalização de worker pode ser definida por uso dessa sinalização e apenas para uma mnemônica.
Os workers não são criados separadamente para cada mnemônico, mas também para
variações nas sinalizações de inicialização. Cada combinação de sinalizações mnemônicas e de inicialização
é combinada em uma WorkerKey
, e para cada WorkerKey
até
worker_max_instances
workers podem ser criados. Consulte a próxima seção para ver como a
configuração de ação também pode especificar sinalizações de configuração.
É possível usar a sinalização
--high_priority_workers
para especificar um mnemônico a ser executado, em vez da mnemônica de
prioridade normal. Isso ajuda a priorizar ações que estão sempre no caminho crítico. Se houver dois ou mais workers de alta prioridade executando solicitações, os demais 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 do sandbox separado para todas as
entradas. A configuração do sandbox demora um pouco mais,
especialmente no macOS, mas oferece uma garantia de correção melhor.
A sinalização
--worker_quit_after_build
é útil principalmente para depuração e criação de perfil. Essa sinalização força todos os workers a sair quando um build é concluído. Você também pode transmitir
--worker_verbose
para
saber mais sobre o que os workers estão fazendo. Essa sinalização é refletida no campo
verbosity
em WorkRequest
, permitindo que implementações de 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 código do worker e o mnemônico. Como pode haver mais de um
WorkerKey
por mnemônico, você pode ver mais de worker_max_instances
arquivos de registro para um determinado mnemônico.
Para versões do Android, consulte os detalhes na página "Desempenho da versão do Android".
Como implementar workers permanentes
Consulte as informações sobre como criar um worker na página Como criar workers permanentes.
Este exemplo mostra uma configuração 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çaria com a execução da linha de comando /bin/some_compiler -max_mem=4G --persistent_worker
. Uma solicitação para compilar Foo.java
ficaria assim:
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 usará como padrão a comunicação do worker para usar protobuf.
O Bazel deriva o WorkerKey
da sinalização mnemônica e compartilhada. Se essa
configuração permitir a alteração 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.
Atualmente, cada worker só pode processar uma solicitação por vez. O recurso experimental de trabalhadores multiplex permite usar 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, é possível 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 podem ser úteis.
Como os workers afetam o sandbox?
Usar a estratégia worker
por padrão não executa a ação em um
sandbox, semelhante à estratégia local
. É possível definir a sinalização
--worker_sandboxing
para executar todos os workers dentro dos sandboxes, garantindo que cada
execução da ferramenta veja apenas os arquivos de entrada que ela deveria ter. A ferramenta
ainda pode vazar informações entre solicitações internamente, por exemplo, por meio de um
cache. O uso da estratégia dynamic
exige que os workers sejam colocados 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 ter que ler o arquivo.
Mesmo ao usar as sínteses de entrada para proteger o 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ó poderão ser colocados no sandbox se a implementação do worker for compatível com ela, e esse sandbox precisa ser ativado separadamente com a sinalização --experimental_worker_multiplex_sandboxing
. Veja mais detalhes no
documento de design.
Leitura adicional
Para mais informações sobre workers permanentes, consulte:
- Postagem original do blog sobre trabalhadores permanentes
- Descrição da implementação de Haskell {: .external}
- Postagem do blog de Mike Morearty {: .external}
- Desenvolvimento de front-end com o Bazel: Angular/TypeScript e Persistent workers com Asana {: .external}
- Estratégias do Bazel explicadas {: .external}
- Discussão sobre estratégia de workers informativas na lista de e-mails bazel-discuss {: .external}