Anotación incremental en proceso para acelerar las construcciones de gradle

imagen


A partir de las versiones Gradle 4.7 y Kotlin 1.3.30, fue posible obtener un ensamblaje incremental acelerado de proyectos debido a la operación correcta del procesamiento incremental de anotaciones. En este artículo, entendemos cómo funciona la teoría de la compilación incremental en Gradle, qué se debe hacer para liberar todo su potencial (sin perder la generación de código al mismo tiempo) y qué tipo de aumento en la velocidad de los ensamblajes incrementales se puede lograr mediante la activación del procesamiento incremental de anotaciones en la práctica.


Cómo funciona la compilación incremental


Las construcciones incrementales en Gradle se implementan en dos niveles. El primer nivel es cancelar el inicio de los módulos de recompilación utilizando evitación de compilación . El segundo es la compilación incremental directa, iniciando el compilador dentro del marco de un módulo solo en aquellos archivos que han cambiado, o dependen directamente de los archivos cambiados.


Consideremos evitar la compilación en un ejemplo (tomado de un artículo de Gradle) de un proyecto de tres módulos: aplicación , núcleo y utilidades .


La clase principal del módulo de la aplicación (depende del núcleo ):


public class Main { public static void main(String... args) { WordCount wc = new WordCount(); wc.collect(new File(args[0]); System.out.println("Word count: " + wc.wordCount()); } } 

En el módulo central (depende de las utilidades ):


 public class WordCount { // ... void collect(File source) { IOUtils.eachLine(source, WordCount::collectLine); } } 

En el módulo de utilidades :


 public class IOUtils { void eachLine(File file, Callable<String> action) { try { try (BufferedReader reader = new BufferedReader(new FileReader(file))) { // ... } } catch (IOException e) { // ... } } } 

El orden de la primera compilación de los módulos es el siguiente (de acuerdo con el orden de las dependencias):


1) utils
2) núcleo
3) aplicación


Ahora considere lo que sucede cuando cambia la implementación interna de la clase IOUtils:


 public class IOUtils { // IOUtils lives in project `utils` void eachLine(File file, Callable<String> action) { try { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8") )) { // ... } } catch (IOException e) { // ... } } } 

Este cambio no afecta al módulo ABI. ABI (Application Binary Interface) es una representación binaria de la interfaz pública del módulo ensamblado. En el caso en que el cambio se relacione solo con la implementación interna del módulo y no afecte su interfaz pública de ninguna manera, Gradle utilizará la evitación de compilación e iniciará la recompilación solo del módulo utils . Si la ABI del módulo de utilidades se ve afectada (por ejemplo, aparece un método público adicional o cambia la firma del existente), entonces la compilación del módulo principal comenzará adicionalmente, pero el módulo de la aplicación dependiente del núcleo no se volverá a compilar transitivamente si la dependencia en él está conectada a través de la implementación .



Ilustración de la evitación de compilación a nivel del módulo del proyecto


El segundo nivel de incremento es el incremento en el nivel de inicio del compilador para archivos modificados directamente dentro de módulos individuales.


Por ejemplo, agregue una nueva clase al módulo principal :


 public class NGrams { // NGrams lives in project `core` // ... void collect(String source, int ngramLength) { collectInternal(StringUtils.sanitize(source), ngramLength); } // ... } 

Y en utilidades :


 public class StringUtils { static String sanitize(String dirtyString) { ... } } 

En este caso, en ambos módulos, solo se deben volver a compilar dos archivos nuevos (sin afectar a WordCount y IOUtils existentes y no modificados), ya que no hay dependencias entre las clases nuevas y antiguas.


Por lo tanto, el compilador incremental analiza las dependencias entre clases y solo vuelve a compilar:


  • clases que contienen cambios
  • clases que dependen directamente de las clases cambiantes


    Procesamiento de anotación incremental


    ingrese la descripción de la imagen aquí



Generar código usando APT y KAPT reduce el tiempo que lleva escribir y depurar código repetitivo, pero el procesamiento de anotaciones puede aumentar significativamente el tiempo de construcción. Para empeorar las cosas, durante mucho tiempo, el procesamiento de anotaciones rompió fundamentalmente las posibilidades de compilación incremental en Gradle.


Cada procesador de anotaciones en un proyecto le dice al compilador información sobre la lista de anotaciones que procesa. Pero desde el punto de vista del ensamblaje, el procesamiento de anotaciones es un cuadro negro: Gradle no sabe qué hará el procesador, en particular, qué archivos generará y dónde. Hasta Gradle 4.7, la compilación incremental se deshabilitaba automáticamente en aquellos conjuntos de origen donde se usaban procesadores de anotación.


Con el lanzamiento de Gradle 4.7, la compilación incremental ahora admite el procesamiento de anotaciones, pero solo para APT. En KAPT, el soporte para la anotación incremental se ha introducido con Kotlin 1.3.30. También requiere soporte de bibliotecas que proporcionan procesadores de anotaciones. Los desarrolladores de procesadores de anotaciones tienen la oportunidad de establecer explícitamente la categoría del procesador, informando a Gradle de la información necesaria para que funcione la compilación incremental.


Categorías del procesador de anotaciones


Gradle admite dos categorías de procesadores:


Aislamiento : dichos procesadores deben tomar todas las decisiones para la generación de código basándose únicamente en la información de AST que está asociada con un elemento de una anotación particular. Esta es la categoría más rápida de procesadores de anotaciones, ya que Gradle puede no reiniciar el procesador y usar los archivos que generó previamente si no hubiera cambios en el archivo fuente.


Agregación : se utiliza para procesadores que toman decisiones basadas en varias entradas (por ejemplo, análisis de anotaciones en varios archivos a la vez o en el estudio de AST, al que se puede acceder de forma transitiva desde un elemento anotado). Cada vez, Gradle iniciará el procesador para archivos que usan anotaciones del procesador de agregación, pero no volverá a compilar los archivos que genera si no hay cambios en ellos.


Para muchas bibliotecas populares basadas en la generación de código, el soporte de compilación incremental ya está implementado en las últimas versiones. Vea la lista de bibliotecas que lo admiten aquí .


Nuestra experiencia implementando procesamiento de anotaciones incrementales


Ahora, para los proyectos que comienzan desde cero y usan las últimas versiones de bibliotecas y complementos de Gradle, es probable que las compilaciones incrementales estén activas de manera predeterminada. Pero la mayor parte del aumento en la productividad del ensamblaje se puede lograr mediante la incrementalidad del procesamiento de anotaciones en proyectos grandes y de larga duración. En este caso, puede ser necesaria una actualización masiva de la versión. ¿Vale la pena en la práctica? A ver!


Entonces, para que el procesamiento incremental de anotaciones funcione, necesitamos:


  • Gradle 4.7+
  • Kotlin 1.3.30+
  • Todos los procesadores de anotaciones en nuestro proyecto deben tener su apoyo. Esto es muy importante, porque si en un solo módulo al menos un procesador no admite incrementalidad, Gradle lo deshabilitará para todo el módulo. ¡Todos los archivos en el módulo serán compilados nuevamente cada vez! Una de las opciones alternativas para obtener soporte para la compilación incremental sin actualizar las versiones es la eliminación de todo el código utilizando procesadores de anotaciones en un módulo separado. En los módulos que no tienen procesadores de anotaciones, la compilación incremental funcionará bien

Para detectar procesadores que no satisfacen la última condición, puede ejecutar el ensamblaje con el indicador -Pkapt.verbose = true . Si Gradle se vio obligado a deshabilitar el procesamiento de anotaciones incrementales para un solo módulo, en el registro de compilación veremos un mensaje sobre qué procesadores y en qué módulos está sucediendo esto (vea el nombre de la tarea):


 > Task :common:kaptDebugKotlin w: [kapt] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: toothpick.compiler.factory.FactoryProcessor (NON_INCREMENTAL), toothpick.compiler.memberinjector.MemberInjectorProcessor (NON_INCREMENTAL). 

En nuestro proyecto de biblioteca con procesadores de anotaciones no incrementales, había 3:


  • Palillo de dientes
  • Habitacion
  • PermisosDispatcher

Afortunadamente, estas bibliotecas son compatibles activamente, y sus últimas versiones ya tienen soporte incremental. Además, todos los procesadores de anotaciones en las últimas versiones de estas bibliotecas tienen una categoría óptima: aislar. En el proceso de subir las versiones, tuve que lidiar con la refactorización debido a cambios en la API de la biblioteca Toothpick, que afectó a casi todos los módulos nuestros. Pero en este caso, tuvimos suerte, y resultó ser una refactorización completamente automática utilizando los nombres de reemplazo automático de los métodos de biblioteca pública utilizados.


Tenga en cuenta que si usa la biblioteca de salas , deberá pasar explícitamente la marca room.incremental: true al procesador de anotaciones. Un ejemplo En el futuro, los desarrolladores de Room planean habilitar esta bandera por defecto.


Para las versiones de Kotlin 1.3.30-1.3.50, debe habilitar el soporte para el procesamiento incremental de anotaciones explícitamente a través de kapt.incremental.apt = true en el archivo gradle.properties del proyecto. A partir de la versión 1.3.50, esta opción se establece en true de forma predeterminada.


Perfiles incrementales de ensamblaje


Después de que se hayan generado las versiones de todas las dependencias necesarias, es hora de probar la velocidad de las compilaciones incrementales. Para hacer esto, utilizamos el siguiente conjunto de herramientas y técnicas:


  • Escaneo de construcción Gradle
  • gradle-profiler
  • Para ejecutar scripts con procesamiento de anotación incremental habilitado y deshabilitado, se usó la propiedad gradle kapt.incremental.apt = [true | false]
  • Para obtener resultados consistentes e informativos, las asambleas se generaron en un entorno de CI separado. La incrementalidad de construcción se reprodujo utilizando gradle-profiler

gradle-profiler permite preparar scripts declarativamente para puntos de referencia de compilación incremental. Se compilaron 4 escenarios basados ​​en las siguientes condiciones:


  • La modificación de un archivo afecta / no afecta su ABI
  • Soporte para el procesamiento de anotación incremental activado / desactivado

La ejecución de cada uno de los escenarios es una secuencia de:


  • Reiniciar el demonio gradle
  • Lanzar compilaciones de calentamiento
  • Ejecute 10 ensamblajes incrementales, antes de cada uno de los cuales se cambia un archivo agregando un nuevo método (privado para cambios no ABI y público para cambios ABI)

Todas las compilaciones se realizaron con Gradle 5.4.1. El archivo involucrado en los cambios se refiere a uno de los módulos principales del proyecto (común), del cual dependen directamente 40 módulos (incluidos el núcleo y la función). Este archivo usa la anotación para aislar el procesador.


También vale la pena señalar que la ejecución de referencia se realizó en dos tareas básicas : ompileDebugSources y assembleDebug . El primero solo inicia la compilación de archivos con código fuente, sin hacer ningún trabajo con recursos y agrupando la aplicación en un archivo .apk. Basado en el hecho de que la compilación incremental afecta solo a los archivos .kt y .java, se eligió la tarea compileDedugSource para una evaluación comparativa más aislada y más rápida. En condiciones de desarrollo reales, cuando reinicia la aplicación, Android Studio utiliza la tarea assembleDebug , que incluye la generación completa de la versión de depuración de la aplicación.


Resultados de referencia


En todos los gráficos generados por gradle-profiler, el eje vertical muestra el tiempo de ensamblaje incremental en milisegundos, y el eje horizontal muestra el número de inicio del ensamblaje.


: compileDebugSource antes de actualizar los procesadores de anotaciones


ingrese la descripción de la imagen aquí
El tiempo de ejecución promedio para cada escenario fue de 38 segundos antes de actualizar los procesadores de anotaciones a versiones que admitan incrementalidad. En este caso, Gradle desactiva el soporte para la compilación incremental, por lo que no hay una diferencia significativa entre los scripts.


: compileDebugSource después de actualizar los procesadores de anotaciones



EscenarioCambio incremental de ABICambio de ABI no incrementalCambio incremental no ABICambio no incremental no abi
malvado23978353702351434602
mediana23879350192342434749
min22618339692234333292
max26820380972565135843
stddev1193,291240.81888,24815,91

La reducción media en el tiempo de montaje debido a la incrementalidad fue del 31% para los cambios ABI y del 32,5% para los cambios no ABI. En valor absoluto, unos 10 segundos.


: assembleDebug después de actualizar los procesadores de anotaciones



EscenarioCambio incremental de ABICambio de ABI no incrementalCambio incremental no ABICambio no incremental no abi
malvado39902498503900552123
mediana38974496913871350336
min38563487823823348944
max48255523644173265941
stddev2953,281011.201015,375039.11

Para compilar la versión de depuración completa de la aplicación en nuestro proyecto, la disminución media en el tiempo de compilación debido al incremento fue del 21.5% para cambios ABI y del 23% para cambios no ABI. En términos absolutos, aproximadamente los mismos 10 segundos, ya que el incremento de la compilación del código fuente no afecta la velocidad de ensamblaje de los recursos.


Build Scan Anatomy en Gradle Build Scan


Para una comprensión más profunda de cómo se logró el incremento durante la compilación incremental, comparamos escaneos de ensamblajes incrementales y no incrementales.


En el caso de un incremento KAPT deshabilitado, la parte principal del tiempo de compilación es la compilación del módulo de la aplicación, que no se puede paralelizar con otras tareas. La línea de tiempo para KAPT no incremental es la siguiente:


ingrese la descripción de la imagen aquí


Ejecución de la tarea: kaptDebugKotlin de nuestro módulo de aplicación tarda unos 8 segundos en este caso.


Línea de tiempo para el caso con el incremento KAPT habilitado:


ingrese la descripción de la imagen aquí


Ahora el módulo de la aplicación se ha vuelto a compilar en menos de un segundo. Vale la pena prestar atención a la desproporción visual de las escalas de los dos escaneos en la imagen de arriba. Las tareas que parecen más cortas en la primera imagen no son necesariamente más largas en la segunda, donde parecen más largas. Pero es muy notable cuánto ha disminuido la proporción de recompilación del módulo de la aplicación cuando activa KAPT incremental. En nuestro caso, ganamos unos 8 segundos en este módulo y unos 2 segundos adicionales en módulos más pequeños que se compilan en paralelo.


Al mismo tiempo, el tiempo total de ejecución de todas las tareas * kapt para la incrementalidad deshabilitada de las anotaciones de procesamiento es de 1 minuto y 36 segundos contra 55 segundos cuando está activado. Es decir, sin tener en cuenta el ensamblaje paralelo de los módulos, la ganancia es más sustancial.


También vale la pena señalar que los resultados de referencia anteriores se prepararon en un entorno CI con la capacidad de ejecutar 24 subprocesos paralelos para el ensamblaje. En un entorno de 8 subprocesos, la ganancia de habilitar el procesamiento de anotación incremental es de aproximadamente 20-30 segundos en nuestro proyecto.


Incremental vs (?) Paralelo


Otra forma de acelerar significativamente el ensamblaje (tanto incremental como limpio) es realizar tareas de gradle en paralelo al dividir el proyecto en una gran cantidad de módulos sueltos. De una forma u otra, la modularización representa un potencial mucho mayor para acelerar los ensamblajes que el uso de KAPT incremental. Pero cuanto más monolítico sea el proyecto, y cuanto más generación de código se use en él, mayor será el procesamiento incremental de las anotaciones. Es más fácil obtener el efecto de la incrementalidad completa de los ensamblajes que dividir una aplicación en módulos. Sin embargo, ambos enfoques no se contradicen y se complementan perfectamente entre sí.


Resumen


  • La inclusión del procesamiento incremental de anotaciones en nuestro proyecto nos permitió lograr un aumento del 20% en la velocidad de la reconstrucción local.
  • Para habilitar el procesamiento de anotación incremental, será útil estudiar el registro completo de los ensamblados actuales y buscar mensajes de advertencia con el texto "Procesamiento de anotación incremental solicitado, pero el soporte está deshabilitado porque los siguientes procesadores no son incrementales ...". Es necesario actualizar las versiones de las bibliotecas a versiones con soporte para el procesamiento incremental de anotaciones y tener versiones Gradle 4.7+, Kotlin 1.3.30+

Materiales y qué leer sobre el tema.


Source: https://habr.com/ru/post/484918/


All Articles