En esta página, se explica cómo usar trabajadores persistentes, los beneficios, los requisitos y cómo los trabajadores afectan la zona de pruebas.
Un trabajador persistente es un proceso de larga duración que inicia el servidor de Bazel, que
funciona como un wrapper alrededor de la herramienta real (por lo general, un compilador) o es
la herramienta en sí. Para beneficiarse de los trabajadores persistentes, la herramienta debe
admitir la realización de una secuencia de compilaciones, y el wrapper debe traducir
entre la API de la herramienta y el formato de solicitud/respuesta que se describe a continuación. Se puede llamar al mismo
trabajador con y sin la marca --persistent_worker en la
misma compilación, y es responsable de iniciar y hablar con la
herramienta de forma adecuada, así como de cerrar los trabajadores al salir. A cada instancia de trabajador se le asigna
(pero no se le hace chroot) un directorio de trabajo independiente en
<outputBase>/bazel-workers.
El uso de trabajadores persistentes es una estrategia de ejecución que disminuye la sobrecarga de inicio, permite más compilación JIT y habilita el almacenamiento en caché de, por ejemplo, los árboles de sintaxis abstracta en la ejecución de acciones. Esta estrategia logra estas mejoras mediante el envío de varias solicitudes a un proceso de larga duración.
Los trabajadores persistentes se implementan para varios lenguajes, incluidos Java, Scala, Kotlin, y muchos más.
Los programas que usan un entorno de ejecución de NodeJS pueden usar la @bazel/worker biblioteca auxiliar para implementar el protocolo de trabajador.
Usa trabajadores persistentes
Bazel 0.27 y versiones posteriores
usan trabajadores persistentes de forma predeterminada cuando ejecutan compilaciones, aunque la ejecución
remota tiene prioridad. Para las acciones que no admiten trabajadores persistentes,
Bazel recurre a iniciar una instancia de herramienta para cada acción. Puedes configurar explícitamente
tu compilación para que use trabajadores persistentes si configuras la worker
estrategia para los mnemónicos de herramientas aplicables. Como práctica recomendada, este ejemplo incluye la especificación de local como alternativa a la estrategia worker strategy:
bazel build //my:target --strategy=Javac=worker,localUsar la estrategia de trabajadores en lugar de la estrategia local puede aumentar la velocidad de compilación de manera significativa, según la implementación. En el caso de Java, las compilaciones pueden ser de 2 a 4 veces más rápidas y, a veces, más para la compilación incremental. La compilación de Bazel es aproximadamente 2.5 veces más rápida con los trabajadores. Para obtener más detalles, consulta la "Elige la cantidad de trabajadores" sección.
Si también tienes un entorno de compilación remota que coincida con tu entorno de compilación local
environment, puedes usar la estrategia experimental
dinámica strategy,
que compite con una ejecución remota y una ejecución de trabajador. Para habilitar la dinámica
estrategia, pasa la
--experimental_spawn_scheduler
marca. Esta estrategia habilita automáticamente a los trabajadores, por lo que no es necesario
especificar la worker estrategia, pero puedes usar local o sandboxed como
alternativas.
Elige la cantidad de trabajadores
La cantidad predeterminada de instancias de trabajador por mnemónico es 4, pero se puede ajustar
con la
worker_max_instances
marca. Existe una compensación entre el buen uso de las CPUs disponibles y la
cantidad de compilación JIT y aciertos de caché que obtienes. Con más trabajadores, más
objetivos pagarán los costos de inicio de la ejecución de código no JIT y de los aciertos de caché en frío. Si tienes una pequeña cantidad de objetivos para compilar, un solo trabajador puede ofrecer
la mejor compensación entre la velocidad de compilación y el uso de recursos (por ejemplo,
consulta el problema #8586.
La marca worker_max_instances establece la cantidad máxima de instancias de trabajador por
mnemónico y conjunto de marcas (consulta a continuación), por lo que, en un sistema mixto, podrías terminar usando
mucha memoria si mantienes el valor predeterminado. Para las compilaciones incrementales, el
beneficio de varias instancias de trabajador es aún menor.
En este gráfico, se muestran los tiempos de compilación desde cero para Bazel (objetivo
//src:bazel) en una estación de trabajo Linux Intel Xeon de 6 núcleos con hiperprocesamiento de 3.5 GHz
con 64 GB de RAM. Para cada configuración de trabajador, se ejecutan cinco compilaciones limpias y
se toma el promedio de las últimas cuatro.

Figura 1: Gráfico de las mejoras de rendimiento de las compilaciones limpias
Para esta configuración, dos trabajadores ofrecen la compilación más rápida, aunque con solo un 14% de mejora en comparación con un trabajador. Un trabajador es una buena opción si deseas usar menos memoria.
Por lo general, la compilación incremental se beneficia aún más. Las compilaciones limpias son relativamente raras, pero es común cambiar un solo archivo entre compilaciones, en particular en el desarrollo basado en pruebas. El ejemplo anterior también tiene algunas acciones de empaquetado que no son de Java que pueden eclipsar el tiempo de compilación incremental.
La recompilación de las fuentes de Java solo
(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar)
después de cambiar una constante de cadena interna en
AbstractContainerizingSandboxedSpawn.java
ofrece una aceleración de 3 veces (promedio de 20 compilaciones incrementales con una compilación de preparación
descartada):

Figura 2: Gráfico de las mejoras de rendimiento de las compilaciones incrementales
La aceleración depende del cambio que se realice. En la situación anterior, se mide una aceleración de un factor 6 cuando se cambia una constante de uso común.
Modifica trabajadores persistentes
Puedes pasar la
--worker_extra_flag
marca para especificar marcas de inicio a los trabajadores, con clave por mnemónico. Por ejemplo,
pasar --worker_extra_flag=javac=--debug activa la depuración solo para Javac.
Solo se puede configurar una marca de trabajador por uso de esta marca y solo para un mnemónico.
Los trabajadores no solo se crean por separado para cada mnemónico, sino también para
las variaciones en sus marcas de inicio. Cada combinación de mnemónico y marcas de inicio
se combina en una WorkerKey, y para cada WorkerKey, se pueden crear hasta
worker_max_instances trabajadores. Consulta la siguiente sección para ver cómo la
configuración de acciones también puede especificar marcas de configuración.
Puedes usar la
--high_priority_workers
marca para especificar un mnemónico que se debe ejecutar en preferencia a los mnemónicos de prioridad normal. Esto puede ayudar a priorizar las acciones que siempre están en la ruta crítica. Si hay dos o más trabajadores de alta prioridad que ejecutan solicitudes, se impide que se ejecuten todos los
demás trabajadores. Esta marca se puede usar varias veces.
Si pasas la
--worker_sandboxing
marca, cada solicitud de trabajador usará un directorio de zona de pruebas independiente para todas sus
entradas. Configurar la zona de pruebas lleva un tiempo adicional,
en especial en macOS, pero ofrece una mejor garantía de corrección.
La marca
--worker_quit_after_build
es útil principalmente para la depuración y la generación de perfiles. Esta marca obliga a todos los trabajadores
a salir una vez que se realiza una compilación. También puedes pasar
--worker_verbose para
obtener más información sobre lo que hacen los trabajadores. Esta marca se refleja en el
verbosity campo en WorkRequest, lo que permite que las implementaciones de trabajadores también sean
más detalladas.
Los trabajadores almacenan sus registros en el directorio <outputBase>/bazel-workers, por
ejemplo
/tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log.
El nombre del archivo incluye el ID del trabajador y el mnemónico. Como puede haber más
de uno WorkerKey por mnemónico, es posible que veas más de worker_max_instances
archivos de registro para un mnemónico determinado.
Para las compilaciones de Android, consulta los detalles en la página Rendimiento de la compilación de Android.
Implementa trabajadores persistentes
Consulta la página Cómo crear trabajadores persistentes para obtener más información sobre cómo crear un trabajador.
En este ejemplo, se muestra una configuración de Starlark para un trabajador 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" }
)
Con esta definición, el primer uso de esta acción comenzaría con la ejecución de
la línea de comandos /bin/some_compiler -max_mem=4G --persistent_worker. Una solicitud
para compilar Foo.java se vería de la siguiente manera:
NOTA: Si bien la especificación del búfer de protocolo usa "snake case" (request_id),
el protocolo JSON usa "camel case" (requestId). En este documento, usaremos
camel case en los ejemplos de JSON, pero snake case cuando hablemos del campo,
independientemente del protocolo.
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
El trabajador recibe esto en stdin en formato JSON delimitado por saltos de línea (porque
requires-worker-protocol está configurado como JSON). Luego, el trabajador realiza la acción,
y envía un WorkResponse con formato JSON a Bazel en su stdout. Luego, Bazel
analiza esta respuesta y la convierte de forma manual en un proto WorkResponse. Para
comunicarse con el trabajador asociado mediante protobuf codificado en formato binario en lugar de
JSON, requires-worker-protocol se establecería en proto, de la siguiente manera:
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
Si no incluyes requires-worker-protocol en los requisitos de ejecución,
Bazel establecerá la comunicación del trabajador de forma predeterminada para usar protobuf.
Bazel deriva la WorkerKey del mnemónico y las marcas compartidas, por lo que, si esta
configuración permitiera cambiar el parámetro max_mem, se generaría un trabajador independiente para cada valor usado. Esto puede generar un consumo excesivo de memoria si
se usan demasiadas variaciones.
Actualmente, cada trabajador solo puede procesar una solicitud a la vez. La función experimental de trabajadores multiplex permite usar varios subprocesos, si la herramienta subyacente es multiproceso y el wrapper está configurado para comprender esto.
En este repositorio de GitHub, puedes ver ejemplos de wrappers de trabajadores escritos en Java y Python. Si trabajas en JavaScript o TypeScript, el paquete@bazel/worker y el ejemplo de trabajador de nodejs pueden ser útiles.
¿Cómo afectan los trabajadores a la zona de pruebas?
El uso de la estrategia worker de forma predeterminada no ejecuta la acción en una
zona de pruebas, de manera similar a la estrategia local. Puedes configurar la
--worker_sandboxing marca para ejecutar todos los trabajadores dentro de las zonas de pruebas, lo que garantiza que cada
ejecución de la herramienta solo vea los archivos de entrada que se supone que debe tener. Es posible que la herramienta
aún filtre información entre solicitudes de forma interna, por ejemplo, a través de una
caché. El uso de la estrategia dynamic
requiere que los trabajadores estén en la zona de pruebas.
Para permitir el uso correcto de las cachés del compilador con los trabajadores, se pasa un resumen junto con cada archivo de entrada. Por lo tanto, el compilador o el wrapper pueden verificar si la entrada es aún válida sin tener que leer el archivo.
Incluso cuando se usan los resúmenes de entrada para protegerse contra el almacenamiento en caché no deseado, los trabajadores en la zona de pruebas ofrecen una zona de pruebas menos estricta que una zona de pruebas pura, ya que la herramienta puede mantener otro estado interno que se vio afectado por solicitudes anteriores.
Los trabajadores multiplex solo pueden estar en la zona de pruebas si la implementación del trabajador lo admite,
y esta zona de pruebas se debe habilitar por separado con la
--experimental_worker_multiplex_sandboxing marca. Consulta más detalles en
el documento de diseño).
Lecturas adicionales
Para obtener más información sobre los trabajadores persistentes, consulta lo siguiente:
- Entrada de blog original sobre trabajadores persistentes
- Descripción de la implementación de Haskell {: .external}
- Entrada de blog de Mike Morearty {: .external}
- Desarrollo de frontend con Bazel: Angular/TypeScript y trabajadores persistentes con Asana {: .external}
- Explicación de las estrategias de Bazel {: .external}
- Debate informativo sobre la estrategia de trabajadores en la lista de distribución bazel-discuss {: .external}