State-of-the-art Gradle Android Builds

Gradle Night Berlin

Volker Leck (@devisnik) - Gradle Inc.

Gradle.{org,com}

Gradle.org

Gradle is a build and automation tool.

  • agnostic Build System

  • 100% Free Open Source - Apache Standard License 2.0

Gradle Inc.

The company behind Gradle.

  • Build Happiness

  • Employs full time engineers

  • Providing Gradle Build Scans and Gradle Enterprise

  • Consulting and Training

What is a modern build?

  • reliable / reproducible

  • customizable

  • universal

  • self-contained

  • fast, smart

  • maintainable

Build engineering

In a healthy culture, developers and engineering leaders understand that the domain of building and integrating software is just as complex and challenging as the domain of application development.

— Hans Dockter, Founder and CEO of Gradle Inc.

Gradle in a nutshell

Core Engine

gradle task dag

Core Engine

  • Groovy and Kotlin build scripts

  • Task configuration and execution

  • Dependency resolution

  • Work avoidance

├── [ 962]  build.gradle
├── [  96]  gradle
│       ├── [ 54K]  gradle-wrapper.jar
│       └── [ 202]  gradle-wrapper.properties
├── [5.8K]  gradlew
├── [2.9K]  gradlew.bat
├── [ 359]  settings.gradle

Plugins

  • adapt core engine to a domain

  • make build declarative

  • Core Plugins

    • java-library, application, groovy, maven-publish …​

  • Community Plugins

    • kotlin, android, golang, pygradle, asciidoctor …​

  • Plugins contribute

    • reusable and configurable tasks

    • configurable extensions

  • Plugins contribute a model to configure

    • in build scripts

    • using a DSL

A Java library (Groovy DSL)

plugins {
   id "java-library"
}

dependencies {
   api "com.acme:foo:1.0"
   implementation "com.zoo:monkey:1.1"
}

tasks.withType(JavaCompile) {
    // ...
}

A native app (Kotlin DSL)

plugins {
    `cpp-application`
}

application {
    baseName = "my-app"
}

toolChains {
    // ...
}

An Android App

apply plugin: 'com.android.application'

dependencies {
    implementation "androidx.appcompat:appcompat:1.0.2"
    // ...
}

android {
    compileSdkVersion 28
    buildToolsVersion "28.0.3"
    // ...
    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 28
    }
    // ...
}

DEMO

Perf matters

What is slow?

  • structured approach

    • define scenario

    • profile

    • identify bottlenecks

    • fix

    • verify fix

    • repeat

Toolbelt

  • --profile

  • --scan

  • Gradle profiler

Build Scans

Build Scan Overview

Gradle Profiler

  • runs pre-defined scenarios

  • can produce Build scans

check-no-tests {
    tasks = ["check"]
    gradle-args = ["-x", "test"]
    cleanup-tasks = ["clean"]
    run-using = tooling-api // value can also be "cli" or "no-daemon"
    warm-ups = 2
}

General Advice

Stay up-to-date

./gradlew wrapper --gradle-version 5.3.1

build.gradle
classpath 'com.android.tools.build:gradle:3.3.2'

JVM Tuning

org.gradle.jvmargs=-Xmx4G

  • provide enough heap space

  • tweaks often do more harm than good

  • rather work on structural improvements

Daemon

org.gradle.daemon = true

  • enabled by default

  • in-memory caches

  • when building repeatedly

  • Do not disable it!

Parallelism

org.gradle.parallel = true

  • not enabled by default

  • usually safe to enable

  • speeds up multi-project builds

Build Cache

org.gradle.caching = true

// root build.gradle
// only for kotlin < 1.3.30
subprojects {
	pluginManager.withPlugin("kotlin-kapt") {
		kapt.useBuildCache = true
	}
}

Where is the problem?

Build Lifecycle

the build lifecycle

Red flags

// settings.gradle
// do not do this
new File('.').eachFile(FileType.DIRECTORIES) { dir ->
    include dir.name
}

Startup / Settings / BuildSrc

keep daemon healty

./gradlew --status

pgrep -lf "GradleDaemon"

Configuration

  • Applying plugins

  • Evaluating build scripts

  • Running afterEvaluate {} blocks

Configuration time

  • Before running any task

  • Even gradlew help / gradlew tasks

  • Android Studio sync

Avoid Dependency resolution at configuration time!

Be aware of inefficient plugins!

Execution

  • Executing selected tasks

  • Incremental

  • Cacheable

  • Parallelizable

Incremental Builds

Nothing changed? Executed tasks should be zero!

Watch out for volatile inputs!

Faster Compilation

  • Modularization ⇒ Compile avoidance

  • Decoupled code ⇒ Faster incremental compilation

  • Careful with Kotlin annotation processing (for now)

Resources

Configuration

What

  • applying plugins

  • evaluating build scripts

  • running afterEvaluate{} blocks

When

  • on any Gradle run

  • Android Studio sync

Resolution at configuration time

Eager Resolution

task fatJar(type: Jar) {
    from configurations.compile.collect {
	it.isDirectory() ? it : zipTree(it)
    }
    with jar
    classifier = 'uber-jar'
}

Lazy Resolution

task fatJar(type: Jar) {
    from {
	configurations.compile.collect {
	    it.isDirectory() ? it : zipTree(it)
	}
    }
    with jar
    classifier = 'uber-jar'
}

Configuration Avoidance

tasks.register("fatJar", Jar) {
    from configurations.compile.collect {
        it.isDirectory() ? it : zipTree(it)
    }
    with jar
    classifier = 'uber-jar'
}

I/O at configuration time

That build script seems expensive

I/O at configuration time

task projectStats {
    def statsFile = new File(buildDir, 'stats.txt')
    statsFile.parentFile.mkdirs()
    def javaFiles = sourceSets.main.java.size()
    def javaSize = sourceSets.main.java
	.collect{it.text.bytes}
	.flatten()
	.sum()
    statsFile.text = """
    |SourceFiles:  ${javaFiles}
    |Source size:  ${javaSize} bytes
    """.stripMargin()
}

Where is the problem?

I/O at configuration time

task projectStats {
    def statsFile = new File(buildDir, 'stats.txt')
    input.files sourceSet.main.java
    outputs.file statsFile
    doLast {
        statsFile.parentFile.mkdirs()
        def javaFiles = sourceSets.main.java.size()
        def javaSize = sourceSets.main.java
        	.collect{it.text.bytes}
        	.flatten()
        	.sum()
        statsFile.text = """
        |SourceFiles:  ${javaFiles}
        |Source size:  ${javaSize} bytes
        """.stripMargin()
    }
}

Don’t forget doLast{}

I/O at configuration time

task projectStats(type: ProjectStats) {
    statsFile = new File(buildDir, 'stats.txt')
    sources = sourceSet.main.java
}

class ProjectStats extends DefaultTask {
    @InputFiles FileCollection sources
    @OutputFile File statsFile

    @TaskAction def stats() {
      statsFile.text = """
      |Files: ${sources.size()}
      |Total: ${sources.collect{it.text.bytes}.flatten().sum()} bytes
      """.stripMargin()
    }
}

Inefficient Plugins

// version.gradle
def out = new ByteArrayOutputStream()
exec {
    commandLine 'git','rev-parse','HEAD'
    standardOutput = out
    workingDir = rootDir
}
version = new String(out.toByteArray())

// root 'build.gradle'
allprojects {
    apply from:'$rootDir/version.gradle'
}

Re-use expensive logic

// root 'build.gradle'
apply from:"$rootDir/version.gradle"

subprojects {
    version = rootProject.version
}

Variant explosion

variantFilter { variant ->
    def flavorName = variant.flavors[0].name
    def freeFlavor = flavorName == 'free'
    if(!freeFlavor && variant.buildType.name == 'release') {
        variant.ignore = true
    }
}

Configuration Time

  • Avoid dependency resolution

  • Avoid I/O

  • Dont repeat yourself

Gradle build cache

Task caching

  • cache key based on inputs

  • outputs fetched from cache if available

  • no task action (re-)executed

  • opt-in (task declares cacheability)

Usage scenarios

  • Local branch switching

  • CI populated cache for (distributed) team

  • Faster CI builds with stateless agents

Mechanics

  • stable inputs

  • repeatable outputs

  • classpath normalization

  • Path relocatability

Enable caching

org.gradle.caching = true

build.gradle
// only for kotlin < 1.3.30
subprojects {
    pluginManager.withPlugin("kotlin-kapt") {
        kapt.useBuildCache = true
    }
}
  • uses local cache by default

Cached unit tests

  • the default for java projects

  • coming in AGP 3.5

  • be aware of includeAndroidResources = true

Android best practices

Remote cache

build cache
settings.gradle
def runsOnCI = System.getEnv("CI") != null

buildCache {
    local {
        enabled = !runsOnCI
    }
    remote(HttpBuildCache) {
        push = runsOnCI
        url = 'https://my.server.com:8080/cache/'
	// if cache access is secured
	credentials {
	    username = 'cache-user'
	    password = 'super-secret-pw'
        }
    }
}

Gradle Remote Cache Backend

docker run -d \
	   -v /opt/build-cache-node:/data \
           -p 80:5071 \
	   gradle/build-cache-node:latest

Gradle Enterprise cache $$$

Improvements in AGP?

Improving Build Speed

Configuration Avoidance

K-9: tasks created in 3.2.1 vs 3.3.2

Cacheability

K-9: tests in mail subprojects

Lint

K-9: check -x test

Cacheability

Annotation Processing Improvements

Memory Leak Fixes

Databinding

Conclusion

Takeaways

  • know your build

  • monitor and measure

  • avoid unnecessary configuration

  • avoid repeated work

Resources

Thank you!