Desglose del rendimiento de la compilación

Informa un problema Ver código fuente

Bazel es complejo y realiza muchas acciones durante la compilación, algunas de las cuales pueden tener un impacto en el rendimiento. En esta página, se intenta asignar algunos de estos conceptos de Bazel a sus implicaciones en el rendimiento de la compilación. Si bien no es exhaustivo, incluimos algunos ejemplos de cómo detectar problemas de rendimiento de compilación a través de la extracción de métricas y lo que puedes hacer para solucionarlos. Con esto, esperamos que puedas aplicar estos conceptos cuando investigues las regresiones de rendimiento de compilación.

Compilaciones incrementales y limpias

Una compilación limpia es aquella que compila todo desde cero, mientras que una compilación incremental reutiliza algún trabajo ya completado.

Te recomendamos analizar las compilaciones incrementales y limpias por separado, en especial, cuando recopilas o agregas métricas que dependen del estado de las cachés de Bazel (por ejemplo, las métricas de tamaño de solicitud de compilación). También representan dos experiencias del usuario diferentes. En comparación con iniciar una compilación limpia desde cero (que tarda más tiempo debido a una caché en frío), las compilaciones incrementales ocurren con mucha más frecuencia a medida que los desarrolladores iteran en el código (por lo general, más rápido, ya que la caché suele estar lista).

Puedes usar el campo CumulativeMetrics.num_analyses en el BEP para clasificar compilaciones. Si es num_analyses <= 1, es una compilación limpia; de lo contrario, podemos categorizarla en términos generales; es probable que sea una compilación incremental; el usuario podría haber cambiado a marcas o diferentes destinos, lo que causó una compilación limpia de forma efectiva. Cualquier definición más rigurosa de incrementalidad probablemente venirá en forma de heurística, por ejemplo, observando la cantidad de paquetes cargados (PackageMetrics.packages_loaded).

Métricas de compilación deterministas como proxy para el rendimiento de la compilación

Medir el rendimiento de la compilación puede ser difícil debido a la naturaleza no determinista de ciertas métricas (por ejemplo, el tiempo de CPU de Bazel o los tiempos de cola en un clúster remoto). Como tal, puede ser útil usar métricas deterministas como proxy para la cantidad de trabajo que realiza Bazel, lo que, a su vez, afecta su rendimiento.

El tamaño de una solicitud de compilación puede tener una implicación significativa en el rendimiento de la compilación. Una compilación más grande podría representar más trabajo en el análisis y la construcción de los grafos de compilación. El crecimiento orgánico de las compilaciones se produce de forma natural con el desarrollo, a medida que se agregan o crean más dependencias, lo que aumenta su complejidad y su compilación es más costosa.

Podemos dividir este problema en las distintas fases de compilación y usar las siguientes métricas como métricas de proxy para el trabajo realizado en cada fase:

  1. PackageMetrics.packages_loaded: Es la cantidad de paquetes que se cargaron de forma correcta. Una regresión aquí representa más trabajo que se debe realizar para leer y analizar cada archivo BUILD adicional en la fase de carga.

    • A menudo, esto se debe a la adición de dependencias y la carga de su cierre transitivo.
    • Usa query / cquery para encontrar las dependencias nuevas que se puedan haber agregado.
  2. TargetMetrics.targets_configured: Representa la cantidad de objetivos y aspectos configurados en la compilación. Una regresión representa más trabajo en la construcción y el recorrido del grafo de destino configurado.

    • A menudo, esto se debe a la adición de dependencias y la construcción del grafo del cierre transitivo.
    • Usa cquery para descubrir dónde se agregaron las dependencias nuevas.
  3. ActionSummary.actions_created: Representa las acciones creadas en la compilación y una regresión representa más trabajo en la construcción del grafo de acción. Ten en cuenta que también se incluyen las acciones no utilizadas que podrían no haberse ejecutado.

    • Usa aquery para realizar regresiones de depuración. Te sugerimos que comiences con --output=summary antes de desglosar .
  4. ActionSummary.actions_executed: La cantidad de acciones ejecutadas. Una regresión representa directamente más trabajo en la ejecución de estas acciones.

    • El BEP escribe las estadísticas de acción ActionData que muestran los tipos de acciones más ejecutados. De forma predeterminada, recopila los 20 tipos de acciones principales, pero puedes pasar el --experimental_record_metrics_for_all_mnemonics para recopilar estos datos de todos los tipos de acciones que se ejecutaron.
    • Esto debería ayudarte a determinar qué tipo de acciones se ejecutaron (además).
  5. BuildGraphSummary.outputArtifactCount: Es la cantidad de artefactos que crearon las acciones ejecutadas.

    • Si la cantidad de acciones ejecutadas no aumentó, es probable que haya cambiado una implementación de regla.

Todas estas métricas se ven afectadas por el estado de la caché local, por lo que deberás asegurarte de que las compilaciones de las que extraes sean compilaciones limpias.

Notamos que una regresión en cualquiera de estas métricas puede ir acompañada de regresiones en tiempo real, tiempo de CPU y uso de memoria.

Uso de recursos locales

Bazel consume una variedad de recursos en tu máquina local (para analizar el grafo de compilación, impulsar la ejecución y ejecutar acciones locales), lo que puede afectar el rendimiento o la disponibilidad de la máquina cuando se realiza la compilación, así como otras tareas.

Tiempo transcurrido

Quizás las métricas más susceptibles al ruido (y pueden variar mucho de una compilación a otra) son el tiempo, en particular el tiempo real, el tiempo de CPU y el sistema. Puedes usar bazel-bench a fin de obtener una comparativa para estas métricas y, con una cantidad suficiente de --runs, puedes aumentar la importancia estadística de tu medición.

  • El tiempo real es el tiempo real transcurrido.

    • Si solo ocurre una regresión de tiempo, te recomendamos que recopiles un perfil de seguimiento JSON y busques diferencias. De lo contrario, sería más eficiente investigar otras métricas de regresión, ya que podrían haber afectado el tiempo.
  • El tiempo de CPU es el tiempo que dedica la CPU a la ejecución del código de usuario.

    • Si el tiempo de CPU vuelve en dos confirmaciones de proyecto, te sugerimos que recopiles un perfil de CPU de Starlark. Es probable que también debas usar --nobuild para restringir la compilación a la fase de análisis, ya que allí es donde se realiza la mayor parte del trabajo pesado de la CPU.
  • El tiempo del sistema es el que ocupa la CPU en el kernel.

    • Si hay una regresión del tiempo del sistema, está mayormente correlacionada con la E/S cuando Bazel lee archivos de tu sistema de archivos.

Creación de perfiles de carga en todo el sistema

Con la marca --experimental_collect_load_average_in_profiler que se introdujo en Bazel 6.0, el generador de perfiles de JSON recopila el promedio de carga del sistema durante la invocación.

Perfil que incluye el promedio de carga del sistema

Figura 1: Perfil que incluye el promedio de carga del sistema.

Una carga alta durante una invocación de Bazel puede indicar que Bazel programa demasiadas acciones locales en paralelo para tu máquina. Te recomendamos que ajustes --local_cpu_resources y --local_ram_resources, especialmente en entornos de contenedor (al menos hasta que #16512 se fusione).

Supervisa el uso de memoria de Bazel

Hay dos fuentes principales para obtener el uso de memoria de Bazel: info de Bazel y el BEP.

  • bazel info used-heap-size-after-gc: La cantidad de memoria usada en bytes después de una llamada a System.gc().

    • El banco de Bazel también proporciona comparativas de esta métrica.
    • Además, hay peak-heap-size, max-heap-size, used-heap-size y committed-heap-size (consulta la documentación), pero son menos relevantes.
  • MemoryMetrics.peak_post_gc_heap_size de BEP: Tamaño del tamaño máximo del montón de JVM en bytes después de la recolección (requiere la configuración de --memory_profile que intenta forzar una recolección de elementos no utilizados completa).

Una regresión en el uso de memoria suele ser el resultado de una regresión en las métricas de tamaño de solicitudes de compilación, que a menudo se deben a dependencias o a un cambio en la implementación de la regla.

Para analizar la huella de memoria de Bazel a un nivel más detallado, te recomendamos usar el generador de perfiles de memoria integrado para las reglas.

Creación de perfiles de memoria de trabajadores persistentes

Si bien los trabajadores persistentes pueden ayudar a acelerar las compilaciones de manera significativa (especialmente para los idiomas interpretados), su huella de memoria puede ser problemática. Bazel recopila métricas sobre sus trabajadores, en particular, el campo WorkerMetrics.WorkerStats.worker_memory_in_kb indica la cantidad de memoria que usan los trabajadores (por mnemónica).

El generador de perfiles de seguimiento de JSON también recopila el uso persistente de memoria de los trabajadores durante la invocación mediante la marca --experimental_collect_system_network_usage (nueva en Bazel 6.0).

Perfil que incluye el uso de la memoria de los trabajadores

Figura 2: Perfil que incluye el uso de memoria de los trabajadores.

Reducir el valor de --worker_max_instances (el valor predeterminado 4) puede ayudar a reducir la cantidad de memoria que usan los trabajadores persistentes. Estamos trabajando activamente para que el administrador de recursos y el programador de Bazel sean más inteligentes a fin de que este ajuste se requiera con menos frecuencia en el futuro.

Supervisa el tráfico de red para compilaciones remotas

En la ejecución remota, Bazel descarga artefactos que se compilaron como resultado de la ejecución de acciones. Por lo tanto, el ancho de banda de tu red puede afectar el rendimiento de tu compilación.

Si usas la ejecución remota para tus compilaciones, es posible que desees considerar supervisar el tráfico de red durante la invocación mediante el proto NetworkMetrics.SystemNetworkStats de BEP (requiere pasar --experimental_collect_system_network_usage).

Además, los perfiles de seguimiento JSON te permiten ver el uso de la red de todo el sistema durante la compilación pasando la marca --experimental_collect_system_network_usage (nueva en Bazel 6.0).

Perfil que incluye el uso de la red en todo el sistema

Figura 3: Perfil que incluye el uso de la red en todo el sistema.

Un uso de red alto, pero bastante plano cuando se usa la ejecución remota puede indicar que la red es el cuello de botella en tu compilación; si aún no la usas, considera activar la compilación sin los bytes pasando --remote_download_minimal. Esto acelerará las compilaciones, ya que evitará la descarga de artefactos intermedios innecesarios.

Otra opción es configurar una caché de disco local para ahorrar ancho de banda de descarga.