Optimización del rendimiento

Informar un problema Ver fuente Por la noche · 7.2 · 7.1 · 7.0 · 6.5 · 6.4

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.

Reduce la cantidad de llamadas a depset

Llamar a depset dentro de un bucle suele ser un error. Puede dar lugar a desilusiones con un anidamiento muy profundo, 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 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 gastos.

  • ¿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á todas las instancias de tu regla.

  • Si los args 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 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 manualmente 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 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

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

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