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

Gradle recipes for reducing your build times (d...

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →
Avatar for Adam Ahmed Adam Ahmed
October 29, 2022

Gradle recipes for reducing your build times (droidcon London 2022)

Lessons learned from how we reduced the build times on a large project from 14 minutes down to 2 minutes.

In this talk, we’ll go through some of the issues that we discovered were causing our builds to take so long and what we learned from them, then we’ll deep-dive into how we went about fixing these issues, and how we tried to ensure that we never end up in this position again.

You don’t need to be a Gradle expert to attend this talk or to make these changes to your code-base to improve your own build times.

Avatar for Adam Ahmed

Adam Ahmed

October 29, 2022

More Decks by Adam Ahmed

Other Decks in Programming

Transcript

  1. What We’ll Cover • Gradle jargon • The state of

    our build setup • Easy wins @oheyadam
  2. What We’ll Cover • Gradle jargon • The state of

    our build setup • Easy wins • General advice and best practices @oheyadam
  3. What We’ll Cover • Gradle jargon • The state of

    our build setup • Easy wins • General advice and best practices • Our build setup now @oheyadam
  4. What We’ll Cover • Gradle jargon • The state of

    our build setup • Easy wins • General advice and best practices • Our build setup now • Measuring @oheyadam
  5. Tasks • Basic units of work • Builds are modeled

    as Directed Acyclic Graphs of tasks @oheyadam
  6. Tasks • Basic units of work • Builds are modeled

    as Directed Acyclic Graphs of tasks • DAGs are wired and generated based on task dependencies @oheyadam
  7. Tasks • Basic units of work • Builds are modeled

    as Directed Acyclic Graphs of tasks • DAGs are wired and generated based on task dependencies • Tasks can have Actions, Inputs, and Outputs @oheyadam
  8. Tasks • Basic units of work • Builds are modeled

    as Directed Acyclic Graphs of tasks • DAGs are wired and generated based on task dependencies • Tasks can have Actions, Inputs, and Outputs • Cacheable @oheyadam
  9. Tasks De fi ning Tasks @CacheableTask abstract class GreetingsTask :

    DefaultTask() { @get:Input abstract val place: Property<String> @get:OutputFile abstract val output: RegularFileProperty @TaskAction fun greet() { val greeting = "Hello ${place.get()}" println(greeting) output.asFile.get().writeText(greeting) } }
  10. Tasks De fi ning Tasks @CacheableTask abstract class GreetingsTask :

    DefaultTask() { @get:Input abstract val place: Property<String> @get:OutputFile abstract val output: RegularFileProperty @TaskAction fun greet() { val greeting = "Hello ${place.get()}" println(greeting) output.asFile.get().writeText(greeting) } }
  11. Tasks De fi ning Tasks @CacheableTask abstract class GreetingsTask :

    DefaultTask() { @get:Input abstract val place: Property<String> @get:OutputFile abstract val output: RegularFileProperty @TaskAction fun greet() { val greeting = "Hello ${place.get()}" println(greeting) output.asFile.get().writeText(greeting) } }
  12. Tasks De fi ning Tasks @CacheableTask abstract class GreetingsTask :

    DefaultTask() { @get:Input abstract val place: Property<String> @get:OutputFile abstract val output: RegularFileProperty @TaskAction fun greet() { val greeting = "Hello ${place.get()}" println(greeting) output.asFile.get().writeText(greeting) } }
  13. Tasks De fi ning Tasks @CacheableTask abstract class GreetingsTask :

    DefaultTask() { @get:Input abstract val place: Property<String> @get:OutputFile abstract val output: RegularFileProperty @TaskAction fun greet() { val greeting = "Hello ${place.get()}" println(greeting) output.asFile.get().writeText(greeting) } }
  14. Projects/Modules • Gradle builds are made up of one or

    more projects • Work that Gradle can do on a project is defined by one or more tasks @oheyadam
  15. Projects/Modules • Gradle builds are made up of one or

    more projects • Work that Gradle can do on a project is defined by one or more tasks • Tasks are provided by applying a plugin, or you can write them yourself @oheyadam
  16. Projects/Modules • Gradle builds are made up of one or

    more projects • Work that Gradle can do on a project is defined by one or more tasks • Tasks are provided by applying a plugin, or you can write them yourself • Android Modules are Gradle Projects @oheyadam
  17. Plugins • Plugins are an amalgamation of Tasks • Used

    for configuring Projects • Encapsulate repetitive logic @oheyadam
  18. Plugins • Plugins are an amalgamation of Tasks • Used

    for configuring Projects • Encapsulate repetitive logic • Android Gradle Plugin @oheyadam
  19. Plugins • Plugins are an amalgamation of Tasks • Used

    for configuring Projects • Encapsulate repetitive logic • Android Gradle Plugin • com.android.application • com.android.library @oheyadam
  20. The State of Our Build Setup • An average of

    14 minutes for local build times @oheyadam
  21. The State of Our Build Setup • An average of

    14 minutes for local build times • A lot of code duplication @oheyadam
  22. The State of Our Build Setup • An average of

    14 minutes for local build times • A lot of code duplication • Outdated tooling dependencies @oheyadam
  23. The State of Our Build Setup • An average of

    14 minutes for local build times • A lot of code duplication • Outdated tooling dependencies • Plugins applied needlessly everywhere @oheyadam
  24. The State of Our Build Setup • An average of

    14 minutes for local build times • A lot of code duplication • Outdated tooling dependencies • Plugins applied needlessly everywhere • Tasks are always created eagerly @oheyadam
  25. Easy Wins • Enable file-system watching • org.gradle.vfs.watch=true • Enable

    configuration on demand • org.gradle.configureondemand=true @oheyadam
  26. Easy Wins • Enable file-system watching • org.gradle.vfs.watch=true • Enable

    configuration on demand • org.gradle.configureondemand=true • Enable parallel execution • org.gradle.parallel=true @oheyadam
  27. Easy Wins • Enable parallel execution • org.gradle.parallel=true • Enable

    build caching • org.gradle.caching=true @oheyadam
  28. General Advice and Best Practices • Be vigilant about adding

    new plugins to your projects • Check if you can disable plugins on debug builds • e.g., the Firebase Performance Monitoring plugin @oheyadam
  29. General Advice and Best Practices • Check if you can

    disable plugins on debug builds • e.g., the Firebase Performance Monitoring plugin android { buildTypes { debug { FirebasePerformance { instrumentationEnabled false } } } }
  30. General Advice and Best Practices • Be vigilant about adding

    new plugins to your projects • Check if you can disable plugins on debug builds • e.g., the Firebase Performance Monitoring plugin • Replace KAPT with KSP wherever possible • Room and Moshi are already compatible @oheyadam
  31. General Advice and Best Practices • Be vigilant about adding

    new plugins to your projects • Check if you can disable plugins on debug builds • e.g., the Firebase Performance Monitoring plugin • Replace KAPT with KSP wherever possible • Room and Moshi are already compatible • Look into square/anvil or JakeWharton/dagger-reflect https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android#disable-gradle-plugin https://kotlinlang.org/docs/ksp-overview.html#supported-libraries https://github.com/square/anvil https://github.com/JakeWharton/dagger-reflect
  32. General Advice and Best Practices • Look into square/anvil or

    JakeWharton/dagger-reflect • Modularize @oheyadam
  33. General Advice and Best Practices • Look into square/anvil or

    JakeWharton/dagger-reflect • Modularize • Not every module needs to be an Android module @oheyadam
  34. General Advice and Best Practices • Look into square/anvil or

    JakeWharton/dagger-reflect • Modularize • Not every module needs to be an Android module • Favor implementation() configurations over api() @oheyadam
  35. General Advice and Best Practices • Look into square/anvil or

    JakeWharton/dagger-reflect • Modularize • Not every module needs to be an Android module • Favor implementation() configurations over api() • Do as little computation as possible during the configuration phase @oheyadam
  36. General Advice and Best Practices • Do as little computation

    as possible during the configuration phase • Favor custom tasks over scripts @oheyadam
  37. General Advice and Best Practices • Do as little computation

    as possible during the configuration phase • Favor custom tasks over scripts • Make most tasks cacheable using @CacheableTask • copy/jar/zip types are faster to rerun • Non-stable inputs make cache hits difficult @oheyadam
  38. General Advice and Best Practices • Do as little computation

    as possible during the configuration phase • Favor custom tasks over scripts • Make most tasks cacheable using @CacheableTask • copy/jar/zip types are faster to rerun • Non-stable inputs make cache hits difficult • Prefer lazy Gradle APIs over eager ones to take advantage of Configuration Avoidance https://github.com/liutikas/gradle-best-practices
  39. Lazily register tasks, don’t eagerly create them tasks.register("greetings") { sleep

    2000 doLast { println("Hello Droidcon!”) } } tasks.create(“greetings") { sleep 2000 doLast { println("Hello Droidcon!”) } } @oheyadam
  40. General Advice and Best Practices • Prefer lazy Gradle APIs

    over eager ones to take advantage of Configuration Avoidance • Enable non-transitive R classes @oheyadam
  41. General Advice and Best Practices • Prefer lazy Gradle APIs

    over eager ones to take advantage of Configuration Avoidance • Enable non-transitive R classes • Disable Jetifier @oheyadam
  42. General Advice and Best Practices • Prefer lazy Gradle APIs

    over eager ones to take advantage of Configuration Avoidance • Enable non-transitive R classes • Disable Jetifier • Disable build variants that aren’t relevant https://blog.blundellapps.co.uk/speed-up-your-build-non-transitive-r-files/ https://twitter.com/n8ebel/status/1455347318199259137 https://adambennett.dev/2020/08/disabling-jetifier/
  43. General Advice and Best Practices Disable build variants that aren’t

    relevant androidComponents { beforeVariants { variantBuilder -> if (variantBuilder.productFlavors.containsAll(listOf("api" to "minApi21", "mode" to "demo"))) { variantBuilder.enabled = false } } } https://developer.android.com/studio/build/build-variants#filter-variants
  44. General Advice and Best Practices • Disable build variants that

    aren’t relevant • Don’t use buildSrc. Use Included Builds instead @oheyadam
  45. General Advice and Best Practices • Disable build variants that

    aren’t relevant • Don’t use buildSrc. Use Included Builds instead • Use Version Catalogs and Type-safe Project Accessors for easier dependency management dependencies { implementation(projects.libraryAnalytics) implementation(libs.androidx.fragment.ktx) } https://github.com/android/nowinandroid https://developer.squareup.com/blog/herding-elephants/ https://docs.gradle.org/7.0/userguide/declaring_dependencies.html#sec:type-safe-project-accessors
  46. General Advice and Best Practices • Use Version Catalogs and

    Type-safe Project Accessors for easier dependency management • Enable Configuration Caching • org.gradle.unsafe.configuration-cache=true @oheyadam
  47. General Advice and Best Practices • Use Version Catalogs and

    Type-safe Project Accessors for easier dependency management • Enable Configuration Caching
  48. General Advice and Best Practices • Use Version Catalogs and

    Type-safe Project Accessors for easier dependency management • Enable Configuration Caching
  49. General Advice and Best Practices • Enable Configuration Caching •

    You no longer need to add a Kotlin file to your Java modules to make them cooperate with Kotlin’s IC • kotlin.incremental.useClasspathSnapshot=true @oheyadam
  50. General Advice and Best Practices • You no longer need

    to add a Kotlin file to your Java modules to make them cooperate with Kotlin’s IC* • kotlin.incremental.useClasspathSnapshot=true • Disable BuildConfig generation for modules that don’t need it • android.defaults.buildfeatures.buildconfig=false @oheyadam
  51. General Advice and Best Practices • Disable BuildConfig generation for

    modules that don’t need it • android.defaults.buildfeatures.buildconfig=false • In fact, you can opt out of many android build features that are enabled by default if you don’t need them https://kotlinlang.org/docs/whatsnew17.html#a-new-approach-to-incremental-compilation
  52. General Advice and Best Practices • In fact, you can

    opt out of many android build features that are enabled by default if you don’t need them • android.defaults.buildfeatures.aidl=false • android.defaults.buildfeatures.renderscript=false • android.defaults.buildfeatures.resvalues=false • android.defaults.buildfeatures.shaders=false @oheyadam
  53. Our Build Setup Now • ~2 minutes for incremental changes

    • Down to ~1 minute after switching to M1 MacBook Pros @oheyadam
  54. Our Build Setup Now • ~2 minutes for incremental changes

    • Down to ~1 minute after switching to M1 MacBook Pros • Convention plugins to reuse and contain build logic @oheyadam
  55. Our Build Setup Now • ~2 minutes for incremental changes

    • Down to ~1 minute after switching to M1 MacBook Pros • Convention plugins to reuse and contain build logic • Very lean build scripts @oheyadam
  56. Our Build Setup Now Very Lean Build Scrips plugins {

    id("android.feature") } android { namespace = "com.company.module.name" } dependencies { implementation(projects.libraryAnalytics) implementation(libs.androidx.fragment.ktx) testImplementation(projects.coreTesting) androidTestImplementation(projects.coreUiTesting) }
  57. Measuring • Measure, measure, measure! • Use the Gradle-profiler •

    Use Gradle Enterprise • Write your own BuildLifecycleListener https://github.com/gradle/gradle-pro fi ler https://gradle.com/roi-calculator/ https://gist.github.com/oheyadam/d30a104091753fc79793bc32aea39d2e
  58. Write Your Own BuildLifecycleListener class BuildLifecycleListener : BuildAdapter() { override

    fun buildFinished(result: BuildResult) { // … } } class BuildLifecyclePlugin : Plugin<Project> { override fun apply(target: Project) { target.gradle.addBuildListener(BuildLifecycleListener()) } }