Optimización del rendimiento

Cuando escribes reglas, el error de rendimiento más común es recorrer o copiar datos que se acumulan a partir de las dependencias. Cuando se agregan en toda la compilación, estas operaciones pueden tardar fácilmente O(N^2) 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 que el costo de escribir una regla ineficiente puede no ser evidente hasta que se use de forma generalizada.

Usa depsets

Siempre que acumules 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, 'a' se menciona cuatro veces. Con gráficos más grandes, este problema solo empeorará.

Este es un ejemplo de una implementación de reglas 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 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) costo. En lo posible, evita cualquier aplanamiento de 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 objetivos de nivel superior, como una regla <xx>_binary, ya que, luego, 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 objetivos 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 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 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 líneas de comandos

Cuando crees líneas de comandos, debes usar ctx.actions.args(). Esto difiere la expansión de cualquier depset 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.

Aquí tienes algunos trucos:

  • Pasa depsets y listas directamente como argumentos, en lugar de aplanarlos tú mismo. 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 algo se ajusta a tus necesidades.

  • ¿Estás pasando File#path como argumentos? No hace falta. Cualquier archivo se convierte automáticamente en su ruta de acceso, diferida al 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 con ctx.actions.args#use_param_file. Esto se hace en segundo plano cuando se ejecuta la acción. Si necesitas controlar explícitamente el archivo de parámetros, puedes escribirlo de forma manual 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 acciones transitivas deben ser depsets

Cuando crees una acción con ctx.actions.run, no olvides que el inputs campo acepta un depset. Usa esto siempre que las entradas se recopilen de forma transitiva de las dependencias.

inputs = depset(...)
ctx.actions.run(
  inputs = inputs,  # Do *not* turn inputs into a list
  ...
)

Colgantes

Si Bazel parece estar colgado, puedes presionar Ctrl-\ o enviarle 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.

Como es posible que no puedas ejecutar bazel info si Bazel está colgado, el output_base directorio suele ser el superior del bazel-<workspace> vínculo simbólico en el directorio de tu espacio de trabajo.

Generación de perfiles de rendimiento

El perfil de seguimiento de JSON puede ser muy útil para comprender rápidamente en qué invirtió tiempo Bazel durante la invocación.

Se puede usar la --experimental_command_profile marca para capturar perfiles de Java Flight Recorder de varios tipos (tiempo de CPU, tiempo de pared, asignaciones de memoria y contención de bloqueo).

Se puede usar la --starlark_cpu_profile marca para escribir un perfil de pprof del uso de CPU 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 de código exacta que lo causa.

Cómo habilitar 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.4.jar> \
  --host_jvm_args=-DRULE_MEMORY_TRACKER=1

Estos inician el servidor en el modo de seguimiento de memoria. Si los olvidas incluso para una invocación de Bazel, el servidor se reiniciará y deberás comenzar de nuevo.

Cómo usar el Seguimiento de memoria

Como ejemplo, observa el objetivo foo y ve qué hace. Para ejecutar solo el análisis y no la fase de ejecución de la compilación, agrega la --nobuild marca.

$ 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

Para desglosarlo por clase de regla, usa 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 va 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 con líneas anotadas:

$ 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)