Processus d'annotation incrémentielle pour accélérer les constructions Gradle

image


À partir des versions Gradle 4.7 et Kotlin 1.3.30, il est devenu possible d'obtenir un assemblage incrémentiel accéléré des projets grâce au bon fonctionnement du traitement incrémentiel des annotations. Dans cet article, nous comprenons comment la théorie de la compilation incrémentielle dans Gradle fonctionne en théorie, ce qui doit être fait pour libérer son plein potentiel (sans perdre la génération de code en même temps), et quel type d'augmentation de la vitesse des assemblages incrémentiels peut être réalisé par l'activation du traitement incrémentiel des annotations dans la pratique.


Fonctionnement de la compilation incrémentielle


Les versions incrémentielles de Gradle sont implémentées à deux niveaux. Le premier niveau consiste à annuler le démarrage des modules de recompilation à l'aide de l' évitement de la compilation . La seconde est une compilation incrémentielle directe, démarrant le compilateur dans le cadre d'un module uniquement sur les fichiers qui ont changé ou qui dépendent directement des fichiers modifiés.


Considérons l'évitement de la compilation sur un exemple (extrait d'un article de Gradle) d'un projet de trois modules: app , core et utils .


La classe principale du module d' application (dépend du noyau ):


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()); } } 

Dans le module de base (dépend des utils ):


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

Dans le module utils :


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

L'ordre de la première compilation des modules est le suivant (conformément à l'ordre des dépendances):


1) utils
2) noyau
3) l' application


Considérez maintenant ce qui se passe lorsque vous modifiez l'implémentation interne de la classe 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) { // ... } } } 

Cette modification n'affecte pas le module ABI. ABI (Application Binary Interface) est une représentation binaire de l'interface publique du module assemblé. Dans le cas où le changement ne concerne que l'implémentation interne du module et n'affecte en rien son interface publique, Gradle utilisera l'évitement de la compilation et commencera la recompilation uniquement du module utils . Si l'ABI du module utils est affecté (par exemple, une méthode publique supplémentaire apparaît ou la signature de la méthode existante change), alors la compilation du module principal démarrera également, mais le module d' application dépendant du cœur ne sera pas recompilé de manière transitoire si la dépendance en elle est connectée via l' implémentation .



Illustration de l'Ă©vitement de la compilation au niveau du module de projet


Le deuxième niveau d'incrément est l'incrément au niveau du lancement du compilateur pour les fichiers modifiés directement à l'intérieur des modules individuels.


Par exemple, ajoutez une nouvelle classe au module principal :


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

Et en utils :


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

Dans ce cas, dans les deux modules, seuls deux nouveaux fichiers doivent être recompilés (sans affecter les WordCount et IOUtils existants et inchangés), car il n'y a pas de dépendances entre les nouvelles et les anciennes classes.


Ainsi, le compilateur incrémentiel analyse uniquement les dépendances entre les classes et recompile:


  • classes contenant des modifications
  • classes qui dĂ©pendent directement des classes changeantes


    Traitement d'annotations incrémentielles


    entrez la description de l'image ici



La génération de code à l'aide d'APT et de KAPT réduit le temps nécessaire pour écrire et déboguer du code passe-partout, mais le traitement des annotations peut augmenter considérablement le temps de génération. Pour aggraver les choses, pendant longtemps, le traitement des annotations a fondamentalement rompu les possibilités de compilation incrémentielle dans Gradle.


Chaque processeur d'annotations d'un projet indique au compilateur des informations sur la liste des annotations qu'il traite. Mais du point de vue de l'assemblage, le traitement des annotations est une boîte noire: Gradle ne sait pas ce que le processeur va faire, en particulier, quels fichiers il va générer et où. Jusqu'à Gradle 4.7, la compilation incrémentielle était automatiquement désactivée sur les ensembles source où les processeurs d'annotation étaient utilisés.


Avec la sortie de Gradle 4.7, la compilation incrémentielle prend désormais en charge le traitement des annotations, mais uniquement pour APT. Dans KAPT, la prise en charge des annotations incrémentielles a été introduite avec Kotlin 1.3.30. Il nécessite également le support des bibliothèques qui fournissent des processeurs d'annotation. Les développeurs de processeurs d'annotations ont la possibilité de définir explicitement la catégorie de processeurs, informant ainsi Gradle des informations nécessaires au fonctionnement de la compilation incrémentielle.


Catégories de processeurs d'annotation


Gradle prend en charge deux catégories de processeurs:


Isolement - ces processeurs doivent prendre toutes les décisions pour la génération de code en se basant uniquement sur les informations d' AST associées à un élément d'une annotation particulière. Il s'agit de la catégorie la plus rapide de processeurs d'annotation, car Gradle peut ne pas redémarrer le processeur et utiliser les fichiers qu'il a précédemment générés s'il n'y a eu aucune modification dans le fichier source.


Agrégation - utilisée pour les processeurs qui prennent des décisions sur la base de plusieurs entrées (par exemple, l'analyse des annotations dans plusieurs fichiers à la fois ou sur la base de l'étude de l'AST, qui est accessible de manière transitoire à partir d'un élément annoté). À chaque fois, Gradle démarrera le processeur pour les fichiers qui utilisent des annotations du processeur d'agrégation, mais ne recompilera pas les fichiers qu'il génère s'il n'y a aucune modification.


Pour de nombreuses bibliothèques populaires basées sur la génération de code, la prise en charge de la compilation incrémentielle est déjà implémentée dans les dernières versions. Consultez la liste des bibliothèques qui le prennent en charge ici .


Notre expérience dans la mise en œuvre du traitement d'annotations incrémentielles


Désormais, pour les projets qui partent de zéro et utilisent les dernières versions des bibliothèques et des plugins Gradle, les builds incrémentiels sont plus susceptibles d'être actifs par défaut. Mais la plus grande part de l'augmentation de la productivité de l'assemblage peut être obtenue grâce à l'incrémentalité du traitement des annotations sur les projets de grande taille et de longue durée. Dans ce cas, une mise à jour massive de la version peut être requise. Est-ce que cela en vaut la peine dans la pratique? Voyons voir!


Donc, pour que le traitement incrémentiel des annotations fonctionne, nous avons besoin de:


  • Gradle 4.7+
  • Kotlin 1.3.30+
  • Tous les processeurs d'annotation de notre projet doivent avoir son support. Ceci est très important, car si dans un seul module au moins un processeur ne prend pas en charge l'incrĂ©mentalitĂ©, Gradle le dĂ©sactivera pour l'ensemble du module. Tous les fichiers du module seront Ă  nouveau compilĂ©s Ă  chaque fois! L'une des options alternatives pour obtenir la prise en charge de la compilation incrĂ©mentielle sans mettre Ă  niveau les versions est la suppression de tout le code Ă  l'aide de processeurs d'annotation dans un module distinct. Dans les modules qui n'ont pas de processeurs d'annotation, la compilation incrĂ©mentielle fonctionnera correctement

Afin de détecter les processeurs qui ne remplissent pas la dernière condition, vous pouvez exécuter l'assembly avec l'indicateur -Pkapt.verbose = true . Si Gradle a été forcé de désactiver le traitement d'annotations incrémentielles pour un seul module, alors dans le journal de construction, nous verrons un message sur quels processeurs et dans quels modules cela se produit (voir le nom de la tâche):


 > 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). 

Sur notre projet de bibliothèque avec des processeurs d'annotation non incrémentiels, il y en avait 3:


  • Cure-dent
  • Pièce
  • PermissionsDispatcher

Heureusement, ces bibliothèques sont activement prises en charge et leurs dernières versions prennent déjà en charge l'incrémentalité. De plus, tous les processeurs d'annotation des dernières versions de ces bibliothèques ont une catégorie optimale - isolement. Dans le processus d'augmentation des versions, j'ai dû faire face à une refactorisation en raison de changements dans l'API de la bibliothèque Toothpick, qui ont affecté presque tous nos modules. Mais dans ce cas, nous avons eu de la chance, et il s'est avéré que le refactoring était entièrement automatique en utilisant les noms de remplacement automatique des méthodes de bibliothèque publique utilisées.


Notez que si vous utilisez la bibliothèque Room, vous devrez passer explicitement l' indicateur room.incremental: true au processeur d'annotation. Un exemple . À l'avenir, les développeurs de Room prévoient d'activer ce drapeau par défaut.


Pour les versions de Kotlin 1.3.30-1.3.50, vous devez activer explicitement la prise en charge du traitement incrémentiel des annotations via kapt.incremental.apt = true dans le fichier gradle.properties du projet. À partir de la version 1.3.50, cette option est définie sur true par défaut.


Profilage d'assemblage incrémentiel


Une fois les versions de toutes les dépendances nécessaires augmentées, il est temps de tester la vitesse des générations incrémentielles. Pour ce faire, nous avons utilisé l'ensemble d'outils et de techniques suivant:


  • Scan de construction Gradle
  • gradle-profiler
  • Pour exĂ©cuter des scripts avec un traitement d'annotation incrĂ©mentiel activĂ© et dĂ©sactivĂ©, la propriĂ©tĂ© gradle kapt.incremental.apt = [true | false] a Ă©tĂ© utilisĂ©e
  • Pour des rĂ©sultats cohĂ©rents et informatifs, les assemblages ont Ă©tĂ© crĂ©Ă©s dans un environnement CI sĂ©parĂ©. L'incrĂ©mentalitĂ© de build a Ă©tĂ© reproduite Ă  l'aide de gradle-profiler

gradle-profiler permet de préparer de manière déclarative des scripts pour des benchmarks de construction incrémentiels. 4 scénarios ont été compilés sur la base des conditions suivantes:


  • La modification d'un fichier affecte / n'affecte pas son ABI
  • Prise en charge du traitement d'annotations incrĂ©mentielles activĂ© / dĂ©sactivĂ©

L'exécution de chacun des scénarios est une séquence de:


  • RedĂ©marrer le dĂ©mon gradle
  • Lancer des builds d'Ă©chauffement
  • ExĂ©cutez 10 assemblys incrĂ©mentiels, avant chacun desquels un fichier est modifiĂ© en ajoutant une nouvelle mĂ©thode (privĂ©e pour les modifications non ABI et publique pour les modifications ABI)

Toutes les versions ont été réalisées avec Gradle 5.4.1. Le fichier impliqué dans les modifications fait référence à l'un des modules de base du projet (commun), dont 40 modules (y compris le noyau et la fonctionnalité) sont directement dépendants. Ce fichier utilise l'annotation pour isoler le processeur.


Il convient également de noter que l'analyse comparative a été effectuée sur deux tâches gradle : ompileDebugSources et assembleDebug . Le premier ne démarre que la compilation des fichiers avec le code source, sans travailler avec les ressources et regrouper l'application dans un fichier .apk. Sur la base du fait que la compilation incrémentielle affecte uniquement les fichiers .kt et .java, la tâche compileDedugSource a été choisie pour une analyse comparative plus isolée et plus rapide. Dans des conditions de développement réelles, lorsque vous redémarrez l'application, Android Studio utilise la tâche assembleDebug , qui inclut la génération complète de la version de débogage de l'application.


Résultats de référence


Dans tous les graphiques générés par gradle-profiler, l'axe vertical affiche le temps d'assemblage incrémentiel en millisecondes et l'axe horizontal indique le numéro de début de l'assemblage.


: compileDebugSource avant de mettre Ă  jour les processeurs d'annotation


entrez la description de l'image ici
Le temps d'exécution moyen de chaque scénario était de 38 secondes avant de mettre à jour les processeurs d'annotation vers des versions qui prennent en charge l'incrémentalité. Dans ce cas, Gradle désactive la prise en charge de la compilation incrémentielle, il n'y a donc pas de différence significative entre les scripts.


: compileDebugSource après la mise à jour des processeurs d'annotation



ScénarioChangement incrémentiel de l'ABIChangement ABI non incrémentielChangement incrémentiel non ABIChangement non incrémentiel non abi
méchant23978353702351434602
médiane23879350192342434749
min22618339692234333292
max26820380972565135843
stddev1193.291240.81888,24815,91

La réduction médiane du temps d'assemblage due à l'incrémentalité était de 31% pour les changements ABI et de 32,5% pour les changements non ABI. En valeur absolue, environ 10 secondes.


: assembleDebug après la mise à jour des processeurs d'annotation



ScénarioChangement incrémentiel de l'ABIChangement ABI non incrémentielChangement incrémentiel non ABIChangement non incrémentiel non abi
méchant39902498503900552123
médiane38974496913871350336
min38563487823823348944
max48255523644173265941
stddev2953.281011.201015,375039.11

Pour générer la version de débogage complète de l'application sur notre projet, la diminution médiane du temps de génération en raison de l'incrément était de 21,5% pour les modifications ABI et de 23% pour les modifications non ABI. En termes absolus, environ les mêmes 10 secondes, car l'incrément de compilation du code source n'affecte pas la vitesse d'assemblage des ressources.


Build Scan Anatomy dans Gradle Build Scan


Pour une compréhension plus approfondie de la façon dont l'incrémentation a été obtenue lors de la compilation incrémentielle, nous comparons les analyses des assemblages incrémentiels et non incrémentiels.


Dans le cas d'un incrément KAPT désactivé, la partie principale du temps de construction est la compilation du module d'application, qui ne peut pas être mis en parallèle avec d'autres tâches. Le calendrier pour KAPT non incrémentiel est le suivant:


entrez la description de l'image ici


Exécution de la tâche: kaptDebugKotlin de notre module d'application prend environ 8 secondes dans ce cas.


Chronologie du cas avec incrément KAPT activé:


entrez la description de l'image ici


Maintenant, le module d'application a été recompilé en moins d'une seconde. Il convient de prêter attention à la disproportion visuelle des échelles des deux scans dans le picch ci-dessus. Les tâches qui semblent plus courtes dans la première image ne sont pas nécessairement plus longues dans la seconde, où elles semblent plus longues. Mais il est très visible de voir combien la proportion de recompilation du module d'application a été réduite lorsque KAPT incrémentiel a été activé. Dans notre cas, nous gagnons environ 8 secondes sur ce module et environ 2 secondes supplémentaires sur les modules plus petits qui sont compilés en parallèle.


Dans le même temps, le temps d'exécution total de toutes les tâches * kapt pour l'incrémentalité désactivée du traitement des annotations est de 1 minute et 36 secondes contre 55 secondes lorsqu'il est activé. Autrement dit, sans tenir compte de l'assemblage parallèle des modules, le gain est plus substantiel.


Il convient également de noter que les résultats de référence ci-dessus ont été préparés sur un environnement CI avec la possibilité d'exécuter 24 threads parallèles pour l'assemblage. Dans un environnement à 8 threads, le gain de l'activation du traitement d'annotations incrémentielles est d'environ 20-30 secondes sur notre projet.


Incrémental vs (?) Parallèle


Une autre façon d'accélérer considérablement l'assemblage (à la fois incrémentiel et propre) consiste à effectuer des tâches gradles en parallèle en divisant le projet en un grand nombre de modules à couplage lâche. D'une manière ou d'une autre, la modularisation représente un potentiel d'accélération des assemblages beaucoup plus important que l'utilisation de KAPT incrémentiel. Mais plus le projet est monolithique et plus la génération de code y est utilisée, plus le traitement incrémentiel des annotations sera important. Il est plus facile d'obtenir l'effet d'une incrémentalité complète des assemblages que de diviser une application en modules. Néanmoins, les deux approches ne se contredisent pas et se complètent parfaitement.


Résumé


  • L'inclusion du traitement incrĂ©mentiel des annotations sur notre projet nous a permis d'atteindre une augmentation de 20% de la vitesse de reconstruction locale
  • Pour activer le traitement d'annotations incrĂ©mentielles, il sera utile d'Ă©tudier le journal complet des assemblys actuels et de rechercher des messages d'avertissement avec le texte "Traitement d'annotations incrĂ©mentielles demandĂ©, mais la prise en charge est dĂ©sactivĂ©e car les processeurs suivants ne sont pas incrĂ©mentiels ...". Il est nĂ©cessaire de mettre Ă  niveau les versions des bibliothèques vers des versions prenant en charge le traitement incrĂ©mentiel des annotations et de disposer des versions Gradle 4.7+, Kotlin 1.3.30+

Matériel et éléments à lire sur le sujet


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


All Articles