Jak skróciłem 9-minutowy build Gradle do 90 sekund
Trzy zmiany, które zrobiły 90% roboty, i profilowanie, które je wskazało.
Nasz pipeline CI po cichu dorósł do dziewięciu minut. Oto jak trzy zmiany — i sporo profilowania — sprowadziły go poniżej dziewięćdziesięciu sekund, bez dotykania kodu aplikacji.
Zasada pierwsza: mierz, nie zgaduj
Zanim cokolwiek zmieniłem, uruchomiłem build z --scan i przeczytałem oś czasu. Dziewięć minut brzmi jak “wszystko jest wolne”, ale scan pokazał inną historię: dwie trzecie czasu siedziały w trzech taskach, a reszta była już w porządku.
Ten wzorzec powtarza się niemal zawsze. Optymalizacja to problem przeszukiwania, a profiler jest mapą. Zgadywanie tylko przekłada pracę w inne miejsce.
Zmiana 1 — zrównoleglenie archiwizacji
Największym winowajcą było pakowanie: zipowanie tysięcy plików klas do jarów, jednowątkowo, na 16-rdzeniowym runnerze. Napisałem więc plugin, który rozprasza archiwizację po rdzeniach, zachowując wynik identyczny bit po bicie (reproducible).
// build.gradle.kts
plugins {
id("io.github.kukis13.parallel-zip") version "1.2.0"
}
tasks.withType<Zip>().configureEach {
parallel = true // wszystkie rdzenie, reprodukowalny wynik
}
Zip 512 modułów: 1.38s → 0.39s (3.5×). Na tasku war było bliżej 11×.
Zmiana 2 — naprawa grafu tasków
Źle skonfigurowana zależność wymuszała pełną rekompilację każdego modułu przy zmianie jednego. Jedna linia w convention pluginie przywróciła kompilację inkrementalną i build cache Gradle zaczął faktycznie trafiać.
Zmiana 3 — koniec robienia tego samego dwa razy
Krok generowania kodu uruchamiał się przy każdym buildzie, nawet gdy jego wejścia się nie zmieniły, bo wyjścia nie były zadeklarowane. Poprawne inputs/outputs pozwoliły Gradle całkowicie go pominąć przy trafieniu w cache.
Razem mediana buildu CI spadła z dziewięciu minut do około dziewięćdziesięciu sekund. Najlepsze jest to, że nic z tego nie dotknęło kodu produktu — tylko maszynerię wokół niego.