Шесть минут. Именно столько занимала сборка нашего монорепозитория с тремя Kotlin-модулями. Локально — 6 минут, в CI — 8 минут (холодный запуск). Разработчики тратили 20% рабочего времени просто ожидая сборку. Я решил это исправить и потратил две недели. Рассказываю по шагам, что дало реальный эффект, а что — нет.
Измеряем прежде чем оптимизировать
Первая ошибка, которую делают все — начинают включать флаги, не замерив текущее состояние. В Gradle есть встроенный profiler:
./gradlew assemble --profile --no-daemon
# Или подробнее:
./gradlew assemble --scan--profile создаёт HTML-отчёт в build/reports/profile/. Там видно разбивку по фазам: Configuration, Execution, Up-to-date checking. В нашем случае: Configuration phase: 45 секунд. Execution phase: 4 минуты 30 секунд. Total: 5 минут 15 секунд. 45 секунд на конфигурацию для трёх модулей — очень много.
Build Cache
Build Cache в Gradle — механизм, который кэширует результаты тасок на основе хешей входных файлов. Если входы не изменились — таска не выполняется, результат берётся из кэша:
// gradle.properties
org.gradle.caching=trueBuild Cache работает только для тасок, которые его поддерживают. Большинство стандартных тасок поддерживают, но кастомная таска требует явного указания входов и выходов:
abstract class GenerateOpenApi : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val specFile: RegularFileProperty
@get:OutputFile
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
// генерация OpenAPI спецификации
}
}После включения Build Cache наша сборка при неизменённом коде упала с 5:15 до 1:20. Почти 4 минуты сэкономили. Но холодная сборка в CI всё ещё занимала 5+ минут.
Configuration Cache
Configuration Cache кэширует результат фазы конфигурации — все build.gradle.kts файлы парсятся один раз, затем результат кэшируется. При следующих запусках фаза конфигурации пропускается целиком.
// gradle.properties
org.gradle.configuration-cache=trueЗвучит просто, но половина плагинов сломалась. Kotlin Spring Plugin, Jooq Plugin — использовали API, несовместимый с Configuration Cache. Пришлось фиксить. Самая частая проблема — обращение к project в не lazy-контексте:
// БЫЛО (не работает с Configuration Cache):
tasks.test {
val testDbUrl = project.property("test.db.url") as String
environment("DB_URL", testDbUrl)
}
// СТАЛО (работает):
tasks.test {
val testDbUrl = providers.gradleProperty("test.db.url")
environment("DB_URL", testDbUrl)
}После фикса конфигурация упала с 45 секунд до 3 секунд. 93% экономии.
Configuration Avoidance API
Не конфигурировать таски, которые не будут выполняться. Если запускаете ./gradlew test, зачем конфигурировать jar и dockerBuild?
// СТАЛО (lazy configuration):
tasks.registering(DockerBuildTask::class) {
inputDir.set(layout.buildDirectory.dir("libs"))
}
tasks.named("test") {
useJUnitPlatform()
} Configuration Avoidance дал ещё 5 секунд экономии. Мало, но в совокупности — важно.
Итоговые результаты
Холодная сборка: 5:15 до 3:40 (Build Cache в CI шарится между раннерами). Горячая сборка (без изменений): 5:15 до 0:45. Ускорение в 7 раз. Build Cache дал 75% экономии execution phase, Configuration Cache — 93% экономии configuration phase. Главный совет: не верьте туториалам — измеряйте. Build Cache без правильно объявленных входов/выходов кастомных тасок не работает. Тратите время на профилирование, это окупается.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье