Cuando se escriben reglas, el error de rendimiento más común es recorrer o copiar los datos que se acumulan a partir de las dependencias. Cuando se agregan a toda la compilación, estas operaciones pueden ocupar fácilmente tiempo o espacio de O(N^2). Para evitar esto, es fundamental comprender cómo usar los conjuntos de dependencias 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 ayuda a encontrar lugares en los que podrías haber cometido un error. Ten en cuenta que el costo de escribir una regla ineficiente puede no ser evidente hasta que se use de forma generalizada.
Usa depsets
Cada vez que resumas información de las dependencias de reglas, debes usar depsets. Usa solo listas o diccionarios simples para publicar información local a la regla actual.
Un conjunto de dependencias 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 conjuntos de dependencias, 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, este problema solo empeorará.
A continuación, se muestra un ejemplo de una implementación de reglas que usa depsets correctamente para publicar información transitiva. Ten en cuenta que está bien publicar información local de reglas con 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 depset overview para obtener más información.
Evita llamar a depset.to_list()
Puedes forzar un conjunto de dependencias a una lista plana con to_list()
, pero hacerlo suele generar un costo de O(N^2). Si es posible, evita aplanar los conjuntos de dependencias, excepto para depuración.
Un error común es pensar que puedes aplanar los conjuntos de dependencias de forma libre si solo lo haces en objetivos de nivel superior, como una regla <xx>_binary
, ya que el costo no se acumula en cada nivel del gráfico de compilación. Sin embargo, esto sigue siendo O(N^2) 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
A menudo, llamar a depset
dentro de un bucle es un error. Puede generar conjuntos de dependencias con anidamiento muy profundo, que tienen un rendimiento bajo. 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 conjuntos de dependencias transitivos y cómbínalos todos a la vez:
transitive = []
for i in inputs:
transitive.append(i.deps)
x = depset(transitive = transitive)
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 compilas líneas de comandos, debes usar ctx.actions.args(). Esto aplaza la expansión de cualquier conjunto de dependencias a 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.
Estos son algunos trucos:
Pasa depsets y listas directamente como argumentos, en lugar de aplanarlos por tu cuenta.
ctx.actions.args()
los expandirá por ti. Si necesitas realizar alguna transformación en el contenido de depset, consulta ctx.actions.args#add para ver si hay algo que se adapte a tus necesidades.¿Estás pasando
File#path
como argumentos? No es necesario. Cualquier archivo se convierte automáticamente en su ruta, que se aplaza hasta el momento de la expansión.Evita la construcción de cadenas concatenando cadenas. 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, un objeto
ctx.actions.args()
se puede escribir de forma condicional o no 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 de forma manual 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 acciones transitivas deben ser depsets
Cuando compiles una acción con ctx.actions.run, no olvides que el campo inputs
acepta un conjunto de dependencias. Úsalo cada vez que se recopilen entradas de dependencias de forma transitiva.
inputs = depset(...)
ctx.actions.run(
inputs = inputs, # Do *not* turn inputs into a list
...
)
Colgado
Si Bazel parece estar bloqueado, puedes presionar Ctrl-\ o enviarle a Bazel una señal SIGQUIT
(kill -3 $(bazel info server_pid)
) para obtener un volcado de subproceso en el archivo $(bazel info output_base)/server/jvm.out
.
Dado que es posible que no puedas ejecutar bazel info
si bazel está bloqueado, el directorio output_base
suele ser el superior del symlink bazel-<workspace>
en el directorio de tu espacio de trabajo.
Generación de perfiles de rendimiento
El perfil de seguimiento JSON puede ser muy útil para comprender rápidamente en qué se dedicó tiempo Bazel durante la invocación.
La marca --experimental_command_profile
se puede usar para capturar perfiles de Java Flight Recorder de varios tipos (tiempo de CPU, tiempo de ejecución, asignaciones de memoria y contención de bloqueo).
La marca --starlark_cpu_profile
se puede usar para escribir un perfil de pprof del uso de la CPU por parte de todos los subprocesos de Starlark.
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 exacta de código que lo causa.
Habilita el seguimiento de memoria
Debes pasar estas dos marcas de inicio a cada invocación de Bazel:
STARTUP_FLAGS=\
--host_jvm_args=-javaagent:<path to 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 olvidas estos parámetros para una sola invocación de Bazel, el servidor se reiniciará y deberás comenzar de nuevo.
Cómo usar el Monitor de memoria
A modo de ejemplo, observa el foo
de destino y mira qué hace. Para ejecutar solo el análisis y no la fase de ejecución de 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
Desglosar 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 dirige 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 tipo llama con pprof -flame $HOME/prof.gz
.
Obtén pprof
en https://github.com/google/pprof.
Obtén un volcado de texto de los sitios de llamadas más populares con anotaciones de 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)