Al escribir reglas, la dificultad más común de rendimiento es recorrer o copiar los datos que se acumulan a partir de las dependencias. Cuando se agregan en todo el conjunto de compilación, estas operaciones pueden tardar O(N^2) tiempo o espacio. Para evitar esto, es crucial para entender cómo usar las dependencias con eficacia.
Esto puede ser difícil de lograr, por lo que Bazel también proporciona un generador de perfiles de memoria que ayuda a encontrar lugares en los que podrías haber cometido un error. Advertencia: El costo de escribir una regla ineficaz puede no ser evidente hasta que esté su uso generalizado.
Usar depsets
Cuando incluyas información a partir de dependencias de reglas, debes usar depsets. Usa solo listas o diccionarios sin formato para publicar información local a 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, 'a'
se menciona cuatro veces. Con gráficos más grandes, esta
el problema solo empeorará.
Este es un ejemplo de una implementación de reglas que usa correctamente los depsets para publicar información transitiva. Ten en cuenta que está bien publicar reglas de firewall locales información usando listas si lo deseas, ya que 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 las dependencias para obtener más información.
Evita llamar a depset.to_list()
Puedes convertir un usuario en una lista plana con el siguiente comando
to_list()
, pero hacerlo suele dar como resultado O(N^2)
el costo. Si es posible, evita la compactación de los depsets, excepto para la depuración
comerciales.
Un error común es pensar que se pueden aplanar libremente las dependencias si solo lo haces.
en objetivos de nivel superior, como una regla <xx>_binary
, ya que el costo
acumulados en cada nivel del gráfico de compilación. Pero esto todavía es O(N^2) cuando
y compilas un conjunto de objetivos
con dependencias superpuestas. Esto sucede cuando
compila tus pruebas //foo/tests/...
o cuando importes un proyecto de IDE.
Reducir la cantidad de llamadas a depset
Llamar a depset
dentro de un bucle suele ser un error. Puede dar lugar a pérdidas con
un anidado muy profundo, que tiene un rendimiento deficiente. 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 las dependencias transitivas y combinarlos todos a la vez:
transitive = []
for i in inputs:
transitive.append(i.deps)
x = depset(transitive = transitive)
A veces, esto se puede reducir mediante una comprensión de listas:
x = depset(transitive = [i.deps for i in inputs])
Cómo usar ctx.actions.args() para las líneas de comandos
Cuando crees líneas de comandos, deberías usar ctx.actions.args(). Esto aplaza la expansión de cualquier dependencia a la fase de ejecución.
Además de ser estrictamente más rápido, esto reducirá el consumo de memoria de sus reglas, a veces en un 90% o más.
Estos son algunos trucos:
Pasa los depsets y las listas directamente como argumentos, en lugar de compactarlos tú mismo. Se expandirán en
ctx.actions.args()
para ti. Si necesitas transformaciones en el contenido de la configuración, consulta ctx.actions.args#add para ver si hay algo que se ajuste a tus necesidades¿Pasas
File#path
como argumentos? No es necesario. Cualquiera File se convierte automáticamente en su path, aplazada al tiempo de expansión.Evita construir cadenas concatenándolas juntas. 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 generará un objeto
ctx.actions.args()
. se pueden escribir de forma condicional o incondicional en un archivo de parámetros mediantectx.actions.args#use_param_file
Este es que se hace detrás de escena, cuando se ejecuta la acción. Si necesitas en forma explícita controla el archivo de parámetros, puedes escribirlo manualmentectx.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 acciones transitivas deben ser valores de salida
Cuando crees una acción con ctx.actions.run, no lo hagas
olvidas que el campo inputs
acepta un depósito. Usa esta opción siempre que las entradas
de las dependencias de forma transitiva.
inputs = depset(...)
ctx.actions.run(
inputs = inputs, # Do *not* turn inputs into a list
...
)
Colgantes
Si Bazel parece estar suspendido, puedes presionar Ctrl-\ o enviar
Bazel: un indicador SIGQUIT
(kill -3 $(bazel info server_pid)
) para obtener un subproceso
volcar en el archivo $(bazel info output_base)/server/jvm.out
.
Como es posible que no puedas ejecutar bazel info
si Bazel está bloqueado, el elemento
El directorio output_base
suele ser el superior de bazel-<workspace>
symlink en el directorio de tu espacio de trabajo.
Generación de perfiles de rendimiento
Bazel escribe un perfil JSON en command.profile.gz
en la base de salida de la siguiente manera:
de forma predeterminada. Puedes configurar la ubicación con la
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 y haz clic en
"Cargar" y elige el archivo de perfil (potencialmente comprimido). Para obtener información
haz clic en las casillas de la esquina inferior izquierda.
Puedes usar estos controles del teclado para navegar:
- Presiona
1
para seleccionar . 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 "desplazar" . Luego, arrastra el mouse para mover la vista. Tú también puedes usara
/d
para moverte hacia la izquierda o la derecha. - Presiona
3
para “acercar” . Luego, arrastra el mouse para hacer zoom. Puedes también utilizaw
/s
para acercar y alejar. - Presiona
4
para "temporizar" en el que puedes medir la distancia entre dos eventos. - Presiona
?
para conocer 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 se encuentran en tránsito. Haz clic en para ver el valor real. Debería aumentar al valor de--jobs
en compilaciones limpias.cpu counters
: Por cada segundo de la compilación, muestra la cantidad de CPU. que usa Bazel (un valor de 1 equivale a un núcleo cuando está 100% ocupado).Critical Path
: Muestra un bloque por cada acción en la ruta crítica.grpc-command-1
: Subproceso principal de Bazel. Es útil para obtener un panorama general de lo que hace Bazel, por ejemplo, “Iniciar Bazel”, “evaluateTargetPatterns”, y "runAnalysisphase".Service Thread
: Muestra pausas menores y principales de recolección de elementos no utilizados (GC).
Otras filas representan los subprocesos de Bazel y muestran todos los eventos en ese subproceso.
Problemas comunes de rendimiento
Cuando analices los perfiles de rendimiento, ten en cuenta lo siguiente:
- Fase de análisis más lenta de lo esperado (
runAnalysisPhase
), especialmente en compilaciones incrementales. Esto puede ser una señal de una mala implementación de reglas, en uno que aplana los dependencias. La carga de paquetes puede ser lenta debido a un una cantidad excesiva de objetivos, macros complejas o globs recursivos. - Acciones individuales lentas, especialmente aquellas en la ruta crítica. Podría ser
dividir acciones grandes en varias más pequeñas o reducir
de dependencias (transitivas) para acelerarlas. También comprueba si hay
una puntuación alta que no sea
PROCESS_TIME
(comoREMOTE_SETUP
oFETCH
). - Cuellos de botella, es decir, un pequeño número de hilos están ocupados, mientras que los demás están inactividad o espera del resultado (observa alrededor de 15 s a 30 s en la captura de pantalla anterior). Lo más probable es que para optimizar esto requiera tocar las implementaciones de reglas o Bazel para introducir más paralelismo. Esto también puede ocurrir cuando hay una cantidad inusual de GC.
Formato de 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
) de los eventos de seguimiento se presentan en
microsegundos. La categoría (cat
) es uno de los valores de enumeración de ProfilerTask
.
Ten en cuenta que algunos eventos se fusionan si son muy cortos y están
entre sí; pasa --noslim_json_profile
si deseas
y evitar que se combinen los eventos.
Consulta también la Especificación de formato de eventos de registro de Chrome.
analyze-profile
Este método de generación de perfiles consta de dos pasos. Primero, debes ejecutar
compila 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
se procesan y analizan con el comando analyze-profile
:
$ bazel analyze-profile /tmp/prof
De forma predeterminada, se imprime la información resumida del análisis del perfil especificado. de Terraform. Esto incluye estadísticas acumulativas sobre los diferentes tipos de tareas para cada una 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 invertido. en 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 comprobar el estado el uso de la memoria. Si existe algún problema, puedes volcar el montón para encontrar el la línea de código exacta que causa el problema.
Habilita el seguimiento de memoria
Debes pasar estas dos marcas 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 inician el servidor en el modo de seguimiento de memoria. Si los olvidas por incluso una invocación a Bazel, el servidor se reiniciará y tendrás que empezar de nuevo.
Cómo usar el Monitor de memoria
A modo de ejemplo, observa el foo
de destino y observa lo que hace. Solo para
ejecuta el análisis y no ejecuta la fase de ejecución de compilación, agrega
--nobuild
.
$ bazel $(STARTUP_FLAGS) build --nobuild //foo:foo
A continuación, observa cuánta memoria consume toda la instancia de Bazel:
$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc
> 2594MB
Desglosa 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)
Crea un archivo pprof
para observar hacia dónde se dirige la memoria.
usando 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
Se obtiene un gráfico tipo llama 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 activos 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)