Desglose del rendimiento de la compilación

Informar un problema Ver fuente Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

Compilaciones limpias vs. incrementales

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

Sugerimos que analices las compilaciones limpias y las incrementales por separado, en especial cuando recopiles o agregues métricas que dependen del estado de las memorias caché de Bazel (por ejemplo, las 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 (que lleva más tiempo debido a una caché fría), las compilaciones incrementales se producen con mucha más frecuencia a medida que los desarrolladores iteran el código (por lo general, son más rápidas, ya que la caché suele estar ya activa).

Puedes usar el campo CumulativeMetrics.num_analyses en el BEP para clasificar compilaciones. Si es num_analyses <= 1, se trata de una compilación limpia; de lo contrario, podemos categorizarla de forma general como una compilación incremental, ya que el usuario podría haber cambiado a diferentes marcas o destinos, lo que generaría una compilación limpia efectiva. Es probable que cualquier definición más rigurosa de la incrementalidad deba presentarse en forma de una heurística, por ejemplo, observando la cantidad de paquetes cargados (PackageMetrics.packages_loaded).

Métricas de compilación determinísticas como proxy del rendimiento de la compilación

Medir el rendimiento de la compilación puede ser difícil debido a la naturaleza no determinística 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 de 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 implicaciones significativas 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, y, por lo tanto, aumentan en complejidad y se vuelven más costosas de compilar.

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

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

    • Esto suele deberse a la adición de dependencias y a la necesidad de cargar su cierre transitivo.
    • Usa query o cquery para encontrar dónde se podrían haber agregado dependencias nuevas.
  2. 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.

    • Esto suele deberse a la adición de dependencias y a la necesidad de construir el gráfico de su cierre transitivo.
    • Usa cquery para encontrar dónde se podrían haber agregado 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 gráfico de acciones. Ten en cuenta que esto también incluye las acciones no utilizadas que podrían no haberse ejecutado.

  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 acción más ejecutados. De forma predeterminada, recopila los 20 tipos de acciones principales, pero puedes pasar --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 creados por las acciones ejecutadas.

    • Si no aumentó la cantidad de acciones ejecutadas, 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 extraes estas métricas sean compilaciones limpias.

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

Uso de recursos locales

Bazel consume una variedad de recursos en tu máquina local (tanto para analizar el gráfico de compilación y controlar la ejecución como para ejecutar acciones locales), lo que puede afectar el rendimiento o la disponibilidad de tu máquina para realizar la compilación y otras tareas.

Tiempo transcurrido

Quizás las métricas más susceptibles al ruido (y que pueden variar mucho de una compilación a otra) sean las de tiempo, en particular, el tiempo real, el tiempo de CPU y el tiempo del sistema. Puedes usar bazel-bench para obtener una comparativa de 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 transcurrido en el mundo real.

    • Si solo se produce una regresión en el tiempo de pared únicamente, te sugerimos que recopiles un perfil de seguimiento JSON y busques diferencias. De lo contrario, probablemente sería más eficiente investigar otras métricas que hayan disminuido, ya que podrían haber afectado el tiempo de pared.
  • El tiempo de CPU es el tiempo que la CPU invierte en ejecutar el código del usuario.

    • Si el tiempo de CPU disminuye entre dos confirmaciones del proyecto, te sugerimos que recopiles 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 donde se realiza la mayor parte del trabajo que requiere mucha CPU.
  • El tiempo del sistema es el tiempo que la CPU dedica al kernel.

    • Si el tiempo del sistema disminuye, se correlaciona principalmente con la E/S cuando Bazel lee archivos de tu sistema de archivos.

Generación de perfiles de carga en todo el sistema

Con la marca --experimental_collect_load_average_in_profiler introducida en Bazel 6.0, el generador de perfiles de seguimiento 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. Es posible que desees 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: Bazel info y el BEP.

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

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

Por lo general, una regresión en el uso de 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 deben a la adición de dependencias o a un cambio en la implementación de la regla.

Para analizar el espacio de memoria de Bazel a 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 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 cuánta memoria usan los trabajadores (por mnemónico).

El generador de perfiles de seguimiento de JSON también recopila el uso de memoria persistente del trabajador durante la invocación pasando la marca --experimental_collect_system_network_usage (novedad en Bazel 6.0).

Perfil que incluye el uso de 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 es 4) puede ayudar a reducir la cantidad de memoria que usan los trabajadores persistentes. Estamos trabajando activamente para que el administrador y el programador de recursos de Bazel sean más inteligentes, de modo que este ajuste fino se requiera con menos frecuencia en el futuro.

Supervisión del tráfico de red para compilaciones remotas

En la ejecución remota, Bazel descarga los 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, te recomendamos que supervises 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 la compilación pasando la marca --experimental_collect_system_network_usage (novedad 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 uniforme, cuando se usa la ejecución remota podría indicar que la red es el cuello de botella en tu compilación. Si aún no lo usas, considera activar Build without the Bytes pasando --remote_download_minimal. Esto acelerará tus 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.