Servicio al Cliente

clientes@chileguia.cl
Categorias

Problemas fundamentales de diseño del sistema Gradle Build

Como ingeniero de infraestructura, se dedica mucho tiempo apoyando entornos de desarrollo. Este trabajo incluye soporte de compilación (principalmente JVM / Android como plataformas de destino), así como escribir herramientas de CLI, soporte de CI, servicios de backend de infraestructura de escritura, Kubernetes, etc.

Tabla de contenido

  • Problemas de diseño y soluciones 
    • Fase de confiuración 
      • La configuración no está en caché
      • La configuración es serial
    • Fase de ejecución
      • Los módulos no están construidos contra ABI
  • Conclusión

Notas:

  • "Module" en el artículo significa "Project" en terminología de Gradle API
  • "Project" en el artículo significa todo el proyecto, incluidos todos sus módulos
  • El artículo cubre Gradle a partir de 5.0
  • Si encuentra un error, inexactitud o una mentira descarada, comuníquese, todas las ediciones se acreditarán a sus autores, o simplemente puede destruirme en los comentarios, también está bien
  • El artículo es una opinión de Artem Zinnatullin, que no forma parte del equipo de Gradle, ni refleja las opiniones del empleador de Artem.

Problemas de diseño y soluciones

A diferencia de Bazel y Buck, Gradle, desafortunadamente, no escala bien para proyectos de módulos múltiples. En términos generales, alrededor de ~ 500 módulos La sobrecarga de Gradle es imposible de ignorar y empeora a medida que se agregan más módulos.

Problemas de diseño: fase de configuración

  • Gradle: Build Lifecycle - Inicialización - Configuración - Ejecución

Como cualquier otro sistema de compilación, Gradle necesita saber qué es lo que necesita construir: módulos y tareas en módulos. Para resolverlo, Gradle necesita evaluar los archivos de configuración (ya sea basados ​​en Groovy o basados ​​en build.gradle Kotlin build.gradle.kts).

Durante la fase de configuración, los módulos pueden aplicar complementos, registrar tareas, realizar cálculos, realizar efectos secundarios como E / S de archivos o redes y leer / modificar la configuración de otros módulos.

Problema de diseño: la configuración no está en caché

Gradle ejecuta la fase de configuración cada vez que construye.

Podría ejecutarlo gradle assembley luego ejecutarlo una y otra vez, y cada vez tendré que esperar a que Gradle configure el proyecto aunque no haya cambiado nada.

Esta es una desafortunada promesa de Gradle API. Los complementos y / o el código de configuración del usuario pueden y a veces se basan en el hecho de que la fase de configuración se ejecuta cada vez para realizar efectos secundarios, como la configuración de la versión del módulo en función del tiempo actual de Unix.

Sin embargo, si ninguno de sus módulos realiza efectos secundarios, Gradle aún evalúa la configuración para cada compilación.

  • El siguiente gráfico es una ilustración, representa aproximadamente la situación en nuestro proyecto con ~ 830 módulos. Hardware: MacBook Pro 2018, Intel Core i9, 32 GB de memoria, todos los complementos innecesarios de Gradle desactivados, Gradle 5.0 y AGP 3.4.0-alpha07.

 

Tiempo de configuración de Gradle dependiendo del número de módulos

Gradle debe almacenar en caché y evitar la configuración de módulos.

Solución propuesta: Modo de configuración estricto: pureza

Para permitir que Gradle almacene en caché la configuración de un módulo dado, necesitamos agregar una API opcional que permita que un módulo declare que su configuración es 100% reproducible (lo que significa que el código / complementos de configuración no dependen de entradas que cambian sin Aviso de Gradle).

Afortunadamente, muchas veces la configuración ya es pura. Un módulo generalmente aplica uno o más complementos para admitir un lenguaje de programación particular como Java o Kotlin y luego define algunas dependencias. Los efectos secundarios deben declararse como Tareas y Gradle sabe cómo manejarlos de manera eficiente.

Por ejemplo, la configuración del siguiente módulo debe ser almacenable en caché tal cual, no hay nada impuro al respecto *:

         apply plugin: 'java-library'

         dependencies {
             api 'io.reactivex.rxjava2:rxjava:2.2.1'
         }

        dependencies {
            implementation 'com.google.protobuf:protobuf-lite:3.0.1'       
            implementation project(':internal-lib')
        }

       dependencies {
           testImplementation 'org.junit:junit:5.1.0'
       }

 

  • Por supuesto, uno necesita saber si un complemento dado tiene efectos secundarios bajo el capó, pero la mayoría de los complementos no deberían hacerlo por diseño. Gradle tiene tareas destinadas a representar efectos secundarios (¡y las tareas pueden ser incrementales y compilables en caché!).

Sin embargo, la configuración aún debe invalidarse si:

  • build.gradle del módulo ha cambiado (idealmente, solo si el código real ha cambiado)
  • La configuración de un módulo dependiente ha cambiado ya que puede traer / eliminar dependencias transitivas (idealmente, solo si hay un cambio significativo real)

La API propuesta parece ser parte de la fase de inicialización ( settings.gradle) en lugar de la fase de configuración ( build.gradle) para que Gradle sepa si la configuración se puede almacenar en caché por adelantado antes de la fase de configuración real.

API propuesta:

                 settings.gradle include ':module-a'
                 project(':module-a').pure = true

Resultado:

Tal cambio permitirá a Gradle conocer la configuración de qué módulos debe almacenar en caché (en memoria / disco / memoria caché de compilación) y omitirla para las compilaciones posteriores, reduciendo significativamente la penalización de la fase de configuración para cada compilación después de la primera.

Problema de diseño: la configuración es serial

Gradle configura los módulos uno por uno, aumentando linealmente el tiempo de configuración con la cantidad de módulos en el proyecto.

Este es un desafortunado efecto secundario del diseño de Gradle API.

Gradle API permite que los complementos y / o el código de usuario lean / modifiquen la configuración de otros módulos u otro estado global. Por lo tanto, no hay una manera fácil para que Gradle configure los módulos en paralelo.

Este problema combinado con "Problema de diseño: la configuración no está en caché" da como resultado una ~20 secondspenalización del tiempo de configuración para cada compilación de Gradle en el proyecto en el que trabajo (~ 830 módulos).

Gradle debe configurar módulos en paralelo.

Solución propuesta: Modo de configuración estricto: aislamiento

Para permitir que Gradle configure módulos en paralelo, necesitamos agregar una API opcional que permita que un módulo declare que su configuración está aislada de la configuración de otros módulos, lo que significa que el módulo no modifica el estado global o el estado de otros módulos y no depende del orden de inicialización (está bien leer el estado global inmutable que ya se ha inicializado).

Para el contexto, Buck puede configurar nuestro proyecto en 2 seconds(sin almacenamiento en caché) que incluye el análisis y el procesamiento de archivos de compilación definidos en Python / Skylark / Starlark, Gradle necesita ~20 secondseso.

API propuesta:

settings.gradle

          include ':module-a'
          project(':module-a').isolated = true

La API propuesta parece tener que ser parte de la fase de inicialización ( settings.gradle) para que Gradle sepa esto por adelantado, antes de la fase de configuración.

Resultado:

Tal cambio permitirá a Gradle saber qué módulos se pueden configurar en paralelo, lo que hace que la fase de configuración sea significativamente más rápida.

Combinados, la "Pureza" y el "Aislamiento" de la Configuración permitirán a Gradle maximizar la eficiencia de la Fase de Configuración al almacenarla en caché / evitarla, así como hacerlo en paralelo si no se puede evitar la configuración.

Problemas de diseño: fase de ejecución

Problema de diseño: los módulos no están construidos contra ABI

Considere el siguiente proyecto:

  • Módulo A
  • Módulo B
  • A depende de B

Normalmente, para construir Auno primero necesitaría construir (y todas sus dependencias). Que es lo que hace Gradle.

B-> A

Esto, sin embargo, es ineficiente . No solo en grandes proyectos, sino en general. Sin embargo, se pone especialmente mal en grandes proyectos.

Debido a un orden de compilación tan estricto, Gradle a menudo no puede aprovechar el hardware (particularmente las CPU multinúcleo) en su totalidad. Ciertos módulos se convierten en "bloqueos" de larga vida en el gráfico de compilación, bloqueando la compilación de docenas de otros módulos solo porque ellos y sus dependencias tienen que construirse primero.

B-> A

Gradle debería construir módulos contra ABI de sus dependencias.

Solución propuesta: Construir contra ABI

Como se mencionó anteriormente, considere la siguiente configuración:

  • Módulo A
  • Módulo B
  • A depende de B

Normalmente, para construir Anecesitaría construir Bprimero y todas sus dependencias.

Pero en realidad, Ano necesita Bque se construya en absoluto. Lo importante para Ael ABI de B.

Ahora tenemos que dar un paso atrás y aclarar que ABI tiene diferentes significados. Generalmente significa Interfaz Binaria de Aplicación, pero el nivel de detalle puede variar en el compilador / enlazador / etc.

Sin embargo, en aras de esta discusión, puede pensar en ABI como una versión eliminada de entidades públicas (clases, métodos, interfaces, etc.) en un módulo.

Por ejemplo, considere la siguiente clase de Java:

                               public class MyClass {

                                   private String x;

                                   public void a() {
                                   this.x = b();
                                   }

                                   private String b() {
                                   return incrediblyComplicatedLogic.execute().explain()
                                   }

                                   public static Runnable c(String s) {
                                   return () -> { … }
                                   }
                                 }

Una versión de ABI legible para humanos MyClass puede representarse así:

                                   public class MyClass {
                                     public void a() { throws NotImplementedException() }
                                     public static Runnable c(String s)
{ throws NotImplementedException() }
                                   }

B-> B \ '

Como puede ver, solo quedaron miembros públicos . Pero eso no es todo, ¡los detalles de implementación también se han ido !

Como puede imaginar, la extracción de ABI puede ser mucho más rápida que la compilación real , para calcular ABI no necesita compilar el código.

Si busca comprender cómo ABI se puede representar en diferentes idiomas, aquí hay algunos ejemplos:

  • Casi cualquier lenguaje JVM puede tener ABI en forma de archivos de clase con solo miembros públicos y sin detalles de implementación *
  • C (y opcionalmente C ++) tiene archivos de encabezado que representan ABI
  • La compilación de cadenas de herramientas diseñadas para compilarse contra fuentes puede tener ABI representado como archivos fuente con solo miembros públicos y sin detalles de implementación, de manera similar a los archivos de encabezado *

* Hay advertencias, por supuesto:

  • Algunos idiomas como Kotlin tienen una línea de tiempo de compilación. Esto requiere que ABI contenga el código fuente / byte / máquina de las funciones que deben estar en línea, lo que requiere al menos una compilación parcial
  • Algunos idiomas permiten la generación de código en tiempo de compilación. Esto puede dar lugar a situaciones difíciles cuando el compilador debe generar primero el tipo expuesto públicamente, lo que requiere al menos una compilación parcial / generación de código.

Sin embargo, en muchos casos ABI es extraíble y esta técnica se utiliza con mucho éxito en Bazel y Buck, lo que les permite utilizar completamente los recursos de hardware al eliminar bloqueos de larga vida del gráfico de construcción.

B-> B \ '-> A

Curiosamente, Gradle ya conoce los detalles de ABI para la compilación incremental de muchos idiomas que admite.

Cambios propuestos:
  • Gradle debe extraer ABI de módulos escritos en lenguajes populares y construir módulos contra ABI
  • Gradle debe vincularse con el binario real producido por un módulo si se solicita el binario ejecutable o fat / uber
  • Gradle debería extender la evitación de compilación actual basada en apiimplementationa la ABI, por lo tanto, no reconstruir módulos si la ABI de sus dependencias no cambió (que es un caso muy común cuando los cambios solo afectan los detalles de implementación de funciones / clases / etc.)

Resultado :

Tal cambio permitirá a Gradle utilizar hardware con mucha mayor eficiencia, ahorrando una enorme cantidad de tiempo en la fase de ejecución.

Conclusión

Gradle es un gran sistema de construcción genérico. Si bien tiene ventajas muy fuertes como la compilación realmente incremental, tiene problemas de diseño importantes que, si no se solucionan, provocarán que Gradle pierda su terreno ahora que Bazel está cobrando impulso después de años de ser un sistema de código cerrado.

Últimos de Foro (0)
Publicidad
Chileguía

El directorio en línea más completo para encontrar detalles de negocios, contactos, productos, servicios y precios.

info@chileguia.cl

© Chileguia. Todos los derechos reservados.
Implementado por Chileguia