Inkrementelle Annotation zur Beschleunigung von Gradle-Builds

Bild


Ab den Versionen 4.7 und 1.3.30 von Kotlin war es aufgrund der korrekten inkrementellen Verarbeitung von Anmerkungen möglich, die inkrementelle Montage von Projekten zu beschleunigen. In diesem Artikel erfahren Sie, wie die Theorie der inkrementellen Kompilierung in Gradle theoretisch funktioniert, was getan werden muss, um das volle Potenzial freizusetzen (ohne gleichzeitig die Codegenerierung zu verlieren) und welche Geschwindigkeitssteigerung in inkrementellen Assemblys durch die Aktivierung der inkrementellen Verarbeitung von Annotationen in der Praxis erzielt werden kann.


So funktioniert die inkrementelle Kompilierung


Inkrementelle Builds in Gradle werden auf zwei Ebenen implementiert. Die erste Ebene besteht darin, den Start des Neukompilierens von Modulen mit Hilfe der Kompilierumgehung abzubrechen. Die zweite ist die direkte inkrementelle Kompilierung, bei der der Compiler im Rahmen eines Moduls nur für die Dateien gestartet wird, die geändert wurden oder direkt von den geänderten Dateien abhängig sind.


Betrachten wir die Vermeidung von Kompilierungen an einem Beispiel (aus einem Artikel von Gradle) für ein Projekt mit drei Modulen: App , Core und Utils .


Die Hauptklasse des App- Moduls (abhängig vom Kern ):


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

Im Kernmodul (abhängig von Utils ):


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

Im utils- Modul:


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

Die Reihenfolge der ersten Kompilierung der Module ist wie folgt (gemäß der Reihenfolge der Abhängigkeiten):


1) utils
2) Kern
3) App


Überlegen Sie nun, was passiert, wenn Sie die interne Implementierung der IOUtils-Klasse ändern:


 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) { // ... } } } 

Diese Änderung wirkt sich nicht auf das ABI-Modul aus. ABI (Application Binary Interface) ist eine binäre Darstellung der öffentlichen Schnittstelle des zusammengesetzten Moduls. In dem Fall, dass sich die Änderung nur auf die interne Implementierung des Moduls bezieht und die öffentliche Schnittstelle in keiner Weise beeinflusst, verwendet Gradle die Kompilierungsumgehung und startet die Neukompilierung nur des utils- Moduls. Wenn die ABI des Moduls utils betroffen ist (z. B. eine zusätzliche öffentliche Methode angezeigt wird oder die Signatur der vorhandenen geändert wird), wird zusätzlich die Kompilierung des Kernmoduls gestartet, das kernabhängige App- Modul wird jedoch nicht transitiv neu kompiliert, wenn die Abhängigkeit in ihm durch Implementierung verbunden ist .



Darstellung der Kompilierungsvermeidung auf Projektmodulebene


Die zweite Inkrementstufe ist die Inkrementstufe auf Compiler-Startebene für geänderte Dateien direkt in einzelnen Modulen.


Fügen Sie dem Kernmodul beispielsweise eine neue Klasse hinzu:


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

Und in utils :


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

In diesem Fall müssen in beiden Modulen nur zwei neue Dateien neu kompiliert werden (ohne die vorhandenen und nicht geänderten WordCount- und IOUtils-Dateien zu beeinflussen), da keine Abhängigkeiten zwischen der neuen und der alten Klasse bestehen.


Der inkrementelle Compiler analysiert daher nur Abhängigkeiten zwischen Klassen und kompiliert neu:


  • Klassen mit Änderungen
  • Klassen, die direkt von den wechselnden Klassen abhängen


    Inkrementelle Anmerkungsverarbeitung


    Bildbeschreibung hier eingeben



Durch das Generieren von Code mit APT und KAPT wird der Zeitaufwand für das Schreiben und Debuggen von Boilerplate-Code verringert, die Verarbeitung von Anmerkungen kann jedoch die Erstellungszeit erheblich verlängern. Um die Sache noch schlimmer zu machen, hat die Verarbeitung von Anmerkungen die Möglichkeiten der inkrementellen Kompilierung in Gradle für lange Zeit grundlegend zerstört.


Jeder Anmerkungsprozessor in einem Projekt informiert den Compiler über die Liste der von ihm verarbeiteten Anmerkungen. Aus Assembler-Sicht ist die Annotation-Verarbeitung jedoch eine Black Box: Gradle weiß nicht, was der Prozessor tun wird, insbesondere welche Dateien er an welcher Stelle erstellen wird. Bis zu Gradle 4.7 wurde die inkrementelle Kompilierung in den Quellensätzen, in denen Anmerkungsprozessoren verwendet wurden, automatisch deaktiviert.


Mit der Veröffentlichung von Gradle 4.7 unterstützt die inkrementelle Kompilierung jetzt die Verarbeitung von Anmerkungen, jedoch nur für APT. In KAPT wurde mit Kotlin 1.3.30 die Unterstützung für inkrementelle Annotationen eingeführt . Es erfordert auch die Unterstützung von Bibliotheken, die Anmerkungsprozessoren bereitstellen. Entwickler von Anmerkungsprozessoren haben die Möglichkeit, die Prozessorkategorie explizit festzulegen, um Gradle über die Informationen zu informieren, die für die Funktion der inkrementellen Kompilierung erforderlich sind.


Anmerkungsprozessorkategorien


Gradle unterstützt zwei Kategorien von Prozessoren:


Isolieren - Diese Prozessoren müssen alle Entscheidungen für die Codegenerierung nur auf der Grundlage der Informationen von AST treffen, die einem Element einer bestimmten Annotation zugeordnet sind. Dies ist die schnellste Kategorie von Annotation-Prozessoren, da Gradle den Prozessor möglicherweise nicht neu startet und die zuvor generierten Dateien verwendet, wenn die Quelldatei nicht geändert wurde.


Aggregieren - Wird für Prozessoren verwendet, die Entscheidungen auf der Grundlage mehrerer Eingaben treffen (z. B. Analyse von Anmerkungen in mehreren Dateien auf einmal oder auf der Grundlage der Untersuchung von AST, die von einem mit Anmerkungen versehenen Element aus transitiv erreichbar ist). Jedes Mal startet Gradle den Prozessor für Dateien, die Anmerkungen des Aggregationsprozessors verwenden, kompiliert die generierten Dateien jedoch nicht neu, wenn keine Änderungen daran vorgenommen wurden.


Bei vielen gängigen Bibliotheken, die auf der Codegenerierung basieren, ist die Unterstützung der inkrementellen Kompilierung bereits in den neuesten Versionen implementiert. Eine Liste der Bibliotheken, die dies unterstützen, finden Sie hier .


Unsere Erfahrung mit der Implementierung inkrementeller Anmerkungsverarbeitung


Bei Projekten, die von vorne beginnen und die neuesten Versionen von Bibliotheken und Gradle-Plug-ins verwenden, sind inkrementelle Builds wahrscheinlich standardmäßig aktiviert. Der größte Teil der Steigerung der Montageproduktivität kann jedoch durch die inkrementelle Verarbeitung von Anmerkungen bei großen und langlebigen Projekten erzielt werden. In diesem Fall ist möglicherweise ein umfangreiches Versionsupdate erforderlich. Lohnt es sich in der Praxis? Mal sehen


Damit die inkrementelle Verarbeitung von Anmerkungen funktioniert, benötigen wir:


  • Gradle 4.7+
  • Kotlin 1.3.30+
  • Alle Anmerkungsprozessoren in unserem Projekt müssen ihre Unterstützung haben. Dies ist sehr wichtig, da Gradle diese Funktion für das gesamte Modul deaktiviert, wenn in einem einzelnen Modul mindestens ein Prozessor keine Inkrementalität unterstützt. Alle Dateien im Modul werden jedes Mal neu kompiliert! Eine der alternativen Optionen, um Unterstützung für die inkrementelle Kompilierung zu erhalten, ohne die Versionen zu aktualisieren, besteht darin, den gesamten Code mithilfe von Annotation-Prozessoren in einem separaten Modul zu entfernen. In Modulen ohne Anmerkungsprozessor funktioniert die inkrementelle Kompilierung problemlos

Um Prozessoren zu erkennen, die die letzte Bedingung nicht erfüllen, können Sie die Assembly mit dem Flag -Pkapt.verbose = true ausführen . Wenn Gradle gezwungen war, die inkrementelle Anmerkungsverarbeitung für ein einzelnes Modul zu deaktivieren, wird im Erstellungsprotokoll eine Meldung angezeigt, welche Prozessoren und in welchen Modulen dies geschieht (siehe den Namen der Aufgabe):


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

In unserem Bibliotheksprojekt mit nicht inkrementellen Annotationsprozessoren gab es 3:


  • Zahnstocher
  • Zimmer
  • PermissionsDispatcher

Glücklicherweise werden diese Bibliotheken aktiv unterstützt und ihre neuesten Versionen unterstützen bereits Inkrementalität. Darüber hinaus haben alle Annotation-Prozessoren in den neuesten Versionen dieser Bibliotheken eine optimale Kategorietrennung. Beim Anheben der Versionen musste ich mich wegen Änderungen an der Toothpick-Bibliotheks-API, die fast jedes unserer Module betrafen, mit dem Refactoring befassen. In diesem Fall hatten wir jedoch Glück und es stellte sich heraus, dass das Refactoring mithilfe der Namen der verwendeten öffentlichen Bibliotheksmethoden, die automatisch ersetzt wurden, vollständig automatisch erfolgte.


Beachten Sie, dass Sie bei Verwendung der Raumbibliothek das Flag room.incremental: true explizit an den Anmerkungsprozessor übergeben müssen. Ein Beispiel . In Zukunft planen Raumentwickler, dieses Flag standardmäßig zu aktivieren.


Für Versionen von Kotlin 1.3.30-1.3.50 müssen Sie die Unterstützung für die inkrementelle Verarbeitung von Anmerkungen explizit über kapt.incremental.apt = true in der Datei gradle.properties des Projekts aktivieren. Ab Version 1.3.50 ist diese Option standardmäßig auf true gesetzt.


Inkrementelle Montageprofilierung


Nachdem die Versionen aller erforderlichen Abhängigkeiten erstellt wurden, ist es an der Zeit, die Geschwindigkeit inkrementeller Builds zu testen. Dazu haben wir die folgenden Tools und Techniken verwendet:


  • Gradle Build Scan
  • Gradle-Profiler
  • Zum Ausführen von Skripten mit aktivierter und deaktivierter inkrementeller Anmerkungsverarbeitung wurde die gradle-Eigenschaft kapt.incremental.apt = [true | false] verwendet
  • Für konsistente und informative Ergebnisse wurden Assemblys in einer separaten CI-Umgebung erstellt. Die Build-Inkrementalität wurde mit Gradle-Profiler reproduziert

Gradle-Profiler ermöglicht die deklarative Vorbereitung von Skripten für inkrementelle Build-Benchmarks. Es wurden 4 Szenarien unter folgenden Bedingungen erstellt:


  • Das Ändern einer Datei wirkt sich nicht auf deren ABI aus
  • Unterstützung für inkrementelle Anmerkungsverarbeitung ein / aus

Der Ablauf jedes der Szenarien ist eine Folge von:


  • Gradle-Daemon neu starten
  • Starten Sie Warm-Up-Builds
  • Führen Sie 10 inkrementelle Assemblys aus, bevor eine Datei durch Hinzufügen einer neuen Methode geändert wird (privat für Nicht-ABI-Änderungen und öffentlich für ABI-Änderungen).

Alle Builds wurden mit Gradle 5.4.1 erstellt. Die Datei, die an den Änderungen beteiligt ist, verweist auf eines der Kernmodule des Projekts (allgemein), von dem 40 Module (einschließlich Kern und Feature) direkt abhängig sind. Diese Datei verwendet die Annotation zum Isolieren des Prozessors.


Es ist auch erwähnenswert, dass der Benchmark-Lauf mit zwei Gradle-Tasks durchgeführt wurde: ompileDebugSources und assembleDebug . Mit der ersten Option wird nur die Kompilierung von Dateien mit Quellcode gestartet, ohne dass mit Ressourcen gearbeitet und die Anwendung in einer APK-Datei gebündelt wird. Aufgrund der Tatsache, dass die inkrementelle Kompilierung nur .kt- und .java-Dateien betrifft, wurde die compileDedugSource- Task für ein isolierteres und schnelleres Benchmarking ausgewählt. Unter realen Entwicklungsbedingungen verwendet Android Studio beim Neustart der Anwendung den Task assembleDebug , der die vollständige Generierung der Debug-Version der Anwendung umfasst.


Benchmark-Ergebnisse


In allen vom Gradle-Profiler unten generierten Diagrammen zeigt die vertikale Achse die inkrementelle Erstellungszeit in Millisekunden und die horizontale Achse die Erstellungsstartnummer.


: compileDebugSource vor dem Aktualisieren von Anmerkungsprozessoren


Bildbeschreibung hier eingeben
Die durchschnittliche Laufzeit für jedes Szenario betrug 38 Sekunden, bevor Anmerkungsprozessoren auf Versionen aktualisiert wurden, die Inkrementalität unterstützen. In diesem Fall deaktiviert Gradle die Unterstützung für die inkrementelle Kompilierung, sodass zwischen den Skripten kein wesentlicher Unterschied besteht.


: compileDebugSource nach Aktualisierung der Anmerkungsprozessoren



SzenarioInkrementelle ABI-ÄnderungNicht inkrementelle ABI-ÄnderungInkrementelle Non-ABI-ÄnderungNicht inkrementelle Non-Abi-Änderung
gemein23978353702351434602
Median23879350192342434749
min22618339692234333292
max26820380972565135843
stddev1193,291240,81888,24815,91

Die mediane Verkürzung der Montagezeit aufgrund der Inkrementalität betrug 31% für ABI-Änderungen und 32,5% für Nicht-ABI-Änderungen. Absolut ungefähr 10 Sekunden.


: assembleDebug nach Aktualisierung der Annotation-Prozessoren



SzenarioInkrementelle ABI-ÄnderungNicht inkrementelle ABI-ÄnderungInkrementelle Non-ABI-ÄnderungNicht inkrementelle Non-Abi-Änderung
gemein39902498503900552123
Median38974496913871350336
min38563487823823348944
max48255523644173265941
stddev2953,281011,201015,375039.11

Um die vollständige Debug-Version der Anwendung für unser Projekt zu erstellen, betrug die durchschnittliche Verringerung der Erstellungszeit aufgrund des Inkrements 21,5% für ABI-Änderungen und 23% für Nicht-ABI-Änderungen. In absoluten Zahlen ungefähr die gleichen 10 Sekunden, da das Inkrement der Kompilierung des Quellcodes die Assemblierungsgeschwindigkeit der Ressourcen nicht beeinflusst.


Erstellen Sie die Scan-Anatomie in Gradle Build Scan


Um zu verstehen, wie das Inkrement während der inkrementellen Kompilierung erzielt wurde, vergleichen wir die Scans inkrementeller und nicht inkrementeller Baugruppen.


Im Falle eines deaktivierten KAPT-Inkrements ist der Hauptteil der Erstellungszeit die Kompilierung des App-Moduls, die nicht mit anderen Aufgaben parallelisiert werden kann. Die Zeitleiste für nicht inkrementelle KAPT lautet wie folgt:


Bildbeschreibung hier eingeben


Aufgabenausführung: kaptDebugKotlin unseres App-Moduls dauert in diesem Fall ca. 8 Sekunden.


Zeitleiste für den Fall mit aktiviertem KAPT-Inkrement:


Bildbeschreibung hier eingeben


Jetzt wurde das App-Modul in weniger als einer Sekunde neu kompiliert. Es lohnt sich, auf die visuelle Disproportionalität der Skalen der beiden Scans im obigen Bild zu achten. Aufgaben, die im ersten Bild kürzer erscheinen, sind im zweiten nicht unbedingt länger, da sie länger erscheinen. Es ist jedoch sehr auffällig, wie sehr sich der Anteil der Neukompilierung des App-Moduls verringert hat, wenn Sie das inkrementelle KAPT einschalten. In unserem Fall gewinnen wir ungefähr 8 Sekunden auf diesem Modul und zusätzliche ungefähr 2 Sekunden auf kleineren Modulen, die parallel kompiliert werden.


Gleichzeitig beträgt die Gesamtausführungszeit aller * kapt-Tasks für die deaktivierte Inkrementalität der Verarbeitungsanmerkungen 1 Minute und 36 Sekunden gegenüber 55 Sekunden, wenn sie aktiviert sind. Das heißt, ohne die parallele Anordnung der Module zu berücksichtigen, ist die Verstärkung wesentlich größer.


Es ist auch erwähnenswert, dass die obigen Benchmark-Ergebnisse in einer CI-Umgebung erstellt wurden, in der 24 parallele Threads für die Montage ausgeführt werden können. In einer 8-Thread-Umgebung beträgt der Gewinn durch die Aktivierung der inkrementellen Anmerkungsverarbeitung in unserem Projekt etwa 20 bis 30 Sekunden.


Inkrementelle vs (?) Parallele


Eine andere Möglichkeit, die Montage erheblich zu beschleunigen (sowohl inkrementell als auch sauber), besteht in der parallelen Ausführung einzelner Aufgaben, indem das Projekt in eine große Anzahl lose gekoppelter Module aufgeteilt wird. Auf die eine oder andere Weise bietet die Modularisierung ein viel größeres Potenzial für die Beschleunigung von Baugruppen als die Verwendung von inkrementellem KAPT. Je monolithischer das Projekt ist und je mehr Code generiert wird, desto größer ist die inkrementelle Verarbeitung der Anmerkungen. Es ist einfacher, den Effekt einer vollständigen Inkrementalität von Assemblys zu erzielen, als eine Anwendung in Module zu unterteilen. Trotzdem widersprechen sich beide Ansätze nicht und ergänzen sich perfekt.


Zusammenfassung


  • Durch die inkrementelle Verarbeitung von Anmerkungen in unserem Projekt konnten wir die Geschwindigkeit des lokalen Wiederaufbaus um 20% steigern
  • Um die inkrementelle Anmerkungsverarbeitung zu aktivieren, ist es hilfreich, das vollständige Protokoll der aktuellen Assemblys zu lesen und nach Warnmeldungen mit dem Text "Inkrementelle Anmerkungsverarbeitung angefordert, die Unterstützung ist jedoch deaktiviert, da die folgenden Prozessoren nicht inkrementell sind ..." zu suchen. Es ist erforderlich, Bibliotheksversionen auf Versionen zu aktualisieren, die die inkrementelle Verarbeitung von Anmerkungen unterstützen, und die Versionen Gradle 4.7+, Kotlin 1.3.30+ zu verwenden

Materialien und was zum Thema zu lesen


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


All Articles