Cuando se escriben reglas, el error de rendimiento más común es recorrer o copiar datos que se acumulan a partir de dependencias. Cuando se agregan en toda la compilación, estas operaciones pueden tardar fácilmente O(N^2) en tiempo o espacio. Para evitar esto, es fundamental comprender cómo usar los depsets de manera eficaz.
Esto puede ser difícil de hacer correctamente, por lo que Bazel también proporciona un generador de perfiles de memoria que te ayuda a encontrar los lugares en los que podrías haber cometido un error. Ten en cuenta lo siguiente: El costo de escribir una regla ineficiente puede no ser evidente hasta que se use de forma generalizada.
Usa depsets
Siempre que agregues información de las dependencias de reglas, debes usar depsets. Solo usa listas o diccionarios simples para publicar información local en la regla actual.
Un depset representa la información como un gráfico anidado que permite el uso compartido.
Considera el siguiente gráfico:
C -> B -> A
D ---^
Cada nodo publica una sola cadena. Con los depsets, los datos se ven de la siguiente manera:
a = depset(direct=['a'])
b = depset(direct=['b'], transitive=[a])
c = depset(direct=['c'], transitive=[b])
d = depset(direct=['d'], transitive=[b])
Ten en cuenta que cada elemento solo se menciona una vez. Con las listas, obtendrías lo siguiente:
a = ['a']
b = ['b', 'a']
c = ['c', 'b', 'a']
d = ['d', 'b', 'a']
Ten en cuenta que, en este caso, se menciona 'a'
cuatro veces. Con gráficos más grandes, este problema solo empeorará.
A continuación, se muestra un ejemplo de una implementación de regla que usa depsets correctamente para publicar información transitiva. Ten en cuenta que puedes publicar información local de la regla con listas si lo deseas, ya que esto no es O(N^2).
MyProvider = provider()
def _impl(ctx):
my_things = ctx.attr.things
all_things = depset(
direct=my_things,
transitive=[dep[MyProvider].all_things for dep in ctx.attr.deps]
)
...
return [MyProvider(
my_things=my_things, # OK, a flat list of rule-local things only
all_things=all_things, # OK, a depset containing dependencies
)]
Consulta la página de descripción general de depset para obtener más información.
Evita llamar a depset.to_list()
Puedes forzar un depset a una lista plana con to_list()
, pero, por lo general, esto genera un costo de O(N^2). Si es posible, evita aplanar los depsets, excepto para fines de depuración.
Una idea errónea común es que puedes aplanar libremente los depsets si solo lo haces en los destinos de nivel superior, como una regla <xx>_binary
, ya que, en ese caso, el costo no se acumula en cada nivel del gráfico de compilación. Sin embargo, sigue siendo O(N²) cuando compilas un conjunto de destinos con dependencias superpuestas. Esto sucede cuando compilas tus pruebas //foo/tests/...
o cuando importas un proyecto de IDE.
Reduce la cantidad de llamadas a depset
Llamar a depset
dentro de un bucle suele ser un error. Puede generar depsets con anidamiento muy profundo, lo que afecta el rendimiento. Por ejemplo:
x = depset()
for i in inputs:
# Do not do that.
x = depset(transitive = [x, i.deps])
Este código se puede reemplazar fácilmente. Primero, recopila los depsets transitivos y combínalos todos a la vez:
transitive = []
for i in inputs:
transitive.append(i.deps)
x = depset(transitive = transitive)
A veces, esto se puede reducir con una comprensión de lista:
x = depset(transitive = [i.deps for i in inputs])
Usa ctx.actions.args() para las líneas de comandos
Cuando compiles líneas de comandos, debes usar ctx.actions.args(), ya que esto pospone la expansión de cualquier depsets hasta la fase de ejecución.
Además de ser estrictamente más rápido, esto reducirá el consumo de memoria de tus reglas, a veces en un 90% o más.
Aquí tienes algunos trucos:
Pasa los depsets y las listas directamente como argumentos, en lugar de aplanarlos tú mismo.
ctx.actions.args()
los expandirá por ti. Si necesitas alguna transformación en el contenido de depset, consulta ctx.actions.args#add para ver si algo se ajusta a tus necesidades.¿Estás pasando
File#path
como argumentos? No es necesario. Cualquier archivo se convierte automáticamente en su ruta de acceso y se aplaza hasta el tiempo de expansión.Evita construir cadenas concatenándolas. El mejor argumento de cadena es una constante, ya que su memoria se compartirá entre todas las instancias de tu regla.
Si los argumentos son demasiado largos para la línea de comandos, se puede escribir un objeto
ctx.actions.args()
de forma condicional o incondicional en un archivo de parámetros conctx.actions.args#use_param_file
. Esto se hace en segundo plano cuando se ejecuta la acción. Si necesitas controlar de forma explícita el archivo de parámetros, puedes escribirlo manualmente conctx.actions.write
.
Ejemplo:
def _impl(ctx):
...
args = ctx.actions.args()
file = ctx.declare_file(...)
files = depset(...)
# Bad, constructs a full string "--foo=<file path>" for each rule instance
args.add("--foo=" + file.path)
# Good, shares "--foo" among all rule instances, and defers file.path to later
# It will however pass ["--foo", <file path>] to the action command line,
# instead of ["--foo=<file_path>"]
args.add("--foo", file)
# Use format if you prefer ["--foo=<file path>"] to ["--foo", <file path>]
args.add(format="--foo=%s", value=file)
# Bad, makes a giant string of a whole depset
args.add(" ".join(["-I%s" % file.short_path for file in files])
# Good, only stores a reference to the depset
args.add_all(files, format_each="-I%s", map_each=_to_short_path)
# Function passed to map_each above
def _to_short_path(f):
return f.short_path
Las entradas de acción transitivas deben ser depsets
Cuando compiles una acción con ctx.actions.run, no olvides que el campo inputs
acepta un depset. Úsalo siempre que las entradas se recopilen de dependencias de forma transitiva.
inputs = depset(...)
ctx.actions.run(
inputs = inputs, # Do *not* turn inputs into a list
...
)
Colgantes
Si parece que Bazel se bloqueó, puedes presionar Ctrl-\ o enviar a Bazel un indicador SIGQUIT
(kill -3 $(bazel info server_pid)
) para obtener un volcado de subprocesos en el archivo $(bazel info output_base)/server/jvm.out
.
Como es posible que no puedas ejecutar bazel info
si Bazel se bloquea, el directorio output_base
suele ser el directorio principal del vínculo simbólico bazel-<workspace>
en el directorio de tu espacio de trabajo.
Generación de perfiles de rendimiento
De forma predeterminada, Bazel escribe un perfil JSON en command.profile.gz
en la base de salida. Puedes configurar la ubicación con la marca --profile
, por ejemplo, --profile=/tmp/profile.gz
. Las ubicaciones que terminan en .gz
se comprimen con GZIP.
Para ver los resultados, abre chrome://tracing
en una pestaña del navegador Chrome, haz clic en "Cargar" y elige el archivo de perfil (que podría estar comprimido). Para obtener resultados más detallados, haz clic en los cuadros de la esquina inferior izquierda.
Puedes usar estos controles del teclado para navegar:
- Presiona
1
para activar el modo de selección. En este modo, puedes seleccionar casillas específicas para inspeccionar los detalles del evento (consulta la esquina inferior izquierda). Selecciona varios eventos para obtener un resumen y estadísticas agregadas. - Presiona
2
para activar el modo de desplazamiento. Luego, arrastra el mouse para mover la vista. También puedes usara
/d
para moverte hacia la izquierda o la derecha. - Presiona
3
para activar el modo de zoom. Luego, arrastra el mouse para acercar la imagen. También puedes usarw
/s
para acercar o alejar la imagen. - Presiona
4
para activar el modo "cronómetro", en el que puedes medir la distancia entre dos eventos. - Presiona
?
para obtener información sobre todos los controles.
Información del perfil
Perfil de ejemplo:
Figura 1: Perfil de ejemplo.
Existen algunas filas especiales:
action counters
: Muestra cuántas acciones simultáneas están en curso. Haz clic en él para ver el valor real. Debe aumentar hasta el valor de--jobs
en las compilaciones limpias.cpu counters
: Para cada segundo de la compilación, muestra la cantidad de CPU que usa Bazel (un valor de 1 equivale a un núcleo ocupado al 100%).Critical Path
: Muestra un bloque para cada acción en la ruta crítica.grpc-command-1
: Es el subproceso principal de Bazel. Es útil para obtener una visión general de lo que hace Bazel, por ejemplo, "Launch Bazel", "evaluateTargetPatterns" y "runAnalysisPhase".Service Thread
: Muestra las pausas de recolección de basura (GC) menores y mayores.
Las demás filas representan subprocesos de Bazel y muestran todos los eventos de ese subproceso.
Problemas de rendimiento comunes
Cuando analices los perfiles de rendimiento, busca lo siguiente:
- La fase de análisis (
runAnalysisPhase
) es más lenta de lo esperado, especialmente en las compilaciones incrementales. Esto puede ser un signo de una implementación deficiente de la regla, por ejemplo, una que aplana los depsets. La carga de paquetes puede ser lenta debido a una cantidad excesiva de destinos, macros complejas o comodines recursivos. - Acciones lentas individuales, en especial las que se encuentran en la ruta crítica Es posible que se puedan dividir las acciones grandes en varias acciones más pequeñas o reducir el conjunto de dependencias (transitivas) para acelerarlas. También verifica si hay un valor no
PROCESS_TIME
inusualmente alto (comoREMOTE_SETUP
oFETCH
). - Cuellos de botella, es decir, una pequeña cantidad de subprocesos están ocupados mientras que todos los demás están inactivos o esperando el resultado (consulta alrededor de 15 a 30 segundos en la captura de pantalla anterior). Para optimizar esto, es probable que debas modificar las implementaciones de reglas o Bazel en sí para introducir más paralelismo. Esto también puede ocurrir cuando hay una cantidad inusual de GC.
Formato del archivo de perfil
El objeto de nivel superior contiene metadatos (otherData
) y los datos de seguimiento reales (traceEvents
). Los metadatos contienen información adicional, por ejemplo, el ID de invocación y la fecha de la invocación de Bazel.
Ejemplo:
{
"otherData": {
"build_id": "101bff9a-7243-4c1a-8503-9dc6ae4c3b05",
"date": "Tue Jun 16 08:30:21 CEST 2020",
"output_base": "/usr/local/google/_bazel_johndoe/573d4be77eaa72b91a3dfaa497bf8cd0"
},
"traceEvents": [
{"name":"thread_name","ph":"M","pid":1,"tid":0,"args":{"name":"Critical Path"}},
{"cat":"build phase marker","name":"Launch Bazel","ph":"X","ts":-1824000,"dur":1824000,"pid":1,"tid":60},
...
{"cat":"general information","name":"NoSpawnCacheModule.beforeCommand","ph":"X","ts":116461,"dur":419,"pid":1,"tid":60},
...
{"cat":"package creation","name":"src","ph":"X","ts":279844,"dur":15479,"pid":1,"tid":838},
...
{"name":"thread_name","ph":"M","pid":1,"tid":11,"args":{"name":"Service Thread"}},
{"cat":"gc notification","name":"minor GC","ph":"X","ts":334626,"dur":13000,"pid":1,"tid":11},
...
{"cat":"action processing","name":"Compiling third_party/grpc/src/core/lib/transport/status_conversion.cc","ph":"X","ts":12630845,"dur":136644,"pid":1,"tid":1546}
]
}
Las marcas de tiempo (ts
) y las duraciones (dur
) en los eventos de seguimiento se indican en microsegundos. La categoría (cat
) es uno de los valores de enumeración de ProfilerTask
.
Ten en cuenta que algunos eventos se combinan si son muy cortos y están cerca unos de otros. Pasa --noslim_json_profile
si deseas evitar la combinación de eventos.
Consulta también la Especificación del formato de eventos de Chrome Trace.
analyze-profile
Este método de generación de perfiles consta de dos pasos. Primero, debes ejecutar tu compilación o prueba con la marca --profile
, por ejemplo:
$ bazel build --profile=/tmp/prof //path/to:target
El archivo generado (en este caso, /tmp/prof
) es un archivo binario que se puede posprocesar y analizar con el comando analyze-profile
:
$ bazel analyze-profile /tmp/prof
De forma predeterminada, imprime información de análisis resumida para el archivo de datos de perfil especificado. Esto incluye estadísticas acumulativas para diferentes tipos de tareas en cada fase de compilación y un análisis de la ruta crítica.
La primera sección del resultado predeterminado es una descripción general del tiempo dedicado a las diferentes fases de compilación:
INFO: Profile created on Tue Jun 16 08:59:40 CEST 2020, build ID: 0589419c-738b-4676-a374-18f7bbc7ac23, output base: /home/johndoe/.cache/bazel/_bazel_johndoe/d8eb7a85967b22409442664d380222c0
=== PHASE SUMMARY INFORMATION ===
Total launch phase time 1.070 s 12.95%
Total init phase time 0.299 s 3.62%
Total loading phase time 0.878 s 10.64%
Total analysis phase time 1.319 s 15.98%
Total preparation phase time 0.047 s 0.57%
Total execution phase time 4.629 s 56.05%
Total finish phase time 0.014 s 0.18%
------------------------------------------------
Total run time 8.260 s 100.00%
Critical path (4.245 s):
Time Percentage Description
8.85 ms 0.21% _Ccompiler_Udeps for @local_config_cc// compiler_deps
3.839 s 90.44% action 'Compiling external/com_google_protobuf/src/google/protobuf/compiler/php/php_generator.cc [for host]'
270 ms 6.36% action 'Linking external/com_google_protobuf/protoc [for host]'
0.25 ms 0.01% runfiles for @com_google_protobuf// protoc
126 ms 2.97% action 'ProtoCompile external/com_google_protobuf/python/google/protobuf/compiler/plugin_pb2.py'
0.96 ms 0.02% runfiles for //tools/aquery_differ aquery_differ
Generación de perfiles de memoria
Bazel incluye un generador de perfiles de memoria integrado que puede ayudarte a verificar el uso de memoria de tu regla. Si hay un problema, puedes volcar el montón para encontrar la línea de código exacta que lo causa.
Cómo habilitar el seguimiento de memoria
Debes pasar estos dos parámetros de inicio a cada invocación de Bazel:
STARTUP_FLAGS=\
--host_jvm_args=-javaagent:$(BAZEL)/third_party/allocation_instrumenter/java-allocation-instrumenter-3.3.0.jar \
--host_jvm_args=-DRULE_MEMORY_TRACKER=1
Estos parámetros inician el servidor en el modo de seguimiento de memoria. Si olvidas estos argumentos incluso para una sola invocación de Bazel, el servidor se reiniciará y deberás comenzar de nuevo.
Cómo usar el Memory Tracker
Como ejemplo, observa el objetivo foo
y mira lo que hace. Para ejecutar solo el análisis y no la fase de ejecución de la compilación, agrega la marca --nobuild
.
$ bazel $(STARTUP_FLAGS) build --nobuild //foo:foo
A continuación, consulta cuánta memoria consume toda la instancia de Bazel:
$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc
> 2594MB
Desglosa los datos por clase de regla con bazel dump --rules
:
$ bazel $(STARTUP_FLAGS) dump --rules
>
RULE COUNT ACTIONS BYTES EACH
genrule 33,762 33,801 291,538,824 8,635
config_setting 25,374 0 24,897,336 981
filegroup 25,369 25,369 97,496,272 3,843
cc_library 5,372 73,235 182,214,456 33,919
proto_library 4,140 110,409 186,776,864 45,115
android_library 2,621 36,921 218,504,848 83,366
java_library 2,371 12,459 38,841,000 16,381
_gen_source 719 2,157 9,195,312 12,789
_check_proto_library_deps 719 668 1,835,288 2,552
... (more output)
Para ver a dónde se destina la memoria, genera un archivo pprof
con bazel dump --skylark_memory
:
$ bazel $(STARTUP_FLAGS) dump --skylark_memory=$HOME/prof.gz
> Dumping Starlark heap to: /usr/local/google/home/$USER/prof.gz
Usa la herramienta pprof
para investigar el montón. Un buen punto de partida es obtener un gráfico de llamas con pprof -flame $HOME/prof.gz
.
Obtén pprof
de https://github.com/google/pprof.
Obtén un volcado de texto de los sitios de llamadas más populares anotados con líneas:
$ pprof -text -lines $HOME/prof.gz
>
flat flat% sum% cum cum%
146.11MB 19.64% 19.64% 146.11MB 19.64% android_library <native>:-1
113.02MB 15.19% 34.83% 113.02MB 15.19% genrule <native>:-1
74.11MB 9.96% 44.80% 74.11MB 9.96% glob <native>:-1
55.98MB 7.53% 52.32% 55.98MB 7.53% filegroup <native>:-1
53.44MB 7.18% 59.51% 53.44MB 7.18% sh_test <native>:-1
26.55MB 3.57% 63.07% 26.55MB 3.57% _generate_foo_files /foo/tc/tc.bzl:491
26.01MB 3.50% 66.57% 26.01MB 3.50% _build_foo_impl /foo/build_test.bzl:78
22.01MB 2.96% 69.53% 22.01MB 2.96% _build_foo_impl /foo/build_test.bzl:73
... (more output)