Slide 1

Slide 1 text

Improving Android Build Performance Cédric Champeau, Gradle Inc.

Slide 2

Slide 2 text

About Gradle

Slide 3

Slide 3 text

| 3 What is Gradle? - Gradle != Gradle plugin for Android != Android Studio - Gradle is a build and automation tool - JVM based - 100% Free Open Source - Apache Standard License 2.0 - Multi-ecosystem - JVM: Java, Kotlin, Groovy, Scala, … - Native: C/C++, Go… - Android - …

Slide 4

Slide 4 text

| 4 Gradle, Inc. The company behind Gradle. • Build Happiness • Employs full time engineers • Providing Gradle Build Scans and Gradle Enterprise • (Gradle consulting, support, development services etc.) • (Training: online, public and in-house)

Slide 5

Slide 5 text

| 5 We’re hiring • Fully distributed development team • Exciting project used by millions • Build tool team and Gradle enterprise positions If anything you hear from now on sounds like a great problem to solve, Talk to us! https:/ /gradle.com/careers/

Slide 6

Slide 6 text

The Cost of Builds

Slide 7

Slide 7 text

| 7 60s waste * 50 builds per day * 50 developers = 42h wasted per day … and that’s not even considering lost focus Fast Builds Matter

Slide 8

Slide 8 text

General Advice

Slide 9

Slide 9 text

| 9 Process 1. Define scenario to improve 2. Profile scenario 3. Identify biggest bottleneck 4. Fix bottleneck 5. Verify fix by measurement 6. Repeat

Slide 10

Slide 10 text

|10 Automate your measurements github.com/gradle/gradle-profiler configurationTime { tasks = ["help"] } sync { tasks = ["generateDebugSources"] } cleanBuild { tasks = ["build"] cleanup-tasks = ["clean"] } cachedCleanBuild { tasks = ["build"] cleanup-tasks = ["clean"] gradle-args = ["--build-cache"] } configurationTime { tasks = ["help"] } sync { tasks = ["generateDebugSources"] } cleanBuild { tasks = ["build"] cleanup-tasks = ["clean"] } cachedCleanBuild { tasks = ["build"] cleanup-tasks = ["clean"] gradle-args = ["--build-cache"] }

Slide 11

Slide 11 text

|11 Stay Up-To-Date ./gradlew wrapper --gradle-version 4.10.2 buildscript { repositories { google() } dependencies { classpath ‘com.android.tools.build:gradle:3.1.2' } } buildscript { repositories { google() } dependencies { classpath ‘com.android.tools.build:gradle:3.1.2' } }

Slide 12

Slide 12 text

|12 JVM tuning Provide enough heap space Other tweaks often do more harm than good Spend your time on structural improvements

Slide 13

Slide 13 text

|13 The Gradle Daemon The Gradle Daemon is enabled by default Provides a number of in-memory caches Useful for repeated builds Do not disable it!

Slide 14

Slide 14 text

Where is the Problem?

Slide 15

Slide 15 text

|15 Build Scans

Slide 16

Slide 16 text

|16 The Build Lifecycle

Slide 17

Slide 17 text

|17 Red Flags Startup/Settings/buildSrc > 1s Configuration time >10ms/project Single-line change ≅ clean build No-op build doing any work at all High GC time

Slide 18

Slide 18 text

Startup, buildSrc, Settings

Slide 19

Slide 19 text

|19 Startup, buildSrc, Settings Use the Daemon, keep it healthy

Slide 20

Slide 20 text

Configuration Time

Slide 21

Slide 21 text

|21 Configuration Time Applying plugins Evaluating build scripts Running afterEvaluate {} blocks

Slide 22

Slide 22 text

|22 Configuration Time When running any task Even gradle help/gradle tasks Android Studio sync

Slide 23

Slide 23 text

|23 Resolution at Configuration Time

Slide 24

Slide 24 text

|24 Eager resolution task uberJar(type:Jar) { from sourceSets.main.output from configurations.runtime.collect { it.directory ? it : zipTree(it) } classifier "uber-jar" } task uberJar(type:Jar) { from sourceSets.main.output from configurations.runtime.collect { it.directory ? it : zipTree(it) } classifier "uber-jar" } Resolution at Configuration Time

Slide 25

Slide 25 text

|25 Use lazy evaluation instead task uberJar(type:Jar) { inputs.files configurations.runtime from sourceSets.main.output from { configurations.runtime.collect { it.directory ? it : zipTree(it) } } classifier "uber-jar" } task uberJar(type:Jar) { inputs.files configurations.runtime from sourceSets.main.output from { configurations.runtime.collect { it.directory ? it : zipTree(it) } } classifier "uber-jar" } Resolution at Configuration Time

Slide 26

Slide 26 text

|26 This build script seems expensive I/O at Configuration Time

Slide 27

Slide 27 text

|27 task projectStats() { def statsFile = new File(buildDir, 'stats.txt') statsFile.parentFile.mkdirs() statsFile.text = "Source files: ${sourceSets.main.java.size()}" } task projectStats() { def statsFile = new File(buildDir, 'stats.txt') statsFile.parentFile.mkdirs() statsFile.text = "Source files: ${sourceSets.main.java.size()}" } I/O at Configuration Time Careful when writing custom tasks!

Slide 28

Slide 28 text

|28 I/O at Configuration Time task projectStats() { def statsFile = new File(buildDir, 'stats.txt') inputs.files sourceSets.main.java outputs.file statsFile doLast { statsFile.parentFile.mkdirs() statsFile.text = "Source files: ${sourceSets.main.java.size()}" } } task projectStats() { def statsFile = new File(buildDir, 'stats.txt') inputs.files sourceSets.main.java outputs.file statsFile doLast { statsFile.parentFile.mkdirs() statsFile.text = "Source files: ${sourceSets.main.java.size()}" } } Don’t forget doLast {}

Slide 29

Slide 29 text

|29 task projectStats(type: ProjectStats) { statsFile = new File(buildDir, 'stats.txt') sources = sourceSets.main.java } class ProjectStats extends DefaultTask { @InputFiles FileCollection sources @OutputFile File statsFile @TaskAction def stats() { statsFile.text = "Source files: ${sources.size()}" } } task projectStats(type: ProjectStats) { statsFile = new File(buildDir, 'stats.txt') sources = sourceSets.main.java } class ProjectStats extends DefaultTask { @InputFiles FileCollection sources @OutputFile File statsFile @TaskAction def stats() { statsFile.text = "Source files: ${sources.size()}" } } I/O at Configuration Time

Slide 30

Slide 30 text

|30 Inefficient Plugins

Slide 31

Slide 31 text

|31 Expensive logic for each project def out = new ByteArrayOutputStream() exec { commandLine 'git', 'rev-parse', "HEAD" standardOutput = out workingDir = rootDir } version = new String(out.toByteArray()) def out = new ByteArrayOutputStream() exec { commandLine 'git', 'rev-parse', "HEAD" standardOutput = out workingDir = rootDir } version = new String(out.toByteArray()) subprojects { apply from: "$rootDir/gradle/version-from-status.gradle" } subprojects { apply from: "$rootDir/gradle/version-from-status.gradle" } Inefficient Plugins

Slide 32

Slide 32 text

|32 Reuse expensive calculations def out = new ByteArrayOutputStream() exec { commandLine 'git', 'rev-parse', "HEAD" standardOutput = out workingDir = rootDir } version = new String(out.toByteArray()) subprojects { version = rootProject.version } def out = new ByteArrayOutputStream() exec { commandLine 'git', 'rev-parse', "HEAD" standardOutput = out workingDir = rootDir } version = new String(out.toByteArray()) subprojects { version = rootProject.version } Inefficient Plugins

Slide 33

Slide 33 text

|33 Variant Explosion Use variantFilter {} to exclude useless flavor/buildType combinations

Slide 34

Slide 34 text

Optimising Build Logic

Slide 35

Slide 35 text

|35 Extract Script Plugins Makes finding issues easier apply from: "$rootDir/gradle/environment.gradle" apply from: "gradle/chuck-norris-fact.gradle" apply from: "$rootDir/gradle/version-from-status.gradle" subprojects { apply plugin: 'java' apply from: "$rootDir/gradle/test-fixtures.gradle" apply from: "$rootDir/gradle/compiler-settings.gradle" } apply from: "$rootDir/gradle/environment.gradle" apply from: "gradle/chuck-norris-fact.gradle" apply from: "$rootDir/gradle/version-from-status.gradle" subprojects { apply plugin: 'java' apply from: "$rootDir/gradle/test-fixtures.gradle" apply from: "$rootDir/gradle/compiler-settings.gradle" }

Slide 36

Slide 36 text

|36 Extract Binary Plugins Use buildSrc Use static compilation Keep build scripts declarative

Slide 37

Slide 37 text

|37 Optimise on the Algorithmic Level gradle-profiler --profile jfr

Slide 38

Slide 38 text

Execution Time

Slide 39

Slide 39 text

|39 Execution Time Executing selected tasks Incremental Cacheable Parallelizable

Slide 40

Slide 40 text

|40 Incremental Builds Nothing changed? Executed tasks should be zero!

Slide 41

Slide 41 text

|41 Incremental Builds Find volatile inputs

Slide 42

Slide 42 text

|42 Example: Crashlytics Unique IDs are the bane of local dev performance apply plugin: 'io.fabric' android { buildT ypes { debug { ext.alwaysUpdateBuildId = false } } } apply plugin: 'io.fabric' android { buildT ypes { debug { ext.alwaysUpdateBuildId = false } } }

Slide 43

Slide 43 text

|43 Faster Compilation Modularization => Compile avoidance Decoupled code => Faster incremental compilation Careful with Kotlin migration (for now)

Slide 44

Slide 44 text

|44 Incremental Annotation Processing Since Gradle 4.7 Early adopters: Lombok, Android-State, Dagger, Parceller Others: github.com/gradle/gradle/issues/5277

Slide 45

Slide 45 text

|45 Caching

Slide 46

Slide 46 text

|46 Parallelism Serial execution

Slide 47

Slide 47 text

|47 Parallelism --parallel and small, decoupled projects

Slide 48

Slide 48 text

Keeping track of performance

Slide 49

Slide 49 text

|50 Gradle Enterprise

Slide 50

Slide 50 text

|51 See all Builds Find out what colleagues are struggling with

Slide 51

Slide 51 text

|52 Get Deep Insights

Slide 52

Slide 52 text

|53 Watch Performance over Time

Slide 53

Slide 53 text

Resources

Slide 54

Slide 54 text

|55 Gradle performance guide https://guides.gradle.org/performance Android performance guide https://developer.android.com/studio/build/optimize-your-build.html Plugin development guide https://guides.gradle.org/implementing-gradle-plugins Structuring build logic guide https://docs.gradle.org/current/userguide/organizing_build_logic.html Guides

Slide 55

Slide 55 text

|56 scans.gradle.com

Slide 56

Slide 56 text

w Thank you Devoxx Belgium 2018