Nesta página, explicamos como usar workers persistentes, 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,
como um wrapper em torno da ferramenta real (geralmente um compilador), ou
a própria ferramenta. Para aproveitar os workers persistentes, a ferramenta precisa
suporte a uma sequência de compilações, e o wrapper precisa traduzir
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
mesmo build e é responsável por iniciar e comunicar adequadamente com os
além de desligar os workers na saída. Cada instância de worker é atribuída
(mas não enraizada) a um diretório de trabalho separado em
<outputBase>/bazel-workers
.
O uso de workers persistentes é uma estratégia de execução que diminui a sobrecarga de inicialização, permite mais compilação JIT e ativa o armazenamento em cache de, por exemplo, as árvores de sintaxe abstrata na execução de ações. Essa estratégia alcança essas melhorias enviando várias solicitações para um processo demorado.
Os workers permanentes são implementados em várias linguagens, incluindo Java, Scala, Kotlin e muito mais.
Os programas que usam um ambiente de execução NodeJS podem usar a biblioteca auxiliar @bazel/worker para implementar o protocolo do worker.
Como usar workers permanentes
Bazel versão 0.27 e mais recente
usa workers permanentes por padrão ao executar builds, embora
e a execução têm prioridade. 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
defina o build para usar workers permanentes definindo a propriedade worker
.
estratégia para a ferramenta aplicável
mnemônicas. Como prática recomendada, este exemplo inclui especificar local
como um
para a estratégia worker
:
bazel build //my:target --strategy=Javac=worker,local
Usar a estratégia workers em vez da estratégia local pode impulsionar a compilação. a velocidade significativamente, dependendo da implementação. Para Java, os builds podem ser de 2 a 4 vezes mais rápido e, às vezes, mais para compilação incremental. A compilação do Bazel é cerca de 2,5 vezes mais rápido com workers. Para mais detalhes, consulte a "Como escolher o número de workers" nesta seção.
Se você também tiver um ambiente de build remoto que corresponda ao seu ambiente de build
local, use 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 flag
--experimental_spawn_scheduler. Essa estratégia ativa os workers automaticamente, então não é preciso
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
flag
worker_max_instances
. Há uma compensação entre fazer bom uso das CPUs disponíveis e da
de compilação JIT e de ocorrências em cache. Com mais workers, mais
alvos vão pagar custos de inicialização para executar códigos não JIT e acessar caches
frios. Se você tiver um pequeno número de destinos para criar, um único worker pode oferecer
a melhor compensação entre a velocidade de compilação e o uso de recursos. Por exemplo,
consulte o problema 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). 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 workers é ainda menor.
Este gráfico mostra os tempos de compilação do Bazel 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 executados e
a média dos últimos quatro foi usada.
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 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 traz ainda mais benefícios. Builds limpos são relativamente raro, mas alterar um único arquivo entre compilações é comum, em especialmente no desenvolvimento orientado a testes. O exemplo acima também tem algumas ações de empacotamento que não são Java e que podem ofuscar o tempo de compilação incremental.
A recriação dos arquivos de origem do Java (//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
acelera o processo 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. Um aumento de velocidade de um fator 6 é medido na situação acima quando uma constante usada com frequência é alterada.
Como modificar workers permanentes
É possível transmitir a flag
--worker_extra_flag
para especificar flags de inicialização para workers, com chave mnemotécnica. Por exemplo:
transmitir --worker_extra_flag=javac=--debug
ativa a depuração apenas para Javac.
Só é possível definir uma flag de worker por uso e apenas para um mnemônico.
Os workers não são criados apenas separadamente para cada mnemônica, mas também para
variações nas sinalizações de inicialização. Cada combinação de funções mnemônicas e de inicialização
são combinadas em um WorkerKey
, e para cada WorkerKey
até
É possível criar worker_max_instances
workers. Consulte a próxima seção para saber como a
configuração da ação também pode especificar flags de configuração.
Use a flag
--high_priority_workers
para especificar uma mnemônica que precisa ser executada em vez de mnemônicas de prioridade
normal. Isso pode ajudar a priorizar ações que estão sempre no
caminho. Se houver dois ou mais workers de alta prioridade executando solicitações, todos
outros workers sejam impedidos de ser executados. Essa flag pode ser usada várias vezes.
Transmitir o valor-chave
--worker_sandboxing
faz com que cada solicitação de worker use um diretório de sandbox separado para todas as
de entrada. A configuração do sandbox leva algum 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 sair quando o build for concluído. Também é possível transmitir
--worker_verbose
para
receber mais resultados sobre o que os workers estão fazendo. Essa flag é refletida no campo
verbosity
em WorkRequest
, permitindo que as implementações de workers também sejam
mais detalhadas.
Os workers armazenam os registros no diretório <outputBase>/bazel-workers
para
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ônica, talvez você encontre mais de um arquivo de registro
worker_max_instances
para uma determinada mnemônica.
Para builds do Android, consulte os detalhes na página Desempenho do 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çaria com a execução
da linha de comando /bin/some_compiler -max_mem=4G --persistent_worker
. Uma solicitação
para compilar o Foo.java
ficará 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 WorkResponse
formatado em JSON para o Bazel na stdout. 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
, 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 padronizar a comunicação do worker para usar protobuf.
O Bazel deriva o WorkerKey
da mnemônica e das flags 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 causar consumo excessivo de memória se
muitas variações forem usadas.
No momento, cada worker só pode processar uma solicitação por vez. A fase experimental recurso multiplex workers permite o uso de diversas se a ferramenta subjacente tiver várias linhas de execução e o wrapper estiver configurado para entender isso.
Em neste repositório do GitHub, 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 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
. É possível definir a
flag --worker_sandboxing
para executar todos os workers dentro de sandboxes, garantindo que cada
execução da ferramenta só acesse os arquivos de entrada que ela precisa ter. A ferramenta
ainda podem vazar informações entre solicitações internamente, por exemplo, por meio de um
cache. Usando a 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álido sem precisar ler o arquivo.
Mesmo ao usar os resumos de entrada para evitar o armazenamento em cache indesejado, os workers em sandbox oferecem um sandbox menos rígido 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 em sandbox se a implementação do worker oferecer suporte a ele.
Esse sandbox precisa ser ativado separadamente com a
flag --experimental_worker_multiplex_sandboxing
. Confira mais detalhes em
documento de design).
Leitura adicional
Para mais informações sobre workers persistentes, consulte:
- Postagem original do blog sobre trabalhadores persistentes
- 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