Optimización del rendimiento

Informar un problema Ver fuente

Cuando se escriben reglas, el inconveniente de rendimiento más común es desviar o copiar datos que se acumulan a partir de dependencias. Cuando se agregan en toda la compilación, estas operaciones pueden ocupar tiempo o espacio O(N^2) con facilidad. Para evitar esto, es fundamental comprender cómo usar dependencias de manera eficaz.

Esto puede ser difícil de lograr, por lo que Bazel también proporciona un generador de perfiles de memoria que te ayuda a encontrar puntos en los que podrías haber cometido un error. Advertencia: Es posible que el costo de escribir una regla ineficiente no sea evidente hasta que se use de forma generalizada.

Usar dependencias

Siempre que quieras integrar información de dependencias de reglas, debes usar depsets. Solo usa listas sin formato o diccionarios para publicar información local en la regla actual.

Un depset representa la información como un gráfico anidado que permite compartir.

Considera el siguiente gráfico:

C -> B -> A
D ---^

Cada nodo publica una sola string. Con 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, podrías obtener 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 dependencias correctamente para publicar información transitiva. Ten en cuenta que está bien publicar la información de la regla local mediante 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 la dependencia para obtener más información.

Evita llamar a depset.to_list()

Puedes forzar una dependencia a una lista plana mediante to_list(), pero, por lo general, se genera un costo O(N^2). Si es posible, evita la compactación de dependencias, excepto para fines de depuración.

Un error común es pensar que se pueden compactar los depsets libremente 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, sigue siendo O(N^2) cuando compilas un conjunto de destinos con dependencias superpuestas. Esto sucede cuando se compilan tus pruebas //foo/tests/... o cuando se importa un proyecto de IDE.

Reduce el número de llamadas a depset

Llamar a depset dentro de un bucle suele ser un error. Puede provocar dependencias con anidación muy profunda, que tienen 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 los dependencias transitivos y combínalos todos a la vez:

transitive = []

for i in inputs:
    transitive.append(i.deps)

x = depset(transitive = transitive)

En ocasiones, esto puede reducirse si comprendes las listas:

x = depset(transitive = [i.deps for i in inputs])

Usa ="{.actions.args() para líneas de comandos

Cuando compilas líneas de comandos, debes usar ctx.actions.args(). Esto aplaza la expansión de cualquier dependencia de 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 dependencias y listas directamente como argumentos, en lugar de acoplarlos tú mismo. Se expandirán en ctx.actions.args() para ti. Si necesitas transformaciones en el contenido de la depset, consulta ctx.actions.args#add para ver si algo es adecuado para la factura.

  • ¿Estás pasando File#path como argumentos? No es necesario. Cualquier File se convierte automáticamente en su ruta de acceso, que se aplaza al tiempo de expansión.

  • Evita construir cadenas mediante la concatenación. 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 incondicional en un archivo de parámetros mediante ctx.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 con ctx.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 transitiva deben ser dependencias

Cuando compiles una acción con ctx.actions.run, no olvides que el campo inputs acepta una 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
  ...
)

Colgante

Si Bazel parece estar suspendido, puedes presionar Ctrl-\ o enviar a Bazel una señal SIGQUIT (kill -3 $(bazel info server_pid)) para obtener un volcado de subprocesos en el archivo $(bazel info output_base)/server/jvm.out.

Dado que es posible que no puedas ejecutar bazel info si Bazel está suspendido, el directorio output_base suele ser el superior del symlink bazel-<workspace> en el directorio de tu lugar de trabajo.

Generación de perfiles de rendimiento

El perfil de registro de JSON puede ser muy útil para comprender con rapidez a qué dedicó Bazel tiempo durante la invocación.

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 tus reglas. Si hay un problema, puedes volcar el montón para encontrar la línea de código exacta que lo causa.

Habilita el seguimiento de la memoria

Debes pasar estas dos marcas de inicio a todas las invocaciones de Bazel:

  STARTUP_FLAGS=\
  --host_jvm_args=-javaagent:<path to java-allocation-instrumenter-3.3.0.jar> \
  --host_jvm_args=-DRULE_MEMORY_TRACKER=1

Estas inician el servidor en modo de seguimiento de memoria. Si los olvidas, incluso en una invocación de Bazel, el servidor se reiniciará y tendrás que volver a empezar.

Cómo usar Memory Tracker

A modo de ejemplo, observa el objeto foo de destino y observa lo que hace. Para ejecutar el análisis solo y no la fase de ejecución de compilación, agrega la marca --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 mediante 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 observar hacia dónde va la memoria, produce 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 mediante 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)