Esta página aborda como usar workers permanentes, os benefícios, os requisitos e como os workers afetam o uso de 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 (normalmente um compilador) ou é
a ferramenta em si. Para se beneficiar de workers permanentes, a ferramenta precisa
oferecer suporte à execução de uma sequência de compilações, e o wrapper precisa fazer a traduçã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 no
mesmo build e é responsável por iniciar e se comunicar adequadamente com a
ferramenta, além de encerrar os workers na saída. Cada instância de worker é atribuída
(mas não chrootada) 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 possibilita o armazenamento em cache de, por exemplo, as árvores de sintaxe abstrata na execução da ação. Essa estratégia alcança essas melhorias enviando várias solicitações a um processo de longa duração.
Os workers permanentes são implementados para várias linguagens, incluindo Java, Scala, Kotlin, e muito mais.
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 0.27 e versões mais recentes
usam workers permanentes por padrão ao executar builds, embora a execução
remota tenha precedência. Para ações que não oferecem suporte a workers permanentes,
o Bazel volta a iniciar uma instância de ferramenta para cada ação. É possível definir explicitamente o build para usar workers permanentes definindo a worker
estratégia para os mnemônicos de ferramentas aplicáveis. Como prática recomendada, este exemplo inclui a especificação de local como um
substituto para a worker estratégia:
bazel build //my:target --strategy=Javac=worker,localO uso da 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, poderá usar a estratégia
experimental
dinâmica,
que executa uma execução remota e uma execução de worker. Para ativar a estratégia dinâmica, transmita a
--experimental_spawn_scheduler
flag. 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ônico é 4, mas pode ser ajustado
com a
worker_max_instances
flag. Há uma compensação entre o bom uso das CPUs disponíveis e a
quantidade de compilação JIT e acertos de cache que você recebe. Com mais workers, mais
destinos vão pagar os custos de inicialização da execução de código não JITted e atingir caches frios. Se você tiver um pequeno número de destinos para criar, um único worker poderá oferecer
a melhor compensação entre a velocidade de compilação e o uso de recursos. Por exemplo,
consulte o problema nº 8586.
A flag worker_max_instances define o número máximo de instâncias de worker por
mnemônico e conjunto de flags (consulte abaixo). Portanto, 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 zero para o Bazel (destino
//src:bazel) em uma estação de trabalho Linux Intel Xeon 3,5 GHz com 6 núcleos
e 64 GB de RAM. Para cada configuração de worker, cinco builds limpos são executados e
a média dos últimos quatro é considerada.

Figura 1. Gráfico de melhorias de performance de builds limpos.
Para essa configuração, dois workers oferecem a compilação mais rápida, embora com 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 normalmente se beneficia ainda mais. Os builds limpos são relativamente raros, mas a mudança de um único arquivo entre compilações é comum, em particular no desenvolvimento orientado a 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 fontes Java apenas
(//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 aceleração de 3 vezes (média de 20 builds incrementais com um build de preparação
descartado):

Figura 2. Gráfico de melhorias de performance de builds incrementais.
A aceleração depende da mudança 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
--worker_extra_flag
flag para especificar flags de inicialização para workers, com chave por mnemônico. Por exemplo,
transmitir --worker_extra_flag=javac=--debug ativa a depuração apenas para Javac.
Apenas uma flag de worker pode ser definida por uso dessa flag e apenas para um mnemônico.
Os workers não são criados separadamente para cada mnemônico, mas também para
variações nas flags de inicialização. Cada combinação de mnemônico e flags 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 saber como a
configuração da ação também pode especificar flags de configuração.
É possível usar a
--high_priority_workers
flag para especificar um mnemônico que precisa ser executado de 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 flag pode ser usada várias vezes.
A transmissão da
--worker_sandboxing
flag faz com que cada solicitação de worker use um diretório de sandbox separado para todas as suas
entradas. A configuração do sandbox leva um tempo extra,
especialmente no macOS, mas oferece uma garantia de correção melhor.
A flag
--worker_quit_after_build
é útil principalmente para depuração e criação de perfil. Essa flag força todos os workers
a serem encerrados quando um build é concluído. Também é possível transmitir
--worker_verbose para
receber mais informações sobre o que os workers estão fazendo. Essa flag é refletida no
verbosity campo em WorkRequest, permitindo que as 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 ID do worker e o mnemônico. Como pode haver mais
de um WorkerKey por mnemônico, você pode encontrar mais de worker_max_instances
arquivos de registro para um determinado mnemônico.
Para builds do Android, consulte os detalhes na página Performance do build do Android.
Como implementar workers permanentes
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çaria com a execução
da linha de comando /bin/some_compiler -max_mem=4G --persistent_worker. Uma solicitação
para compilar Foo.java seria assim:
OBSERVAÇÃO: embora a especificação do buffer de protocolo use "snake case" (request_id),
o protocolo JSON usa "camel case" (requestId). Neste documento, vamos usar
camel case nos exemplos JSON, mas snake case ao falar sobre o campo,
independentemente 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 formatado em JSON WorkResponse para o Bazel no stdout. O Bazel então
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, assim:
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 usar o protobuf como padrão para a comunicação do worker.
O Bazel deriva o WorkerKey do mnemônico e das flags compartilhadas. Portanto, se esta
configuração permitisse a mudança do parâmetro max_mem, um worker separado seria
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 de workers multiplexados permite o uso de várias linhas de execução, se a ferramenta subjacente for multithread e o wrapper estiver configurado para entender isso.
Neste repositório do GitHub, você pode conferir 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 poderão ser úteis.
Como os workers afetam o uso de sandbox?
O uso da estratégia worker por padrão não executa a ação em um
sandbox, semelhante à estratégia local. É possível definir a
--worker_sandboxing flag para executar todos os workers em sandboxes, garantindo que cada
execução da ferramenta só veja os arquivos de entrada que ela precisa 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 em sandbox.
Para permitir o uso correto de caches de compilador com workers, um resumo é transmitido junto 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 os resumos de entrada para proteger contra o armazenamento em cache indesejado, os workers em sandbox oferecem um uso de sandbox menos rigoroso do que um sandbox puro, porque a ferramenta pode manter outro estado interno afetado por solicitações anteriores.
Os workers multiplexados só podem ser colocados em sandbox se a implementação do worker oferecer suporte a ele,
e esse uso de sandbox precisa ser ativado separadamente com a
--experimental_worker_multiplex_sandboxing flag. Confira mais detalhes no
documento de design (em inglês).
Leitura adicional
Para mais informações sobre workers permanentes, consulte:
- Postagem original do blog sobre workers permanentes
- Descrição da implementação do Haskell (em inglês)
- Postagem do blog de Mike Morearty (em inglês)
- Desenvolvimento de front-end com o Bazel: Angular/TypeScript e workers permanentes com o Asana
- Estratégias do Bazel explicadas (em inglês)
- Discussão informativa sobre a estratégia de worker na lista de e-mails bazel-discuss (em inglês)