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 com chroot para) um diretório de trabalho separado em
<outputBase>/bazel-workers
:
Usar workers permanentes é uma estratégia de execução que diminui de inicialização, permite mais compilação JIT e permite o armazenamento em cache de as árvores de sintaxe abstratas na execução da ação. Essa estratégia alcança essas melhorias com o envio de várias solicitações para uma interface de usuário de desenvolvimento de software.
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 dão suporte a workers persistentes,
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 build local
ambiente, use o modelo
estratégia dinâmica,
que gera uma execução remota e uma execução de worker. Para ativar
de uma estratégia, transmita a
--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 o
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
as metas vão pagar os custos de inicialização para executar o código não JITted e clicar no
armazenamento em cache. Se você tiver poucos destinos para criar, um único worker poderá
a melhor compensação entre velocidade de compilação e 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 de flag (veja abaixo). Em um sistema misto, você pode acabar usando
muita memória se mantiver o valor padrão. Para builds incrementais,
de várias instâncias de worker é 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 fazem 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 usam menos memória.
A compilação incremental costuma ser ainda mais vantajosa. 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 componentes ações de empacotamento que podem ofuscar o tempo de compilação incremental.
Recompilar apenas as fontes Java
(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
)
depois de mudar uma constante interna de string
AbstractContainerizingSandboxedSpawn.java
proporciona uma aceleração de três vezes (média de 20 builds incrementais com um build de aquecimento)
descartadas):
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
--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ô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 o
a configuração de ações também pode especificar flags de configuração.
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
--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 sinalização é refletida na
O campo verbosity
em WorkRequest
, permitindo que as implementações de worker também sejam
mais detalhado.
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ônico, você poderá ver mais de worker_max_instances
os arquivos de registro de um determinado mnemônico.
Para builds do Android, consulte 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ça com a execução
a 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 letras concatenadas requestId
). Neste documento, usaremos
letras concatenadas 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
, 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 protobuf.
O Bazel deriva o WorkerKey
das flags mnemônicas e compartilhadas. Portanto, se esse
permite mudar o parâmetro max_mem
, um worker separado
gerados 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ê estão trabalhando em JavaScript ou TypeScript, Pacote@bazel/worker e Exemplo de worker nodejs pode ser útil.
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 flag --worker_sandboxing
para executar todos os workers dentro de sandboxes, garantindo que cada
execução da ferramenta só veja os arquivos de entrada que deveria 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 com o uso de resumos de entrada para proteger contra armazenamento em cache indesejado, os oferecem uma sandbox menos rígida do que uma sandbox pura, pois a ferramenta pode manter outro estado interno que foi afetado por solicitações anteriores.
Os workers multiplex só podem ser colocados no sandbox se a implementação do worker for compatível.
e esse sandbox precisa ser ativado separadamente com o
sinalização --experimental_worker_multiplex_sandboxing
. Confira mais detalhes em
documento de design).
Leitura adicional
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