Bazel es complejo y realiza muchas acciones diferentes durante una 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 para detectar problemas de rendimiento de la 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 regresiones de rendimiento de compilación.
Compilaciones limpias vs. compilaciones incrementales
Una compilación limpia es aquella que compila todo desde cero, mientras que una compilación incremental reutiliza un trabajo ya completado.
Te sugerimos que analices las compilaciones incrementales y limpias por separado, en especial cuando recopilas o agregas métricas que dependen del estado de las caché de Bazel (por ejemplo, métricas de tamaño de la solicitud de compilación). También representan dos experiencias del usuario diferentes. En comparación con iniciar una compilación limpia desde cero (lo que tarda más 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é ya suele estar tibia).
Puedes usar el campo CumulativeMetrics.num_analyses
en el BEP para clasificar las compilaciones. Si es num_analyses <= 1
, es una compilación limpia. De lo contrario, podemos categorizarla ampliamente para que sea una compilación incremental. El usuario podría haber cambiado a diferentes marcas o destinos, lo que provocaría una compilación limpia de manera efectiva. Cualquier definición más rigurosa de incrementalidad probablemente deberá adoptar la forma de una heurística, por ejemplo, observar 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 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 espera en un clúster remoto). Por lo tanto, puede ser útil usar métricas determinísticas como un 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 gráficos 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 varias fases de compilación y usar las siguientes métricas como métricas de proxy para el trabajo realizado en cada fase:
PackageMetrics.packages_loaded
: Es la cantidad de paquetes que se cargaron de forma correcta. En este caso, una regresión representa más trabajo que se debe realizar para leer y analizar cada archivo BUILD adicional en la fase de carga.TargetMetrics.targets_configured
: Representa la cantidad de destinos 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 a la necesidad de construir el grafo de su cierre transitivo.
- Usa cquery para encontrar dónde se podrían haber agregado las dependencias nuevas.
ActionSummary.actions_created
: Representa las acciones creadas en la compilación, y una regresión representa más trabajo en la construcción del gráfico de acciones. Ten en cuenta que esto también incluye acciones sin usar que podrían no haberse ejecutado.- Usa aquery para depurar regresiones. Te sugerimos que comiences con
--output=summary
antes de desglosar con--skyframe_state
.
- Usa aquery para depurar regresiones. Te sugerimos que comiences con
ActionSummary.actions_executed
: Es 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 descubrir qué tipo de acciones se ejecutaron (además).
- El BEP escribe las estadísticas de acción
BuildGraphSummary.outputArtifactCount
: Es la cantidad de artefactos creados por las acciones ejecutadas.- Si la cantidad de acciones ejecutadas no aumentó, es probable que se haya cambiado la implementación de una 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 extraigas estas métricas sean compilaciones limpias.
Notamos que una regresión en cualquiera de estas métricas puede estar acompañada de regresiones en el tiempo de pared, en el tiempo de la CPU y en el uso de memoria.
Uso de recursos locales
Bazel consume una variedad de recursos en tu máquina local (para analizar el gráfico de compilación y controlar la ejecución, y para ejecutar acciones locales). Esto puede afectar el rendimiento o la disponibilidad de tu máquina para realizar la compilación, además de 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 de pared, de la CPU y del sistema. Puedes
usar bazel-bench para 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 se produce un muro de tiempo, te sugerimos recopilar un perfil de seguimiento de JSON y buscar las diferencias. De lo contrario, es probable que sea más eficiente investigar otras métricas regresivas, ya que podrían haber afectado el tiempo.
El tiempo de CPU es el tiempo que dedica la CPU a ejecutar el código de usuario.
- Si el tiempo de CPU disminuye en dos confirmaciones del proyecto, te sugerimos recopilar un perfil de CPU de Starlark. También deberías usar
--nobuild
para restringir la compilación a la fase de análisis, ya que es allí donde se realiza la mayor parte del trabajo pesado de la CPU.
- Si el tiempo de CPU disminuye en dos confirmaciones del proyecto, te sugerimos recopilar un perfil de CPU de Starlark. También deberías usar
El tiempo del sistema es el tiempo que dedica la CPU al kernel.
- Si el tiempo del sistema disminuye, esto se correlaciona, en su mayoría, 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 seguimiento de JSON recopila el promedio de carga del sistema durante la invocación.
Figura 1: Perfil que incluye la carga promedio 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 intentes ajustar --local_cpu_resources
y --local_ram_resources
, en especial en entornos de contenedores (al menos hasta que se combine #16512).
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
: Es la cantidad de memoria usada en bytes después de una llamada aSystem.gc()
.- El banco de Bazel también proporciona comparativas para esta métrica.
- Además, existen
peak-heap-size
,max-heap-size
,used-heap-size
ycommitted-heap-size
(consulta la documentación), pero son menos relevantes.
MemoryMetrics.peak_post_gc_heap_size
de BEP: Es el tamaño máximo del montón de JVM en bytes después de la recolección de elementos no utilizados (se requiere configurar--memory_profile
para forzar una recolección de elementos no utilizados completa).
Por lo general, una regresión en el uso de la memoria es el resultado de una regresión en las métricas de tamaño de la solicitud de compilación, que a menudo se debe a la adición de dependencias o a un cambio en la implementación de las reglas.
Para analizar el uso de memoria de Bazel en un nivel más detallado, te recomendamos que uses el generador de perfiles de memoria integrado para las reglas.
Generación de perfiles de memoria de trabajadores persistentes
Si bien los trabajadores persistentes pueden ayudar a acelerar las compilaciones de manera significativa (en especial para los lenguajes interpretados), su uso de memoria puede ser problemático. 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 nombre nemotécnico).
El Generador de perfiles de seguimiento de JSON también
recopila el uso de memoria de los trabajadores persistentes durante la invocación pasando la marca
--experimental_collect_system_network_usage
(nueva en Bazel 6.0).
Figura 2: Perfil que incluye el uso de memoria de los trabajadores.
Disminuir el valor de --worker_max_instances
(valor predeterminado 4) puede ayudar a reducir la cantidad de memoria que usan los trabajadores persistentes. Trabajamos activamente para
hacer que el programador y el administrador de recursos de Bazel sean más inteligentes, de modo 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 la compilación.
Si usas la ejecución remota para tus compilaciones, te recomendamos que consideres
supervisar el tráfico de red durante la invocación con el
proto NetworkMetrics.SystemNetworkStats
del BEP
(requiere pasar --experimental_collect_system_network_usage
).
Además, los perfiles de seguimiento JSON
te permiten ver el uso de la red en todo el sistema durante el transcurso de la compilación
pasando la marca --experimental_collect_system_network_usage
(nuevo en Bazel
6.0).
Figura 3: Perfil que incluye el uso de red en todo el sistema.
Un uso de red alto, pero más bien 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. Para ello, pasa
--remote_download_minimal
.
Esto acelerará tus compilaciones al evitar la descarga de artefactos intermedios innecesarios.
Otra opción es configurar una caché de disco local para ahorrar en el ancho de banda de descarga.