Optimización del rendimiento

Informar un problema Ver fuente Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Cuando se escriben reglas, el error de rendimiento más común es desviar o copiar datos que se acumulan desde 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.

Usar 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 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 cualquier compactación de los depsets, excepto para fines de 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, aún es 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 listas:

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ápida, esta opción reduce el consumo de memoria de tus reglas (en ocasiones, 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. 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 de acceso, diferida al tiempo de 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 con 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 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 valores de salida

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 un indicador SIGQUIT (kill -3 $(bazel info server_pid)) para obtener un volcado de subproceso en el archivo $(bazel info output_base)/server/jvm.out.

Como es posible que no puedas ejecutar bazel info si se cuelga Bazel, 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

Bazel escribe un perfil JSON en command.profile.gz en la base de salida de forma predeterminada. Puedes configurar la ubicación con 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, haz clic en "Cargar" y elige el archivo de perfil (posiblemente comprimido). Para obtener resultados más detallados, haz clic en los cuadros de la esquina inferior izquierda.

Puedes usar estos controles del teclado para navegar:

  • Presiona 1 para activar el modo "seleccionar". En este modo, puedes seleccionar cajas particulares 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 activar el modo "pan". Luego, arrastra el mouse para mover la vista. También puedes usar a/d para moverte hacia la izquierda o la derecha.
  • Presiona 3 para activar el modo "zoom". Luego, arrastra el mouse para hacer zoom. También puedes usar w y s para acercar y alejar la imagen.
  • Presiona 4 para activar el modo "timing", en el que puedes medir la distancia entre dos eventos.
  • Presiona ? para conocer todos los controles.

Información del perfil

Perfil de ejemplo:

Perfil de ejemplo

Figura 1: Perfil de ejemplo.

Existen algunas filas especiales:

  • action counters: Muestra cuántas acciones simultáneas están en curso. Haz clic en él para ver el valor real. Debería aumentar hasta el valor de --jobs en compilaciones limpias.
  • cpu counters: Para cada segundo de la compilación, muestra la cantidad de CPU que usa Bazel (un valor de 1 equivale a un núcleo que está ocupado al 100%).
  • Critical Path: Muestra un bloque para cada acción en la ruta crítica.
  • grpc-command-1: Es el subproceso principal de Bazel. Es útil para obtener una imagen de alto nivel de lo que hace Bazel, por ejemplo, "Launch Bazel", "evaluateTargetPatterns" y "runAnalysisPhase".
  • Service Thread: Muestra las pausas menores y mayores de la recolección de elementos no usados (GC).

Otras filas representan 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:

  • La fase de análisis (runAnalysisPhase) es más lenta de lo esperado, en especial en las compilaciones incrementales. Esto puede ser un signo de una implementación de reglas deficiente, por ejemplo, una que aplana los conjuntos de dependencias. La carga de paquetes puede ser lenta debido a una cantidad excesiva de destinos, macros complejas o globs recursivos.
  • Acciones lentas individuales, especialmente las de la ruta crítica Es posible divirtir acciones grandes en varias acciones más pequeñas o reducir el conjunto de dependencias (transitivas) para acelerarlas. También verifica si hay un valor alto no PROCESS_TIME inusual (como REMOTE_SETUP o FETCH).
  • Los cuellos de botella, es decir, una pequeña cantidad de subprocesos están ocupados mientras que todos los demás están inactivos o esperando el resultado (consulta entre 15 y 30 segundos en la captura de pantalla anterior). Es probable que la optimización de esto requiera modificar las implementaciones de reglas o Bazel en sí para introducir más paralelismo. Esto también puede ocurrir cuando hay una cantidad inusual de recolección de elementos no utilizados.

Formato del archivo de perfil

El objeto de nivel superior contiene metadatos (otherData) y los datos de seguimiento reales (traceEvents). Los metadatos contienen información adicional, como 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 proporcionan en microsegundos. La categoría (cat) es uno de los valores de enumeración de ProfilerTask. Ten en cuenta que algunos eventos se combinan si son muy cortos y están cerca unos de otros. Pasa --noslim_json_profile si deseas evitar la combinación de 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 la compilación 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 postprocesar y analizar con el comando analyze-profile:

$ bazel analyze-profile /tmp/prof

De forma predeterminada, imprime información de análisis de resumen para el archivo de datos de perfil especificado. Esto incluye estadísticas acumulativas sobre diferentes tipos de tareas en cada fase 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 dedicado a 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 verificar el uso de memoria de tu regla. Si ocurre un problema, puedes volcar el montón para encontrar la línea de código exacta 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:$(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 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

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)

Para observar hacia 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 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 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)