Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lazy APIを使ってGradleビルド速度を改善する

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Lazy APIを使ってGradleビルド速度を改善する

DroidKaigi.collect { #30@Tokyo }

Avatar for mikan

mikan

May 29, 2026

More Decks by mikan

Other Decks in Technology

Transcript

  1. Q1. 2つのコードの違いは? tasks.withType<Test> { useJUnitPlatform() } tasks.withType<Test>().configureEach { useJUnitPlatform() }

    A B 両者ともにTest 関連のタスクに JUnit を使う設定を追加している ( 差分は1 行目) 1 / 33
  2. Q2. 2つのコードの違いは? tasks.create<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } tasks.register<Copy>("copyGitHooks") { from("$rootDir/git-hooks/")

    into("$rootDir/.git/hooks") } A B git-hooks ディレクトリの中身を .git/hooks にコピーするタスクを定義している ( どちらも目的は同じ) 1 / 33
  3. 高速なイテレーション = ビルド速度 変更 ビルド 結果確認 プログラムを変更 → ビルド →

    結果確認、このサイク ルを延々と回す 小さい変更でビルドに時間がかかると、開発生産性が 悪化する 1 / 33
  4. 高速なイテレーション = ビルド速度 変更 ビルド 結果確認 プログラムを変更 → ビルド →

    結果確認、このサイク ルを延々と回す 小さい変更でビルドに時間がかかると、開発生産性が 悪化する 巨大なコードベースを抱えるプロダクトでは特に顕著 1 / 33
  5. 高速なイテレーション = ビルド速度 変更 ビルド 結果確認 プログラムを変更 → ビルド →

    結果確認、このサイク ルを延々と回す 小さい変更でビルドに時間がかかると、開発生産性が 悪化する 巨大なコードベースを抱えるプロダクトでは特に顕著 AI に書かせる時代でも (むしろ AI に書かせるからこ そ) イテレーション効率は重要 1 / 33
  6. Gradleのビルドフェーズ INIT Initialization CONFIG Configuration EXEC Execution settings.gradle.kts プロジェクト一覧を確定 ◆

    build.gradle.kts タスク登録 ◆ 依存宣言 ◆ = タスクグラフ作成 ◆ task graph タスクグラフに従い ◆ タスクを実行 ◆ 1 / 33
  7. Gradleのビルドフェーズ INIT Initialization EXEC Execution FOCUS CONFIGURATION settings.gradle.kts プロジェクト一覧を確定 ◆

    CONFIG Configuration build.gradle.kts タスク登録 ◆ 依存宣言 ◆ = タスクグラフ作成 ◆ task graph タスクグラフに従い ◆ タスクを実行 ◆ 1 / 33
  8. Lazy API を​ 意識して​ Configuration Cache を​ 効かせると、​ Gradle の​

    ビルド速度を​ 継続的に​ 改善できる​ 1 / 33
  9. Lazy API と Eager API は見分けにくい Configuration Cache を効かせるには Lazy

    API への置き換えが要件 (詳細は第3章) ▸ Lazy ではない API (= Eager API) と見た目で区別がつかない ▸ 気づかないうちに Eager API を使っていることがある ▸ 今日話すこと ① Task まわりのAPI ② Provider API 既存タスクへの設定追加 / カスタムタスク作成時 に意識するべきAPI plugin のDSL設定 / convention plugin 作成時に 使うAPI 1 / 33
  10. 第1章 Task まわりの API register と create ・ withType /

    configureEach / named ・ Lazy API のメンタルモデル ・ 1 / 33
  11. タスクの登録: create と register [BAD] EAGER tasks.create API CALL ▶▶

    INSTANTIATE 呼び出し時に即座にインスタンス生成 [GOOD] LAZY tasks.register API CALL ▶▶ ⌛ ▶▶ INSTANTIATE 必要になるまでインスタンス生成を遅延 VS 1 / 33
  12. # tasks.create — Eager API build.gradle.kts KOTLIN EAGER API ・

    DEPRECATED tasks.create<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  13. # tasks.create — Eager API ▸ 呼び出されたときに即座にTaskインスタンスを生成 build.gradle.kts KOTLIN EAGER

    API ・ DEPRECATED tasks.create<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  14. # tasks.create — Eager API ▸ 呼び出されたときに即座にTaskインスタンスを生成 ▸ 依存グラフに含まれないTaskでも生成される build.gradle.kts

    KOTLIN EAGER API ・ DEPRECATED tasks.create<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  15. # tasks.create — Eager API ▸ 呼び出されたときに即座にTaskインスタンスを生成 ▸ 依存グラフに含まれないTaskでも生成される ▸

    ⊘ create は deprecated → IDEなら警告で気付ける build.gradle.kts KOTLIN EAGER API ・ DEPRECATED tasks.create<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  16. # tasks.create — Eager API ▸ 呼び出されたときに即座にTaskインスタンスを生成 ▸ 依存グラフに含まれないTaskでも生成される ▸

    ⊘ create は deprecated → IDEなら警告で気付ける ▸ AIに生成してもらった場合は気付きにくい → レビューでも見落としやすい build.gradle.kts KOTLIN EAGER API ・ DEPRECATED tasks.create<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  17. # tasks.create — Eager API ▸ 呼び出されたときに即座にTaskインスタンスを生成 ▸ 依存グラフに含まれないTaskでも生成される ▸

    ⊘ create は deprecated → IDEなら警告で気付ける ▸ AIに生成してもらった場合は気付きにくい → レビューでも見落としやすい ▸ 小さなタスクならコストは無視できるが、数百〜数千あると Configuration フェーズが肥大化 build.gradle.kts KOTLIN EAGER API ・ DEPRECATED tasks.create<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  18. # tasks.register — Lazy API build.gradle.kts KOTLIN LAZY API tasks.register<Copy>("copyGitHooks")

    { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  19. # tasks.register — Lazy API ▸ 呼ばれた瞬間にはインスタンス生成されない build.gradle.kts KOTLIN LAZY

    API tasks.register<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  20. # tasks.register — Lazy API ▸ 呼ばれた瞬間にはインスタンス生成されない ▸ Executionフェーズまで 評価が遅延される

    build.gradle.kts KOTLIN LAZY API tasks.register<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  21. # tasks.register — Lazy API ▸ 呼ばれた瞬間にはインスタンス生成されない ▸ Executionフェーズまで 評価が遅延される

    ▸ 不要なタスクのインスタンス化コストを払わずに済む build.gradle.kts KOTLIN LAZY API tasks.register<Copy>("copyGitHooks") { from("$rootDir/git-hooks/") into("$rootDir/.git/hooks") } 1 / 33
  22. 既存タスクへの設定追加 Eager tasks.withType<Test> { ... } 該当する全タスクを即座に評価 Lazy tasks.withType<Test>().confi gureEach

    { ... } 該当する全 Test タスクに共通設定を入 れたいとき (遅延評価) Lazy tasks.named<Test>("testDebug UnitTest") { ... } 特定の名前のタスク 1 つだけに設定した いとき (遅延評価) 無駄に Configuration フェーズが長くなるので Lazy API を使う。 withType だけ → 該当タスクを即座に評価 (Eager) withType + configureEach → 評価を遅延 (Lazy) named → 単一タスクの遅延評価 (Lazy) 1 / 33
  23. Plugin の引数をセットする spotless {} , roborazzi {} , android {}

    , androidComponents {} ... build.gradle.kts roborazzi { ... } ▶▶ Extension (DSL) RoborazziExtension ▶▶ Plugin RoborazziPlugin ▶▶ Task RecordRoborazziTask プリミティブ値だけならコストは気にしなくてよい ▸ オブジェクト や 外部プロセスの結果 を渡す場合は要注意 ▸ → Lazy なAPIで値を渡すことで Configuration フェーズのコストを削減 ▸ 1 / 33
  24. Provider<T> 値の評価を遅延させるためのインターフェース get() が呼ばれるまで評価されない providers プロパティ経由でいろいろな種類が作れる snippet.kts KOTLIN val greeting:

    Provider<String> = providers.provider { "Hello, Gradle!" } // 基本: providers.provider { ... } で任意の計算を遅延 println(greeting.get()) // → Hello, Gradle! println(greeting.orNull) // → missing なら null println(greeting.getOrElse("fallback")) // → missing なら fallback 1 / 33
  25. Provider<T> 値の評価を遅延させるためのインターフェース get() が呼ばれるまで評価されない providers プロパティ経由でいろいろな種類が作れる snippet.kts KOTLIN println(greeting.get()) //

    → Hello, Gradle! println(greeting.orNull) // → missing なら null println(greeting.getOrElse("fallback")) // → missing なら fallback // 基本: providers.provider { ... } で任意の計算を遅延 val greeting: Provider<String> = providers.provider { "Hello, Gradle!" } 1 / 33
  26. Provider<T> のバリエーション H U B providers environmentVariable 環境変数 gradleProperty gradle.properties

    fileContents ファイル内容 exec 外部プロセス ValueSource カスタム 1 / 33
  27. # providers.environmentVariable / providers.gradleProperty // aws/aws-toolkit-jetbrains fun Project.isCi() : Boolean

    = providers.environmentVariable("CI").isPresent // JetBrains/compose-hot-reload // chr.tests.sequential プロパティがあれば並列実行系のシステムプロパティをセット if (!providers.gradleProperty("chr.tests.sequential").isPresent) { val parallelism = providers.gradleProperty("chr.tests.parallelism") .getOrElse("2").toInt() systemProperty("junit.jupiter.execution.parallel.enabled", true) systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") systemProperty("junit.jupiter.execution.parallel.config.strategy", "fixed") systemProperty("junit.jupiter.execution.parallel.config.fixed.parallelism", parallelism) systemProperty("junit.jupiter.execution.parallel.config.fixed.max-pool-size", parallelism } 1 / 33
  28. # providers.fileContents / providers.exec // androidx/androidx — ファイル内容を遅延評価 val generateTestConfigurationTask

    = tasks.register( taskName, GenerateTestConfigurationTask::class.java ) { task -> task.applicationId.set( project.providers.fileContents(applicationIdFile).asText) task.testApk.set(testApk) // RevenueCat/purchases-kmp — 外部プロセスの結果を遅延評価 private fun Project.getToolchainPath(): Provider<String> = providers.exec { commandLine("xcrun", "--find", "swift") }.standardOutput.asText.map { swiftPath -> // swift is at .../XcodeDefault.xctoolchain/usr/bin/swift // but we need .../Toolchains/XcodeDefault.xctoolchain File(swiftPath.trim()) .parentFile.parentFile.absolutePath } 1 / 33
  29. Property<T> Provider<T> READ-ONLY 継承 Property<T> READ / WRITE Provider は一度セットすると上書き不可

    Property は後から上書き可能 DSL の引数として使われることが多い 1 / 33
  30. # Property への値のセット build.gradle.kts KOTLIN クリックで切り替え // Kotlin Gradle DSL

    なら = も使える roborazzi { outputDir = file("src/screenshots") } 1 / 33
  31. 例: versionName のサフィックスにブランチ名 を入れたい 通常: android.buildTypes.debug { versionNameSuffix = ...

    } ▸ 問題: versionNameSuffix は String? 型 → Provider を渡せない ▸ 解決: androidComponents.onVariants を使う ▸ 1 / 33
  32. # .get() を呼ぶ vs Provider のまま渡す app/build.gradle.kts KOTLIN // 悪い例:

    Configuration フェーズで .get() を呼ぶ androidComponents { onVariants(selector().withBuildType("debug")) { variant -> val mainOutput = variant.outputs.first { it.outputType == VariantOutputConfiguration.OutputType.SINGLE } mainOutput.versionName = providers .exec { commandLine("git", "rev-parse", "--abbrev-ref", "HEAD") } .standardOutput.asText.map { it.trim() } .map { branch -> "1.0.0-$branch" } .get() // ⚠️ Configuration フェーズで即評価される } } 1 / 33
  33. # .get() を呼ぶ vs Provider のまま渡す app/build.gradle.kts KOTLIN // 良い例:

    Provider のまま Property に代入 androidComponents { onVariants(selector().withBuildType("debug")) { variant -> val mainOutput = variant.outputs.first { it.outputType == VariantOutputConfiguration.OutputType.SINGLE } mainOutput.versionName = providers .exec { commandLine("git", "rev-parse", "--abbrev-ref", "HEAD") } .standardOutput.asText.map { it.trim() } .map { branch -> "1.0.0-$branch" } // ← .get() を消して Provider のまま代入 } } 1 / 33
  34. Configuration Cache とは 01 初回ビルド [ CACHE ] Configuration Cache

    02 2 回目以降 INIT ▶▶ CONFIGURATION ▶▶ EXEC INIT ▶▶ ▶▶ EXEC CONFIG SKIP タスクグラフを保存 タスクグラフを復元 ただし、キャッシュ生成にはいくつかの制約がある 1 / 33
  35. # 制約: Project を Execution フェーズで直接参照しない build.gradle.kts KOTLIN PROJECT を

    EXECUTION フェーズで参照している例 tasks.register("printVersion") { doLast { println("group=${project.group} version=${project.version}") // ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ // Execution フェーズで Project を参照している } } 1 / 33
  36. # エラー出力 - Task `:printVersion` of type `org.gradle.api.DefaultTask`: invocation of

    'Task.project' at execution time is unsupported $ ./gradlew printVersion Calculating task graph as no cached configuration is available > Task :printVersion FAILED 1 problem was found storing the configuration cache. with the configuration cache. See https://docs.gradle.org/9.5.0/userguide/configuration_cache_requirements.html FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':printVersion'. > Invocation of 'Task.project' by task ':printVersion' 1 / 33
  37. # 修正方法 // 修正前: Execution フェーズで project を参照 tasks.register("printVersion") {

    doLast { println("group=${project.group} version=${project.version}") } } 1 / 33
  38. # 修正方法 // 修正案 1: Configuration フェーズで変数に格納 tasks.register("printVersion") { val

    groupName = project.group.toString() val version = project.version.toString() doLast { println("group=${groupName} version=${version}") } } 1 / 33
  39. # 修正方法 provider { ... } の中で Project を参照する分には Configuration

    Cache と互換になる。 // 修正案 2: Provider に包んで渡す tasks.register("printVersion") { val groupName = provider { project.group.toString() } val version = provider { project.version.toString() } doLast { println("group=${groupName.get()} version=${version.get()}") } } 1 / 33
  40. Configuration Cache の効果 Square (Android, 4,000 modules) 136s → 74s

    3× faster (CC が事前 検証) −45% / 3× ローカルビルド (avg) 設定エラーの検出 出典: Square Engineering Blog https://developer.squareup.com/blog/5-400-hours-a-year-saving-developers-time-and-sanity-with-gradles/ 1 / 33
  41. まとめ 1. ビルド速度改善には Configuration フェーズの短縮が鍵 2. Task の登録は register (Lazy)、設定追加は

    configureEach / named (Lazy) を使う 3. DSL に値を渡すときは Provider / Property で評価を遅延させる 4. Property に値を渡すときは .get() を呼ばずに Provider のまま渡す 5. Configuration Cache 互換にするため、Execution フェーズで Project を直接参照しない 1 / 33