Profilowanie kompilacji

W przypadku większych projektów lub takich, które implementują dużo niestandardowej logiki kompilacji, może być konieczne dokładniejsze przeanalizowanie procesu kompilacji w celu znalezienia wąskich gardeł. Możesz to zrobić, profilując czas, jaki Gradle potrzebuje na wykonanie każdego etapu cyklu życia kompilacji i każdego zadania kompilacji. Jeśli na przykład profil kompilacji pokazuje, że Gradle poświęca zbyt dużo czasu na konfigurowanie projektu, może to oznaczać, że musisz przenieść niestandardową logikę kompilacji poza fazę konfiguracji. Dodatkowo, jeśli zadanie mergeDevDebugResources zajmuje dużo czasu kompilacji, może to oznaczać, że musisz przekonwertować obrazy na format WebP lub wyłączyć kompresję PNG.

Jeśli używasz Android Studio 4.0 lub nowszego, najlepszym sposobem na zbadanie problemów z wydajnością kompilacji jest użycie Analizatora kompilacji.

Oprócz tego istnieją 2 opcje profilowania kompilacji poza Android Studio:

  1. Samodzielne narzędzie gradle-profiler, które jest zaawansowanym narzędziem do szczegółowej analizy kompilacji.

  2. Opcja --profile Gradle, czyli wygodne narzędzie dostępne w wierszu poleceń Gradle.

Korzystanie z samodzielnego narzędzia gradle-profiler

Aby znaleźć konfigurację projektu, która zapewnia najlepszą szybkość kompilacji, użyj profilera Gradle, czyli narzędzia do zbierania informacji o profilowaniu i testach porównawczych kompilacji Gradle. Profiler Gradle umożliwia tworzenie scenariuszy kompilacji i ich wielokrotne uruchamianie, co zapobiega dużym rozbieżnościom między wynikami i zapewnia ich powtarzalność.

Tryb testu porównawczego służy do zbierania informacji o czystych i przyrostowych kompilacjach, a tryb profilowania – do zbierania bardziej szczegółowych informacji o uruchomieniach, w tym zrzutów procesora.

Niektóre konfiguracje konfiguracji projektu do testów porównawczych obejmują:

  • Wersje wtyczek
  • Wersje Gradle
  • ustawienia JVM (rozmiar sterty, rozmiar permgen, odśmiecanie pamięci itp.);
  • Liczba instancji roboczych Gradle (org.gradle.workers.max)
  • Opcje poszczególnych wtyczek umożliwiające dalszą optymalizację wydajności

Pierwsze kroki

  • Zainstaluj gradle-profiler, postępując zgodnie z tymi instrukcjami.
  • Bieganie: gradle-profiler --benchmark --project-dir <root-project> :app:assembleDebug

Spowoduje to przetestowanie w pełni aktualnej kompilacji, ponieważ --benchmark uruchamia zadanie wielokrotnie bez wprowadzania zmian w projekcie. Następnie wygeneruje raport HTML w katalogu profile-out/, który pokazuje czasy kompilacji.

Istnieją inne scenariusze, które mogą być bardziej przydatne do analizy porównawczej:

  • zmiany kodu w treści metody w klasie, w której wykonujesz większość pracy;
  • Zmiany w interfejsie API w module, który jest używany w całym projekcie. Chociaż zmiany te są rzadsze niż zmiany we własnym kodzie, mają większy wpływ i warto je mierzyć.
  • zmiany układu w celu symulowania iteracji nad interfejsem.
  • Zmiany ciągów znaków symulujące pracę nad tłumaczeniem.
  • Wyczyść kompilacje, aby zasymulować zmiany w samej kompilacji (np. aktualizację wtyczki Androida do obsługi Gradle, aktualizację Gradle lub zmiany we własnym kodzie kompilacji w folderze buildSrc).

Aby przeprowadzić test porównawczy tych przypadków użycia, możesz utworzyć scenariusz, który będzie używany do wykonywania gradle-profiler i który zastosuje odpowiednie zmiany w źródłach. W sekcjach poniżej opisujemy kilka typowych scenariuszy.

Profilowanie różnych ustawień pamięci i procesora

Aby przeprowadzić test porównawczy różnych ustawień pamięci i procesora, możesz utworzyć kilka scenariuszy, które używają różnych wartości dla org.gradle.jvmargs. Możesz na przykład utworzyć scenariusze:

# <root-project>/scenarios.txt
clean_build_2gb_4workers {
    tasks = [":app:assembleDebug"]
    gradle-args = ["--max-workers=4"]
    jvm-args = ["-Xmx2048m"]
    cleanup-tasks = ["clean"]
}
clean_build_parallelGC {
    tasks = [":app:assembleDebug"]
    jvm-args = ["-XX:+UseParallelGC"]
    cleanup-tasks = ["clean"]
}

clean_build_G1GC_4gb {
    tasks = [":app:assembleDebug"]
    jvm-args = ["-Xmx4096m", "-XX:+UseG1GC"]
    cleanup-tasks = ["clean"]
}

Uruchomienie polecenia gradle-profiler --benchmark --project-dir <root-project> --scenario-file scenarios.txt spowoduje uruchomienie 3 scenariuszy i będzie można porównać, ile czasu zajmuje :app:assembleDebug w każdej z tych konfiguracji.

Profilowanie różnych wersji wtyczki do obsługi Gradle

Aby dowiedzieć się, jak zmiana wersji wtyczki Gradle wpływa na czas kompilacji, utwórz scenariusz do testów porównawczych. Wymaga to pewnych przygotowań, aby wersja wtyczki mogła zostać wstrzyknięta ze scenariusza. Zmień główny plik build.gradle:

# <root-project>/build.gradle
buildscript {
    def agpVersion = providers.systemProperty("agpVersion").forUseAtConfigurationTime().orNull ?: '4.1.0'

    ext.kotlin = providers.systemProperty('kotlinVersion').forUseAtConfigurationTime().orNull ?: '1.4.0'

    dependencies {
        classpath "com.android.tools.build:gradle:$agpVersion"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin"
    }
}

Teraz możesz określić wersje wtyczki Androida do obsługi Gradle i wtyczki Kotlin do obsługi Gradle w pliku scenariuszy i dodać nową metodę do plików źródłowych:

# <root-project>/scenarios.txt
non_abi_change_agp4.1.0_kotlin1.4.10 {
    tasks = [":app:assembleDebug"]
    apply-abi-change-to ["app/src/main/java/com/example/your_app/your_code_file.java,
                              "app/src/main/java/com/example/your_app/your_code_file.kt"]
    System-properties {
      "agpVersion" = "4.1.0"
      "kotlinVersion" = "1.4.10"
}

non_abi_change_agp4.2.0_kotlin1.4.20 {
    tasks = [":app:assembleDebug"]
    apply-abi-change-to ["app/src/main/java/com/example/your_app/your_code_file.java,
                              "app/src/main/java/com/example/your_app/your_code_file.kt"]
    System-properties {
      "agpVersion" = "4.2.0-alpha16"
      "kotlinVersion" = "1.4.20"
}

Profilowanie kompilacji przyrostowej

Większość kompilacji jest przyrostowa, co sprawia, że jest to jeden z najważniejszych scenariuszy do profilowania. Profiler Gradle zapewnia szerokie wsparcie dla profilowania kompilacji przyrostowych. Może automatycznie wprowadzać zmiany w pliku źródłowym, zmieniając treść metody, dodając nową metodę lub zmieniając układ albo zasób tekstowy. Możesz na przykład utworzyć takie scenariusze przyrostowe:

# <root-project>/scenarios.txt
non_abi_change {
    tasks = [":app:assembleDebug"]
    apply-non-abi-change-to = ["app/src/main/java/com/example/your_app/your_code_file.java,
                              "app/src/main/java/com/example/your_app/your_code_file.kt"]
}

abi_change {
    tasks = [":app:assembleDebug"]
    apply-abi-change-to = ["app/src/main/java/com/example/your_app/your_code_file.java,
                              "app/src/main/java/com/example/your_app/your_code_file.kt"]
}

layout_change {
    tasks = [":app:assembleDebug"]
    apply-android-layout-change-to = "app/src/main/res/your_layout_file.xml"
}
string_resource_change {
    tasks = [":app:assembleDebug"]
    apply-android-resource-value-change-to = "app/src/main/res/values/strings.xml"
}

Uruchomienie gradle-profiler --benchmark --project-dir <root-project> --scenario-file scenarios.txt spowoduje wygenerowanie raportu HTML z danymi analizy porównawczej.

Scenariusze przyrostowe możesz łączyć z innymi ustawieniami, takimi jak rozmiar sterty, liczba instancji roboczych czy wersja Gradle:

# <root-project>/scenarios.txt
non_abi_change_4g {
    tasks = [":app:assembleDebug"]
    apply-non-abi-change-to ["app/src/main/java/com/example/your_app/your_code_file.java,
                              "app/src/main/java/com/example/your_app/your_code_file.kt"]
    jvm-args = ["-Xmx4096m"]
}

non_abi_change_4g_8workers {
    tasks = [":app:assembleDebug"]
    apply-non-abi-change-to ["app/src/main/java/com/example/your_app/your_code_file.java,
                              "app/src/main/java/com/example/your_app/your_code_file.kt"]
    jvm-args = ["-Xmx4096m"]
    gradle-args = ["--max-workers=8"]
}

non_abi_change_3g_gradle67 {
    tasks = [":app:assembleDebug"]
    apply-non-abi-change-to ["app/src/main/java/com/example/your_app/your_code_file.java,
                              "app/src/main/java/com/example/your_app/your_code_file.kt"]
    jvm-args = ["-Xmx3072m"]
    version = ["6.7"]
}

Profilowanie czystej kompilacji

Aby przeprowadzić test porównawczy czystej kompilacji, możesz utworzyć scenariusz, który będzie używany do uruchamiania profilera Gradle:

# <root-project>/scenarios.txt
clean_build {
    tasks = [":app:assembleDebug"]
    cleanup-tasks = ["clean"]
}

Aby uruchomić ten scenariusz, użyj tego polecenia:

gradle-profiler --benchmark --project-dir <root-project> --scenario-file scenarios.txt

Za pomocą opcji Gradle --profile

Aby wygenerować i wyświetlić profil kompilacji z poziomu wiersza poleceń Gradle, wykonaj te czynności:

  1. Otwórz terminal wiersza poleceń w głównym katalogu projektu.

  2. Wykonaj czystą kompilację, wpisując to polecenie. Podczas profilowania kompilacji należy wykonać czystą kompilację między każdą profilowaną kompilacją, ponieważ Gradle pomija zadania, gdy dane wejściowe zadania (takie jak kod źródłowy) nie ulegają zmianie. Dlatego druga kompilacja bez zmian w danych wejściowych zawsze działa szybciej, ponieważ zadania nie są uruchamiane ponownie. Dlatego uruchamianie zadania clean między kompilacjami zapewnia profilowanie całego procesu kompilacji.

    // On Mac or Linux, run the Gradle wrapper using "./gradlew".
    gradlew clean
    
  3. Uruchom kompilację do debugowania jednego z wariantów usługi, np. „dev”, z tymi flagami:

    gradlew --profile --offline --rerun-tasks assemble<var>Flavor</var>Debug
    
    • --profile: włącza profilowanie.

    • --offline: uniemożliwia Gradle pobieranie zależności online. Dzięki temu opóźnienia spowodowane próbą aktualizacji zależności przez Gradle nie będą zakłócać danych profilowania. Projekt powinien być już raz skompilowany, aby Gradle pobrał i zapisał w pamięci podręcznej zależności.

    • --rerun-tasks: wymusza na Gradle ponowne uruchomienie wszystkich zadań i ignorowanie wszelkich optymalizacji zadań.

  4. Po zakończeniu kompilacji użyj okna Project (Projekt), aby przejść do katalogu project-root/build/reports/profile/ (jak pokazano na rysunku 1).

    Widok projektu w Android Studio z wygenerowanymi raportami profilu HTML w folderze build/reports/profile.
    Rysunek 1. Widok projektu wskazuje lokalizację raportów profilu.
  5. Kliknij prawym przyciskiem myszy plik profile-timestamp.html i wybierz Otwórz w przeglądarce > Domyślna. Raport powinien wyglądać podobnie do tego na ilustracji 2. Możesz sprawdzić każdą kartę w raporcie, aby dowiedzieć się więcej o kompilacji, np. kartę Wykonywanie zadania, która pokazuje, ile czasu zajęło Gradle wykonanie każdego zadania kompilacji.

    Raport profilu kompilacji w przeglądarce pokazujący bazowy czas kompilacji wynoszący 46,567 s.
    Rysunek 2. Raport dotyczący profilu kompilacji otworzy się w przeglądarce.
  6. Opcjonalnie: zanim wprowadzisz zmiany w projekcie lub konfiguracji kompilacji, powtórz polecenie z kroku 3, ale pomiń flagę --rerun-tasks. Gradle próbuje zaoszczędzić czas, nie wykonując ponownie zadań, których dane wejściowe się nie zmieniły, dlatego na karcie Task Execution (Wykonywanie zadań) raportu niezmienione zadania są oznaczone jako UP-TO-DATE, jak pokazano na rysunku 3. Dzięki temu możesz określić, które zadania wykonują pracę, gdy nie powinny. Jeśli na przykład zadanie :app:processDevUniversalDebugManifest nie jest oznaczone jako UP-TO-DATE, może to oznaczać, że konfiguracja kompilacji dynamicznie aktualizuje plik manifestu przy każdej kompilacji. Niektóre zadania, takie jak :app:checkDevDebugManifest, muszą być jednak wykonywane podczas każdej kompilacji.

    Karta Wykonywanie zadań w raporcie profilu kompilacji, na której widać zadania aplikacji oznaczone jako aktualne, z łącznym czasem kompilacji wynoszącym 0,280 s.
    Rysunek 3. Na karcie Wykonywanie zadania wyświetlane są wyniki wykonywania zadania.

Gdy masz już raport profilu kompilacji, możesz zacząć szukać możliwości optymalizacji, sprawdzając informacje na każdej karcie raportu. Niektóre ustawienia kompilacji wymagają eksperymentowania, ponieważ korzyści mogą się różnić w zależności od projektu i stacji roboczej. Na przykład w przypadku projektów z dużą bazą kodu warto zastosować zmniejszanie kodu, aby usunąć nieużywany kod i zmniejszyć rozmiar aplikacji. W przypadku mniejszych projektów lepsze może być jednak całkowite wyłączenie zmniejszania kodu. Dodatkowo zwiększenie rozmiaru sterty Gradle (za pomocą org.gradle.jvmargs) może negatywnie wpłynąć na wydajność na komputerach z małą ilością pamięci.

Po wprowadzeniu zmian w konfiguracji kompilacji sprawdź ich wyniki, powtarzając poprzednie kroki i generując nowy profil kompilacji. Na przykład rysunek 4 przedstawia raport dotyczący tej samej przykładowej aplikacji po zastosowaniu niektórych podstawowych optymalizacji opisanych w tym dokumencie.

      Raport dotyczący profilu kompilacji pokazujący czas kompilacji po optymalizacji wynoszący 29,668 s, czyli mniej niż 46,567 s na rysunku 2.
Rysunek 4. Raport profilu kompilacji pokazuje krótsze czasy kompilacji po zoptymalizowaniu szybkości kompilacji.